project-auto-wizard 0.11.0 → 0.12.1

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 (78) hide show
  1. package/README.md +24 -21
  2. package/package.json +2 -2
  3. package/payload/config/breaking-changes.json +1 -1
  4. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  5. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  6. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
  7. package/payload/scripts/changelog_manager.py +84 -31
  8. package/payload/scripts/version_manager.py +2 -2
  9. package/payload/version.yml.template +4 -2
  10. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +30 -10
  11. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +37 -12
  12. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +3 -6
  13. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +0 -12
  14. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +27 -6
  15. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +3 -7
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +7 -34
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +2 -25
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +2 -20
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +5 -30
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +0 -4
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +7 -51
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +5 -29
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +9 -29
  24. package/payload/workflows/go/PROJECT-GO-CI.yaml +5 -28
  25. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +19 -99
  26. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +18 -89
  27. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +11 -41
  28. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +9 -45
  29. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +1 -27
  30. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +14 -97
  31. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +14 -92
  32. package/payload/workflows/react/PROJECT-REACT-CI.yaml +11 -40
  33. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +9 -43
  34. package/payload/workflows/spring/{nexus/PROJECT-SPRING-NEXUS-CI.yml → PROJECT-SPRING-CI.yml} +2 -25
  35. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +19 -63
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +19 -59
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +23 -86
  38. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +20 -80
  39. package/src/cli/args.js +8 -15
  40. package/src/cli/help.js +2 -3
  41. package/src/commands/doctor.js +9 -8
  42. package/src/commands/dry-run.js +1 -1
  43. package/src/commands/full.js +9 -10
  44. package/src/commands/interactive-flutter.js +1 -1
  45. package/src/commands/interactive.js +34 -46
  46. package/src/commands/purge.js +2 -2
  47. package/src/commands/status.js +5 -6
  48. package/src/commands/uninstall.js +3 -4
  49. package/src/context.js +5 -6
  50. package/src/core/assets.js +2 -2
  51. package/src/core/baseline.js +1 -1
  52. package/src/core/branches.js +3 -3
  53. package/src/core/breaking-check.js +1 -1
  54. package/src/core/breaking.js +1 -1
  55. package/src/core/copy/flutter-app.js +2 -2
  56. package/src/core/copy/gitignore.js +8 -8
  57. package/src/core/copy/workflows.js +24 -69
  58. package/src/core/deploy-style.js +2 -2
  59. package/src/core/detect-fs.js +11 -11
  60. package/src/core/detect.js +7 -7
  61. package/src/core/flutter-options.js +1 -1
  62. package/src/core/installed-stores.js +1 -1
  63. package/src/core/logger.js +1 -1
  64. package/src/core/paths-resolve.js +1 -1
  65. package/src/core/removal-plan.js +4 -4
  66. package/src/core/verify.js +4 -4
  67. package/src/core/version-yml.js +27 -34
  68. package/src/core/wizard-env.js +4 -4
  69. package/src/index.js +14 -14
  70. package/src/ui/banner.js +2 -2
  71. package/src/ui/env-plan.js +10 -16
  72. package/src/ui/prompts.js +9 -14
  73. package/src/ui/status-cards.js +10 -12
  74. package/src/ui/summary.js +13 -13
  75. package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +0 -210
  76. package/payload/workflows/spring/nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +0 -92
  77. package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +0 -70
  78. package/src/core/options-ask.js +0 -102
