projectops 3.0.193 → 3.0.195
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/bin/projectops.js +7 -34
- package/package.json +7 -2
- package/src/cli/args.js +84 -0
- package/src/cli/help.js +23 -0
- package/src/commands/full.js +64 -0
- package/src/commands/interactive.js +135 -0
- package/src/commands/issues.js +8 -0
- package/src/commands/skills.js +82 -0
- package/src/commands/version.js +30 -0
- package/src/commands/workflows.js +53 -0
- package/src/context.js +25 -0
- package/src/core/assets.js +44 -0
- package/src/core/breaking.js +26 -0
- package/src/core/copy/coderabbit.js +25 -0
- package/src/core/copy/gitignore.js +55 -0
- package/src/core/copy/readme.js +30 -0
- package/src/core/copy/simple.js +55 -0
- package/src/core/copy/util.js +22 -0
- package/src/core/copy/workflows.js +145 -0
- package/src/core/detect-fs.js +64 -0
- package/src/core/detect.js +62 -0
- package/src/core/exclusions.js +25 -0
- package/src/core/fsutil.js +40 -0
- package/src/core/ide/adapter.js +36 -0
- package/src/core/ide/adapters/claude.js +94 -0
- package/src/core/ide/adapters/codex.js +49 -0
- package/src/core/ide/adapters/cursor.js +76 -0
- package/src/core/ide/adapters/gemini.js +34 -0
- package/src/core/ide/adapters/pi-common.js +63 -0
- package/src/core/ide/adapters/pi-harness.js +48 -0
- package/src/core/ide/adapters/pi.js +45 -0
- package/src/core/ide/registry.js +27 -0
- package/src/core/ide/runner.js +59 -0
- package/src/core/ide/util.js +18 -0
- package/src/core/paths.js +14 -0
- package/src/core/version-yml.js +146 -0
- package/src/core/wizard-env.js +101 -0
- package/src/index.js +108 -0
- package/src/ui/prompts.js +82 -0
- package/src/ui/skills-prompts.js +37 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// version.yml 파싱·생성 (.sh create_version_yml 등가, 전체 재생성 전략 D4).
|
|
2
|
+
// ⚠️ YAML 재직렬화 금지 — 주석이 데이터. .sh heredoc과 바이트 동일한 템플릿 문자열.
|
|
3
|
+
// 실측 기준: template_integrator.sh 2184~2354.
|
|
4
|
+
|
|
5
|
+
const HEADER = `# ===================================================================
|
|
6
|
+
# 프로젝트 버전 관리 파일
|
|
7
|
+
# ===================================================================
|
|
8
|
+
#
|
|
9
|
+
# 이 파일은 다양한 프로젝트 타입에서 버전 정보를 중앙 관리하기 위한 파일입니다.
|
|
10
|
+
# GitHub Actions 워크플로우가 이 파일을 읽어 자동으로 버전을 관리합니다.
|
|
11
|
+
#
|
|
12
|
+
# 사용법:
|
|
13
|
+
# 1. version: "1.0.0" - 사용자에게 표시되는 버전
|
|
14
|
+
# 2. version_code: 1 - Play Store/App Store 빌드 번호 (1부터 자동 증가)
|
|
15
|
+
# 3. project_type: 프로젝트 타입 지정
|
|
16
|
+
# 4. project_paths: 타입별 프로젝트 폴더 (레포 루트 기준 상대경로, 모노레포용)
|
|
17
|
+
#
|
|
18
|
+
# 자동 버전 업데이트:
|
|
19
|
+
# - patch: 자동으로 세 번째 자리 증가 (x.x.x -> x.x.x+1)
|
|
20
|
+
# - version_code: 매 빌드마다 자동으로 1씩 증가
|
|
21
|
+
# - minor/major: 수동으로 직접 수정 필요
|
|
22
|
+
#
|
|
23
|
+
# 프로젝트 타입별 동기화 파일:
|
|
24
|
+
# - spring: build.gradle (version = "x.y.z")
|
|
25
|
+
# - flutter: pubspec.yaml (version: x.y.z+i, buildNumber 포함)
|
|
26
|
+
# - react/node: package.json ("version": "x.y.z")
|
|
27
|
+
# - react-native: iOS Info.plist 또는 Android build.gradle
|
|
28
|
+
# - react-native-expo: app.json (expo.version)
|
|
29
|
+
# - python: pyproject.toml (version = "x.y.z")
|
|
30
|
+
# - basic/기타: version.yml 파일만 사용
|
|
31
|
+
#
|
|
32
|
+
# 연관된 워크플로우:
|
|
33
|
+
# - .github/workflows/PROJECT-VERSION-CONTROL.yaml
|
|
34
|
+
# - .github/workflows/PROJECT-README-VERSION-UPDATE.yaml
|
|
35
|
+
# - .github/workflows/PROJECT-AUTO-CHANGELOG-CONTROL.yaml
|
|
36
|
+
#
|
|
37
|
+
# 주의사항:
|
|
38
|
+
# - project_type은 최초 설정 후 변경하지 마세요
|
|
39
|
+
# - 버전은 항상 높은 버전으로 자동 동기화됩니다
|
|
40
|
+
# ===================================================================
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
// 기존 version.yml에서 값 추출 (.sh grep/sed 등가, 주석 라인 오탐 방지).
|
|
44
|
+
export function parseExisting(content) {
|
|
45
|
+
const text = String(content || "");
|
|
46
|
+
const line = (re) => {
|
|
47
|
+
for (const l of text.split("\n")) {
|
|
48
|
+
if (l.startsWith("#")) continue; // 주석 제외
|
|
49
|
+
const m = l.match(re);
|
|
50
|
+
if (m) return m[1];
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
// version: "x.y.z" (숫자.숫자.숫자 형태만)
|
|
55
|
+
const version = line(/^version:\s*["']?([0-9][0-9.]*)["']?/) || "";
|
|
56
|
+
// version_code: N (양의 정수, 아니면 1)
|
|
57
|
+
let versionCode = parseInt(line(/^version_code:\s*([0-9]+)/) || "", 10);
|
|
58
|
+
if (!Number.isInteger(versionCode) || versionCode <= 0) versionCode = 1;
|
|
59
|
+
// project_types: ["a","b"]
|
|
60
|
+
const typesRaw = line(/^project_types:\s*(\[[^\]]*\])/);
|
|
61
|
+
let types = [];
|
|
62
|
+
if (typesRaw) types = [...typesRaw.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
63
|
+
// project_paths 블록: " type: "path""
|
|
64
|
+
const paths = new Map();
|
|
65
|
+
let inPaths = false;
|
|
66
|
+
for (const l of text.split("\n")) {
|
|
67
|
+
if (/^project_paths:/.test(l)) { inPaths = true; continue; }
|
|
68
|
+
if (inPaths) {
|
|
69
|
+
const m = l.match(/^\s+([a-z-]+):\s*"([^"]*)"/);
|
|
70
|
+
if (m) paths.set(m[1], m[2]);
|
|
71
|
+
else if (/^\S/.test(l)) inPaths = false; // 들여쓰기 끝 → 블록 종료
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// template: 블록 내 version
|
|
75
|
+
let templateVersion = "";
|
|
76
|
+
let inTemplate = false;
|
|
77
|
+
for (const l of text.split("\n")) {
|
|
78
|
+
if (/^\s*template:/.test(l)) { inTemplate = true; continue; }
|
|
79
|
+
if (inTemplate) {
|
|
80
|
+
const m = l.match(/^\s*version:\s*"([0-9][0-9.]*)"/);
|
|
81
|
+
if (m) { templateVersion = m[1]; break; }
|
|
82
|
+
if (/^\S/.test(l)) break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { version, versionCode, types, paths, templateVersion };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// version.yml 전체 생성 (.sh create_version_yml + save_template_options 신규 케이스 등가).
|
|
89
|
+
// opts: { version, types:[], primaryType?, paths:Map, pathMarkers?:Map, branch, versionCode, now, today, templateOptions? }
|
|
90
|
+
// now = "YYYY-MM-DD HH:MM:SS" (UTC) — 결정성 위해 주입
|
|
91
|
+
// today = "YYYY-MM-DD" (UTC)
|
|
92
|
+
// pathMarkers = Map<type, markerFilename> (project_paths 주석용)
|
|
93
|
+
// templateOptions = { templateVersion, includeNexus, includeSecretBackup, optionsDate } (template 블록)
|
|
94
|
+
export function buildVersionYml({ version, types = [], primaryType, paths = new Map(), pathMarkers = new Map(), branch = "main", versionCode = 1, now, today, templateOptions = null, deployValues = new Map() }) {
|
|
95
|
+
const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(",")}]` : `["basic"]`;
|
|
96
|
+
const primary = primaryType || types[0] || "basic";
|
|
97
|
+
|
|
98
|
+
let out = HEADER + "\n";
|
|
99
|
+
out += `version: "${version}"\n`;
|
|
100
|
+
out += `version_code: ${versionCode} # app build number\n`;
|
|
101
|
+
out += `project_types: ${typesJson} # 멀티타입 배열 — 첫 항목이 primary, 직접 편집 가능\n`;
|
|
102
|
+
out += `project_type: "${primary}" # project_types[0] 자동 미러 — 직접 수정 금지 (spring, flutter, next, react, react-native, react-native-expo, node, python, basic)\n`;
|
|
103
|
+
|
|
104
|
+
// project_paths 블록. pathMarkers: Map<type, markerFilename> (있으면 " type: "path" # path/marker" 주석).
|
|
105
|
+
if (paths.size) {
|
|
106
|
+
out += `project_paths: # 타입별 프로젝트 폴더 (레포 루트 기준 상대경로)\n`;
|
|
107
|
+
for (const [t, p] of paths) {
|
|
108
|
+
const marker = pathMarkers.get(t) || "";
|
|
109
|
+
const pf = p === "." ? marker : (marker ? `${p}/${marker}` : p);
|
|
110
|
+
out += marker ? ` ${t}: "${p}" # ${pf}\n` : ` ${t}: "${p}"\n`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
out += `metadata:\n`;
|
|
115
|
+
out += ` last_updated: "${now}"\n`;
|
|
116
|
+
out += ` last_updated_by: "template_integrator"\n`;
|
|
117
|
+
out += ` default_branch: "${branch}"\n`;
|
|
118
|
+
out += ` integrated_from: "projectops"\n`;
|
|
119
|
+
out += ` integration_date: "${today}"\n`;
|
|
120
|
+
|
|
121
|
+
// deploy 블록 (.sh update_version_yml_deploy). deployValues: Map<type, Map<key,value>>.
|
|
122
|
+
// WF ask 값이 있는 타입만. metadata 뒤, template 앞. (앞에 빈 줄 1개)
|
|
123
|
+
const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t) && deployValues.get(t).size > 0);
|
|
124
|
+
if (deployTypes.length) {
|
|
125
|
+
out += `\n`;
|
|
126
|
+
out += `deploy: # 마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)\n`;
|
|
127
|
+
for (const t of deployTypes) {
|
|
128
|
+
out += ` ${t}:\n`;
|
|
129
|
+
for (const [k, v] of deployValues.get(t)) out += ` ${k}: "${v}"\n`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// template 옵션 블록 (.sh save_template_options 신규 추가 케이스). templateOptions 지정 시.
|
|
134
|
+
if (templateOptions) {
|
|
135
|
+
const { templateVersion = "unknown", includeNexus = false, includeSecretBackup = false, optionsDate = today } = templateOptions;
|
|
136
|
+
out += ` template:\n`;
|
|
137
|
+
out += ` source: "projectops"\n`;
|
|
138
|
+
out += ` version: "${templateVersion}"\n`;
|
|
139
|
+
out += ` integrated_date: "${optionsDate}"\n`;
|
|
140
|
+
out += ` last_update_date: "${optionsDate}"\n`;
|
|
141
|
+
out += ` options:\n`;
|
|
142
|
+
out += ` nexus: ${includeNexus}\n`;
|
|
143
|
+
out += ` secret_backup: ${includeSecretBackup}\n`;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @wizard env 토큰 엔진 (.sh configure_workflow_env / _wf_set_env / _wf_is_unchanged 등가).
|
|
2
|
+
// ⚠️ YAML 파싱/재직렬화 금지 — 라인 단위 문자열 처리 (포맷·주석 보존이 unchanged 판정 전제).
|
|
3
|
+
// 실측 기준: template_integrator.sh 3282~3360, 3003~3012.
|
|
4
|
+
|
|
5
|
+
// KEY 정규식: .sh는 [A-Z_]+ (대문자+언더스코어만). ask/auto 마커가 있는 라인만 대상.
|
|
6
|
+
const MARKER_RE = /#\s*@wizard\s+(ask|auto):(.*)$/;
|
|
7
|
+
const KEY_RE = /^(\s*)([A-Z_]+):/;
|
|
8
|
+
const PATHS_ANCHOR_RE = /#\s*@wizard\s+paths-anchor/;
|
|
9
|
+
|
|
10
|
+
// 한 라인을 파싱해 {indent,key,action,arg} 반환. ask/auto 마커 없으면 null.
|
|
11
|
+
export function parseWizardLine(line) {
|
|
12
|
+
const marker = line.match(MARKER_RE);
|
|
13
|
+
if (!marker) return null;
|
|
14
|
+
const km = line.match(KEY_RE);
|
|
15
|
+
if (!km) return null; // KEY: 형식 아니면 (예: paths-anchor 주석) 무시
|
|
16
|
+
return { indent: km[1], key: km[2], action: marker[1], arg: marker[2].trim() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// .sh _wf_set_env 등가: `KEY: "..."` 따옴표 안 값 치환 + 그 줄 끝 `# @wizard ...` 주석 제거.
|
|
20
|
+
// 라인 하나에 대해 수행. value가 빈문자면 (.sh는 [ -n "$_val" ] 가드) 치환 스킵.
|
|
21
|
+
export function setEnvLine(line, key, value) {
|
|
22
|
+
if (value === "" || value == null) return line;
|
|
23
|
+
// CRLF 안전: 라인 끝 \r을 분리해 처리 후 복원 (autocrlf 프로젝트 대응)
|
|
24
|
+
const cr = line.endsWith("\r") ? "\r" : "";
|
|
25
|
+
const body = cr ? line.slice(0, -1) : line;
|
|
26
|
+
// 값 치환: KEY: "기존값" → KEY: "value"
|
|
27
|
+
let out = body.replace(
|
|
28
|
+
new RegExp(`^(\\s*${key}:\\s*")[^"]*(")`),
|
|
29
|
+
(_m, p1, p2) => `${p1}${value}${p2}`,
|
|
30
|
+
);
|
|
31
|
+
// 그 줄 끝 # @wizard ... 주석 제거 (앞 공백째)
|
|
32
|
+
out = out.replace(/(\S)[^\S\r\n]*#[^\S\r\n]*@wizard[^\S\r\n].*$/, "$1");
|
|
33
|
+
return out + cr;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// resolver — .sh resolve_token 등가. 값 계산은 주입된 resolvers로 위임(순수성 유지).
|
|
37
|
+
// resolvers: { repo, "spring-app-yml-dir"(type), "spring-app-yml-path"(type), "flutter-root" }
|
|
38
|
+
export function resolveToken(name, type, resolvers = {}) {
|
|
39
|
+
const fn = resolvers[name];
|
|
40
|
+
return typeof fn === "function" ? (fn(type) ?? "") : "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 파일 전체 치환 (configure_workflow_env 등가).
|
|
44
|
+
// content: 원본 워크플로우 텍스트. 반환: 치환된 텍스트.
|
|
45
|
+
// opts:
|
|
46
|
+
// type - 프로젝트 타입 (resolver·값 조회용)
|
|
47
|
+
// values - Map<key,value>: ask 키의 사용자 선택값 (없으면 기본값=arg 또는 resolver)
|
|
48
|
+
// useDefaults - true면 ask도 기본값 사용 (WF_USE_DEFAULTS=true, unchanged 비교의 전제)
|
|
49
|
+
// resolvers - resolveToken용
|
|
50
|
+
// repoName - __PROJECT_NAME__/__APP_ARTIFACT_NAME__ 치환값
|
|
51
|
+
// projectPath - paths-anchor 치환용 ('.'이면 anchor 미변경)
|
|
52
|
+
export function substituteEnv(content, opts = {}) {
|
|
53
|
+
const { type = "", values = new Map(), useDefaults = true, resolvers = {}, repoName = "", projectPath = ".", collectAsks = null } = opts;
|
|
54
|
+
if (!content.includes("@wizard")) return content;
|
|
55
|
+
|
|
56
|
+
// CRLF 안전: EOL을 분리해 LF 기준으로 파싱·치환하고, 원래 EOL 스타일을 복원한다.
|
|
57
|
+
// (JS 정규식의 `.`은 \r을 매칭하지 않아 `(.*)$` 마커 파싱이 CRLF에서 실패하기 때문.)
|
|
58
|
+
const usesCRLF = content.includes("\r\n");
|
|
59
|
+
const lines = content.split(/\r?\n/);
|
|
60
|
+
for (let i = 0; i < lines.length; i++) {
|
|
61
|
+
const p = parseWizardLine(lines[i]); // 이미 \r 제거된 라인
|
|
62
|
+
if (!p) continue;
|
|
63
|
+
let val = "";
|
|
64
|
+
if (p.action === "auto") {
|
|
65
|
+
val = resolveToken(p.arg, type, resolvers);
|
|
66
|
+
} else { // ask
|
|
67
|
+
let def = p.arg.startsWith("@") ? resolveToken(p.arg.slice(1), type, resolvers) : p.arg;
|
|
68
|
+
const chosen = values.get(p.key);
|
|
69
|
+
if (chosen != null && chosen !== "" && !useDefaults) val = chosen;
|
|
70
|
+
else val = def;
|
|
71
|
+
// ask 키만 수집 (.sh wf_deploy_set — auto는 저장 안 함). deploy 블록용.
|
|
72
|
+
if (collectAsks) collectAsks.set(p.key, val);
|
|
73
|
+
}
|
|
74
|
+
lines[i] = setEnvLine(lines[i], p.key, val);
|
|
75
|
+
}
|
|
76
|
+
let out = lines.join(usesCRLF ? "\r\n" : "\n");
|
|
77
|
+
|
|
78
|
+
// 잔여 전역 토큰 (.sh 3347~3351)
|
|
79
|
+
if (out.includes("__PROJECT_NAME__") || out.includes("__APP_ARTIFACT_NAME__")) {
|
|
80
|
+
out = out.replaceAll("__PROJECT_NAME__", repoName).replaceAll("__APP_ARTIFACT_NAME__", repoName);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// paths-anchor (.sh 3353~3360): 경로가 '.'이 아니면 주석 라인 전체를 paths 라인으로 교체
|
|
84
|
+
if (PATHS_ANCHOR_RE.test(out) && projectPath && projectPath !== ".") {
|
|
85
|
+
const eol = out.includes("\r\n") ? "\r\n" : "\n";
|
|
86
|
+
out = out.split(/\r?\n/).map((line) => {
|
|
87
|
+
if (PATHS_ANCHOR_RE.test(line)) {
|
|
88
|
+
const indent = (line.match(/^(\s*)/) || ["", ""])[1];
|
|
89
|
+
return `${indent}paths: ['${projectPath}/**']`;
|
|
90
|
+
}
|
|
91
|
+
return line;
|
|
92
|
+
}).join(eol);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// .sh _wf_is_unchanged 등가: 원본을 "기본값으로 가상 치환한 최종형"과 설치본을 바이트 비교.
|
|
98
|
+
export function isUnchanged(templateContent, installedContent, opts = {}) {
|
|
99
|
+
const virtual = substituteEnv(templateContent, { ...opts, useDefaults: true });
|
|
100
|
+
return virtual === installedContent;
|
|
101
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// projectops CLI 진입 파이프라인 (.sh main + execute_integration 등가).
|
|
2
|
+
// 감지 → 다운로드 → 모드 라우팅 → 통합 실행 → 정리. 비대화형(--force) 우선.
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseArgs, parsePathsCsv, CliError } from "./cli/args.js";
|
|
5
|
+
import { HELP_TEXT } from "./cli/help.js";
|
|
6
|
+
import { createContext } from "./context.js";
|
|
7
|
+
import { PATHS } from "./core/paths.js";
|
|
8
|
+
import { remove } from "./core/fsutil.js";
|
|
9
|
+
import { acquireTemplate, readTemplateVersion } from "./core/assets.js";
|
|
10
|
+
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName } from "./core/detect-fs.js";
|
|
11
|
+
import { runFull } from "./commands/full.js";
|
|
12
|
+
import { runVersion } from "./commands/version.js";
|
|
13
|
+
import { runWorkflows } from "./commands/workflows.js";
|
|
14
|
+
import { runIssues } from "./commands/issues.js";
|
|
15
|
+
import { runInteractive } from "./commands/interactive.js";
|
|
16
|
+
import { runSkills } from "./commands/skills.js";
|
|
17
|
+
|
|
18
|
+
// 결정적 UTC 타임스탬프 (주입 가능 — 테스트/골든용)
|
|
19
|
+
function utcNow(date = new Date()) {
|
|
20
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
21
|
+
const d = `${date.getUTCFullYear()}-${p(date.getUTCMonth() + 1)}-${p(date.getUTCDate())}`;
|
|
22
|
+
const t = `${p(date.getUTCHours())}:${p(date.getUTCMinutes())}:${p(date.getUTCSeconds())}`;
|
|
23
|
+
return { now: `${d} ${t}`, today: d };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// run(argv, opts) → exitCode. opts: { cwd, source?, clock? }
|
|
27
|
+
// source: acquireTemplate용 (기본 git clone). 테스트는 {type:'local', path} 주입.
|
|
28
|
+
// clock: {now, today} 주입 (기본 현재 UTC).
|
|
29
|
+
export async function run(argv, { cwd = process.cwd(), source = { type: "git" }, clock } = {}) {
|
|
30
|
+
let opts;
|
|
31
|
+
try {
|
|
32
|
+
opts = parseArgs(argv);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e instanceof CliError) { console.error(e.message); return 1; }
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
if (opts.help) { console.log(HELP_TEXT); return 0; }
|
|
38
|
+
|
|
39
|
+
// skills 모드 — IDE 스킬 설치/업데이트/제거 (템플릿 통합 없음).
|
|
40
|
+
// Cursor 복사용 skills/ 소스가 필요하므로 템플릿을 획득한 뒤 실행한다.
|
|
41
|
+
if (opts.mode === "skills") {
|
|
42
|
+
const tempDir = join(cwd, PATHS.tempDir);
|
|
43
|
+
const interactive = !opts.force && process.stdout.isTTY;
|
|
44
|
+
try {
|
|
45
|
+
acquireTemplate({ tempDir, source });
|
|
46
|
+
const templateVersion = readTemplateVersion(tempDir);
|
|
47
|
+
return await runSkills({ templateVersion, tempDir, interactive });
|
|
48
|
+
} finally {
|
|
49
|
+
remove(tempDir);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// 대화형 모드 — 인자 없이 실행 or --mode interactive
|
|
53
|
+
if (opts.mode === "interactive") {
|
|
54
|
+
if (!process.stdout.isTTY) {
|
|
55
|
+
console.error("대화형 입력이 불가능한 환경입니다. --mode <full|version|workflows|issues> 와 --force 를 지정하세요.");
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
return await runInteractive({}, { cwd, source, clock });
|
|
59
|
+
}
|
|
60
|
+
// 명시 모드인데 --force 없으면 (비대화형 CLI는 --force 필요)
|
|
61
|
+
if (!opts.force && !process.stdout.isTTY) {
|
|
62
|
+
console.error("비대화형 환경에서는 --force 옵션이 필요합니다.");
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 감지 (CLI 인자 우선, 없으면 자동 감지 — version.yml 우선 규칙은 detectTypes/detectVersion 내부)
|
|
67
|
+
const types = opts.types.length ? opts.types : detectTypes(cwd);
|
|
68
|
+
const version = opts.version || detectVersion(cwd);
|
|
69
|
+
const branch = detectDefaultBranch(cwd);
|
|
70
|
+
const repoName = detectRepoName(cwd);
|
|
71
|
+
const paths = parsePathsCsv(opts.pathsCsv);
|
|
72
|
+
// paths 미지정 타입은 루트(".") — basic 제외
|
|
73
|
+
for (const t of types) if (t !== "basic" && !paths.has(t)) paths.set(t, ".");
|
|
74
|
+
|
|
75
|
+
const { now, today } = clock || utcNow();
|
|
76
|
+
const tempDir = join(cwd, PATHS.tempDir);
|
|
77
|
+
|
|
78
|
+
const context = createContext({
|
|
79
|
+
mode: opts.mode, force: true, types, version, branch,
|
|
80
|
+
paths, includeNexus: opts.includeNexus === true, includeSecretBackup: opts.includeSecretBackup === true,
|
|
81
|
+
repoName,
|
|
82
|
+
resolvers: {
|
|
83
|
+
repo: () => repoName,
|
|
84
|
+
"spring-app-yml-dir": () => "",
|
|
85
|
+
"spring-app-yml-path": () => "",
|
|
86
|
+
"flutter-root": () => paths.get("flutter") || ".",
|
|
87
|
+
},
|
|
88
|
+
now, today,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
acquireTemplate({ tempDir, source });
|
|
93
|
+
context.templateVersion = readTemplateVersion(tempDir);
|
|
94
|
+
|
|
95
|
+
switch (opts.mode) {
|
|
96
|
+
case "full": runFull(context, tempDir, cwd); break;
|
|
97
|
+
case "version": runVersion(context, tempDir, cwd); break;
|
|
98
|
+
case "workflows": runWorkflows(context, tempDir, cwd); break;
|
|
99
|
+
case "issues": runIssues(context, tempDir, cwd); break;
|
|
100
|
+
default:
|
|
101
|
+
// 알 수 없는 모드 → .sh와 동일하게 복사 0건, 에러 아님
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
remove(tempDir);
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// 대화형 프롬프트 래핑 (.sh interactive_menu/choose_menu/ask_* 등가) — @clack/prompts 기반.
|
|
2
|
+
// 취소(ESC/Ctrl+C)는 각 함수가 CANCEL 심볼을 반환 → 호출부가 정상 종료(exit 0) 처리.
|
|
3
|
+
import * as clack from "@clack/prompts";
|
|
4
|
+
|
|
5
|
+
export const CANCEL = Symbol("cancel");
|
|
6
|
+
const wrap = (v) => (clack.isCancel(v) ? CANCEL : v);
|
|
7
|
+
|
|
8
|
+
// 모드 선택 — 한국어 라벨, 내부 키 반환. 취소 시 CANCEL.
|
|
9
|
+
export async function selectMode() {
|
|
10
|
+
const v = await clack.select({
|
|
11
|
+
message: "무엇을 설치할까요?",
|
|
12
|
+
options: [
|
|
13
|
+
{ value: "full", label: "전체 설치 — 버전관리 + 자동화 워크플로우 + 이슈·PR 템플릿 (처음이라면 추천)" },
|
|
14
|
+
{ value: "version", label: "버전 관리만 — 버전 자동 증가·동기화 시스템만 설치" },
|
|
15
|
+
{ value: "workflows", label: "워크플로우만 — 빌드·배포 GitHub Actions만 설치" },
|
|
16
|
+
{ value: "issues", label: "이슈·PR 템플릿만 — GitHub 이슈/PR 양식만 설치" },
|
|
17
|
+
{ value: "skills", label: "AI 스킬만 — Claude·Cursor·Gemini·Codex·PI용 스킬만 설치" },
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
return wrap(v);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 프로젝트 확인 화면 메뉴 (계속/수정/취소).
|
|
24
|
+
export async function confirmProjectMenu() {
|
|
25
|
+
const v = await clack.select({
|
|
26
|
+
message: "이 정보로 진행할까요?",
|
|
27
|
+
options: [
|
|
28
|
+
{ value: "continue", label: "예, 계속 진행" },
|
|
29
|
+
{ value: "edit", label: "수정하기" },
|
|
30
|
+
{ value: "cancel", label: "아니오, 취소" },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
return wrap(v);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 수정 메뉴 — 어떤 항목을 고칠지. showOptional=full/workflows에서만 nexus/secret 노출.
|
|
37
|
+
export async function editMenu({ showOptional = false } = {}) {
|
|
38
|
+
const options = [
|
|
39
|
+
{ value: "type", label: "프로젝트 타입" },
|
|
40
|
+
{ value: "version", label: "버전" },
|
|
41
|
+
{ value: "branch", label: "기본 브랜치" },
|
|
42
|
+
];
|
|
43
|
+
if (showOptional) {
|
|
44
|
+
options.push({ value: "nexus", label: "Nexus publish 포함 여부" });
|
|
45
|
+
options.push({ value: "secret", label: "Secret 백업 포함 여부" });
|
|
46
|
+
}
|
|
47
|
+
options.push({ value: "done", label: "모두 맞음, 계속" });
|
|
48
|
+
const v = await clack.select({ message: "어떤 항목을 수정할까요?", options });
|
|
49
|
+
return wrap(v);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 타입 멀티선택.
|
|
53
|
+
export async function selectTypes(current = []) {
|
|
54
|
+
const all = ["spring", "flutter", "next", "react", "react-native", "react-native-expo", "node", "python", "basic"];
|
|
55
|
+
const v = await clack.multiselect({
|
|
56
|
+
message: "프로젝트 타입을 선택하세요 (Space 토글, Enter 확정)",
|
|
57
|
+
options: all.map((t) => ({ value: t, label: t })),
|
|
58
|
+
initialValues: current.length ? current : ["basic"],
|
|
59
|
+
required: true,
|
|
60
|
+
});
|
|
61
|
+
return wrap(v);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 텍스트 입력 (빈 입력=기본값 유지).
|
|
65
|
+
export async function askText(message, defaultValue = "") {
|
|
66
|
+
const v = await clack.text({ message, placeholder: defaultValue, defaultValue });
|
|
67
|
+
const w = wrap(v);
|
|
68
|
+
if (w === CANCEL) return CANCEL;
|
|
69
|
+
return w === "" || w == null ? defaultValue : w;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 예/아니오.
|
|
73
|
+
export async function askYesNo(message, initial = true) {
|
|
74
|
+
const v = await clack.confirm({ message, initialValue: initial });
|
|
75
|
+
return wrap(v);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 배너·안내 출력.
|
|
79
|
+
export function intro(text) { clack.intro(text); }
|
|
80
|
+
export function outro(text) { clack.outro(text); }
|
|
81
|
+
export function note(text, title) { clack.note(text, title); }
|
|
82
|
+
export function cancelMessage(text = "취소했습니다.") { clack.cancel(text); }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// IDE Skills 대화형 프롬프트 (.sh 라우터 choose_menu 등가) — @clack/prompts 기반.
|
|
2
|
+
import * as clack from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
export const CANCEL = Symbol("cancel");
|
|
5
|
+
const wrap = (v) => (clack.isCancel(v) ? CANCEL : v);
|
|
6
|
+
|
|
7
|
+
// 동작 선택: 설치/업데이트 · 제거 · 그대로. 취소=skip.
|
|
8
|
+
export async function selectAction() {
|
|
9
|
+
const v = await clack.select({
|
|
10
|
+
message: "AI 스킬을 어떻게 할까요?",
|
|
11
|
+
options: [
|
|
12
|
+
{ value: "apply", label: "설치 / 업데이트 — 최신 상태로 맞추기" },
|
|
13
|
+
{ value: "remove", label: "제거 — 설치된 스킬 삭제하기" },
|
|
14
|
+
{ value: "skip", label: "그대로 두기" },
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
const w = wrap(v);
|
|
18
|
+
return w === CANCEL ? "skip" : w;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// IDE 멀티셀렉트. choices=[{id,label,disabled}], preselect=[id...], action.
|
|
22
|
+
export async function selectTargets(choices, preselect = [], action = "apply") {
|
|
23
|
+
const options = choices.map((c) => ({
|
|
24
|
+
value: c.id,
|
|
25
|
+
label: c.label + (c.disabled ? " (미감지)" : ""),
|
|
26
|
+
hint: c.disabled ? "CLI 없음" : undefined,
|
|
27
|
+
}));
|
|
28
|
+
const v = await clack.multiselect({
|
|
29
|
+
message: `${action === "apply" ? "설치 / 업데이트" : "제거"}할 IDE를 고르세요 (Space 토글, Enter 확정)`,
|
|
30
|
+
options,
|
|
31
|
+
initialValues: preselect,
|
|
32
|
+
required: false,
|
|
33
|
+
});
|
|
34
|
+
return wrap(v);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function note(text, title) { clack.note(text, title); }
|