projectops 3.0.192 → 3.0.194
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 +132 -0
- package/src/commands/issues.js +8 -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/paths.js +14 -0
- package/src/core/version-yml.js +146 -0
- package/src/core/wizard-env.js +101 -0
- package/src/index.js +99 -0
- package/src/ui/prompts.js +82 -0
|
@@ -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,99 @@
|
|
|
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
|
+
|
|
17
|
+
// 결정적 UTC 타임스탬프 (주입 가능 — 테스트/골든용)
|
|
18
|
+
function utcNow(date = new Date()) {
|
|
19
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
20
|
+
const d = `${date.getUTCFullYear()}-${p(date.getUTCMonth() + 1)}-${p(date.getUTCDate())}`;
|
|
21
|
+
const t = `${p(date.getUTCHours())}:${p(date.getUTCMinutes())}:${p(date.getUTCSeconds())}`;
|
|
22
|
+
return { now: `${d} ${t}`, today: d };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// run(argv, opts) → exitCode. opts: { cwd, source?, clock? }
|
|
26
|
+
// source: acquireTemplate용 (기본 git clone). 테스트는 {type:'local', path} 주입.
|
|
27
|
+
// clock: {now, today} 주입 (기본 현재 UTC).
|
|
28
|
+
export async function run(argv, { cwd = process.cwd(), source = { type: "git" }, clock } = {}) {
|
|
29
|
+
let opts;
|
|
30
|
+
try {
|
|
31
|
+
opts = parseArgs(argv);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
if (e instanceof CliError) { console.error(e.message); return 1; }
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
if (opts.help) { console.log(HELP_TEXT); return 0; }
|
|
37
|
+
|
|
38
|
+
// skills 모드는 IDE 설치 전용 (SP2-D)
|
|
39
|
+
if (opts.mode === "skills") {
|
|
40
|
+
console.error("skills 모드는 아직 지원되지 않습니다 (SP2-D 예정). 기존 template_integrator를 사용하세요.");
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
// 대화형 모드 — 인자 없이 실행 or --mode interactive
|
|
44
|
+
if (opts.mode === "interactive") {
|
|
45
|
+
if (!process.stdout.isTTY) {
|
|
46
|
+
console.error("대화형 입력이 불가능한 환경입니다. --mode <full|version|workflows|issues> 와 --force 를 지정하세요.");
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
return await runInteractive({}, { cwd, source, clock });
|
|
50
|
+
}
|
|
51
|
+
// 명시 모드인데 --force 없으면 (비대화형 CLI는 --force 필요)
|
|
52
|
+
if (!opts.force && !process.stdout.isTTY) {
|
|
53
|
+
console.error("비대화형 환경에서는 --force 옵션이 필요합니다.");
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 감지 (CLI 인자 우선, 없으면 자동 감지 — version.yml 우선 규칙은 detectTypes/detectVersion 내부)
|
|
58
|
+
const types = opts.types.length ? opts.types : detectTypes(cwd);
|
|
59
|
+
const version = opts.version || detectVersion(cwd);
|
|
60
|
+
const branch = detectDefaultBranch(cwd);
|
|
61
|
+
const repoName = detectRepoName(cwd);
|
|
62
|
+
const paths = parsePathsCsv(opts.pathsCsv);
|
|
63
|
+
// paths 미지정 타입은 루트(".") — basic 제외
|
|
64
|
+
for (const t of types) if (t !== "basic" && !paths.has(t)) paths.set(t, ".");
|
|
65
|
+
|
|
66
|
+
const { now, today } = clock || utcNow();
|
|
67
|
+
const tempDir = join(cwd, PATHS.tempDir);
|
|
68
|
+
|
|
69
|
+
const context = createContext({
|
|
70
|
+
mode: opts.mode, force: true, types, version, branch,
|
|
71
|
+
paths, includeNexus: opts.includeNexus === true, includeSecretBackup: opts.includeSecretBackup === true,
|
|
72
|
+
repoName,
|
|
73
|
+
resolvers: {
|
|
74
|
+
repo: () => repoName,
|
|
75
|
+
"spring-app-yml-dir": () => "",
|
|
76
|
+
"spring-app-yml-path": () => "",
|
|
77
|
+
"flutter-root": () => paths.get("flutter") || ".",
|
|
78
|
+
},
|
|
79
|
+
now, today,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
acquireTemplate({ tempDir, source });
|
|
84
|
+
context.templateVersion = readTemplateVersion(tempDir);
|
|
85
|
+
|
|
86
|
+
switch (opts.mode) {
|
|
87
|
+
case "full": runFull(context, tempDir, cwd); break;
|
|
88
|
+
case "version": runVersion(context, tempDir, cwd); break;
|
|
89
|
+
case "workflows": runWorkflows(context, tempDir, cwd); break;
|
|
90
|
+
case "issues": runIssues(context, tempDir, cwd); break;
|
|
91
|
+
default:
|
|
92
|
+
// 알 수 없는 모드 → .sh와 동일하게 복사 0건, 에러 아님
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
remove(tempDir);
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
@@ -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); }
|