@@ -59,7 +59,7 @@ function renderVirtual(templateContent, envOpts) {
59
59
 
60
60
  // 분류 — 대상 워크플로우 디렉토리 기준. srcText: 브랜치 치환이 적용된 원본 로더 (makeSrcText).
61
61
  //
62
- // baseline이 있으면 3-way로 가른다 (issue #69). base가 없던 시절에는 업스트림이 한 글자만 고쳐도
62
+ // baseline이 있으면 3-way로 가른다. base가 없던 시절에는 업스트림이 한 글자만 고쳐도
63
63
  // 사용자가 손대지 않은 파일이 changed로 떨어져, "전부 skip" 아니면 "전부 backup" 둘 중 하나만
64
64
  // 고를 수 있었다.
65
65
  //
@@ -101,7 +101,7 @@ function classify(srcDir, workflowsDir, envOpts, srcText, baseline = null, filte
101
101
  // 한 원본 디렉토리를 분류 결과대로 처리한다. common·타입별·server-deploy가 같은 규칙을 쓴다.
102
102
  // filter: trunk-based 제외 같은 파일 단위 필터.
103
103
  //
104
- // 자동 처리되는 두 버킷이 이 함수의 핵심이다 (issue #69):
104
+ // 자동 처리되는 두 버킷이 이 함수의 핵심이다:
105
105
  // upstreamOnly — 사용자가 손대지 않았으니 그냥 최신으로 교체한다. 물어볼 이유가 없다.
106
106
  // localOnly — 업스트림이 그대로니 사용자 수정본을 그대로 둔다. 역시 물어볼 이유가 없다.
107
107
  function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () => true) {
@@ -153,25 +153,25 @@ function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () =>
153
153
  }
154
154
 
155
155
  // copy_workflows 본체 (동기 — 기존 호출부 무변경).
156
- // context: { types:[], paths:Map, includeNexus, includeSecretBackup, force, repoName, resolvers,
156
+ // context: { types:[], paths:Map, force, repoName, resolvers,
157
157
  // envValues?:Map<key,value>, envUseDefaults?:boolean } ← env 계획(promptEnvPlan) 결과 주입점
158
158
  // flutterStore?:string[]|null } ← Flutter 스토어 대상 (null=필터 없음, 배열=선택된 플랫폼만)
159
159
  // hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'>, — 진짜 충돌(changed) 결정
160
160
  // restoreRemoved?: Set<filename> } — 사용자가 지운 파일 중 복원할 것
161
161
  // 미지정 파일은 'skip'(현행 force 동작 100% 유지). 대화형 수집은 copyWorkflowsInteractive 참조.
162
- // 반환: {copied, skipped, templateAdded, optionalCopied, backupAdded, autoUpdated, keptLocal, removedKept, restoredFiles}
162
+ // 반환: {copied, skipped, templateAdded, backupAdded, autoUpdated, keptLocal, removedKept, restoredFiles}
163
163
  export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}) {
164
- const { types = [], paths = new Map(), includeNexus = false, includeSecretBackup = false, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true } = context;
164
+ const { types = [], paths = new Map(), repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true } = context;
165
165
  const decisions = hooks.decisions instanceof Map ? hooks.decisions : new Map();
166
166
  const restoreRemoved = hooks.restoreRemoved instanceof Set ? hooks.restoreRemoved : new Set();
167
167
  const workflowsDir = join(targetRoot, PATHS.workflowsDir);
168
168
  const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
169
169
  if (!exists(projectTypesDir)) throw new Error("패키지 구조 오류 — payload/workflows 폴더를 찾지 못했습니다.");
170
170
 
171
- const counters = { copied: 0, skipped: 0, templateAdded: 0, optionalCopied: 0, backupAdded: 0 };
171
+ const counters = { copied: 0, skipped: 0, templateAdded: 0, backupAdded: 0 };
172
172
  const deployValues = new Map(); // Map<type, Map<key,value>> — deploy 블록용 ask 값
173
173
  counters.deployValues = deployValues;
174
- counters.copiedFiles = []; // 이번 실행에서 실제로 새로 쓰여진 파일명 (issue #19 — printSummary 정확성용)
174
+ counters.copiedFiles = []; // 이번 실행에서 실제로 새로 쓰여진 파일명 (printSummary 정확성용)
175
175
  counters.unchangedFiles = []; // skip(unchanged) 대상 — 로그에서 "왜 안 바뀌었나"의 근거
176
176
  counters.autoUpdated = []; // 질문 없이 최신으로 교체된 파일 (사용자 미수정)
177
177
  counters.keptLocal = []; // 질문 없이 사용자 수정본을 유지한 파일 (업스트림 무변경)
@@ -185,7 +185,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
185
185
  const envOptsFor = (type) => ({ type, projectPath: paths.get(type) || ".", repoName, resolvers, values: envValues, useDefaults: envUseDefaults });
186
186
  const dirCtx = { srcText, baseline, decisions, restoreRemoved, baselineTargets };
187
187
 
188
- // (1) common — 타입별과 동일 규칙 (README 계약, issue #20 H3).
188
+ // (1) common — 타입별과 동일 규칙 (README 계약).
189
189
  // trunk-based 모드는 VERSION-CONTROL·AUTO-CHANGELOG 미설치 (RELEASE-PUBLISH 단독).
190
190
  const branchMode = context.branches?.mode || "pr-flow";
191
191
  const commonDir = join(projectTypesDir, "common");
@@ -194,7 +194,7 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
194
194
  const c = processDir(commonDir, workflowsDir, envOptsFor("common"), dirCtx, counters, notExcluded);
195
195
  // env 치환 — 타입별 폴더(copyWorkflowsForType)와 동일하게, 손대지 않기로 한 파일(unchanged/localOnly)은
196
196
  // 건너뛴다. common 최상위는 지금까지 @wizard 마커가 없어 이 루프가 없어도 드러나지 않았지만,
197
- // ISSUE_HELPER_CREATE_BRANCH(issue #94)부터는 실제로 값이 반영돼야 한다.
197
+ // ISSUE_HELPER_CREATE_BRANCH부터는 실제로 값이 반영돼야 한다.
198
198
  const untouched = [...c.unchanged, ...c.localOnly];
199
199
  for (const filename of listYamlFiles(commonDir)) {
200
200
  if (!notExcluded(filename)) continue;
@@ -208,27 +208,10 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
208
208
  // (2~4) 타입별
209
209
  for (const type of types) {
210
210
  const asks = new Map();
211
- copyWorkflowsForType(type, projectTypesDir, workflowsDir, { includeNexus, ...context, deployStyle, envOptsFor, collectAsks: asks, dirCtx }, counters);
211
+ copyWorkflowsForType(type, projectTypesDir, workflowsDir, { ...context, deployStyle, envOptsFor, collectAsks: asks, dirCtx }, counters);
212
212
  if (asks.size) deployValues.set(type, asks);
213
213
  }
214
214
 
215
- // (5) common/secret-backup — 있으면 무조건 스킵/신규만 복사
216
- const secretDir = join(commonDir, "secret-backup");
217
- if (exists(secretDir) && includeSecretBackup) {
218
- for (const filename of listYamlFiles(secretDir)) {
219
- const dst = join(workflowsDir, filename);
220
- if (existsSync(dst)) continue; // 이미 존재하면 스킵
221
- writeText(dst, srcText(join(secretDir, filename)));
222
- // 이 경로는 타입별 복사 루프 밖이라 env 치환 루프가 닿지 않는다. 여기서 직접 걸어주지
223
- // 않으면 이 파일의 @wizard 마커가 통째로 무시돼 __PROJECT_NAME__ 같은 값이 그대로 설치된다.
224
- configureEnv(dst, envOptsFor("common"));
225
- counters.optionalCopied++;
226
- counters.copied++;
227
- counters.copiedFiles.push(filename);
228
- baselineTargets.set(filename, { srcPath: join(secretDir, filename), envOpts: envOptsFor("common"), wrote: true });
229
- }
230
- }
231
-
232
215
  counters.baselineTargets = baselineTargets; // 호출부(runFull)가 env 치환 완료 후 baseline을 기록한다
233
216
  return counters;
234
217
  }
@@ -276,14 +259,14 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcTe
276
259
  log.info("copy", "skip", `${filename} (사용자 결정: 기존 유지)`);
277
260
  }
