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 CHANGED
@@ -7,7 +7,7 @@
7
7
  > 이슈 등록부터 커밋, 보고서, 배포까지. 개발자는 코드만 작성하세요.
8
8
 
9
9
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
10
- ## 최신 버전 : v4.0.3 (2026-07-08)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectops",
3
- "version": "4.0.4",
3
+ "version": "4.1.2",
4
4
  "description": "ProjectOps — 완전 자동화 GitHub 프로젝트 관리 템플릿 통합 CLI (구 SUH-DEVOPS-TEMPLATE 마법사)",
5
5
  "keywords": [
6
6
  "devops",
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 next react react-native
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 이 도움말 표시
@@ -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
  }
@@ -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", "next", "react",
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);
@@ -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
- if (s.includes('"next"')) return "next";
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
 
@@ -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", "next", "node", "react-native", "react-native-expo", "python", "spring",
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"], next: ["package.json"], node: ["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"],
@@ -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. project_type: 프로젝트 타입 지정
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
- # - project_type은 최초 설정 후 변경하지 마세요
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:[], primaryType?, paths:Map, pathMarkers?:Map, branch, versionCode, now, today, templateOptions? }
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 = [], primaryType, paths = new Map(), pathMarkers = new Map(), branch = "main", versionCode = 1, now, today, templateOptions = null, deployValues = new Map() }) {
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", "next", "react", "react-native", "react-native-expo", "node", "python", "basic"];
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 })),
@@ -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 !== ".");