projectops 4.2.18 → 4.2.20
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 +1 -1
- package/package.json +1 -1
- package/src/commands/interactive.js +3 -2
- package/src/core/options-ask.js +69 -17
- package/src/index.js +13 -4
- package/src/ui/prompts.js +9 -5
- package/src/ui/summary.js +4 -2
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/src/core/options-ask.js
CHANGED
|
@@ -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
|
|
128
|
-
//
|
|
129
|
-
// (
|
|
130
|
-
|
|
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
|
-
//
|
|
163
|
-
if (
|
|
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 ??
|
|
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
|
-
|
|
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) &&
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
// 배포/publish 축(#439): CLI 플래그 최우선 → 저장값 →
|
|
112
|
-
// 없으면 intent가 유도한다 (#485 비대화형):
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
@@ -128,10 +128,12 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
128
128
|
|
|
129
129
|
err(" 📖 TEMPLATE REPO: https://github.com/Cassiiopeia/projectops");
|
|
130
130
|
err(" 📚 워크플로우 가이드: .github/workflows/project-types/README.md");
|
|
131
|
-
// #493 — 이번 실행의 마이그레이션
|
|
131
|
+
// #493 — 이번 실행의 마이그레이션 기록. "뭐가 남았고 AI에게 어떻게 시키는지"가 바로 보이게 행동 유도형으로 안내.
|
|
132
132
|
if (ctx?.migrationGuidePath) {
|
|
133
133
|
err(` 🧭 마이그레이션 가이드: ${ctx.migrationGuidePath}`);
|
|
134
|
-
err("
|
|
134
|
+
err(" 이번 설치에서 바뀐 내용과 직접 확인해야 할 작업이 이 파일에 정리되어 있습니다.");
|
|
135
|
+
err(" 💡 AI Agent(Claude, Cursor 등)에게 \"마이그레이션 가이드 확인하고");
|
|
136
|
+
err(" 남은 작업 처리해줘\"라고 요청하면 이 문서를 읽고 대신 진행합니다.");
|
|
135
137
|
}
|
|
136
138
|
err("");
|
|
137
139
|
|