278
261
 
279
- // 대화형 사전 조사 — 사람이 답해야 하는 것만 뽑는다 (issue #69).
262
+ // 대화형 사전 조사 — 사람이 답해야 하는 것만 뽑는다.
280
263
  // copyWorkflows 본체와 동일한 classify 기준을 써야 결정 Map이 실제 처리 대상과 1:1로 맞는다.
281
- // common도 타입별과 동일하게 스캔한다 (issue #20 H3 — 이전에는 common 충돌이 질문조차 되지 않았다).
264
+ // common도 타입별과 동일하게 스캔한다 (이전에는 common 충돌이 질문조차 되지 않았다).
282
265
  // 반환: { conflicts: [{filename,type}], removed: [{filename,type}] }
283
266
  // conflicts — 양쪽이 다 바뀐 진짜 충돌. upstreamOnly/localOnly는 자동 처리되므로 여기 없다.
284
267
  // removed — 우리가 깔았는데 사용자가 지운 파일. 되살리기 전에 물어봐야 한다.
285
268
  export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
286
- const { types = [], paths = new Map(), includeNexus = false, repoName = "", resolvers = {}, flutterStore = null } = context;
269
+ const { types = [], paths = new Map(), repoName = "", resolvers = {}, flutterStore = null } = context;
287
270
  const workflowsDir = join(targetRoot, PATHS.workflowsDir);
288
271
  const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
289
272
  const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
@@ -313,7 +296,7 @@ export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
313
296
  collect(typeDir, envOpts, type, () => false, buildTypeRootFilter(type, deployStyle, flutterStore));
314
297
  }
315
298
  const serverDeployDir = join(typeDir, "server-deploy");
316
- if (exists(serverDeployDir) && !includeNexus && deployStyle !== NO_DEPLOY_STYLE) {
299
+ if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
317
300
  collect(serverDeployDir, envOpts, type, () => false, keepDeploy);
318
301
  }
319
302
  }
@@ -341,7 +324,7 @@ export async function copyWorkflowsInteractive(context, payloadRoot, targetRoot
341
324
  }
342
325
 
