projectops 4.2.19 → 4.2.21

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 CHANGED
@@ -7,7 +7,7 @@
7
7
  > 이슈 등록부터 커밋, 보고서, 배포까지. 개발자는 코드만 작성하세요.
8
8
 
9
9
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
10
- ## 최신 버전 : v4.2.18 (2026-07-14)
10
+ ## 최신 버전 : v4.2.20 (2026-07-17)
11
11
 
12
12
  [전체 버전 기록 보기](CHANGELOG.md)
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectops",
3
- "version": "4.2.19",
3
+ "version": "4.2.21",
4
4
  "description": "ProjectOps — 완전 자동화 GitHub 프로젝트 관리 템플릿 통합 CLI",
5
5
  "keywords": [
6
6
  "devops",
@@ -13,7 +13,7 @@ import { runBreakingCheck } from "../core/breaking-check.js";
13
13
  import { runMigrations } from "../core/migrations/index.js";
14
14
  import { detectOrphanWorkflows, applyOrphanCleanup } from "../core/orphan-workflows.js";
15
15
  import { resolveProjectPaths, filterExcludedTypes } from "../core/paths-resolve.js";
16
- import { askAllOptionalWorkflows, OPTION_AXES } from "../core/options-ask.js";
16
+ import { askAllOptionalWorkflows, OPTION_AXES, applicableTargets } from "../core/options-ask.js";
17
17
  import { createRunTrace } from "../core/run-trace.js";
18
18
  import { appendGuideEntry } from "../core/migration-guide.js";
19
19
  import { promptEnvPlan } from "../ui/env-plan.js";
@@ -154,7 +154,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
154
154
  // edit 루프
155
155
  let editing = true;
156
156
  while (editing) {
157
- const what = await io.editMenu({ showOptional });
157
+ // #498 타입에 적용 불가한 배포 축 항목은 메뉴에서 숨김 (타입을 바꾸면 다음 진입부터 다시 노출)
158
+ const what = await io.editMenu({ showOptional, axes: applicableTargets(types) });
158
159
  if (isCancel(what) || what === "done") { editing = false; break; }
159
160
  if (what === "type") {
160
161
  const t = await io.selectTypes(types);
@@ -5,6 +5,8 @@ import { readdirSync } from "node:fs";
5
5
  import { exists, copyDirSync } from "../fsutil.js";
6
6
 
7
7
  // 반환: {copied:bool, moduleCount:number}. force가 아니면(비TTY) 스킵.
8
+ // ⚠️ overlay 복사다 — 템플릿에서 삭제/리네임된 구 파일은 여기서 지우지 않는다.
9
+ // 구 util 파일 정리는 migrations registry(util-file 카테고리)가 담당한다 (#500).
8
10
  export function copyUtilModules(tempDir, type, { force = false } = {}, targetRoot = ".") {
9
11
  const src = join(tempDir, ".github", "util", type);
10
12
  if (!exists(src)) return { copied: false, moduleCount: 0 };
@@ -10,6 +10,7 @@ const RULES = {
10
10
  workflow: workflowRule,
11
11
  "root-file": rootFileRule,
12
12
  "legacy-dir": legacyDirRule,
13
+ "util-file": rootFileRule, // #500 — util 모듈 내 폐기 파일: 정확 경로 삭제 (root-file rule 재사용)
13
14
  };
14
15
 
15
16
  // 대상 레포에서 레거시 잔재 감지. 반환: { safe: [entry], confirm: [entry], ask: [entry] }
@@ -5,7 +5,9 @@
5
5
  //
6
6
  // 항목 스키마:
7
7
  // id - 고유 식별자 (kebab-case)
8
- // category - "workflow" | "root-file" | "legacy-dir" (rules/ 폴더의 구현이 detect/apply 담당)
8
+ // category - "workflow" | "root-file" | "legacy-dir" | "util-file" (rules/ 폴더의 구현이 detect/apply 담당)
9
+ // util-file: .github/util/ 모듈 안의 폐기 파일 — root-file rule 재사용(정확 경로 삭제).
10
+ // util 복사(copy/util.js)는 overlay라 구 파일을 지우지 않는다 — 정리는 여기가 유일 경로.
9
11
  // tier - "safe" : 신형이 같은 기능을 대체(순수 리네임). 공존 시 이중 트리거 실해
10
12
  // → 확인 1회 후 자동 무해화(.bak) / 삭제
11
13
  // "confirm" : 배포 파이프라인일 수 있음(그 레포의 유일한 현역 배포 가능성)
@@ -161,4 +163,44 @@ export const MIGRATIONS = [
161
163
  { id: "dir-docs-suh-template", category: "legacy-dir", tier: "ask",
162
164
  file: "docs/suh-template", replacedBy: "docs/projectops",
163
165
  since: "4.2.9", reason: "리브랜딩 — 스킬 산출물(이슈·보고서) 폴더 구명칭. 신 스킬은 docs/projectops/에 저장" },
166
+
167
+ // ── util-file / safe — Flutter 마법사 스크립트 Python 단일화 (#500) ──────────
168
+ // 로컬 실행 도구라 CI 중복 실행 위험은 없지만, 신형 py와 공존 시 사용자가 구 스크립트를
169
+ // 실행해 갈라진 동작을 볼 수 있다. 마법사 폴더에 사용자 상태가 없어 삭제가 기대 동작.
170
+ { id: "util-playstore-setup-sh", category: "util-file", tier: "safe",
171
+ file: ".github/util/flutter/playstore-wizard/playstore-wizard-setup.sh", replacedBy: "playstore-wizard.py",
172
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
173
+ { id: "util-playstore-setup-ps1", category: "util-file", tier: "safe",
174
+ file: ".github/util/flutter/playstore-wizard/playstore-wizard-setup.ps1", replacedBy: "playstore-wizard.py",
175
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
176
+ { id: "util-playstore-apply-sh", category: "util-file", tier: "safe",
177
+ file: ".github/util/flutter/playstore-wizard/playstore-wizard-apply.sh", replacedBy: "playstore-wizard.py",
178
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — apply 서브커맨드로 대체" },
179
+ { id: "util-playstore-apply-ps1", category: "util-file", tier: "safe",
180
+ file: ".github/util/flutter/playstore-wizard/playstore-wizard-apply.ps1", replacedBy: "playstore-wizard.py",
181
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — apply 서브커맨드로 대체" },
182
+ { id: "util-playstore-detect-sh", category: "util-file", tier: "safe",
183
+ file: ".github/util/flutter/playstore-wizard/detect-application-id.sh", replacedBy: "playstore-wizard.py",
184
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — detect-app-id 서브커맨드로 대체" },
185
+ { id: "util-playstore-detect-ps1", category: "util-file", tier: "safe",
186
+ file: ".github/util/flutter/playstore-wizard/detect-application-id.ps1", replacedBy: "playstore-wizard.py",
187
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — detect-app-id 서브커맨드로 대체" },
188
+ { id: "util-playstore-patch-py", category: "util-file", tier: "safe",
189
+ file: ".github/util/flutter/playstore-wizard/patch-build-gradle.py", replacedBy: "playstore-wizard.py",
190
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — 내부 함수로 흡수" },
191
+ { id: "util-firebase-setup-sh", category: "util-file", tier: "safe",
192
+ file: ".github/util/flutter/firebase-wizard/firebase-wizard-setup.sh", replacedBy: "firebase-wizard.py",
193
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
194
+ { id: "util-firebase-setup-ps1", category: "util-file", tier: "safe",
195
+ file: ".github/util/flutter/firebase-wizard/firebase-wizard-setup.ps1", replacedBy: "firebase-wizard.py",
196
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
197
+ { id: "util-firebase-test-sh", category: "util-file", tier: "safe",
198
+ file: ".github/util/flutter/firebase-wizard/test/setup-script-test.sh", replacedBy: "test/setup-script-test.py",
199
+ since: "4.3.0", reason: "마법사 테스트 Python 단일화" },
200
+ { id: "util-firebase-test-ps1", category: "util-file", tier: "safe",
201
+ file: ".github/util/flutter/firebase-wizard/test/setup-script-test.ps1", replacedBy: "test/setup-script-test.py",
202
+ since: "4.3.0", reason: "마법사 테스트 Python 단일화" },
203
+ { id: "util-testflight-setup-sh", category: "util-file", tier: "safe",
204
+ file: ".github/util/flutter/testflight-wizard/testflight-wizard-setup.sh", replacedBy: "testflight-wizard.py",
205
+ since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
164
206
  ];
@@ -56,6 +56,42 @@ export { parseTemplateOptions };
56
56
 
57
57
  export const DEPLOY_TARGETS = ["docker-ssh", "vercel", "none"];
58
58
  export const PUBLISH_TARGETS = ["nexus", "npm", "github-packages"];
59
+
60
+ // #498 — 타입별 적용 가능 deploy/publish 타겟 선언.
61
+ // 빈 배열 = 그 축이 개념상 성립하지 않는 타입. 모바일 앱(flutter/react-native/expo)은 스토어 배포
62
+ // (Play Store/TestFlight/Firebase 등) 워크플로우가 타입 자체에 항상 포함되므로 서버 deploy 축과
63
+ // 무관하고, 레지스트리 publish 개념도 없다. 두 축이 모두 빈 타입만 선택되면 질문 자체를 스킵한다.
64
+ // 서버형 타입은 현행 선택지 전체를 유지한다 (동작 무변경 — 타입별 세분화는 후속 과제).
65
+ export const TYPE_DEPLOY_TARGETS = {
66
+ spring: ["docker-ssh", "vercel"],
67
+ react: ["docker-ssh", "vercel"],
68
+ node: ["docker-ssh", "vercel"],
69
+ python: ["docker-ssh", "vercel"],
70
+ flutter: [],
71
+ "react-native": [],
72
+ "react-native-expo": [],
73
+ basic: [],
74
+ };
75
+ export const TYPE_PUBLISH_TARGETS = {
76
+ spring: ["nexus", "npm", "github-packages"],
77
+ react: ["nexus", "npm", "github-packages"],
78
+ node: ["nexus", "npm", "github-packages"],
79
+ python: ["nexus", "npm", "github-packages"],
80
+ flutter: [],
81
+ "react-native": [],
82
+ "react-native-expo": [],
83
+ basic: [],
84
+ };
85
+
86
+ // 선택된 타입 집합의 합집합으로 적용 가능 타겟 산출 (#498). 'none'은 항상 허용이라 목록에서 제외.
87
+ // 미선언 타입은 보수적으로 전체 허용(질문 유지) — 새 타입 추가 시 위 선언도 함께 넣는 게 원칙.
88
+ export function applicableTargets(types = []) {
89
+ const deployAll = DEPLOY_TARGETS.filter((t) => t !== "none");
90
+ return {
91
+ deploy: deployAll.filter((t) => types.some((ty) => (TYPE_DEPLOY_TARGETS[ty] ?? deployAll).includes(t))),
92
+ publish: PUBLISH_TARGETS.filter((t) => types.some((ty) => (TYPE_PUBLISH_TARGETS[ty] ?? PUBLISH_TARGETS).includes(t))),
93
+ };
94
+ }
59
95
  // #455 — changelog 생성기 provider. github-ai가 기본(설정 제로). openai/gemini/claude/ollama는 openai 호환 한 갈래.
60
96
  export const CHANGELOG_PROVIDERS = ["github-ai", "coderabbit", "openai", "gemini", "claude", "ollama", "commit"];
61
97
 
@@ -124,10 +160,12 @@ export async function askAllOptionalWorkflows({
124
160
  let deployBranchCreated = null; // #493 — 이번 실행에서 마법사가 직접 생성했는지 (가이드 기록용)
125
161
  let intent = current.intent ?? null; // #485 프로젝트 성격 (app/library/both/none/manual)
126
162
 
127
- // basic 단독 타입은 서버 배포도 라이브러리 publish도 개념상 성립하지 않는다.
128
- // 배포/publish 질문을 건너뛰고 none·[]로 조용히 확정한다 (타입 변경 시 재질문됨).
129
- // (basic은 "그 외" 폴백이라 항상 단독으로만 존재every로 안전 판정)
130
- const isBasicOnly = types.length > 0 && types.every((t) => t === "basic");
163
+ // 두 축이 모두 빈 타입 조합(#498 — basic 단독, flutter 모바일 단독)은 서버 배포도
164
+ // 라이브러리 publish 개념상 성립하지 않는다. 질문을 전부 건너뛰고 none·[]로 조용히 확정한다
165
+ // (모바일 스토어 배포는 타입 워크플로우가 항상 포함되므로 안내조차 불필요 타입 변경 시 재질문됨).
166
+ // isBasicOnly 특례를 일반화한 판정이다.
167
+ const applicable = applicableTargets(types);
168
+ const noAxes = types.length > 0 && applicable.deploy.length === 0 && applicable.publish.length === 0;
131
169
 
132
170
  // ① --force-ask가 아니면 version.yml 저장값을 먼저 읽어 재질문을 건너뛴다.
133
171
  // CLI 명시값(current)이 이미 있으면 그쪽이 우선 — 저장값은 빈 자리만 채운다.
@@ -158,9 +196,14 @@ export async function askAllOptionalWorkflows({
158
196
  }
159
197
  }
160
198
 
199
+ // 적용 불가 타겟 조용한 정리 (#498) — 구버전에서 저장된 무의미 값(예: flutter 단독 + docker-ssh)은
200
+ // 어차피 복사 결과가 동일하므로 경고 없이 none/교집합으로 정리한다 (SSOT — 무의미 값 잔존 금지).
201
+ if (deploy !== null && deploy !== "none" && !applicable.deploy.includes(deploy)) deploy = "none";
202
+ if (Array.isArray(publish)) publish = publish.filter((t) => applicable.publish.includes(t));
203
+
161
204
  // ── ② intent(프로젝트 성격) 우선 분기 → 배포 축 유도 (#485) ──
162
- // basic 단독이면 intent 개념 자체가 없어 질문 스킵, none/[]로 확정.
163
- if (isBasicOnly) {
205
+ // 모두 빈 타입 조합이면 intent 개념 자체가 없어 질문 스킵, none/[]로 확정 (#498).
206
+ if (noAxes) {
164
207
  if (deploy === null) deploy = "none";
165
208
  if (publish === null) publish = [];
166
209
  intent = "none";
@@ -216,22 +259,28 @@ export async function askAllOptionalWorkflows({
216
259
  say(" 1) 실행물 배포 — 서버/호스팅에 올려 돌리는 것 (Docker, Vercel …)");
217
260
  say(" 2) 라이브러리 배포(publish) — 남이 가져다 쓰게 레지스트리에 내는 것 (Nexus, npm …)");
218
261
  }
262
+ // 비대화형/폴백 기본값 (#498) — docker-ssh가 적용 가능할 때만 기본, 아니면 none.
263
+ const deployDefault = applicable.deploy.includes("docker-ssh") ? "docker-ssh" : "none";
219
264
  if (willAskDeploy) {
220
265
  if (force || !tty || typeof io.select !== "function") {
221
- deploy = deploy ?? "docker-ssh";
266
+ deploy = deploy ?? deployDefault;
222
267
  } else {
223
268
  say("");
224
269
  say("🚀 (1) 실행물(서버/앱)을 어디에 올리나요?");
225
270
  say(" 서버·호스팅에 올릴 계획이 있으면 고르고, 없으면 '서버에 올리지 않음'으로 두세요.");
271
+ // 선택지는 적용 가능 타겟으로 필터링 (#498) — 'none'은 항상 노출.
272
+ const deployLabels = [
273
+ { value: "docker-ssh", label: "Docker + SSH 서버 배포 (기본)" },
274
+ { value: "vercel", label: "Vercel" },
275
+ ];
226
276
  const ans = await io.select({
227
277
  message: "실행물 배포 방식을 선택하세요",
228
278
  options: [
229
- { value: "docker-ssh", label: "Docker + SSH 서버 배포 (기본)" },
230
- { value: "vercel", label: "Vercel" },
279
+ ...deployLabels.filter((o) => applicable.deploy.includes(o.value)),
231
280
  { value: "none", label: "서버에 올리지 않음 (빌드 검증만 · 라이브러리는 다음에서 선택)" },
232
281
  ],
233
282
  });
234
- deploy = (!isCancel(ans) && DEPLOY_TARGETS.includes(ans)) ? ans : (deploy ?? "docker-ssh");
283
+ deploy = (!isCancel(ans) && (ans === "none" || applicable.deploy.includes(ans))) ? ans : (deploy ?? deployDefault);
235
284
  say(`실행물 배포: ${deploy}`);
236
285
  }
237
286
  }
@@ -244,18 +293,20 @@ export async function askAllOptionalWorkflows({
244
293
  say("");
245
294
  say("📦 (2) 이 프로젝트를 남이 가져다 쓰는 라이브러리로도 배포(publish)하나요?");
246
295
  say(" (1) 실행물 배포와 별개입니다. 해당되는 걸 고르고, 라이브러리 배포를 안 하면 아무것도 고르지 말고 Enter.");
296
+ // 선택지는 적용 가능 타겟으로 필터링 (#498)
297
+ const publishLabels = [
298
+ { value: "nexus", label: "사내 Maven(Nexus) 라이브러리 배포" },
299
+ { value: "npm", label: "공개 npmjs 패키지 배포 (NPM_TOKEN)" },
300
+ { value: "github-packages", label: "GitHub Packages 라이브러리 배포" },
301
+ ];
247
302
  const ans = await io.multiselect({
248
303
  message: "라이브러리 배포 타겟 (없으면 선택 없이 Enter = 배포 안 함)",
249
- options: [
250
- { value: "nexus", label: "사내 Maven(Nexus) 라이브러리 배포" },
251
- { value: "npm", label: "공개 npmjs 패키지 배포 (NPM_TOKEN)" },
252
- { value: "github-packages", label: "GitHub Packages 라이브러리 배포" },
253
- ],
304
+ options: publishLabels.filter((o) => applicable.publish.includes(o.value)),
254
305
  initialValues: publish ?? [],
255
306
  required: false,
256
307
  });
257
308
  publish = (!isCancel(ans) && Array.isArray(ans))
258
- ? ans.filter((t) => PUBLISH_TARGETS.includes(t))
309
+ ? ans.filter((t) => applicable.publish.includes(t))
259
310
  : (publish ?? []);
260
311
  say(publish.length ? `라이브러리 배포: ${publish.join(", ")}` : "라이브러리 배포: 안 함");
261
312
  }
@@ -351,7 +402,8 @@ export async function askAllOptionalWorkflows({
351
402
  current: secretBackup, force, tty, io, forceAsk: ask("secret"), say,
352
403
  });
353
404
 
354
- const finalDeploy = deploy ?? "docker-ssh";
405
+ // 최종 폴백도 적용 가능 타겟 기준 (#498) — 모바일/basic 조합에서 docker-ssh가 새어 나가지 않게.
406
+ const finalDeploy = deploy ?? (applicable.deploy.includes("docker-ssh") ? "docker-ssh" : "none");
355
407
  const finalPublish = publish ?? [];
356
408
  return {
357
409
  deploy: finalDeploy, publish: finalPublish, secretBackup: secretBackup === true,
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ import { detectOrphanWorkflows } from "./core/orphan-workflows.js";
17
17
  import { createRunTrace } from "./core/run-trace.js";
18
18
  import { appendGuideEntry } from "./core/migration-guide.js";
19
19
  import { resolveProjectPaths } from "./core/paths-resolve.js";
20
+ import { applicableTargets } from "./core/options-ask.js";
20
21
  import { printBannerCompact } from "./ui/banner.js";
21
22
  import { printSummary } from "./ui/summary.js";
22
23
  import { runFull } from "./commands/full.js";
@@ -107,15 +108,23 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
107
108
  const tempDir = join(cwd, PATHS.tempDir);
108
109
 
109
110
  // 프로젝트 성격(#485): CLI --intent → version.yml 저장값. deploy/publish 유도의 기준.
110
- const intent = opts.intent ?? existing?.options?.intent ?? null;
111
- // 배포/publish 축(#439): CLI 플래그 최우선 → 저장값 → 기본값. --intent가 명시됐고 해당 축 플래그가
112
- // 없으면 intent가 유도한다 (#485 비대화형): library/none이면 deploy=none, app/none이면 publish=[].
113
- let deployTarget = opts.deployTarget ?? existing?.options?.deploy ?? "docker-ssh";
111
+ let intent = opts.intent ?? existing?.options?.intent ?? null;
112
+ // 배포/publish 축(#439): CLI 플래그 최우선 → 저장값 → 기본값(적용 가능할 때만 docker-ssh #498).
113
+ // 단 --intent가 명시됐고 해당 축 플래그가 없으면 intent가 유도한다 (#485 비대화형):
114
+ // library/none이면 deploy=none, app/none이면 publish=[].
115
+ const applicable = applicableTargets(types);
116
+ let deployTarget = opts.deployTarget ?? existing?.options?.deploy
117
+ ?? (applicable.deploy.includes("docker-ssh") ? "docker-ssh" : "none");
114
118
  let publishTargets = opts.publishTargets ?? existing?.options?.publish ?? [];
115
119
  if (opts.intent != null) {
116
120
  if ((intent === "library" || intent === "none") && opts.deployTarget == null) deployTarget = "none";
117
121
  if ((intent === "app" || intent === "none") && opts.publishTargets == null) publishTargets = [];
118
122
  }
123
+ // 적용 불가 타겟 조용한 정리 (#498) — 대화형과 동일 규칙. 타입에 성립하지 않는 축 값은
124
+ // 복사 결과가 동일하므로 경고 없이 none/교집합으로 정리한다 (모바일 앱/basic 단독 등).
125
+ if (deployTarget !== "none" && !applicable.deploy.includes(deployTarget)) deployTarget = "none";
126
+ publishTargets = publishTargets.filter((t) => applicable.publish.includes(t));
127
+ if (types.length > 0 && applicable.deploy.length === 0 && applicable.publish.length === 0) intent = "none";
119
128
 
120
129
  const context = createContext({
121
130
  mode: opts.mode, force: true, types, version, versionCode, branch,
package/src/ui/prompts.js CHANGED
@@ -32,18 +32,22 @@ export async function confirmProjectMenu() {
32
32
  }
33
33
 
34
34
  // 수정 메뉴 — 어떤 항목을 고칠지. showOptional=full/workflows에서만 nexus/secret 노출.
35
- export async function editMenu({ showOptional = false } = {}) {
35
+ // axes(#498): applicableTargets(types) 결과 적용 불가 축의 항목은 숨긴다 (모바일 앱/basic 단독 등).
36
+ // null이면 기존처럼 전부 노출 (테스트 스텁 하위호환).
37
+ export async function editMenu({ showOptional = false, axes = null } = {}) {
36
38
  const options = [
37
39
  { value: "type", label: "프로젝트 타입" },
38
40
  { value: "version", label: "버전" },
39
41
  { value: "branch", label: "기본 브랜치" },
40
42
  ];
41
43
  if (showOptional) {
42
- // #485 프로젝트 성격(intent): 재선택 배포/publish 축을 재유도한다.
43
- options.push({ value: "intent", label: "프로젝트 성격 (배포 유형)" });
44
+ const hasDeploy = axes == null || axes.deploy.length > 0;
45
+ const hasPublish = axes == null || axes.publish.length > 0;
46
+ // #485 — 프로젝트 성격(intent): 재선택 시 배포/publish 축을 재유도한다. 축이 하나도 없으면 무의미 → 숨김.
47
+ if (hasDeploy || hasPublish) options.push({ value: "intent", label: "프로젝트 성격 (배포 유형)" });
44
48
  // #483 — 항목별 격리: 한 축만 골라 그 축만 재질문한다 (통짜 "배포/Publish 방식" 분해)
45
- options.push({ value: "deploy", label: "배포 방식 (서버 실행물)" });
46
- options.push({ value: "publish", label: "라이브러리 배포(publish) 타겟" });
49
+ if (hasDeploy) options.push({ value: "deploy", label: "배포 방식 (서버 실행물)" });
50
+ if (hasPublish) options.push({ value: "publish", label: "라이브러리 배포(publish) 타겟" });
47
51
  options.push({ value: "code-review", label: "CodeRabbit 코드 리뷰" });
48
52
  options.push({ value: "changelog", label: "릴리스 노트(changelog) 생성기" });
49
53
  options.push({ value: "release-branch", label: "릴리스 소스(개발) 브랜치" });
package/src/ui/summary.js CHANGED
@@ -120,8 +120,9 @@ export function printSummary(ctx, targetRoot = ".") {
120
120
  }
121
121
  if (types.includes("flutter") && utilModulesCopied > 0) {
122
122
  err(" 💡 Flutter 배포 마법사 사용법:");
123
- err(" • iOS TestFlight: .github/util/flutter/ios-testflight-setup-wizard/index.html");
124
- err(" • Android Play Store: .github/util/flutter/android-playstore-setup-wizard/index.html");
123
+ err(" • iOS TestFlight: .github/util/flutter/testflight-wizard/testflight-wizard.html");
124
+ err(" • Android Play Store: .github/util/flutter/playstore-wizard/playstore-wizard.html");
125
+ err(" • Firebase App Distribution: .github/util/flutter/firebase-wizard/firebase-wizard.html");
125
126
  err(" • 브라우저에서 열어 필요한 정보 입력 후 파일 생성");
126
127
  err("");
127
128
  }