projectops 4.59.1 → 4.60.0
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 -2
- package/package.json +1 -1
- package/src/cli/args.js +2 -2
- package/src/commands/doctor.js +3 -1
- package/src/commands/full.js +9 -11
- package/src/commands/interactive.js +13 -13
- package/src/commands/issues.js +1 -1
- package/src/commands/skills.js +1 -1
- package/src/commands/version.js +5 -6
- package/src/commands/workflows.js +3 -3
- package/src/context.js +2 -2
- package/src/core/agent-guide.js +2 -1
- package/src/core/assets.js +3 -3
- package/src/core/breaking-check.js +6 -6
- package/src/core/breaking.js +2 -2
- package/src/core/copy/coderabbit.js +1 -1
- package/src/core/copy/gitignore.js +3 -3
- package/src/core/copy/readme.js +2 -3
- package/src/core/copy/simple.js +4 -2
- package/src/core/copy/util.js +2 -2
- package/src/core/copy/workflows.js +9 -13
- package/src/core/detect-fs.js +6 -6
- package/src/core/detect.js +3 -3
- package/src/core/exclusions.js +1 -1
- package/src/core/fsutil.js +2 -4
- package/src/core/ide/adapters/claude.js +3 -3
- package/src/core/ide/adapters/codex.js +3 -3
- package/src/core/ide/adapters/cursor.js +3 -3
- package/src/core/ide/adapters/gemini.js +4 -3
- package/src/core/ide/adapters/pi-common.js +8 -7
- package/src/core/ide/adapters/pi-harness.js +4 -4
- package/src/core/ide/adapters/pi.js +1 -1
- package/src/core/ide/runner.js +2 -2
- package/src/core/options-ask.js +7 -9
- package/src/core/options-schema.js +5 -5
- package/src/core/paths-resolve.js +27 -28
- package/src/core/paths.js +7 -3
- package/src/core/resolve-options.js +14 -0
- package/src/core/run-trace.js +1 -1
- package/src/core/version-yml.js +44 -25
- package/src/core/wizard-env.js +11 -12
- package/src/core/wizard-labels.js +15 -17
- package/src/i18n/ide.en.js +1 -0
- package/src/i18n/ide.ko.js +1 -0
- package/src/index.js +8 -8
- package/src/ui/banner.js +2 -1
- package/src/ui/env-plan.js +22 -25
- package/src/ui/prompts.js +1 -1
- package/src/ui/readline-engine.js +1 -1
- package/src/ui/skills-prompts.js +1 -1
- package/src/ui/summary.js +3 -3
package/src/core/detect.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// package.json 내용 분류
|
|
2
|
-
//
|
|
1
|
+
// package.json 내용 분류 — 원본 파일 텍스트에 대한 부분문자열 매칭.
|
|
2
|
+
// dependencies 파싱이 아니라 raw 텍스트 검사 (깨진 JSON에도 동작).
|
|
3
3
|
// 입력은 package.json의 원문 문자열(raw). 순서 중요.
|
|
4
4
|
export function classifyPackageText(raw) {
|
|
5
5
|
const s = String(raw || "");
|
|
@@ -61,7 +61,7 @@ export function extraMarkers(type) {
|
|
|
61
61
|
return { python: ["setup.py", "requirements.txt"], spring: ["build.gradle.kts", "pom.xml"] }[type] || [];
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// 확장자 빈도 스캔 추천 (
|
|
64
|
+
// 확장자 빈도 스캔 추천 (마커 없을 때 2단계, #458).
|
|
65
65
|
// 마커가 전혀 없을 때(=basic 폴백)만 쓰는 2차 추천. files = 프루닝된 상대경로 목록.
|
|
66
66
|
// 임계: dart≥1→flutter, java+kt+gradle≥3→spring, tsx+jsx≥3→react, py≥3→python,
|
|
67
67
|
// 아무것도 없고 ts+js≥3→node. 반환은 메뉴 정의 순서로 정렬.
|
package/src/core/exclusions.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 템플릿 다운로드 후 제거하는 항목 (복사 제외 목록).
|
|
2
2
|
// ⚠️ CLAUDE.md "템플릿 전용 파일 추가" 규칙의 동기화 지점:
|
|
3
3
|
// template_initializer.py(삭제) / 이 파일(복사 제외)
|
|
4
|
-
// (
|
|
4
|
+
// (여기가 유일한 복사 제외 지점)
|
|
5
5
|
export const DOCS_TO_REMOVE = [
|
|
6
6
|
"CONTRIBUTING.md",
|
|
7
7
|
"SECURITY.md",
|
package/src/core/fsutil.js
CHANGED
|
@@ -31,10 +31,8 @@ export function remove(p) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// 디렉토리 직하위 .yaml/.yml 파일명 목록. 하위 폴더 제외.
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// (단순 .sort()는 확장자를 섞어 정렬해 .sh와 파일 순회 순서가 갈리고,
|
|
37
|
-
// 그 결과 version.yml deploy 블록의 키 순서까지 달라진다. 확장자 그룹핑으로 바이트 등가 확보.)
|
|
34
|
+
// 정렬: 확장자로 1차 그룹(.yaml 먼저 → .yml 나중), 각 그룹 안에서 알파벳순.
|
|
35
|
+
// (순회 순서가 version.yml deploy 블록의 키 순서를 정함 — 기존 설치본과 바이트 동일 유지)
|
|
38
36
|
export function listYamlFiles(dir) {
|
|
39
37
|
if (!existsSync(dir)) return [];
|
|
40
38
|
const names = readdirSync(dir, { withFileTypes: true })
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// Claude Code 어댑터 (
|
|
2
|
-
// _remove_claude_section / _remove_claude_plugin_data 등가).
|
|
1
|
+
// Claude Code 어댑터 (플러그인 설치·제거, 플러그인 데이터 정리).
|
|
3
2
|
// 마켓플레이스 Cassiiopeia/projectops, 플러그인 projectops@projectops-marketplace.
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { existsSync, readdirSync, mkdirSync, cpSync, rmSync } from "node:fs";
|
|
6
5
|
import { compareCacheName } from "../util.js";
|
|
7
6
|
import { migrateConfigRoot } from "../legacy.js";
|
|
8
7
|
import { t } from "../../../i18n/index.js";
|
|
8
|
+
import { TEMPLATE_REPO_SLUG } from "../../paths.js";
|
|
9
9
|
|
|
10
|
-
const MARKETPLACE =
|
|
10
|
+
const MARKETPLACE = TEMPLATE_REPO_SLUG;
|
|
11
11
|
const PLUGIN = "projectops@projectops-marketplace";
|
|
12
12
|
const LEGACY_PLUGINS = ["cassiiopeia@cassiiopeia-marketplace", "cassiiopeia"];
|
|
13
13
|
const LEGACY_MARKETPLACES = ["cassiiopeia-marketplace"];
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// Codex CLI 어댑터 (
|
|
2
|
-
// _remove_codex_section 등가).
|
|
1
|
+
// Codex CLI 어댑터 (skills 관리·마켓플레이스 등록·제거).
|
|
3
2
|
// marketplace 등록/업그레이드가 주 경로. native ~/.agents/skills/projectops 심링크는 감지·제거에 사용.
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { existsSync, lstatSync, rmSync } from "node:fs";
|
|
6
5
|
import { migrateConfigRoot } from "../legacy.js";
|
|
7
6
|
import { t } from "../../../i18n/index.js";
|
|
7
|
+
import { TEMPLATE_REPO_SLUG } from "../../paths.js";
|
|
8
8
|
|
|
9
|
-
const MARKETPLACE =
|
|
9
|
+
const MARKETPLACE = TEMPLATE_REPO_SLUG;
|
|
10
10
|
const PLUGIN = "projectops";
|
|
11
11
|
const LEGACY_NATIVES = ["SUH-DEVOPS-TEMPLATE"];
|
|
12
12
|
const LEGACY_MARKETPLACES = ["SUH-DEVOPS-TEMPLATE"];
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// Cursor 어댑터 (
|
|
2
|
-
// _write_cursor_skills_meta / _remove_cursor_section 등가).
|
|
1
|
+
// Cursor 어댑터 (skills 복사·메타 기록·제거).
|
|
3
2
|
// 마켓플레이스 없음 → ~/.cursor/skills/ 에 skills/ 폴더를 복사하고 meta.json으로 버전 추적.
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { existsSync, readFileSync, mkdirSync, cpSync, rmSync, writeFileSync, readdirSync } from "node:fs";
|
|
6
5
|
import { migrateConfigRoot, isLegacyVersion } from "../legacy.js";
|
|
7
6
|
import { t } from "../../../i18n/index.js";
|
|
7
|
+
import { TEMPLATE_REPO_WEB_URL } from "../../paths.js";
|
|
8
8
|
|
|
9
9
|
const LEGACY_NAMES = ["cassiiopeia", "suh-devops-template"];
|
|
10
10
|
const LEGACY_MAX_VERSION = "4.2.4";
|
|
@@ -127,7 +127,7 @@ function writeMeta(io, destDir, templateVersion) {
|
|
|
127
127
|
if (existsSync(file)) { try { installedAt = JSON.parse(readFileSync(file, "utf8")).installedAt || now; } catch { /* 무해 */ } }
|
|
128
128
|
const meta = {
|
|
129
129
|
name: "projectops", version, scope: "user",
|
|
130
|
-
source:
|
|
130
|
+
source: TEMPLATE_REPO_WEB_URL,
|
|
131
131
|
installPath: destDir, installedAt, lastUpdated: now,
|
|
132
132
|
};
|
|
133
133
|
mkdirSync(destDir, { recursive: true });
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
// Gemini CLI 어댑터 (
|
|
1
|
+
// Gemini CLI 어댑터 (확장 설치·제거).
|
|
2
2
|
// extension install/update/uninstall. 마켓플레이스 대신 git URL 확장.
|
|
3
3
|
import { migrateConfigRoot } from "../legacy.js";
|
|
4
4
|
import { t } from "../../../i18n/index.js";
|
|
5
|
+
import { TEMPLATE_REPO_WEB_URL } from "../../paths.js";
|
|
5
6
|
|
|
6
7
|
const EXT = "projectops";
|
|
7
|
-
const URL =
|
|
8
|
+
const URL = TEMPLATE_REPO_WEB_URL;
|
|
8
9
|
const LEGACY_EXTS = ["SUH-DEVOPS-TEMPLATE"];
|
|
9
10
|
|
|
10
11
|
function detect(io) {
|
|
11
12
|
if (!io.which("gemini")) return { installed: false, version: null, cliMissing: true, note: t("ide.cliMissing") };
|
|
12
|
-
//
|
|
13
|
+
// gemini 설치 상태는 정밀 조회하지 않고 "설치 가능"으로만 표기 → installed 미상.
|
|
13
14
|
return { installed: false, version: null, cliMissing: false, note: t("ide.installable") };
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// PI 공용 경로·상태 로직 (pi 어댑터와 pi-harness 어댑터가 공유).
|
|
2
|
-
// .sh: PI_PACKAGE_URL / _pi_is_installed / _pi_clone_dir / _pi_harness_loader_path /
|
|
3
|
-
// _pi_settings_path / _pi_harness_enabled 등가.
|
|
4
2
|
import { join } from "node:path";
|
|
5
3
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
6
4
|
import { t } from "../../../i18n/index.js";
|
|
5
|
+
import { TEMPLATE_REPO_OWNER, TEMPLATE_REPO_WEB_URL } from "../../paths.js";
|
|
7
6
|
|
|
8
|
-
export const PI_PACKAGE_URL =
|
|
7
|
+
export const PI_PACKAGE_URL = TEMPLATE_REPO_WEB_URL;
|
|
9
8
|
|
|
10
9
|
// 'pi list' 출력에 우리 레포가 잡히면 설치됨. (구 레포명·프로젝트명 모두 허용)
|
|
11
10
|
export function piInstalled(io) {
|
|
@@ -17,7 +16,7 @@ export function piInstalled(io) {
|
|
|
17
16
|
|
|
18
17
|
// pi 클론 경로(harness loader가 사는 곳). 레포명 변경 전 구 경로 하위호환.
|
|
19
18
|
export function piCloneDir(io) {
|
|
20
|
-
const base = join(io.home(), ".pi/agent/git/github.com
|
|
19
|
+
const base = join(io.home(), ".pi/agent/git/github.com", TEMPLATE_REPO_OWNER);
|
|
21
20
|
const newDir = join(base, "projectops");
|
|
22
21
|
const oldDir = join(base, "SUH-DEVOPS-TEMPLATE");
|
|
23
22
|
return (!existsSync(newDir) && existsSync(oldDir)) ? oldDir : newDir;
|
|
@@ -41,8 +40,10 @@ export function harnessAdd(io) {
|
|
|
41
40
|
const settings = piSettingsPath(io), loader = harnessLoaderPath(io);
|
|
42
41
|
if (!existsSync(settings)) return { ok: false, reason: "no-settings" };
|
|
43
42
|
if (!existsSync(loader)) return { ok: false, reason: "no-loader" };
|
|
44
|
-
let s
|
|
45
|
-
|
|
43
|
+
let s;
|
|
44
|
+
// 해석 실패 시 덮어쓰지 않음 ({} 로 쓰면 사용자 설정 키가 전부 지워진다)
|
|
45
|
+
try { s = JSON.parse(readFileSync(settings, "utf8")); } catch { return { ok: false, reason: "invalid-settings" }; }
|
|
46
|
+
if (s === null || typeof s !== "object" || Array.isArray(s)) return { ok: false, reason: "invalid-settings" };
|
|
46
47
|
const exts = (Array.isArray(s.extensions) ? s.extensions : []).filter(Boolean);
|
|
47
48
|
if (!exts.includes(loader)) exts.push(loader);
|
|
48
49
|
s.extensions = exts;
|
|
@@ -66,7 +67,7 @@ export function harnessRemove(io) {
|
|
|
66
67
|
// 신·구 clone 공존 시 옛 SUH-DEVOPS-TEMPLATE clone 제거 + settings.json에서 그 loader 경로 제거.
|
|
67
68
|
// 공존일 때만 정리(옛것만 있으면 pi install이 신규를 만들 때까지 그대로 둔다). 실패 무해.
|
|
68
69
|
export function migratePiLegacy(io) {
|
|
69
|
-
const base = join(io.home(), ".pi/agent/git/github.com
|
|
70
|
+
const base = join(io.home(), ".pi/agent/git/github.com", TEMPLATE_REPO_OWNER);
|
|
70
71
|
const oldDir = join(base, "SUH-DEVOPS-TEMPLATE");
|
|
71
72
|
const newDir = join(base, "projectops");
|
|
72
73
|
if (!(existsSync(oldDir) && existsSync(newDir))) return;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// PI Persona Harness 어댑터 (.
|
|
2
|
-
// _pi_harness_add / _pi_harness_remove / _pi_harness_enabled 등가).
|
|
1
|
+
// PI Persona Harness 어댑터 (켜기·끄기·상태 조회).
|
|
3
2
|
// PI skill과 독립 — skill은 그대로 두고 harness 등록(settings.json extensions)만 켜고/끈다.
|
|
4
3
|
import { existsSync } from "node:fs";
|
|
5
|
-
import { harnessEnabled, harnessAdd, harnessRemove, harnessLoaderPath } from "./pi-common.js";
|
|
4
|
+
import { harnessEnabled, harnessAdd, harnessRemove, harnessLoaderPath, piSettingsPath } from "./pi-common.js";
|
|
6
5
|
import { t } from "../../../i18n/index.js";
|
|
7
6
|
|
|
8
7
|
// 이 어댑터는 "설치 가능" 여부가 pi 존재 + loader 파일 존재에 달림.
|
|
@@ -20,7 +19,8 @@ function apply(io) {
|
|
|
20
19
|
if (harnessEnabled(io)) { io.log(t("ide.harness.already")); return true; }
|
|
21
20
|
const r = harnessAdd(io);
|
|
22
21
|
if (r.ok) { io.log(t("ide.harness.enabledDone")); return true; }
|
|
23
|
-
|
|
22
|
+
const messageKeyByReason = { "no-settings": "ide.harness.noSettings", "invalid-settings": "ide.harness.invalidSettings" };
|
|
23
|
+
io.log(t(messageKeyByReason[r.reason] ?? "ide.harness.needPackage", { file: piSettingsPath(io) }));
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// PI 패키지 어댑터 (
|
|
1
|
+
// PI 패키지 어댑터 (설치·제거).
|
|
2
2
|
// pi install/update/remove. skill은 복사되지 않고 pi가 startup마다 스캔 → 설치 검증은 'pi list'.
|
|
3
3
|
import { PI_PACKAGE_URL, piInstalled, harnessEnabled, harnessRemove, migratePiLegacy } from "./pi-common.js";
|
|
4
4
|
import { migrateConfigRoot } from "../legacy.js";
|
package/src/core/ide/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 외부 CLI 실행·감지
|
|
1
|
+
// 외부 CLI 실행·감지 래퍼.
|
|
2
2
|
// 전부 주입 가능 — 테스트는 stub runner/which/home 를 넘겨 실제 CLI 없이 검증한다.
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { delimiter, join } from "node:path";
|
|
@@ -54,6 +54,6 @@ export function defaultIo() {
|
|
|
54
54
|
which,
|
|
55
55
|
run,
|
|
56
56
|
home: () => homedir(),
|
|
57
|
-
log: (msg) => console.error(msg), //
|
|
57
|
+
log: (msg) => console.error(msg), // 안내는 stderr로 (stdout 파이프 오염 방지)
|
|
58
58
|
};
|
|
59
59
|
}
|
package/src/core/options-ask.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// 배포/publish 축(#439) + Secret 백업 opt-in
|
|
2
|
-
// ask_optional_workflow / ask_all_optional_workflows 등가).
|
|
1
|
+
// 배포/publish 축(#439) + Secret 백업 opt-in 질문.
|
|
3
2
|
//
|
|
4
3
|
// io 주입 계약(readline-engine 시그니처):
|
|
5
4
|
// io.confirm({message, initialValue}) → bool | CANCEL(symbol)
|
|
@@ -110,7 +109,7 @@ export function migrateProvider(saved) {
|
|
|
110
109
|
|
|
111
110
|
const isCancel = (v) => typeof v === "symbol";
|
|
112
111
|
|
|
113
|
-
// Secret 백업 등 폴더 기반 opt-in 1종
|
|
112
|
+
// Secret 백업 등 폴더 기반 opt-in 1종 질문.
|
|
114
113
|
// 반환: true/false/null(폴더 없음·파일 0개로 질문 자체 생략 → 현재값 유지).
|
|
115
114
|
async function askOptionalWorkflow({ dir, icon, short, desc, current, force, tty, io, forceAsk, say }) {
|
|
116
115
|
// 폴더가 없거나 yaml이 0개면 조용히 건너뜀 — 질문 자체가 성립 안 함
|
|
@@ -148,7 +147,7 @@ export const OPTION_AXES = ["intent", "deploy", "publish", "code-review", "chang
|
|
|
148
147
|
const INTENT_ASKS_DEPLOY = { app: true, both: true, manual: true, library: false, none: false };
|
|
149
148
|
const INTENT_ASKS_PUBLISH = { library: true, both: true, manual: true, app: false, none: false };
|
|
150
149
|
|
|
151
|
-
// 배포/publish 축 + Secret 백업을 순서대로
|
|
150
|
+
// 배포/publish 축 + Secret 백업을 순서대로 질문.
|
|
152
151
|
// tempDir: 템플릿 다운로드 루트 — project-types는 {tempDir}/.github/workflows/project-types
|
|
153
152
|
// current: { deploy: string|null, publish: string[]|null, secretBackup: bool|null } — CLI 명시값
|
|
154
153
|
// scope: null이면 전 축(초기 통합·전체 재질문). Set/배열이면 그 축만 forceAsk 대상 (#483 수정 메뉴 격리).
|
|
@@ -204,7 +203,7 @@ export async function askAllOptionalWorkflows({
|
|
|
204
203
|
if (aiPrSummary === null && saved.aiPrSummary != null) aiPrSummary = saved.aiPrSummary;
|
|
205
204
|
if (changelogProvider === null && saved.changelogProvider !== null) changelogProvider = saved.changelogProvider;
|
|
206
205
|
if (changelogBaseUrl === null && saved.changelogBaseUrl !== null) changelogBaseUrl = saved.changelogBaseUrl;
|
|
207
|
-
// #456
|
|
206
|
+
// #456 개발 브랜치 저장값 재사용
|
|
208
207
|
if (deployBranch === null && saved.deployBranch != null) deployBranch = saved.deployBranch;
|
|
209
208
|
// #485 intent 저장값 재사용 — 없으면 저장된 deploy/publish에서 역추론(구 version.yml 하위호환)
|
|
210
209
|
if (intent === null) intent = saved.intent ?? inferIntent(saved.deploy, saved.publish);
|
|
@@ -332,9 +331,8 @@ export async function askAllOptionalWorkflows({
|
|
|
332
331
|
}
|
|
333
332
|
|
|
334
333
|
// ── pr_comment: PR에 달 리뷰·요약 (#566에서 통합) ──
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
// 저장소가 생겼다. 둘은 같은 자리(PR 댓글)를 놓고 경쟁하므로 한 번에 고르게 한다.
|
|
334
|
+
// CodeRabbit 과 AI 변경 요약은 같은 자리(PR 댓글)를 놓고 경쟁하므로 한 번에 고르게 한다
|
|
335
|
+
// (따로 물으면 둘 다 안 다는 저장소가 생긴다).
|
|
338
336
|
if (ask("code-review") || codeReviewCoderabbit === null || aiPrSummary === null) {
|
|
339
337
|
if (force || !tty || typeof io.select !== "function") {
|
|
340
338
|
// 비대화형 기본값: CodeRabbit은 별도 앱 설치가 있어야 실제로 동작하므로
|
|
@@ -392,7 +390,7 @@ export async function askAllOptionalWorkflows({
|
|
|
392
390
|
changelogBaseUrl = "";
|
|
393
391
|
}
|
|
394
392
|
|
|
395
|
-
// ── 릴리스 소스(개발) 브랜치 (#456
|
|
393
|
+
// ── 릴리스 소스(개발) 브랜치 (#456, version.yml development_branch — #915) ──
|
|
396
394
|
// 이 값은 릴리스 PR(개발→기본)의 head, 즉 개발한 걸 모아 기본 브랜치로 올리는 브랜치다.
|
|
397
395
|
// "배포 브랜치"가 아니다 — 배포가 도는 곳은 기본 브랜치(default) 쪽 개념. #482 참조.
|
|
398
396
|
if (ask("release-branch") || deployBranch === null) {
|
|
@@ -47,7 +47,7 @@ export const FLAGS = [
|
|
|
47
47
|
{ flag: "--publish", value: "CSV", values: [...PUBLISH_TARGETS], default: "none",
|
|
48
48
|
description: "Registries a library is published to. Several allowed.", versionYml: "metadata.template.options.publish" },
|
|
49
49
|
{ flag: "--deploy-branch", value: "NAME", default: "develop",
|
|
50
|
-
description: "Development branch: the head of the release PR. Not the repository default branch.", versionYml: "metadata.
|
|
50
|
+
description: "Development branch: the head of the release PR. Not the repository default branch.", versionYml: "metadata.development_branch" },
|
|
51
51
|
{ flag: "--language", value: "LANG", values: [...REPO_LANGUAGES], default: "en for new installs, ko for existing installs",
|
|
52
52
|
description: "Language of issue/PR templates and bot messages written into the repo.", versionYml: "metadata.template.options.language" },
|
|
53
53
|
{ flag: "--label-style", value: "STYLE", values: [...LABEL_STYLES], default: "en for new installs, ko for existing installs",
|
|
@@ -84,8 +84,8 @@ export const VERSION_YML = [
|
|
|
84
84
|
{ path: "project_types", type: "string[]", description: "Project types; the first is primary. Do not change after setup.", edit: "wizard" },
|
|
85
85
|
{ path: "project_paths", type: "map", description: "Per-type project folder relative to the repo root (monorepo).", edit: "wizard (--paths)" },
|
|
86
86
|
{ path: "metadata.default_branch", type: "string", default: "main", description: "Repository default (production) branch. Pushes here deploy.", edit: "wizard" },
|
|
87
|
-
{ path: "metadata.
|
|
88
|
-
description: "Development branch: head of the release PR.
|
|
87
|
+
{ path: "metadata.development_branch", parserKey: "deployBranch", type: "string", default: "develop",
|
|
88
|
+
description: "Development branch: head of the release PR. Old name deploy_branch is still read and written with the same value.", edit: "wizard (--deploy-branch)" },
|
|
89
89
|
{ path: "metadata.template.options.intent", parserKey: "intent", type: "enum", values: [...INTENT_VALUES],
|
|
90
90
|
description: "Project kind; derives the deploy and publish questions.", edit: "wizard (--intent)" },
|
|
91
91
|
{ path: "metadata.template.options.deploy", parserKey: "deploy", type: "enum", values: [...DEPLOY_TARGETS], default: "docker-ssh",
|
|
@@ -183,7 +183,7 @@ export const FLAG_DETAILS = {
|
|
|
183
183
|
},
|
|
184
184
|
"--deploy-branch": {
|
|
185
185
|
when: "Only if your development branch is not named develop.",
|
|
186
|
-
effect: "Writes metadata.deploy_branch and rewrites the branch name inside the installed workflows (release PR head, CI triggers, PR summary base). The branch is NOT created by this flag.",
|
|
186
|
+
effect: "Writes metadata.development_branch (and the old deploy_branch with the same value) and rewrites the branch name inside the installed workflows (release PR head, CI triggers, PR summary base). The branch is NOT created by this flag.",
|
|
187
187
|
omitted: "develop.",
|
|
188
188
|
example: "npx projectops --mode full --force --deploy-branch dev",
|
|
189
189
|
caution: "Despite the name this is the development branch (head of the release PR), not the branch that deploys. The deploying branch is the repository default branch.",
|
|
@@ -262,7 +262,7 @@ export const KEY_DETAILS = {
|
|
|
262
262
|
"project_types": { when: "Set by the wizard. Do not change after setup.", effect: "Decides which type workflows run and which build files are synced. The first entry is primary.", omitted: "Detected at install." },
|
|
263
263
|
"project_paths": { when: "Monorepos only.", effect: "Maps a type to its subfolder so version sync and workflow path filters look there.", omitted: "Every type at the repo root." },
|
|
264
264
|
"metadata.default_branch": { when: "Do not edit unless the repository default branch was renamed.", effect: "The branch whose pushes deploy and publish. Workflows trigger on it.", omitted: "main" },
|
|
265
|
-
"metadata.
|
|
265
|
+
"metadata.development_branch": { when: "Only if the development branch is not develop.", effect: "Head of the release PR; workflows rewrite their develop references to this name.", omitted: "develop", caution: "Old name deploy_branch is read when this key is missing. The wizard writes both with the same value; keep them equal if you edit by hand." },
|
|
266
266
|
"metadata.template.options.intent": { when: "Leave as is.", effect: "Remembers the project kind so a re-run does not ask the deploy questions again.", omitted: "Inferred from deploy and publish." },
|
|
267
267
|
"metadata.template.options.deploy": { when: "Edit via --deploy, not by hand.", effect: "Which deploy workflow family is installed on the next update (docker-ssh, vercel, none).", omitted: "docker-ssh for server stacks." },
|
|
268
268
|
"metadata.template.options.publish": { when: "Edit via --publish.", effect: "Which registry publish workflows are installed on the next update.", omitted: "[]" },
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// 타입별 프로젝트 경로
|
|
2
|
-
// resolve_project_paths L1362~1589 등가). 모노레포에서 각 타입의 버전 파일이
|
|
1
|
+
// 타입별 프로젝트 경로 감지·확정. 모노레포에서 각 타입의 버전 파일이
|
|
3
2
|
// 어느 폴더에 있는지 5단계 우선순위로 확정한다.
|
|
4
3
|
//
|
|
5
4
|
// io 주입 계약(readline-engine 시그니처 그대로):
|
|
@@ -16,8 +15,8 @@ import { normalizePath } from "../cli/args.js";
|
|
|
16
15
|
// 취소(ESC/Ctrl+C)는 CANCEL 심볼 — ui를 import하지 않고 심볼 여부로만 판정 (core→ui 역참조 방지)
|
|
17
16
|
const isCancel = (v) => typeof v === "symbol";
|
|
18
17
|
|
|
19
|
-
// 타입의 대표 마커
|
|
20
|
-
// detect.js는 미지 타입에 package.json을 기본
|
|
18
|
+
// 타입의 대표 마커 파일명.
|
|
19
|
+
// detect.js는 미지 타입에 package.json을 기본 반환 — 여기서는 빈 문자열이 되도록 래핑.
|
|
21
20
|
const KNOWN_MARKER_TYPES = new Set([
|
|
22
21
|
"flutter", "react", "node", "react-native", "react-native-expo", "python", "spring",
|
|
23
22
|
]);
|
|
@@ -26,7 +25,7 @@ export function markerForType(type) {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
// 디렉토리에 실재하는 마커 파일명 반환 — 보조 마커 포함, 없으면 대표 마커 (표시용).
|
|
29
|
-
// (
|
|
28
|
+
// (spring build.gradle/.kts/pom.xml, python pyproject/setup.py/requirements.txt)
|
|
30
29
|
export function existingMarkerInDir(type, dir) {
|
|
31
30
|
const primary = markerForType(type);
|
|
32
31
|
const names = primary ? [primary, ...extraMarkers(type)] : [];
|
|
@@ -47,7 +46,7 @@ function walkFindDirs(root, { prune, match, maxDepth = 3 }) {
|
|
|
47
46
|
const childDepth = depth + 1;
|
|
48
47
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
49
48
|
if (e.isDirectory()) {
|
|
50
|
-
// prune 폴더는 하위 전체 제외
|
|
49
|
+
// prune 폴더는 하위 전체 제외
|
|
51
50
|
if (prune.has(e.name)) continue;
|
|
52
51
|
// 자식 파일이 depth ≤ maxDepth 안에 들어올 때만 하강
|
|
53
52
|
if (childDepth < maxDepth) walk(childRel, childDepth);
|
|
@@ -59,13 +58,13 @@ function walkFindDirs(root, { prune, match, maxDepth = 3 }) {
|
|
|
59
58
|
}
|
|
60
59
|
};
|
|
61
60
|
walk("", 0);
|
|
62
|
-
return [...new Set(hits)].sort(); //
|
|
61
|
+
return [...new Set(hits)].sort(); // 중복 제거 + 정렬
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
// 타입별 마커 파일 후보
|
|
64
|
+
// 타입별 마커 파일 후보 검색.
|
|
66
65
|
// 반환: 후보 디렉토리 상대경로 배열 (루트는 ".").
|
|
67
66
|
export function findTypePathCandidates(root, type) {
|
|
68
|
-
// ── Spring 멀티모듈: settings.gradle(.kts) 폴더 = 모듈 루트로 축약
|
|
67
|
+
// ── Spring 멀티모듈: settings.gradle(.kts) 폴더 = 모듈 루트로 축약 ──
|
|
69
68
|
// version_manager가 그 폴더 아래 build.gradle 전부를 갱신하므로 하위 모듈을 펼치지 않는다.
|
|
70
69
|
// android/ 의 settings.gradle(Flutter/RN)은 spring이 아니므로 prune.
|
|
71
70
|
if (type === "spring") {
|
|
@@ -92,7 +91,7 @@ export function findTypePathCandidates(root, type) {
|
|
|
92
91
|
"node_modules", ".git", "build", "dist", ".dart_tool", "android", "ios",
|
|
93
92
|
".gradle", "venv", ".venv", "__pycache__",
|
|
94
93
|
]);
|
|
95
|
-
// 우선순위 높은 마커에서 발견되면 그것만 사용
|
|
94
|
+
// 우선순위 높은 마커에서 발견되면 그것만 사용
|
|
96
95
|
let found = [];
|
|
97
96
|
for (const n of names) {
|
|
98
97
|
found = walkFindDirs(root, { prune, match: (name) => name === n });
|
|
@@ -101,13 +100,13 @@ export function findTypePathCandidates(root, type) {
|
|
|
101
100
|
|
|
102
101
|
return found.filter((d) => {
|
|
103
102
|
if (type === "flutter") {
|
|
104
|
-
// example/ 제외 + lib/ 동반 확인 — 오탐 방지
|
|
103
|
+
// example/ 제외 + lib/ 동반 확인 — 오탐 방지
|
|
105
104
|
if (d.includes("example")) return false;
|
|
106
105
|
const libDir = d === "." ? join(root, "lib") : join(root, d, "lib");
|
|
107
106
|
if (!existsSync(libDir)) return false;
|
|
108
107
|
}
|
|
109
108
|
if (type === "spring") {
|
|
110
|
-
// Flutter/RN의 android/build.gradle 오탐 제외
|
|
109
|
+
// Flutter/RN의 android/build.gradle 오탐 제외
|
|
111
110
|
if (d.includes("android")) return false;
|
|
112
111
|
}
|
|
113
112
|
return true;
|
|
@@ -115,7 +114,7 @@ export function findTypePathCandidates(root, type) {
|
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
// 선택된 모든 타입의 경로를 감지·확인하여 Map<type,path> 확정
|
|
118
|
-
// (
|
|
117
|
+
// (5단계 우선순위).
|
|
119
118
|
// ① paths에 이미 있음(--paths) → 유지
|
|
120
119
|
// ② 루트에 마커 존재 → "." 자동
|
|
121
120
|
// ③ existingPaths(version.yml 저장값)
|
|
@@ -127,11 +126,11 @@ export async function resolveProjectPaths({
|
|
|
127
126
|
}) {
|
|
128
127
|
const say = io.log || ((m) => process.stderr.write(`${m}\n`));
|
|
129
128
|
const result = new Map(paths); // --paths 사전값 유지 (호출부 Map은 불변)
|
|
130
|
-
const targets = types.filter((t) => t !== "basic"); // basic은 경로 불필요
|
|
129
|
+
const targets = types.filter((t) => t !== "basic"); // basic은 경로 불필요
|
|
131
130
|
if (targets.length === 0) return result;
|
|
132
131
|
|
|
133
132
|
const total = targets.length;
|
|
134
|
-
// ── 도입부 안내 (
|
|
133
|
+
// ── 도입부 안내 (감지 결과 + 무엇을 할지 설명) ──
|
|
135
134
|
say("");
|
|
136
135
|
if (total > 1) say(msg("paths.multi", { total }));
|
|
137
136
|
else say(msg("paths.single", { type: targets[0] }));
|
|
@@ -145,13 +144,13 @@ export async function resolveProjectPaths({
|
|
|
145
144
|
idx += 1;
|
|
146
145
|
const prog = `[${idx}/${total}]`;
|
|
147
146
|
|
|
148
|
-
// ① --paths 등으로 이미 지정됨 → 최우선
|
|
147
|
+
// ① --paths 등으로 이미 지정됨 → 최우선
|
|
149
148
|
if (result.get(t)) {
|
|
150
149
|
say(msg("paths.byFlag", { type: t, path: result.get(t) }));
|
|
151
150
|
continue;
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
// ② 루트에 마커 존재 → "." 자동 확정 (
|
|
153
|
+
// ② 루트에 마커 존재 → "." 자동 확정 (보조 마커 포함)
|
|
155
154
|
const rootMarker = existingMarkerInDir(t, root);
|
|
156
155
|
if (rootMarker && existsSync(join(root, rootMarker))) {
|
|
157
156
|
result.set(t, ".");
|
|
@@ -159,14 +158,14 @@ export async function resolveProjectPaths({
|
|
|
159
158
|
continue;
|
|
160
159
|
}
|
|
161
160
|
|
|
162
|
-
// ③ 기존 version.yml 저장값 → 기본 제안값
|
|
161
|
+
// ③ 기존 version.yml 저장값 → 기본 제안값
|
|
163
162
|
const existing = existingPaths.get(t) || "";
|
|
164
163
|
|
|
165
|
-
// ④ 후보 검색
|
|
164
|
+
// ④ 후보 검색
|
|
166
165
|
const candidates = findTypePathCandidates(root, t);
|
|
167
166
|
let chosen = "";
|
|
168
167
|
|
|
169
|
-
// ── ⑤-a 비대화형 (--force 또는 TTY
|
|
168
|
+
// ── ⑤-a 비대화형 (--force 또는 TTY 없음) ──
|
|
170
169
|
if (force || !tty) {
|
|
171
170
|
if (existing) {
|
|
172
171
|
chosen = existing;
|
|
@@ -182,7 +181,7 @@ export async function resolveProjectPaths({
|
|
|
182
181
|
continue;
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
// ── ⑤-b 대화형: 후보 개수별 분기
|
|
184
|
+
// ── ⑤-b 대화형: 후보 개수별 분기 + 타입 탈출구 (#487) ──
|
|
186
185
|
const EXCLUDE = "이 타입 제외"; // 센티넬 — 한국어 value로 노출 (기존 "직접 입력" 패턴)
|
|
187
186
|
let excluded = false;
|
|
188
187
|
if (candidates.length === 1) {
|
|
@@ -206,7 +205,7 @@ export async function resolveProjectPaths({
|
|
|
206
205
|
} else if (candidates.length > 1) {
|
|
207
206
|
say("");
|
|
208
207
|
say(msg("paths.multiFound", { prog, type: t, count: candidates.length }));
|
|
209
|
-
// 후보들 + '직접 입력'/'이 타입 제외' 메뉴 — value 자체를 한국어로 (센티넬 노출
|
|
208
|
+
// 후보들 + '직접 입력'/'이 타입 제외' 메뉴 — value 자체를 한국어로 (센티넬 노출 방지)
|
|
210
209
|
const options = candidates.map((c) => ({
|
|
211
210
|
value: c,
|
|
212
211
|
label: `${c} (${existingMarkerInDir(t, c === "." ? root : join(root, c))})`,
|
|
@@ -214,7 +213,7 @@ export async function resolveProjectPaths({
|
|
|
214
213
|
options.push({ value: "직접 입력", label: msg("paths.manual") });
|
|
215
214
|
options.push({ value: EXCLUDE, label: msg("paths.exclude", { type: t }) });
|
|
216
215
|
const sel = await io.select({ message: msg("paths.selectPrompt", { type: t }), options });
|
|
217
|
-
// ESC(취소)도 직접 입력으로 폴백
|
|
216
|
+
// ESC(취소)도 직접 입력으로 폴백
|
|
218
217
|
if (sel === EXCLUDE) excluded = true;
|
|
219
218
|
else if (!isCancel(sel) && sel != null && sel !== "직접 입력") chosen = sel;
|
|
220
219
|
} else {
|
|
@@ -222,7 +221,7 @@ export async function resolveProjectPaths({
|
|
|
222
221
|
say(msg("paths.notFound", { prog, type: t }));
|
|
223
222
|
}
|
|
224
223
|
|
|
225
|
-
// ── 직접 입력 루프 (위에서 미확정
|
|
224
|
+
// ── 직접 입력 루프 (위에서 미확정 시) — 제외 탈출구 포함 (#487) ──
|
|
226
225
|
while (!chosen && !excluded) {
|
|
227
226
|
const hintMarker = existingMarkerInDir(t, root);
|
|
228
227
|
let prompt = msg("paths.inputPrompt", { type: t, marker: hintMarker });
|
|
@@ -231,9 +230,9 @@ export async function resolveProjectPaths({
|
|
|
231
230
|
let input = await io.text({ message: prompt, defaultValue: "" });
|
|
232
231
|
if (isCancel(input) || input == null) input = ""; // ESC → 빈값 (아래 폴백)
|
|
233
232
|
input = String(input).trim();
|
|
234
|
-
// 빈값 → 기존값 또는 루트
|
|
233
|
+
// 빈값 → 기존값 또는 루트 — normalizePath 전에 판정
|
|
235
234
|
input = input === "" ? (existing || ".") : normalizePath(input);
|
|
236
|
-
// 검증: 입력 경로에 마커 존재 확인 (보조 마커
|
|
235
|
+
// 검증: 입력 경로에 마커 존재 확인 (보조 마커 포함)
|
|
237
236
|
const m = existingMarkerInDir(t, input === "." ? root : join(root, input));
|
|
238
237
|
if (m && existsSync(join(root, input === "." ? "" : input, m))) {
|
|
239
238
|
chosen = input;
|
|
@@ -262,7 +261,7 @@ export async function resolveProjectPaths({
|
|
|
262
261
|
say(` ✅ ${t} → ${chosen}`);
|
|
263
262
|
}
|
|
264
263
|
|
|
265
|
-
// ── 요약 + 같은 마커 파일 중복 경고
|
|
264
|
+
// ── 요약 + 같은 마커 파일 중복 경고 ──
|
|
266
265
|
say("");
|
|
267
266
|
say(msg("paths.summary"));
|
|
268
267
|
const fileToTypes = new Map(); // 마커 파일 상대경로 → 그 파일을 쓰는 타입들
|
|
@@ -275,7 +274,7 @@ export async function resolveProjectPaths({
|
|
|
275
274
|
}
|
|
276
275
|
for (const [file, ts] of fileToTypes) {
|
|
277
276
|
if (ts.length > 1) {
|
|
278
|
-
// 멱등 동작이라 막지는 않고 경고만
|
|
277
|
+
// 멱등 동작이라 막지는 않고 경고만
|
|
279
278
|
say(msg("paths.duplicate", { types: ts.join(" "), file }));
|
|
280
279
|
say(msg("paths.duplicateNote"));
|
|
281
280
|
}
|
package/src/core/paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 경로 상수
|
|
1
|
+
// 경로 상수
|
|
2
2
|
export const PATHS = {
|
|
3
3
|
tempDir: ".template_download_temp",
|
|
4
4
|
versionFile: "version.yml",
|
|
@@ -7,8 +7,12 @@ export const PATHS = {
|
|
|
7
7
|
projectTypesDir: "project-types",
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
export const
|
|
10
|
+
// 템플릿 레포 주소 정본: 소유자/이름 > 웹·git·raw 주소 (src/ 직접 기재는 no-repo-url-literal 규칙이 막는다)
|
|
11
|
+
export const TEMPLATE_REPO_OWNER = "Cassiiopeia";
|
|
12
|
+
export const TEMPLATE_REPO_SLUG = `${TEMPLATE_REPO_OWNER}/projectops`;
|
|
13
|
+
export const TEMPLATE_REPO_WEB_URL = `https://github.com/${TEMPLATE_REPO_SLUG}`;
|
|
14
|
+
export const TEMPLATE_RAW_URL = `https://raw.githubusercontent.com/${TEMPLATE_REPO_SLUG}/main`;
|
|
15
|
+
export const TEMPLATE_REPO = `${TEMPLATE_REPO_WEB_URL}.git`;
|
|
12
16
|
export const WORKFLOW_PREFIX = "PROJECT";
|
|
13
17
|
export const WORKFLOW_COMMON_PREFIX = "PROJECT-COMMON";
|
|
14
18
|
export const WORKFLOW_TEMPLATE_INIT = "PROJECT-TEMPLATE-INITIALIZER.yaml";
|
|
@@ -89,3 +89,17 @@ export function resolveOptions({ flags = {}, existing = null, types = [], cwd =
|
|
|
89
89
|
};
|
|
90
90
|
return { values, sources };
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
// version.yml options 로 기록되는 값 (resolveOptions 결과 이름 그대로)
|
|
94
|
+
// 진입점(full·version)이 각자 고르면 빠진 옵션이 생성기 기본값으로 되돌아간다 (#909: version 모드 ai_summary false > true)
|
|
95
|
+
const TEMPLATE_OPTION_FIELDS = [
|
|
96
|
+
"intent", "deployTarget", "publishTargets", "includeSecretBackup", "aiPrSummary", "projectsSync",
|
|
97
|
+
"changelogProvider", "changelogBaseUrl", "codeReviewCoderabbit", "semverAuto", "appRelease",
|
|
98
|
+
"excludedWorkflows", "storeLocales", "storeLocalesIos", "storeLocalesPlay", "closeOnRelease", "language", "labelStyle",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/** 실행 context(resolveOptions 값이 펼쳐진 것) > buildVersionYml templateOptions */
|
|
102
|
+
export function toTemplateOptions(context, { mode, optionsDate }) {
|
|
103
|
+
const options = Object.fromEntries(TEMPLATE_OPTION_FIELDS.map((field) => [field, context[field]]));
|
|
104
|
+
return { ...options, templateVersion: context.templateVersion, mode, optionsDate };
|
|
105
|
+
}
|
package/src/core/run-trace.js
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { writeText } from "./fsutil.js";
|
|
7
7
|
import { getLang } from "../i18n/index.js";
|
|
8
8
|
|
|
9
|
-
// 진단 로그 위치 (#561). docs/
|
|
9
|
+
// 진단 로그 위치 (#561). 추적되는 docs/ 가 아닌 곳에 둔다 — 이 기록은 사람이 읽는 문서가
|
|
10
10
|
// 아니라 Agent가 "지난 실행에서 무슨 일이 있었나"를 확인하는 자료다.
|
|
11
11
|
// 폴더 안에 .gitignore를 함께 써서 폴더가 자기 규칙을 들고 다닌다(루트 .gitignore 무수정).
|
|
12
12
|
// 형제인 .github/.projectops/baseline.json은 팀원 공유 자산이라 계속 추적된다.
|