343
326
  function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters) {
344
- const { includeNexus, deployStyle = "", flutterStore = null, envOptsFor, collectAsks = null, dirCtx } = ctx;
327
+ const { deployStyle = "", flutterStore = null, envOptsFor, collectAsks = null, dirCtx } = ctx;
345
328
  const keepDeploy = deployFilter(deployStyle);
346
329
  const keepTypeRoot = buildTypeRootFilter(type, deployStyle, flutterStore);
347
330
  const { srcText, baselineTargets } = dirCtx;
@@ -361,29 +344,16 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
361
344
 
362
345
  // server-deploy
363
346
  const serverDeployDir = join(typeDir, "server-deploy");
364
- if (exists(serverDeployDir)) {
365
- if (includeNexus || deployStyle === NO_DEPLOY_STYLE) {
366
- // Nexus 프로젝트 또는 "배포 안 함" → 폴더째 제외 (복사 안 함)
367
- } else {
368
- const c = processDir(serverDeployDir, workflowsDir, envOpts, dirCtx, counters, keepDeploy);
369
- untouched.push(...c.unchanged, ...c.localOnly);
370
- }
371
- }
372
-
373
- // nexus (opt-in) — 라이브러리 publish 계열(Nexus + GitHub Packages).
374
- // 다른 폴더와 같은 processDir을 쓴다. 종전에는 이 경로만 별도 로직으로 "존재하면 무조건
375
- // .bak 후 교체"였는데, 그러면 사용자가 손댄 publish 워크플로우가 재실행마다 묻지도 않고
376
- // 밀린다. baseline 3-way·충돌 3지선을 다른 워크플로우와 동일하게 적용한다.
377
- const nexusDir = join(typeDir, "nexus");
378
- if (exists(nexusDir) && includeNexus) {
379
- const c = processDir(nexusDir, workflowsDir, envOpts, dirCtx, counters);
347
+ // "배포 안 함"이면 폴더째 제외 (복사 안 함)
348
+ if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
349
+ const c = processDir(serverDeployDir, workflowsDir, envOpts, dirCtx, counters, keepDeploy);
380
350
  untouched.push(...c.unchanged, ...c.localOnly);
381
351
  }
382
352
 
383
353
  // env 치환 — 이 타입의 원본 디렉토리들에서 복사돼 존재하고, 손대지 않기로 한 것이 아닌 파일만
384
- for (const srcDir of [typeDir, serverDeployDir, nexusDir]) {
354
+ for (const srcDir of [typeDir, serverDeployDir]) {
385
355
  if (!exists(srcDir)) continue;
386
- if (srcDir === serverDeployDir && (includeNexus || deployStyle === NO_DEPLOY_STYLE)) continue; // 폴더째 제외
356
+ if (srcDir === serverDeployDir && deployStyle === NO_DEPLOY_STYLE) continue; // 폴더째 제외
387
357
  for (const filename of listYamlFiles(srcDir)) {
388
358
  const target = join(workflowsDir, filename);
389
359
  if (srcDir === serverDeployDir && !keepDeploy(filename)) continue; // 안 고른 배포 방식
@@ -395,18 +365,18 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
395
365
  }
396
366
  }
397
367
 
398
- // 전체 워크플로우 분류(common + 타입별 + server-deploy + nexus opt-in) — status/dry-run 공용.
368
+ // 전체 워크플로우 분류(common + 타입별 + server-deploy) — status/dry-run 공용.
399
369
  // changed뿐 아니라 newFiles/unchanged까지 전부 반환한다는 점이 listWorkflowConflicts(changed만
400
370
  // 반환)와 다르다(읽기 전용 — 실제로 아무 파일도 쓰지 않는다).
401
371
  export function planWorkflows(context, payloadRoot, targetRoot = ".") {
402
- const { types = [], paths = new Map(), includeNexus = false, includeSecretBackup = false, repoName = "", resolvers = {}, flutterStore = null } = context;
372
+ const { types = [], paths = new Map(), repoName = "", resolvers = {}, flutterStore = null } = context;
403
373
  const workflowsDir = join(targetRoot, PATHS.workflowsDir);
404
374
  const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
405
375
  const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
406
376
  const srcText = makeSrcText(context.branches || null, deployStyle);
407
377
  const baseline = readBaseline(targetRoot);
408
378
  const branchMode = context.branches?.mode || "pr-flow";
409
- // upstreamOnly/localOnly/removed는 baseline이 있을 때만 채워진다 (issue #69).
379
+ // upstreamOnly/localOnly/removed는 baseline이 있을 때만 채워진다.
410
380
  const plan = { newFiles: [], unchanged: [], changed: [], upstreamOnly: [], localOnly: [], removed: [] };
411
381
  const BUCKETS = ["newFiles", "unchanged", "changed", "upstreamOnly", "localOnly", "removed"];
412
382
 
@@ -426,16 +396,6 @@ export function planWorkflows(context, payloadRoot, targetRoot = ".") {
426
396
  branchMode === "trunk-based" ? TRUNK_BASED_EXCLUDED : null);
427
397
  }
428
398
 
429
- // secret-backup은 copyWorkflows처럼 신규 파일만 대상(기존 파일은 절대 덮어쓰지 않는 규약) —
430
- // classify()의 changed 판정과 무관하게, 여기서도 존재 여부만으로 new/unchanged를 가른다.
431
- const secretDir = join(commonDir, "secret-backup");
432
- if (exists(secretDir) && includeSecretBackup) {
433
- for (const filename of listYamlFiles(secretDir)) {
434
- const dst = join(workflowsDir, filename);
435
- plan[existsSync(dst) ? "unchanged" : "newFiles"].push({ filename, type: "common" });
436
- }
437
- }
438
-
439
399
  for (const type of types) {
440
400
  const envOpts = { type, projectPath: paths.get(type) || ".", repoName, resolvers };
441
401
  const typeDir = join(projectTypesDir, type);
@@ -444,14 +404,9 @@ export function planWorkflows(context, payloadRoot, targetRoot = ".") {
444
404
  }
445
405
 
446
406
  const serverDeployDir = join(typeDir, "server-deploy");
447
- if (exists(serverDeployDir) && !includeNexus && deployStyle !== NO_DEPLOY_STYLE) {
407
+ if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
448
408
  merge(classify(serverDeployDir, workflowsDir, envOpts, srcText, baseline, deployFilter(deployStyle)), type);
449
409
  }
450
-
451
- const nexusDir = join(typeDir, "nexus");
452
- if (exists(nexusDir) && includeNexus) {
453
- merge(classify(nexusDir, workflowsDir, envOpts, srcText, baseline), type);
454
- }
455
410
  }
456
411
 
457
412
  return plan;
@@ -1,4 +1,4 @@
1
- // 배포 방식 (이슈 #80) — 서버 배포 CD 워크플로우는 서로 대체재다.
1
+ // 배포 방식 — 서버 배포 CD 워크플로우는 서로 대체재다.
2
2
  // Nginx 무중단과 Traefik 무중단을 동시에 쓰는 경우는 없으므로 하나만 설치한다.
3
3
  // 고른 것은 push 트리거까지 켜서 설치한다 — 설치했는데 안 도는 상태를 만들지 않는다.
4
4
  import { join } from "node:path";
@@ -17,7 +17,7 @@ export const DEFAULT_DEPLOY_STYLE = "simple";
17
17
  // 서버 배포 자체를 하지 않는 프로젝트(프론트엔드 전용, 라이브러리 등)를 위한 값.
18
18
  // DEPLOY_STYLES에는 넣지 않는다 — isDeployWorkflow/suffixOf가 이 배열을 순회하는데, 빈 접미사를
19
19
  // 돌려주면 endsWith("")가 항상 참이라 모든 파일이 CD로 오판되어 cleanupOtherDeployWorkflows가
20
- // 설치된 워크플로우 전체를 지우는 회귀가 생긴다 (이슈 #126).
20
+ // 설치된 워크플로우 전체를 지우는 회귀가 생긴다.
21
21
  export const NO_DEPLOY_STYLE = "none";
22
22
 
23
23
  export const isDeployStyle = (v) => v === NO_DEPLOY_STYLE || DEPLOY_STYLES.some((s) => s.value === v);
@@ -1,5 +1,5 @@
1
1
  // 실 파일시스템 프로젝트 감지 (.sh detect_* 실행부 등가).
2
- // SP2-A detect.js 순수 함수를 fs/git으로 구동한다.
2
+ // detect.js 순수 함수를 fs/git으로 구동한다.
3
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { join, basename } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
@@ -27,8 +27,8 @@ export function detectTypes(root) {
27
27
  return detectTypesFromMarkers({ has: hasFile(root), read: readFile(root) });
28
28
  }
29
29
 
30
- // 버전 감지 — .sh detect_version 순서. jq는 package.json 파싱에 쓰인 적이 없어 게이트를 제거했다(이슈 #22 L4).
31
- // hint: 폴백 경고에 붙일 해결 방법 안내 (대화형/CLI가 다르다 — 이슈 #80).
30
+ // 버전 감지 — .sh detect_version 순서. jq는 package.json 파싱에 쓰인 적이 없어 게이트를 제거했다.
31
+ // hint: 폴백 경고에 붙일 해결 방법 안내 (대화형/CLI가 다르다).
32
32
  export function detectVersion(root, { warn = (m) => console.error(m), hint } = {}) {
33
33
  const read = readFile(root);
34
34
  const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
@@ -36,12 +36,12 @@ export function detectVersion(root, { warn = (m) => console.error(m), hint } = {
36
36
  return detectVersionFromFiles({ read, readJson, gitTag, warn, hint });
37
37
  }
38
38
 
39
- // 타입별 실제 마커 파일 (이슈 #77) — 감지 로그·설치 로그가 같은 근거 파일을 인용하도록.
39
+ // 타입별 실제 마커 파일 — 감지 로그·설치 로그가 같은 근거 파일을 인용하도록.
40
40
  export function detectMarkers(root, types = []) {
41
41
  return resolveMarkers(types, hasFile(root));
42
42
  }
43
43
 
44
- // 빌드 JDK 감지 (이슈 #82) — 배포 워크플로우 JAVA_VERSION 기본값에 실측값을 쓰기 위해.
44
+ // 빌드 JDK 감지 — 배포 워크플로우 JAVA_VERSION 기본값에 실측값을 쓰기 위해.
45
45
  // base: 모노레포에서 spring 프로젝트 루트 (레포 루트 기준 상대경로).
46
46
  export function detectJdk(root, base = ".") {
47
47
  const rel = base && base !== "." ? (r) => `${base}/${r}` : (r) => r;
@@ -49,7 +49,7 @@ export function detectJdk(root, base = ".") {
49
49
  return detectJdkFromFiles({ read: (r) => read(rel(r)) });
50
50
  }
51
51
 
52
- // 빌드 번호 감지 — 신규 통합 시 pubspec.yaml/build.gradle/app.json에서 실제 빌드 번호를 읽는다 (이슈 #41).
52
+ // 빌드 번호 감지 — 신규 통합 시 pubspec.yaml/build.gradle/app.json에서 실제 빌드 번호를 읽는다.
53
53
  export function detectBuildNumber(root, { types = [], warn = (m) => console.error(m) } = {}) {
54
54
  const read = readFile(root);
55
55
  const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
@@ -80,7 +80,7 @@ export function detectRepoName(root) {
80
80
  // find {base} -path "*/src/main/resources/application*.yml" | head -1 의 fs 재귀 구현.
81
81
  // 반환: root 기준 상대경로 (예: "server/src/main/resources/application.yml") 또는 "".
82
82
  //
83
- // .yaml도 인정한다 (이슈 #81). Spring은 .yml/.yaml을 모두 공식 지원하는데 종전 정규식이
83
+ // .yaml도 인정한다. Spring은 .yml/.yaml을 모두 공식 지원하는데 종전 정규식이
84
84
  // .yml만 봐서, application.yaml을 쓰는 프로젝트는 이 값이 빈 문자열이 되고 그 결과
85
85
  // __APPLICATION_YML_DIR__ 가 치환되지 않은 채 설치됐다.
86
86
  //
@@ -116,15 +116,15 @@ export function findSpringAppYml(root, base = ".") {
116
116
 
117
117
  // 실 resolver 세트 생성 (.sh resolve_token 4종 등가) — index/interactive 공용.
118
118
  // paths: Map<type, path> (모노레포 경로).
119
- // flutterOptions: resolveFlutterOptions 결과 또는 같은 필드를 가진 context (이슈 #131). null이면 Flutter 토큰이
119
+ // flutterOptions: resolveFlutterOptions 결과 또는 같은 필드를 가진 context. null이면 Flutter 토큰이
120
120
  // 빈 값이라 템플릿 기본값(dart-define, store_only)이 그대로 남는다.
121
121
  export function makeResolvers(root, repoName, paths, flutterOptions = null) {
122
122
  const springBase = (t) => paths.get(t || "spring") || paths.get("spring") || ".";
123
123
  return {
124
124
  repo: () => repoName,
125
- // 빌드 JDK (이슈 #82) — 배포 워크플로우 JAVA_VERSION의 기본값. 프로젝트 툴체인을 실측한다.
125
+ // 빌드 JDK — 배포 워크플로우 JAVA_VERSION의 기본값. 프로젝트 툴체인을 실측한다.
126
126
  // ⚠️ 빈 문자열을 돌려주면 setEnvLine이 그 줄을 건너뛰어 __JAVA_VERSION__이 그대로 남는다
127
- // (이슈 #81과 같은 실패 형태). 감지 실패 시 반드시 종전 기본값 21로 폴백한다.
127
+ // (같은 실패 형태). 감지 실패 시 반드시 종전 기본값 21로 폴백한다.
128
128
  jdk: (t) => detectJdk(root, springBase(t)) || "21",
129
129
  "spring-app-yml-dir": (t) => {
130
130
  const f = findSpringAppYml(root, springBase(t));
@@ -132,7 +132,7 @@ export function makeResolvers(root, repoName, paths, flutterOptions = null) {
132
132
  },
133
133
  "spring-app-yml-path": (t) => findSpringAppYml(root, springBase(t)) || "",
134
134
  "flutter-root": () => paths.get("flutter") || ".",
135
- // CI changes job의 경로 필터(이슈 #131) — 타입별 프로젝트 루트. 단일 레포·common은 "."(항상 변경됨으로 판정).
135
+ // CI changes job의 경로 필터 — 타입별 프로젝트 루트. 단일 레포·common은 "."(항상 변경됨으로 판정).
136
136
  "project-path": (t) => paths.get(t) || ".",
137
137
  // 빈 문자열이면 setEnvLine/setFallbackLine이 줄을 건너뛰어 템플릿 기본값이 남는다.
138
138
  "flutter-env-mode": () => flutterOptions?.envMode || "",
@@ -36,9 +36,9 @@ export function detectTypesFromMarkers({ has, read }) {
36
36
  const VERSION_RE = /^\d+\.\d+\.\d+$/;
37
37
 
38
38
  // 버전 감지 (동작명세 §3.3) — 순서대로 첫 성공. read(relpath)=>string|null 주입.
39
- // package.json은 이미 Node JSON.parse로 파싱을 마친 값이므로 jq 설치 여부와 무관하게 항상 사용한다(이슈 #22 L4).
39
+ // package.json은 이미 Node JSON.parse로 파싱을 마친 값이므로 jq 설치 여부와 무관하게 항상 사용한다.
40
40
  // hint: 폴백 경고 뒤에 붙일 "그럼 어떻게 고치나" 한 줄. 대화형과 CLI가 서로 다른 방법을
41
- // 안내해야 하므로(이슈 #80) 호출부가 정한다. 미지정 시 CLI 문구를 쓴다.
41
+ // 안내해야 하므로 호출부가 정한다. 미지정 시 CLI 문구를 쓴다.
42
42
  export function detectVersionFromFiles({ read, readJson, gitTag, warn, hint }) {
43
43
  const pkg = readJson?.("package.json");
44
44
  if (pkg?.version && VERSION_RE.test(pkg.version)) return pkg.version;
@@ -52,7 +52,7 @@ export function detectVersionFromFiles({ read, readJson, gitTag, warn, hint }) {
52
52
  let v;
53
53
  const gradleRe = /version\s*=\s*["']?(\d+\.\d+\.\d+)/;
54
54
  // Groovy DSL과 Kotlin DSL은 같은 문법(`version = "x.y.z"`)이라 정규식을 공유한다.
55
- // .kts를 빼먹으면 Kotlin DSL Spring 프로젝트가 전부 0.0.1로 초기화된다 (이슈 #77).
55
+ // .kts를 빼먹으면 Kotlin DSL Spring 프로젝트가 전부 0.0.1로 초기화된다.
56
56
  if ((v = grab(read("build.gradle"), gradleRe))) return v;
57
57
  if ((v = grab(read("build.gradle.kts"), gradleRe))) return v;
58
58
  if ((v = versionFromPom(read("pom.xml")))) return v;
@@ -64,7 +64,7 @@ export function detectVersionFromFiles({ read, readJson, gitTag, warn, hint }) {
64
64
  return "0.0.1";
65
65
  }
66
66
 
67
- // Maven pom.xml의 프로젝트 버전 (이슈 #77). <parent> 블록 안의 버전은 스프링 부트 BOM 버전이라
67
+ // Maven pom.xml의 프로젝트 버전. <parent> 블록 안의 버전은 스프링 부트 BOM 버전이라
68
68
  // 프로젝트 버전이 아니다 — 그 구간을 지운 뒤 첫 <version>을 읽는다.
69
69
  export function versionFromPom(content) {
70
70
  if (!content) return null;
@@ -81,7 +81,7 @@ export function extraMarkers(type) {
81
81
  return { python: ["setup.py", "requirements.txt"], spring: ["build.gradle.kts", "pom.xml"] }[type] || [];
82
82
  }
83
83
 
84
- // 그 타입을 감지하는 데 실제로 쓰인 파일 (이슈 #77). markerForType은 타입당 대표 파일 하나를
84
+ // 그 타입을 감지하는 데 실제로 쓰인 파일. markerForType은 타입당 대표 파일 하나를
85
85
  // 고정 반환하므로, build.gradle.kts만 있는 레포에서도 "build.gradle 발견"이라고 출력돼
86
86
  // 같은 설치 로그 안에서 경로 확정 화면과 파일명이 어긋났다. has()로 실재하는 것을 고른다.
87
87
  // 실재하는 후보가 없으면(감지 전 화면 등) 대표 파일을 쓴다.
@@ -90,7 +90,7 @@ export function resolveMarker(type, has) {
90
90
  return candidates.find(has) ?? candidates[0];
91
91
  }
92
92
 
93
- // 빌드 JDK 감지 (이슈 #82) — 배포 워크플로우의 JAVA_VERSION 기본값이 21로 고정돼 있어
93
+ // 빌드 JDK 감지 — 배포 워크플로우의 JAVA_VERSION 기본값이 21로 고정돼 있어
94
94
  // toolchain이 다른 프로젝트(예: 25)는 그대로 Enter를 누르면 러너 JDK와 어긋나 빌드가 깨진다.
95
95
  // 빌드 번호를 프로젝트 파일에서 읽는 detectBuildNumberFromFiles와 같은 방식으로 실측한다.
96
96
  // 반환: "21" 같은 메이저 버전 문자열, 못 찾으면 null.
@@ -129,7 +129,7 @@ export function resolveMarkers(types = [], has) {
129
129
  return out;
130
130
  }
131
131
 
132
- // 빌드 번호 감지 (이슈 #41) — 신규 통합 시 pubspec.yaml/build.gradle/app.json에 이미 기록된
132
+ // 빌드 번호 감지 — 신규 통합 시 pubspec.yaml/build.gradle/app.json에 이미 기록된
133
133
  // 빌드 번호를 읽어 version_code가 항상 1로 초기화되는 걸 막는다. types 배열에서 먼저 매칭되는
134
134
  // 첫 타입만 사용한다(다른 감지 로직의 types[0]=primary 관례와 동일). read(rel)=>string|null,
135
135
  // readJson(rel)=>object|null 로 주입.
@@ -1,4 +1,4 @@
1
- // Flutter 옵션 (이슈 #131) — 환경변수 방식·스토어 배포 대상·배포 모드의 단일 진실.
1
+ // Flutter 옵션 — 환경변수 방식·스토어 배포 대상·배포 모드의 단일 진실.
2
2
  // 스토어 배포 대상은 deploy-style.js(배포 방식)와 같은 구조 — 값 목록, 파일 필터, 선택 해제 정리 — 를 따른다.
3
3
  import { join } from "node:path";
4
4
  import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
@@ -1,4 +1,4 @@
1
- // 이미 설치된 스토어 배포 워크플로우 파일명으로 플랫폼을 추론한다 (이슈 #131).
1
+ // 이미 설치된 스토어 배포 워크플로우 파일명으로 플랫폼을 추론한다.
2
2
  // version.yml에 flutter_store 저장값이 없는 기존 설치가 대상이다 — 저장값이 없다고 "선택 없음"으로 보면
3
3
  // 잘 쓰던 스토어 워크플로우가 선택 해제 정리 규칙에 의해 삭제된다.
4
4
  import { existsSync, readdirSync } from "node:fs";
@@ -4,7 +4,7 @@
4
4
  // 쓰는 구조는 예외가 나면 아무것도 남기지 못한다(구 install-log.js가 그랬다).
5
5
  //
6
6
  // 왜 로컬 전용인가: 상세도를 제약하지 않기 위해서다. 로그 디렉토리에 .gitignore를 직접
7
- // 두어 그 폴더만 추적에서 뺀다 — 루트 .gitignore는 건드리지 않는다(이슈 #7 원칙).
7
+ // 두어 그 폴더만 추적에서 뺀다 — 루트 .gitignore는 건드리지 않는다.
8
8
  import { appendFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
@@ -166,7 +166,7 @@ export async function resolveProjectPaths({
166
166
  const candidates = findTypePathCandidates(root, t);
167
167
  let chosen = "";
168
168
 
169
- // ── ⑤-a 비대화형 (--force 또는 TTY 없음, .sh L1476~1489 — M4: root 폴백 의도적 불포함) ──
169
+ // ── ⑤-a 비대화형 (--force 또는 TTY 없음, .sh L1476~1489 — root 폴백 의도적 불포함) ──
170
170
  if (force || !tty) {
171
171
  if (existing) {
172
172
  chosen = existing;
@@ -4,7 +4,7 @@
4
4
  // 원칙: (a) 현재 payload에 존재하는 파일명과 정확히 일치하는 것, (b) 설치된 워크플로우 파일 중
5
5
  // 마법사 관리 마커(MANAGED_WORKFLOW_MARKER)로 시작하는 것 — 이 둘의 합집합을 제거 대상으로 삼는다.
6
6
  // (a)만으로는 과거 버전에서 설치된 뒤 이후 릴리스에서 payload 파일명이 바뀌거나 삭제된 파일을
7
- // 인식하지 못했다 (issue #20 L12 원래 버그). 반대로 (b)만으로는 이 수정 이전에 설치되어 마커가
7
+ // 인식하지 못했다 (원래 버그). 반대로 (b)만으로는 이 수정 이전에 설치되어 마커가
8
8
  // 없는(그러나 파일명은 여전히 현재 payload와 일치하는) 기존 설치 전체를 인식하지 못하는 회귀가
9
9
  // 생긴다 — 그래서 두 방식을 합집합으로 병행한다. 마커는 이 수정 이후 배포되는 payload 템플릿부터
10
10
  // 포함되므로, (b) 경로가 실제로 새로 잡아내는 것은 "이름이 바뀌거나 삭제된, 마커가 있는" 파일뿐이다.
@@ -12,7 +12,7 @@
12
12
  // (version.yml은 사용자 버전 데이터 — 제거 대상이 아니라 산출물이다).
13
13
  //
14
14
  // 이 파일은 원래 src/commands/revert.js였다. revert 모드는 uninstall의 부분집합이라 제거됐고
15
- // (issue #70), 판별 로직만 남아 commands가 아닌 core로 옮겨졌다.
15
+ //, 판별 로직만 남아 commands가 아닌 core로 옮겨졌다.
16
16
  import { join } from "node:path";
17
17
  import { existsSync, readFileSync, readdirSync } from "node:fs";
18
18
  import { PATHS, PAYLOAD } from "./paths.js";
@@ -34,7 +34,7 @@ function payloadWorkflowNames(payloadRoot) {
34
34
 
35
35
  // 설치된 워크플로우 디렉토리(평면 구조)에서 관리 마커로 시작하는 파일명 집합.
36
36
  // .bak/.template.yaml 백업본도 원본 텍스트를 그대로 복사한 것이라 마커를 그대로 갖고 있어
37
- // 확장자나 파일명 유추 없이 동일한 방식으로 인식된다 (issue #20 L12).
37
+ // 확장자나 파일명 유추 없이 동일한 방식으로 인식된다.
38
38
  function markedWorkflowNames(wfDir) {
39
39
  const names = new Set();
40
40
  if (!existsSync(wfDir)) return names;
@@ -60,7 +60,7 @@ export function planRemoval(payloadRoot, targetRoot = ".") {
60
60
  if (existsSync(join(wfDir, templateName))) removedWf.add(templateName);
61
61
  if (existsSync(p + ".bak")) removedWf.add(name + ".bak");
62
62
  }
63
- // (b) 관리 마커로 시작하는 것 — payload에서 이름이 바뀌거나 삭제된 파일도 인식 (issue #20 L12).
63
+ // (b) 관리 마커로 시작하는 것 — payload에서 이름이 바뀌거나 삭제된 파일도 인식.
64
64
  for (const name of markedWorkflowNames(wfDir)) removedWf.add(name);
65
65
  }
66
66
  for (const s of ["version_manager.py", "changelog_manager.py", "truncate_release_notes.py", "issue_helper.py"]) {
@@ -1,4 +1,4 @@
1
- // 설치 후 검증 (이슈 #81, #80) — 설치된 워크플로우를 다시 읽어 "이대로 돌아가는가"를 본다.
1
+ // 설치 후 검증 — 설치된 워크플로우를 다시 읽어 "이대로 돌아가는가"를 본다.
2
2
  //
3
3
  // 왜 설치 전이 아니라 후인가: 치환은 파일 단위로 흩어져 일어나고 auto 토큰은 resolver 결과에
4
4
  // 의존한다. 최종 디스크 내용을 보는 것이 실제로 배포될 것과 같은 것을 보는 유일한 방법이다.
@@ -15,7 +15,7 @@ const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g;
15
15
  // 쓰지도 않는 Secret을 "등록하세요"라고 안내하게 된다.
16
16
  const isCommented = (line) => /^\s*#/.test(line);
17
17
 
18
- // 미치환 플레이스홀더 스캔 (이슈 #81).
18
+ // 미치환 플레이스홀더 스캔.
19
19
  // 종전에는 auto 토큰 계산이 실패해도(예: application.yaml을 못 찾아 경로가 빈 문자열) 그 줄을
20
20
  // 건드리지 않고 넘어가, __APPLICATION_YML_DIR__ 이 그대로 남은 워크플로우가 "설치 성공"으로
21
21
  // 끝났다. 배포 시점에야 그 이름의 디렉토리가 만들어지며 문제가 드러난다.
@@ -43,12 +43,12 @@ export function scanUnsubstituted(workflowsDir, filenames = []) {
43
43
  const AUTO_SECRETS = new Set(["GITHUB_TOKEN"]);
44
44
  // 없어도 워크플로우가 도는 secret — 폴백이 문서화돼 있다. 필수와 섞어 "등록해야 동작합니다"라고
45
45
  // 하면 안내 자체를 못 믿게 되므로 분리한다.
46
- // AI_API_KEY → 없으면 GitHub Models(무료) → 규칙 fallback
46
+ // AI_API_KEY → 없으면 Copilot(opt-in, copilot_ai) → 규칙 fallback
47
47
  // WORKFLOW_PAT → 없으면 GITHUB_TOKEN
48
48
  export const OPTIONAL_SECRETS = new Set(["AI_API_KEY", "WORKFLOW_PAT"]);
49
49
  const SECRET_RE = /secrets\.([A-Z][A-Z0-9_]*)/g;
50
50
 
51
- // 설치된 워크플로우가 요구하는 GitHub Secret 목록 (이슈 #80).
51
+ // 설치된 워크플로우가 요구하는 GitHub Secret 목록.
52
52
  // 완료 화면이 WORKFLOW_PAT와 권한만 안내하는 바람에, 배포 워크플로우가 실제로 필요로 하는
53
53
  // SERVER_HOST·SSH_KEY 같은 값이 하나도 안내되지 않았다. 설치 직후 상태로는 배포가 돌지 않는데
54
54
  // 그 사실이 어디에도 드러나지 않는다.