projectops 4.0.4 → 4.1.2
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 +2 -3
- package/package.json +1 -1
- package/src/cli/args.js +3 -0
- package/src/cli/help.js +3 -1
- package/src/commands/full.js +2 -2
- package/src/commands/interactive.js +11 -5
- package/src/commands/version.js +2 -2
- package/src/context.js +3 -1
- package/src/core/copy/workflows.js +19 -2
- package/src/core/detect.js +2 -2
- package/src/core/options-ask.js +16 -3
- package/src/core/paths-resolve.js +2 -2
- package/src/core/version-yml.js +16 -9
- package/src/index.js +1 -0
- package/src/ui/prompts.js +2 -1
- package/src/ui/status-cards.js +2 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
> 이슈 등록부터 커밋, 보고서, 배포까지. 개발자는 코드만 작성하세요.
|
|
8
8
|
|
|
9
9
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
10
|
-
## 최신 버전 : v4.
|
|
10
|
+
## 최신 버전 : v4.1.1 (2026-07-09)
|
|
11
11
|
|
|
12
12
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
13
13
|
|
|
@@ -218,8 +218,7 @@ bash <(curl -fsSL "https://raw.githubusercontent.com/Cassiiopeia/projectops/main
|
|
|
218
218
|
|------|----------|-------|
|
|
219
219
|
| `spring` | build.gradle | SSH+Docker 배포, Nexus |
|
|
220
220
|
| `flutter` | pubspec.yaml | TestFlight, Play Store |
|
|
221
|
-
| `react` | package.json | Docker |
|
|
222
|
-
| `next` | package.json | Docker |
|
|
221
|
+
| `react` (Next.js 포함) | package.json | Docker |
|
|
223
222
|
| `node` | package.json | Docker |
|
|
224
223
|
| `python` | pyproject.toml | SSH+Docker 배포 |
|
|
225
224
|
| `react-native` | Info.plist + build.gradle | — |
|
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -10,6 +10,7 @@ export function parseArgs(argv) {
|
|
|
10
10
|
primaryType: "",
|
|
11
11
|
includeNexus: null, // null=미설정
|
|
12
12
|
includeSecretBackup: null,
|
|
13
|
+
includeNpmPublish: null,
|
|
13
14
|
pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
|
|
14
15
|
force: false,
|
|
15
16
|
help: false,
|
|
@@ -50,6 +51,8 @@ export function parseArgs(argv) {
|
|
|
50
51
|
case "--no-nexus": result.includeNexus = false; break;
|
|
51
52
|
case "--secret-backup": result.includeSecretBackup = true; break;
|
|
52
53
|
case "--no-secret-backup": result.includeSecretBackup = false; break;
|
|
54
|
+
case "--npm-publish": result.includeNpmPublish = true; break;
|
|
55
|
+
case "--no-npm-publish": result.includeNpmPublish = false; break;
|
|
53
56
|
case "--paths": result.pathsCsv = args.shift() ?? ""; break;
|
|
54
57
|
case "-h": case "--help": result.help = true; break;
|
|
55
58
|
default:
|
package/src/cli/help.js
CHANGED
|
@@ -8,12 +8,14 @@ export const HELP_TEXT = `projectops — GitHub 프로젝트 자동화 템플릿
|
|
|
8
8
|
-m, --mode MODE 통합 모드 (full | version | workflows | issues | skills)
|
|
9
9
|
기본: interactive (대화형)
|
|
10
10
|
-t, --type CSV 프로젝트 타입 csv (예: spring,react,python)
|
|
11
|
-
지원: spring flutter
|
|
11
|
+
지원: spring flutter react react-native
|
|
12
12
|
react-native-expo node python basic
|
|
13
|
+
(next는 react로 흡수됨 — Next.js 프로젝트는 react 사용)
|
|
13
14
|
--project-version V 통합 대상의 초기 버전 (예: 1.0.0). 미지정 시 자동 감지
|
|
14
15
|
--paths "t=p,..." 타입별 프로젝트 경로 (모노레포). 예: flutter=app,react=client
|
|
15
16
|
--nexus / --no-nexus Nexus 라이브러리 publish 워크플로우 포함/제외
|
|
16
17
|
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
18
|
+
--npm-publish / --no-npm-publish npm 패키지 publish 워크플로우 포함/제외
|
|
17
19
|
--force 모든 확인 생략, 비대화형 기본값 사용
|
|
18
20
|
-v, --version projectops 버전 출력
|
|
19
21
|
-h, --help 이 도움말 표시
|
package/src/commands/full.js
CHANGED
|
@@ -22,7 +22,7 @@ import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
|
22
22
|
export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
23
23
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
24
24
|
force = true, now, today, templateVersion = "unknown",
|
|
25
|
-
includeNexus = false, includeSecretBackup = false } = context;
|
|
25
|
+
includeNexus = false, includeSecretBackup = false, includeNpmPublish = false } = context;
|
|
26
26
|
|
|
27
27
|
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
|
|
28
28
|
const pathMarkers = new Map();
|
|
@@ -38,7 +38,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
38
38
|
buildVersionYml({
|
|
39
39
|
version, types, paths, pathMarkers, branch, versionCode, now, today,
|
|
40
40
|
deployValues,
|
|
41
|
-
templateOptions: { templateVersion, includeNexus, includeSecretBackup, optionsDate: today },
|
|
41
|
+
templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeNpmPublish, optionsDate: today },
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
44
|
// 2. README 버전 섹션
|
|
@@ -82,6 +82,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
82
82
|
// 선택 워크플로우 초기값: version.yml 저장 옵션 (.sh read_template_options L2361 등가)
|
|
83
83
|
let includeNexus = existing?.options?.nexus ?? false;
|
|
84
84
|
let includeSecretBackup = existing?.options?.secretBackup ?? false;
|
|
85
|
+
let includeNpmPublish = existing?.options?.npmPublish ?? false;
|
|
85
86
|
const showOptional = mode === "full" || mode === "workflows";
|
|
86
87
|
const realTty = process.stdout.isTTY === true;
|
|
87
88
|
|
|
@@ -92,12 +93,13 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
92
93
|
if (showOptional) {
|
|
93
94
|
const r = await askAllOptionalWorkflows({
|
|
94
95
|
tempDir, types, targetRoot: cwd,
|
|
95
|
-
current: { nexus: existing?.options?.nexus ?? null, secretBackup: existing?.options?.secretBackup ?? null },
|
|
96
|
+
current: { nexus: existing?.options?.nexus ?? null, secretBackup: existing?.options?.secretBackup ?? null, npmPublish: existing?.options?.npmPublish ?? null },
|
|
96
97
|
force: false, tty: realTty,
|
|
97
98
|
io: { confirm: ({ message, initialValue }) => io.askYesNo(message, initialValue) },
|
|
98
99
|
});
|
|
99
100
|
includeNexus = r.nexus;
|
|
100
101
|
includeSecretBackup = r.secretBackup;
|
|
102
|
+
includeNpmPublish = r.npmPublish;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
// 확인/수정 루프 — ESC는 '머무르기' (.sh L1877~1881: 명시적 '아니오'만 종료)
|
|
@@ -106,9 +108,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
106
108
|
while (!confirmed) {
|
|
107
109
|
// 층3 — 프로젝트 분석 개요 카드 (#446). 스텁엔 없음 → note 폴백.
|
|
108
110
|
if (io.analysisCard) {
|
|
109
|
-
io.analysisCard({ mode, modeLabel: modeLabel(mode), types, version, branch, includeNexus, includeSecretBackup, showOptional, paths });
|
|
111
|
+
io.analysisCard({ mode, modeLabel: modeLabel(mode), types, version, branch, includeNexus, includeSecretBackup, includeNpmPublish, showOptional, paths });
|
|
110
112
|
} else {
|
|
111
|
-
io.note?.(summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional }), "프로젝트 분석 결과");
|
|
113
|
+
io.note?.(summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, includeNpmPublish, showOptional }), "프로젝트 분석 결과");
|
|
112
114
|
}
|
|
113
115
|
const choice = await io.confirmProjectMenu();
|
|
114
116
|
if (choice === "cancel") { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
|
|
@@ -143,6 +145,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
143
145
|
} else if (what === "secret") {
|
|
144
146
|
const y = await io.askYesNo("Secret 백업 워크플로우를 포함할까요?", includeSecretBackup);
|
|
145
147
|
if (!isCancel(y)) includeSecretBackup = y === true;
|
|
148
|
+
} else if (what === "npm-publish") {
|
|
149
|
+
const y = await io.askYesNo("npm publish 워크플로우를 포함할까요?", includeNpmPublish);
|
|
150
|
+
if (!isCancel(y)) includeNpmPublish = y === true;
|
|
146
151
|
}
|
|
147
152
|
}
|
|
148
153
|
}
|
|
@@ -171,7 +176,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
171
176
|
|
|
172
177
|
const { now, today } = clock || utcNow();
|
|
173
178
|
const ctx = createContext({
|
|
174
|
-
mode, force: true, types, version, versionCode, branch, paths, includeNexus, includeSecretBackup,
|
|
179
|
+
mode, force: true, types, version, versionCode, branch, paths, includeNexus, includeSecretBackup, includeNpmPublish,
|
|
175
180
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
176
181
|
});
|
|
177
182
|
ctx.templateVersion = templateVersion;
|
|
@@ -222,7 +227,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
229
|
|
|
225
|
-
function summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional }) {
|
|
230
|
+
function summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, includeNpmPublish, showOptional }) {
|
|
226
231
|
const lines = [
|
|
227
232
|
`통합 모드 : ${modeLabel(mode)}`,
|
|
228
233
|
`프로젝트 타입 : ${types.join(", ")}${types.length > 1 ? " (멀티)" : ""}`,
|
|
@@ -232,6 +237,7 @@ function summarize({ mode, types, version, branch, includeNexus, includeSecretBa
|
|
|
232
237
|
if (showOptional) {
|
|
233
238
|
lines.push(`Nexus publish : ${includeNexus ? "포함" : "제외"}`);
|
|
234
239
|
lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
|
|
240
|
+
lines.push(`npm publish : ${includeNpmPublish ? "포함" : "제외"}`);
|
|
235
241
|
}
|
|
236
242
|
return lines.join("\n");
|
|
237
243
|
}
|
package/src/commands/version.js
CHANGED
|
@@ -12,7 +12,7 @@ import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
|
12
12
|
|
|
13
13
|
export function runVersion(context, tempDir, targetRoot = ".") {
|
|
14
14
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
15
|
-
now, today, templateVersion = "unknown", includeNexus = false, includeSecretBackup = false } = context;
|
|
15
|
+
now, today, templateVersion = "unknown", includeNexus = false, includeSecretBackup = false, includeNpmPublish = false } = context;
|
|
16
16
|
|
|
17
17
|
const pathMarkers = new Map();
|
|
18
18
|
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
@@ -20,7 +20,7 @@ export function runVersion(context, tempDir, targetRoot = ".") {
|
|
|
20
20
|
writeText(join(targetRoot, PATHS.versionFile),
|
|
21
21
|
buildVersionYml({
|
|
22
22
|
version, types, paths, pathMarkers, branch, versionCode, now, today,
|
|
23
|
-
templateOptions: { templateVersion, includeNexus, includeSecretBackup, optionsDate: today },
|
|
23
|
+
templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeNpmPublish, optionsDate: today },
|
|
24
24
|
}));
|
|
25
25
|
addVersionSectionToReadme(version, targetRoot);
|
|
26
26
|
copyScripts(tempDir, targetRoot);
|
package/src/context.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 마법사 전역 상태를 하나의 객체로 명시화 (bash 전역 변수군 대체)
|
|
2
|
+
// next 타입은 v4.1.0에서 react로 흡수됨 (breaking)
|
|
2
3
|
export const VALID_TYPES = [
|
|
3
|
-
"spring", "flutter", "
|
|
4
|
+
"spring", "flutter", "react",
|
|
4
5
|
"react-native", "react-native-expo", "node", "python", "basic",
|
|
5
6
|
];
|
|
6
7
|
|
|
@@ -15,6 +16,7 @@ export function createContext(overrides = {}) {
|
|
|
15
16
|
branch: "",
|
|
16
17
|
paths: new Map(), // type -> path
|
|
17
18
|
includeNexus: null, // null=미설정, true/false=명시
|
|
19
|
+
includeNpmPublish: null, // null=미설정, true/false=명시 (node/npm-publish/)
|
|
18
20
|
includeSecretBackup: null,
|
|
19
21
|
templateVersion: "",
|
|
20
22
|
tempDir: "",
|
|
@@ -150,7 +150,7 @@ export async function copyWorkflowsInteractive(context, tempDir, targetRoot = ".
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters) {
|
|
153
|
-
const { includeNexus, force = false, paths = new Map(), repoName = "", resolvers = {}, envOptsFor, collectAsks = null, decisions = new Map() } = ctx;
|
|
153
|
+
const { includeNexus, includeNpmPublish = false, force = false, paths = new Map(), repoName = "", resolvers = {}, envOptsFor, collectAsks = null, decisions = new Map() } = ctx;
|
|
154
154
|
const typeDir = join(projectTypesDir, type);
|
|
155
155
|
const envOpts = envOptsFor(type);
|
|
156
156
|
let unchangedNames = [];
|
|
@@ -195,8 +195,25 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// npm-publish (opt-in — 현재 node/npm-publish/만 존재)
|
|
199
|
+
const npmPublishDir = join(typeDir, "npm-publish");
|
|
200
|
+
if (exists(npmPublishDir) && includeNpmPublish) {
|
|
201
|
+
for (const filename of listYamlFiles(npmPublishDir)) {
|
|
202
|
+
const src = join(npmPublishDir, filename);
|
|
203
|
+
const dst = join(workflowsDir, filename);
|
|
204
|
+
if (existsSync(dst) && isUnchanged(readFileSync(src, "utf8"), readFileSync(dst, "utf8"), envOpts)) {
|
|
205
|
+
counters.skipped++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (existsSync(dst)) renameSync(dst, dst + ".bak");
|
|
209
|
+
copyFileSync(src, dst);
|
|
210
|
+
counters.optionalCopied++;
|
|
211
|
+
counters.copied++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
198
215
|
// env 치환 — 이 타입의 원본 디렉토리들에서 복사돼 존재하고 unchanged 아닌 파일만
|
|
199
|
-
for (const srcDir of [typeDir, serverDeployDir, nexusDir]) {
|
|
216
|
+
for (const srcDir of [typeDir, serverDeployDir, nexusDir, npmPublishDir]) {
|
|
200
217
|
if (!exists(srcDir)) continue;
|
|
201
218
|
for (const filename of listYamlFiles(srcDir)) {
|
|
202
219
|
const target = join(workflowsDir, filename);
|
package/src/core/detect.js
CHANGED
|
@@ -6,8 +6,8 @@ export function classifyPackageText(raw) {
|
|
|
6
6
|
if (s.includes("@react-native") || s.includes("react-native")) {
|
|
7
7
|
return s.includes("expo") ? "react-native-expo" : "react-native";
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
if (s.includes('"react"')) return "react";
|
|
9
|
+
// next 타입은 v4.1.0에서 react로 흡수 — "next" 의존성이 있어도 react로 판정
|
|
10
|
+
if (s.includes('"react"') || s.includes('"next"')) return "react";
|
|
11
11
|
return "node";
|
|
12
12
|
}
|
|
13
13
|
|
package/src/core/options-ask.js
CHANGED
|
@@ -49,8 +49,8 @@ async function askOptionalWorkflow({ dir, icon, short, desc, current, force, tty
|
|
|
49
49
|
// 모든 opt-in 워크플로우를 순서대로 질문 (.sh ask_all_optional_workflows 등가).
|
|
50
50
|
// tempDir: 템플릿 다운로드 루트 — project-types는 {tempDir}/.github/workflows/project-types
|
|
51
51
|
// (copyWorkflows와 동일 규약. 테스트 픽스처용으로 {tempDir}/project-types 도 허용.)
|
|
52
|
-
// current: { nexus: bool|null, secretBackup: bool|null } — CLI(--nexus 등) 명시값
|
|
53
|
-
// 반환: { nexus: bool, secretBackup: bool } (미결정 null은 false로 확정)
|
|
52
|
+
// current: { nexus: bool|null, secretBackup: bool|null, npmPublish: bool|null } — CLI(--nexus 등) 명시값
|
|
53
|
+
// 반환: { nexus: bool, secretBackup: bool, npmPublish: bool } (미결정 null은 false로 확정)
|
|
54
54
|
export async function askAllOptionalWorkflows({
|
|
55
55
|
tempDir, types = [], current = {}, targetRoot = ".",
|
|
56
56
|
force = false, tty = true, io = {}, forceAsk = false,
|
|
@@ -58,6 +58,7 @@ export async function askAllOptionalWorkflows({
|
|
|
58
58
|
const say = io.log || ((m) => process.stderr.write(`${m}\n`));
|
|
59
59
|
let nexus = current.nexus ?? null;
|
|
60
60
|
let secretBackup = current.secretBackup ?? null;
|
|
61
|
+
let npmPublish = current.npmPublish ?? null;
|
|
61
62
|
|
|
62
63
|
// ① --force-ask가 아니면 version.yml 저장값을 먼저 읽어 재질문을 건너뛴다 (.sh L2715~2717).
|
|
63
64
|
// CLI 명시값(current)이 이미 있으면 그쪽이 우선 — 저장값은 빈 자리만 채운다.
|
|
@@ -73,6 +74,10 @@ export async function askAllOptionalWorkflows({
|
|
|
73
74
|
secretBackup = saved.secretBackup;
|
|
74
75
|
say(`Secret 백업 옵션: version.yml 저장값(${secretBackup}) 유지 — 재질문 생략`);
|
|
75
76
|
}
|
|
77
|
+
if (npmPublish === null && saved.npmPublish !== null) {
|
|
78
|
+
npmPublish = saved.npmPublish;
|
|
79
|
+
say(`npm publish 옵션: version.yml 저장값(${npmPublish}) 유지 — 재질문 생략`);
|
|
80
|
+
}
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
|
|
@@ -88,6 +93,14 @@ export async function askAllOptionalWorkflows({
|
|
|
88
93
|
current: nexus, force, tty, io, forceAsk, say,
|
|
89
94
|
});
|
|
90
95
|
}
|
|
96
|
+
// ②-2 npm publish: 각 타입의 npm-publish/ 폴더 (현재 node만 존재)
|
|
97
|
+
for (const t of types) {
|
|
98
|
+
npmPublish = await askOptionalWorkflow({
|
|
99
|
+
dir: join(ptDir, t, "npm-publish"), icon: "📦", short: "npm 패키지 publish",
|
|
100
|
+
desc: "패키지를 공개 npmjs 레지스트리에 자동 배포하는 워크플로우입니다. npm 라이브러리/CLI 프로젝트에만 필요합니다 (NPM_TOKEN secret 필요).",
|
|
101
|
+
current: npmPublish, force, tty, io, forceAsk, say,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
91
104
|
// ③ Secret 백업: 공통 폴더 (.sh L2726~2729)
|
|
92
105
|
secretBackup = await askOptionalWorkflow({
|
|
93
106
|
dir: join(ptDir, "common", "secret-backup"), icon: "🔐", short: "Secret 서버 백업",
|
|
@@ -96,5 +109,5 @@ export async function askAllOptionalWorkflows({
|
|
|
96
109
|
});
|
|
97
110
|
|
|
98
111
|
// ④ 미결정(null)은 false로 확정 — .sh에서 빈 INCLUDE_* 가 이후 false 취급되는 것과 동일
|
|
99
|
-
return { nexus: nexus === true, secretBackup: secretBackup === true };
|
|
112
|
+
return { nexus: nexus === true, secretBackup: secretBackup === true, npmPublish: npmPublish === true };
|
|
100
113
|
}
|
|
@@ -18,7 +18,7 @@ const isCancel = (v) => typeof v === "symbol";
|
|
|
18
18
|
// 타입의 대표 마커 파일명 (.sh marker_for_type L1220~1229 등가).
|
|
19
19
|
// detect.js는 미지 타입에 package.json을 기본 반환하지만 .sh는 빈 문자열 — 등가를 위해 래핑.
|
|
20
20
|
const KNOWN_MARKER_TYPES = new Set([
|
|
21
|
-
"flutter", "react", "
|
|
21
|
+
"flutter", "react", "node", "react-native", "react-native-expo", "python", "spring",
|
|
22
22
|
]);
|
|
23
23
|
export function markerForType(type) {
|
|
24
24
|
return KNOWN_MARKER_TYPES.has(type) ? baseMarkerForType(type) : "";
|
|
@@ -78,7 +78,7 @@ export function findTypePathCandidates(root, type) {
|
|
|
78
78
|
|
|
79
79
|
const namesByType = {
|
|
80
80
|
flutter: ["pubspec.yaml"],
|
|
81
|
-
react: ["package.json"],
|
|
81
|
+
react: ["package.json"], node: ["package.json"],
|
|
82
82
|
"react-native": ["package.json"],
|
|
83
83
|
"react-native-expo": ["app.json"],
|
|
84
84
|
python: ["pyproject.toml", "setup.py", "requirements.txt"],
|
package/src/core/version-yml.js
CHANGED
|
@@ -12,7 +12,7 @@ const HEADER = `# ==============================================================
|
|
|
12
12
|
# 사용법:
|
|
13
13
|
# 1. version: "1.0.0" - 사용자에게 표시되는 버전
|
|
14
14
|
# 2. version_code: 1 - Play Store/App Store 빌드 번호 (1부터 자동 증가)
|
|
15
|
-
# 3.
|
|
15
|
+
# 3. project_types: 프로젝트 타입 배열 — 첫 항목이 primary
|
|
16
16
|
# 4. project_paths: 타입별 프로젝트 폴더 (레포 루트 기준 상대경로, 모노레포용)
|
|
17
17
|
#
|
|
18
18
|
# 자동 버전 업데이트:
|
|
@@ -35,17 +35,17 @@ const HEADER = `# ==============================================================
|
|
|
35
35
|
# - .github/workflows/PROJECT-AUTO-CHANGELOG-CONTROL.yaml
|
|
36
36
|
#
|
|
37
37
|
# 주의사항:
|
|
38
|
-
# -
|
|
38
|
+
# - project_types는 최초 설정 후 변경하지 마세요
|
|
39
39
|
# - 버전은 항상 높은 버전으로 자동 동기화됩니다
|
|
40
40
|
# ===================================================================
|
|
41
41
|
`;
|
|
42
42
|
|
|
43
43
|
// metadata.template.options 상태머신 파싱 (.sh read_template_options L2361~2416 등가).
|
|
44
|
-
// 반환: { nexus: bool|null, secretBackup: bool|null } — null=미기재.
|
|
44
|
+
// 반환: { nexus: bool|null, secretBackup: bool|null, npmPublish: bool|null } — null=미기재.
|
|
45
45
|
// 구 synology 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다.
|
|
46
46
|
// (options-ask.js가 이 함수를 import한다 — 순환 방지 위해 여기(version-yml)에 정의.)
|
|
47
47
|
export function parseTemplateOptions(content) {
|
|
48
|
-
const out = { nexus: null, secretBackup: null };
|
|
48
|
+
const out = { nexus: null, secretBackup: null, npmPublish: null };
|
|
49
49
|
// 값 정규화: 따옴표 제거 + 트림 (.sh tr -d '"' | tr -d "'" | xargs 등가)
|
|
50
50
|
const strip = (s) => String(s).replace(/["']/g, "").trim();
|
|
51
51
|
let inTemplate = false;
|
|
@@ -68,6 +68,13 @@ export function parseTemplateOptions(content) {
|
|
|
68
68
|
if (v === "false") out.secretBackup = false;
|
|
69
69
|
continue;
|
|
70
70
|
}
|
|
71
|
+
m = line.match(/^\s+npm_publish:\s*(.+)/);
|
|
72
|
+
if (m) {
|
|
73
|
+
const v = strip(m[1]);
|
|
74
|
+
if (v === "true") out.npmPublish = true;
|
|
75
|
+
if (v === "false") out.npmPublish = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
71
78
|
// 들여쓰기 0~4칸의 다른 키 → options 섹션 종료 (.sh L2404~2408)
|
|
72
79
|
if (/^\s{0,4}[a-z_]+:/.test(line)) { inOptions = false; inTemplate = false; }
|
|
73
80
|
}
|
|
@@ -125,20 +132,19 @@ export function parseExisting(content) {
|
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
// version.yml 전체 생성 (.sh create_version_yml + save_template_options 신규 케이스 등가).
|
|
128
|
-
// opts: { version, types:[],
|
|
135
|
+
// opts: { version, types:[], paths:Map, pathMarkers?:Map, branch, versionCode, now, today, templateOptions? }
|
|
136
|
+
// primary 타입은 별도 키 없이 project_types[0]이다 (v4.1.0 SSOT — 단수 project_type 키 제거).
|
|
129
137
|
// now = "YYYY-MM-DD HH:MM:SS" (UTC) — 결정성 위해 주입
|
|
130
138
|
// today = "YYYY-MM-DD" (UTC)
|
|
131
139
|
// pathMarkers = Map<type, markerFilename> (project_paths 주석용)
|
|
132
140
|
// templateOptions = { templateVersion, includeNexus, includeSecretBackup, optionsDate } (template 블록)
|
|
133
|
-
export function buildVersionYml({ version, types = [],
|
|
141
|
+
export function buildVersionYml({ version, types = [], paths = new Map(), pathMarkers = new Map(), branch = "main", versionCode = 1, now, today, templateOptions = null, deployValues = new Map() }) {
|
|
134
142
|
const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(",")}]` : `["basic"]`;
|
|
135
|
-
const primary = primaryType || types[0] || "basic";
|
|
136
143
|
|
|
137
144
|
let out = HEADER + "\n";
|
|
138
145
|
out += `version: "${version}"\n`;
|
|
139
146
|
out += `version_code: ${versionCode} # app build number\n`;
|
|
140
147
|
out += `project_types: ${typesJson} # 멀티타입 배열 — 첫 항목이 primary, 직접 편집 가능\n`;
|
|
141
|
-
out += `project_type: "${primary}" # project_types[0] 자동 미러 — 직접 수정 금지 (spring, flutter, next, react, react-native, react-native-expo, node, python, basic)\n`;
|
|
142
148
|
|
|
143
149
|
// project_paths 블록. pathMarkers: Map<type, markerFilename> (있으면 " type: "path" # path/marker" 주석).
|
|
144
150
|
if (paths.size) {
|
|
@@ -171,7 +177,7 @@ export function buildVersionYml({ version, types = [], primaryType, paths = new
|
|
|
171
177
|
|
|
172
178
|
// template 옵션 블록 (.sh save_template_options 신규 추가 케이스). templateOptions 지정 시.
|
|
173
179
|
if (templateOptions) {
|
|
174
|
-
const { templateVersion = "unknown", includeNexus = false, includeSecretBackup = false, optionsDate = today } = templateOptions;
|
|
180
|
+
const { templateVersion = "unknown", includeNexus = false, includeSecretBackup = false, includeNpmPublish = false, optionsDate = today } = templateOptions;
|
|
175
181
|
out += ` template:\n`;
|
|
176
182
|
out += ` source: "projectops"\n`;
|
|
177
183
|
out += ` version: "${templateVersion}"\n`;
|
|
@@ -180,6 +186,7 @@ export function buildVersionYml({ version, types = [], primaryType, paths = new
|
|
|
180
186
|
out += ` options:\n`;
|
|
181
187
|
out += ` nexus: ${includeNexus}\n`;
|
|
182
188
|
out += ` secret_backup: ${includeSecretBackup}\n`;
|
|
189
|
+
out += ` npm_publish: ${includeNpmPublish}\n`;
|
|
183
190
|
}
|
|
184
191
|
return out;
|
|
185
192
|
}
|
package/src/index.js
CHANGED
|
@@ -108,6 +108,7 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
108
108
|
// 옵션 워크플로우: CLI 플래그 최우선 → version.yml 저장 옵션(.sh read_template_options 등가) → false
|
|
109
109
|
includeNexus: opts.includeNexus ?? existing?.options?.nexus ?? false,
|
|
110
110
|
includeSecretBackup: opts.includeSecretBackup ?? existing?.options?.secretBackup ?? false,
|
|
111
|
+
includeNpmPublish: opts.includeNpmPublish ?? existing?.options?.npmPublish ?? false,
|
|
111
112
|
repoName,
|
|
112
113
|
// 실 resolver 4종 (.sh resolve_token 등가 — spring-app-yml 스텁 제거)
|
|
113
114
|
resolvers: makeResolvers(cwd, repoName, paths),
|
package/src/ui/prompts.js
CHANGED
|
@@ -41,6 +41,7 @@ export async function editMenu({ showOptional = false } = {}) {
|
|
|
41
41
|
if (showOptional) {
|
|
42
42
|
options.push({ value: "nexus", label: "Nexus publish 포함 여부" });
|
|
43
43
|
options.push({ value: "secret", label: "Secret 백업 포함 여부" });
|
|
44
|
+
options.push({ value: "npm-publish", label: "npm publish 포함 여부" });
|
|
44
45
|
}
|
|
45
46
|
options.push({ value: "done", label: "모두 맞음, 계속" });
|
|
46
47
|
return engine.select({ message: "어떤 항목을 수정할까요?", options });
|
|
@@ -48,7 +49,7 @@ export async function editMenu({ showOptional = false } = {}) {
|
|
|
48
49
|
|
|
49
50
|
// 타입 멀티선택.
|
|
50
51
|
export async function selectTypes(current = []) {
|
|
51
|
-
const all = ["spring", "flutter", "
|
|
52
|
+
const all = ["spring", "flutter", "react", "react-native", "react-native-expo", "node", "python", "basic"];
|
|
52
53
|
return engine.multiselect({
|
|
53
54
|
message: "프로젝트 타입을 선택하세요 (Space 토글, Enter 확정)",
|
|
54
55
|
options: all.map((t) => ({ value: t, label: t })),
|
package/src/ui/status-cards.js
CHANGED
|
@@ -25,7 +25,7 @@ export function printDetectionLog({ types = [], version = "", branch = "" }, out
|
|
|
25
25
|
|
|
26
26
|
// 층3 — 프로젝트 분석 개요 카드 (.ps1 Print-ProjectAnalysis 등가+)
|
|
27
27
|
export function printAnalysisCard({ mode = "", modeLabel = "", types = [], version = "", branch = "",
|
|
28
|
-
includeNexus = null, includeSecretBackup = null, paths = new Map(), showOptional = false },
|
|
28
|
+
includeNexus = null, includeSecretBackup = null, includeNpmPublish = null, paths = new Map(), showOptional = false },
|
|
29
29
|
out = (s) => process.stdout.write(s)) {
|
|
30
30
|
out(`${HEAD} ${paint("프로젝트 분석 결과", A.bold)}\n`);
|
|
31
31
|
const row = (icon, label, value) => out(`${GUT} ${icon} ${label.padEnd(10)} ${value}\n`);
|
|
@@ -36,6 +36,7 @@ export function printAnalysisCard({ mode = "", modeLabel = "", types = [], versi
|
|
|
36
36
|
if (showOptional) {
|
|
37
37
|
row("📦", "Nexus", includeNexus === true ? paint("포함", A.green) : paint("제외", A.dim));
|
|
38
38
|
row("🔐", "Secret백업", includeSecretBackup === true ? paint("포함", A.green) : paint("제외", A.dim));
|
|
39
|
+
row("📦", "npm publish", includeNpmPublish === true ? paint("포함", A.green) : paint("제외", A.dim));
|
|
39
40
|
}
|
|
40
41
|
// 모노레포 경로 — 루트가 아닌 항목이 하나라도 있으면 표시
|
|
41
42
|
const nonRoot = [...paths.entries()].filter(([, p]) => p && p !== ".");
|