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/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**Status: actively maintained.** npm `latest` is exactly what is on the `main` branch; work in progress on `develop` is never published. Versions can still ship several times a day. To stay on a version you have tested, run `npx projectops@<version>` ([releases](https://github.com/Cassiiopeia/projectops/releases)).
|
|
12
12
|
|
|
13
13
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
14
|
-
## Latest version : v4.59.
|
|
14
|
+
## Latest version : v4.59.1 (2026-10-17)
|
|
15
15
|
|
|
16
16
|
[View full version history](CHANGELOG.md)
|
|
17
17
|
|
|
@@ -183,7 +183,7 @@ What it changed: descriptions were cut to short English triggers, a shared contr
|
|
|
183
183
|
| `/pro-ssh` | Connect to a remote server over SSH and run commands (AWS EC2, Synology NAS, Linux, and more) |
|
|
184
184
|
| `/pro-skill-creator` | Create, review, and improve a Skill (CREATE / REVIEW / IMPROVE modes) |
|
|
185
185
|
|
|
186
|
-
**Extras** (optional plugin): `/pro-
|
|
186
|
+
**Extras** (optional plugin): `/pro-code-consult` (keep AI-written code maintainable: audit, refactor one item at a time, check each diff, freeze violations), `/pro-synology-expose` — `claude plugin install projectops-extras@projectops-marketplace --scope user`
|
|
187
187
|
|
|
188
188
|
</details>
|
|
189
189
|
|
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// CLI 인자
|
|
1
|
+
// CLI 인자 파싱.
|
|
2
2
|
import { existsSync, statSync, realpathSync } from "node:fs";
|
|
3
3
|
import { join, relative, isAbsolute } from "node:path";
|
|
4
4
|
import { VALID_TYPES } from "../context.js";
|
|
@@ -196,7 +196,7 @@ export function parseArgs(argv) {
|
|
|
196
196
|
|
|
197
197
|
export class CliError extends Error {}
|
|
198
198
|
|
|
199
|
-
// 경로
|
|
199
|
+
// 경로 정규화: 앞뒤 공백·\→/·끝 /·앞 ./ 제거, 빈값→"."
|
|
200
200
|
export function normalizePath(p) {
|
|
201
201
|
let s = String(p).trim();
|
|
202
202
|
s = s.replace(/\\/g, "/");
|
package/src/commands/doctor.js
CHANGED
|
@@ -134,7 +134,9 @@ export function localChecks(cwd = ".") {
|
|
|
134
134
|
const needsActionsWrite = [];
|
|
135
135
|
for (const f of files) {
|
|
136
136
|
const body = readFileSync(join(wfDir, f), "utf8");
|
|
137
|
-
|
|
137
|
+
// 다른 워크플로우를 깨우는 파일만 — 주석 줄의 언급(깨워지는 쪽 설명)은 호출이 아니다
|
|
138
|
+
const code = body.split("\n").filter((line) => !/^\s*#/.test(line)).join("\n");
|
|
139
|
+
if (!code.includes("dispatch_downstream.py")) continue;
|
|
138
140
|
const perm = body.match(/^permissions:\n((?:\s+.*\n)+)/m);
|
|
139
141
|
const hasActionsWrite = perm ? /^\s+actions:\s*write/m.test(perm[1]) : false;
|
|
140
142
|
if (!hasActionsWrite) needsActionsWrite.push(f);
|
package/src/commands/full.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// full 모드
|
|
1
|
+
// full 모드 오케스트레이터.
|
|
2
2
|
// 복사 순서: version.yml → readme → workflows → (deploy블록) → scripts → config →
|
|
3
3
|
// util(타입별) → issue → discussion → coderabbit → gitignore → setup_guide → (옵션저장)
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -21,18 +21,18 @@ import { applyLabelStyle } from "../core/label-style.js";
|
|
|
21
21
|
import { applyRepoLanguage } from "../core/repo-language.js";
|
|
22
22
|
import { t } from "../i18n/index.js";
|
|
23
23
|
import { verifyInstall } from "../core/verify.js";
|
|
24
|
+
import { toTemplateOptions } from "../core/resolve-options.js";
|
|
24
25
|
|
|
25
26
|
// context: { version, types, paths:Map, branch, versionCode, deployTarget, publishTargets, includeSecretBackup,
|
|
26
27
|
// force, repoName, resolvers, now, today }
|
|
27
28
|
// tempDir: 획득된 템플릿. targetRoot: 통합 대상.
|
|
28
29
|
export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
30
|
+
// 옵션 값은 resolveOptions 가 정해 context 에 넣는다 — 여기서 기본값을 다시 고르지 않는다
|
|
29
31
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
30
|
-
force = true, now, today, templateVersion = "
|
|
31
|
-
deployTarget
|
|
32
|
-
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
33
|
-
deployBranch = "", intent = null, semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = "en", excludedWorkflows = null, storeLocales = null, storeLocalesIos = null, storeLocalesPlay = null } = context;
|
|
32
|
+
force = true, now, today, templateVersion, deployBranch = "",
|
|
33
|
+
deployTarget, publishTargets, codeReviewCoderabbit, labelStyle, language } = context;
|
|
34
34
|
|
|
35
|
-
// project_paths 마커 계산 (
|
|
35
|
+
// project_paths 마커 계산 (대표 마커명)
|
|
36
36
|
const pathMarkers = new Map();
|
|
37
37
|
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
38
38
|
|
|
@@ -46,7 +46,7 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
46
46
|
const deployValues = mergeDeployValues(existsSync(vyFile) ? readFileSync(vyFile, "utf8") : "",
|
|
47
47
|
wfCounters.deployValues || new Map()); // Map<type, Map<key,value>
|
|
48
48
|
|
|
49
|
-
// 1. version.yml 생성 (전체 재생성 — metadata → deploy → template
|
|
49
|
+
// 1. version.yml 생성 (전체 재생성 — metadata → deploy → template 순)
|
|
50
50
|
step("write-version-yml", () => writeText(join(targetRoot, PATHS.versionFile),
|
|
51
51
|
buildVersionYml({
|
|
52
52
|
version, types, paths, pathMarkers, branch, deployBranch, versionCode, now, today,
|
|
@@ -54,16 +54,14 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
54
54
|
// 생성기가 모르는 키(issue_helper 등)를 지킨다 — version.yml 을 매번 다시 쓰므로 안 그러면 업데이트마다 사라진다 (#835)
|
|
55
55
|
...customYml(vyFile),
|
|
56
56
|
updatedBy: resolveUpdatedBy(existsSync(vyFile) ? readFileSync(vyFile, "utf8") : "", targetRoot), // #811
|
|
57
|
-
templateOptions:
|
|
58
|
-
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, intent, mode: "full", semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales, storeLocalesIos, storeLocalesPlay },
|
|
57
|
+
templateOptions: toTemplateOptions(context, { mode: "full", optionsDate: today }),
|
|
59
58
|
})), { version, versionCode });
|
|
60
59
|
|
|
61
60
|
// 2. README 버전 섹션
|
|
62
61
|
step("update-readme", () => addVersionSectionToReadme(version, targetRoot), { version });
|
|
63
62
|
|
|
64
63
|
// 5. scripts / config
|
|
65
|
-
// 워크플로우 밖 영역도 기록한다 (#561) —
|
|
66
|
-
// 로그만 보고 알 수 없었다.
|
|
64
|
+
// 워크플로우 밖 영역도 기록한다 (#561) — "내 스크립트가 갱신됐나"를 로그만 보고 알 수 있게.
|
|
67
65
|
step("copy-scripts", () => copyScripts(tempDir, targetRoot));
|
|
68
66
|
step("copy-config", () => copyConfigFolder(tempDir, targetRoot));
|
|
69
67
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 대화형 마법사 (
|
|
1
|
+
// 대화형 마법사 (#446 UI 5층).
|
|
2
2
|
// io 주입으로 테스트 가능. 실제 실행은 src/ui/prompts.js 함수를 io로 넘긴다.
|
|
3
3
|
// 새 시각 층(banner/detectionLog/analysisCard/ideStatus/installKind/summary)과 저수준 엔진(engineIo)은
|
|
4
4
|
// io의 "옵셔널 멤버" — 스텁이 생략하면 해당 층만 건너뛰고 실행 계약은 동일하다.
|
|
@@ -53,7 +53,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
53
53
|
let skipRecord = false;
|
|
54
54
|
if (io === prompts) trace.mirrorStart();
|
|
55
55
|
try {
|
|
56
|
-
// 템플릿 먼저 획득 — 배너에 실제 템플릿 버전을 표시
|
|
56
|
+
// 템플릿 먼저 획득 — 배너에 실제 템플릿 버전을 표시
|
|
57
57
|
trace.step("acquire-template", () => acquireTemplate({ tempDir, source }), { source: source?.type || "git" });
|
|
58
58
|
const templateVersion = readTemplateVersion(tempDir);
|
|
59
59
|
traceTo = templateVersion;
|
|
@@ -62,7 +62,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
62
62
|
if (io.banner) io.banner({ version: templateVersion, modeLabel: t("banner.interactive") });
|
|
63
63
|
else io.intro?.(`projectops, ${t("banner.interactive")}`);
|
|
64
64
|
|
|
65
|
-
// 기존 version.yml — version/version_code/paths/옵션 보존의 단일 진실
|
|
65
|
+
// 기존 version.yml — version/version_code/paths/옵션 보존의 단일 진실
|
|
66
66
|
const vyPath = join(cwd, "version.yml");
|
|
67
67
|
const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
|
|
68
68
|
traceFrom = existing?.templateVersion || "";
|
|
@@ -82,7 +82,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
82
82
|
const updateRun = picked === "update";
|
|
83
83
|
const mode = updateRun ? (existing?.templateMode || "full") : picked;
|
|
84
84
|
|
|
85
|
-
// Breaking Changes 게이트 (
|
|
85
|
+
// Breaking Changes 게이트 (모든 모드 공통, 대화형은 확인 질문)
|
|
86
86
|
let breakingReport = null; // #493 — 통과 구간 항목을 가이드에 조치 방법 전문으로 임베드
|
|
87
87
|
const proceed = await trace.stepAsync("breaking-check", () => runBreakingCheck({
|
|
88
88
|
cwd, tempDir, templateVersion,
|
|
@@ -194,7 +194,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
194
194
|
intent = r.intent;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// 확인/수정 루프 — ESC는 '머무르기' (
|
|
197
|
+
// 확인/수정 루프 — ESC는 '머무르기' (명시적 '아니오'만 종료)
|
|
198
198
|
let paths = new Map();
|
|
199
199
|
let confirmed = false;
|
|
200
200
|
// 업데이트 모드 — 카드는 보여주되 확인 루프 생략 (#502: 설정 변경은 기존 모드의 몫)
|
|
@@ -227,7 +227,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
227
227
|
const t = await io.selectTypes(types);
|
|
228
228
|
trace.event("prompt", "types", (Array.isArray(t) ? t : []).join(",") || "", { before: types });
|
|
229
229
|
if (!isCancel(t) && Array.isArray(t) && t.length) {
|
|
230
|
-
// 타입 집합이 실제로 바뀌면 경로 재해석 대상으로 초기화 (
|
|
230
|
+
// 타입 집합이 실제로 바뀌면 경로 재해석 대상으로 초기화 (정렬 집합 비교)
|
|
231
231
|
const oldSorted = [...types].sort().join(",");
|
|
232
232
|
types = t.filter((x) => VALID_TYPES.includes(x));
|
|
233
233
|
if ([...types].sort().join(",") !== oldSorted) paths = new Map();
|
|
@@ -235,7 +235,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
235
235
|
} else if (what === "version") {
|
|
236
236
|
const v = await io.askText(t("flow.askVersion"), version);
|
|
237
237
|
if (!isCancel(v) && v !== version) {
|
|
238
|
-
// semver 형식 검증
|
|
238
|
+
// semver 형식 검증
|
|
239
239
|
if (/^\d+\.\d+\.\d+$/.test(v)) version = v;
|
|
240
240
|
else io.note?.(t("flow.badVersion"), t("flow.badVersionTitle"));
|
|
241
241
|
}
|
|
@@ -273,7 +273,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
// 경로 확정 (
|
|
276
|
+
// 경로 확정 (full/version만. 저장값·후보 스캔·질문)
|
|
277
277
|
if (mode === "full" || mode === "version") {
|
|
278
278
|
paths = await resolveProjectPaths({
|
|
279
279
|
root: cwd, types, paths, existingPaths: existing?.paths ?? new Map(),
|
|
@@ -285,7 +285,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
285
285
|
for (const t of types) if (t !== "basic" && !paths.has(t)) paths.set(t, existing?.paths.get(t) || ".");
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
// @wizard env 계획 질문 (
|
|
288
|
+
// @wizard env 계획 질문 (full/workflows만).
|
|
289
289
|
// 업데이트 모드는 생략 — 신규 파일은 기본값, 교체 파일은 아래 carryover가 기존 실값을 이월한다.
|
|
290
290
|
const resolvers = makeResolvers(cwd, repoName, paths);
|
|
291
291
|
let envValues = new Map(), envUseDefaults = true;
|
|
@@ -334,7 +334,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
334
334
|
});
|
|
335
335
|
ctx.templateVersion = templateVersion;
|
|
336
336
|
|
|
337
|
-
// 기존 워크플로우 충돌 3지선 — 타입당 1회 결정을 파일에 캐시 적용
|
|
337
|
+
// 기존 워크플로우 충돌 3지선 — 타입당 1회 결정을 파일에 캐시 적용
|
|
338
338
|
// 업데이트 모드는 위에서 일괄 결정(updateDecisions)했으므로 3지선다를 건너뛴다 (#502).
|
|
339
339
|
let hooks = updateDecisions ? { decisions: updateDecisions } : {};
|
|
340
340
|
if (showOptional && io.engineIo?.select && !updateRun) {
|
|
@@ -352,7 +352,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
352
352
|
{ value: "template", label: t("flow.opt.template") },
|
|
353
353
|
],
|
|
354
354
|
});
|
|
355
|
-
perType.set(type, isCancel(sel) || sel == null ? "skip" : sel); // ESC = 건너뛰기
|
|
355
|
+
perType.set(type, isCancel(sel) || sel == null ? "skip" : sel); // ESC = 건너뛰기
|
|
356
356
|
}
|
|
357
357
|
decisions.set(filename, perType.get(type));
|
|
358
358
|
}
|
|
@@ -402,7 +402,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
402
402
|
else if (mode === "version") result = trace.step("install-version", () => runVersion(ctx, tempDir, cwd));
|
|
403
403
|
else if (mode === "workflows") result = trace.step("install-workflows", () => runWorkflows(ctx, tempDir, cwd, { ...hooks, trace }));
|
|
404
404
|
|
|
405
|
-
// 통합 후 IDE 스킬 제안 (
|
|
405
|
+
// 통합 후 IDE 스킬 제안 (사전 질문 게이트, 기본 N)
|
|
406
406
|
// 업데이트 모드(#502): 이미 설치된 IDE 스킬만 질문 없이 최신화 (미설치 IDE는 건드리지 않음)
|
|
407
407
|
if (updateRun) {
|
|
408
408
|
await skills({ templateVersion, tempDir, interactive: false, installedOnly: true });
|
|
@@ -430,7 +430,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
430
430
|
}).guidePath;
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
// 완료 요약
|
|
433
|
+
// 완료 요약
|
|
434
434
|
const summaryCtx = {
|
|
435
435
|
mode, types, version, deployBranch, deployBranchReady, migrationGuidePath,
|
|
436
436
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
package/src/commands/issues.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// issues 모드 (
|
|
1
|
+
// issues 모드 (이슈·PR 템플릿만 설치).
|
|
2
2
|
// 이슈/PR 템플릿 + Discussion 템플릿만 복사.
|
|
3
3
|
import { applyLabelStyle } from "../core/label-style.js";
|
|
4
4
|
import { applyRepoLanguage } from "../core/repo-language.js";
|
package/src/commands/skills.js
CHANGED
package/src/commands/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// version 모드 (.
|
|
1
|
+
// version 모드 (version.yml 생성·갱신).
|
|
2
2
|
// 순서: version.yml → readme → scripts → config → gitignore → setup_guide.
|
|
3
3
|
// (워크플로우 미복사 → deploy 블록 없음. util·issue·coderabbit도 없음.)
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -12,12 +12,12 @@ import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
|
12
12
|
import { applyLabelStyle } from "../core/label-style.js";
|
|
13
13
|
import { copyScripts, copyConfigFolder, copySetupGuide } from "../core/copy/simple.js";
|
|
14
14
|
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
15
|
+
import { toTemplateOptions } from "../core/resolve-options.js";
|
|
15
16
|
|
|
16
17
|
export function runVersion(context, tempDir, targetRoot = ".") {
|
|
18
|
+
// 옵션 값은 resolveOptions 가 정해 context 에 넣는다 — 여기서 기본값을 다시 고르지 않는다
|
|
17
19
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
18
|
-
now, today, templateVersion = "
|
|
19
|
-
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true,
|
|
20
|
-
deployBranch = "", recordMode = "version", semverAuto = true , appRelease = null, labelStyle = "en", closeOnRelease = null, projectsSync = null, language = null, excludedWorkflows = null, storeLocales = null, storeLocalesIos = null, storeLocalesPlay = null } = context;
|
|
20
|
+
now, today, templateVersion, deployBranch = "", recordMode = "version", labelStyle } = context;
|
|
21
21
|
|
|
22
22
|
const pathMarkers = new Map();
|
|
23
23
|
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
@@ -35,8 +35,7 @@ export function runVersion(context, tempDir, targetRoot = ".") {
|
|
|
35
35
|
updatedBy: resolveUpdatedBy(existsSync(vyFile) ? readFileSync(vyFile, "utf8") : "", targetRoot), // #811
|
|
36
36
|
// mode(#502): version 모드가 기존 full 통합 기록을 "version"으로 강등하지 않도록
|
|
37
37
|
// 호출부가 recordMode로 기존 값을 넘긴다 (full이 우세 — 업데이트 재실행 범위 축소 방지).
|
|
38
|
-
templateOptions:
|
|
39
|
-
changelogProvider, changelogBaseUrl, codeReviewCoderabbit, mode: recordMode, semverAuto, appRelease, labelStyle, closeOnRelease, projectsSync, language, excludedWorkflows, storeLocales, storeLocalesIos, storeLocalesPlay },
|
|
38
|
+
templateOptions: toTemplateOptions(context, { mode: recordMode, optionsDate: today }),
|
|
40
39
|
}));
|
|
41
40
|
addVersionSectionToReadme(version, targetRoot);
|
|
42
41
|
copyScripts(tempDir, targetRoot);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// workflows 모드 (
|
|
1
|
+
// workflows 모드 (워크플로우만 설치).
|
|
2
2
|
// 순서: copy_workflows → update_version_yml_deploy(version.yml 있을 때만) → scripts → config → util(타입별) → setup_guide.
|
|
3
3
|
// (version.yml 생성 안 함 — 기존 version.yml이 있을 때만 deploy 블록 추가.)
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -13,7 +13,7 @@ import { installAgentGuide } from "../core/agent-guide.js";
|
|
|
13
13
|
import { verifyInstall } from "../core/verify.js";
|
|
14
14
|
|
|
15
15
|
export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
16
|
-
const { types = [], force = true, labelStyle
|
|
16
|
+
const { types = [], force = true, labelStyle } = context; // labelStyle 은 resolveOptions 가 정한다
|
|
17
17
|
const wf = copyWorkflows(context, tempDir, targetRoot, hooks);
|
|
18
18
|
|
|
19
19
|
const vy = join(targetRoot, PATHS.versionFile);
|
|
@@ -58,7 +58,7 @@ export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
58
58
|
return { workflows: wf, verification };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// 기존 version.yml에서 deploy: 블록을 제거하고 새로 append (
|
|
61
|
+
// 기존 version.yml에서 deploy: 블록을 제거하고 새로 append (멱등).
|
|
62
62
|
export function upsertDeployBlock(content, deployValues) {
|
|
63
63
|
// 기존 deploy: 블록 제거 (deploy: 라인 ~ 다음 최상위 키 전까지)
|
|
64
64
|
const lines = content.split(/\r?\n/);
|
package/src/context.js
CHANGED
|
@@ -5,7 +5,7 @@ export const VALID_TYPES = [
|
|
|
5
5
|
"react-native", "react-native-expo", "node", "python", "basic",
|
|
6
6
|
];
|
|
7
7
|
|
|
8
|
-
export const DEFAULT_VERSION = "1.3.14"; //
|
|
8
|
+
export const DEFAULT_VERSION = "1.3.14"; // 템플릿 버전 미상 시 (배너 폴백용 — breaking 비교엔 안 씀)
|
|
9
9
|
|
|
10
10
|
export function createContext(overrides = {}) {
|
|
11
11
|
return {
|
|
@@ -25,7 +25,7 @@ export function createContext(overrides = {}) {
|
|
|
25
25
|
changelogProvider: null, // 'commit'(기본) | 'coderabbit' | 'openai' | 'gemini' | 'claude' | 'ollama' | 'commit'
|
|
26
26
|
changelogBaseUrl: null, // ollama일 때만 값
|
|
27
27
|
codeReviewCoderabbit: null,
|
|
28
|
-
deployBranch: "", // 릴리스 PR head 브랜치 (#456). 빈 값=metadata.
|
|
28
|
+
deployBranch: "", // 릴리스 PR head 브랜치 (#456). 빈 값=metadata.development_branch 미출력
|
|
29
29
|
intent: null, // 프로젝트 성격 (#485 — app/library/both/none/manual). null=미설정(역추론)
|
|
30
30
|
semverAuto: null, // semver 자동 승격 (#546). null=미설정 → 신규·기존 모두 true (#814). 명시적 false 만 끈다
|
|
31
31
|
closeOnRelease: null, // 릴리스 시 완료 이슈 닫기 (#771). null=미설정(신규 true / 기존 레포 키 없음)
|
package/src/core/agent-guide.js
CHANGED
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
// 내용에 옵션 표를 박지 않는다 — 박으면 곧 낡는다. 표는 항상 `npx projectops --mode options --json` 이 정본이다.
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { writeText } from "./fsutil.js";
|
|
10
|
+
import { TEMPLATE_REPO_WEB_URL } from "./paths.js";
|
|
10
11
|
|
|
11
12
|
export const AGENT_GUIDE_PATH = ".github/.projectops/AGENT-GUIDE.md";
|
|
12
13
|
|
|
13
14
|
export function agentGuideText(templateVersion = "") {
|
|
14
15
|
return `# projectops — guide for AI agents
|
|
15
16
|
|
|
16
|
-
This repository is managed with [projectops](
|
|
17
|
+
This repository is managed with [projectops](${TEMPLATE_REPO_WEB_URL})${templateVersion ? ` (installed template ${templateVersion})` : ""}.
|
|
17
18
|
GitHub Actions workflows in \`.github/workflows/PROJECT-*\` handle versioning, release notes, CI/CD and issue automation.
|
|
18
19
|
This file is generated by the installer and overwritten on update. Do not edit it.
|
|
19
20
|
|
package/src/core/assets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 템플릿 자산 획득 + 제외
|
|
1
|
+
// 템플릿 자산 획득 + 제외 적용.
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { mkdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -11,7 +11,7 @@ import { TEMPLATE_REPO } from "./paths.js";
|
|
|
11
11
|
// source:
|
|
12
12
|
// {type:'git', repo?} → git clone --depth 1 (기본 repo=TEMPLATE_REPO)
|
|
13
13
|
// {type:'local', path} → 로컬 트리 복사 (네트워크 없는 등가 검증용)
|
|
14
|
-
// 이미 tempDir/.github 있으면 스킵(
|
|
14
|
+
// 이미 tempDir/.github 있으면 스킵(중복 호출 방지). 획득 후 applyExclusions 자동 호출.
|
|
15
15
|
export function acquireTemplate({ tempDir, source = { type: "git" } }) {
|
|
16
16
|
if (exists(join(tempDir, ".github"))) {
|
|
17
17
|
applyExclusions(tempDir);
|
|
@@ -30,7 +30,7 @@ export function acquireTemplate({ tempDir, source = { type: "git" } }) {
|
|
|
30
30
|
applyExclusions(tempDir);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// DOCS_TO_REMOVE + PLUGIN_ITEMS_TO_REMOVE 삭제. skills/ 는
|
|
33
|
+
// DOCS_TO_REMOVE + PLUGIN_ITEMS_TO_REMOVE 삭제. skills/ 는 보존.
|
|
34
34
|
export function applyExclusions(tempDir) {
|
|
35
35
|
for (const doc of DOCS_TO_REMOVE) remove(join(tempDir, doc));
|
|
36
36
|
for (const item of PLUGIN_ITEMS_TO_REMOVE) remove(join(tempDir, item));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// breaking-changes 확인 흐름
|
|
1
|
+
// breaking-changes 확인 흐름 배선.
|
|
2
2
|
// collectBreaking(순수 비교)은 breaking.js — 이 모듈은 로드(원격→clone본 폴백)·표시·확인 게이트.
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -8,7 +8,7 @@ import { t } from "../i18n/index.js";
|
|
|
8
8
|
|
|
9
9
|
const BC_URL = "https://raw.githubusercontent.com/Cassiiopeia/projectops/main/.github/config/breaking-changes.json";
|
|
10
10
|
|
|
11
|
-
// 원격 우선(3초 타임아웃) → clone된 템플릿의 번들본 폴백 → 둘 다 실패 시 null(조용히 스킵
|
|
11
|
+
// 원격 우선(3초 타임아웃) → clone된 템플릿의 번들본 폴백 → 둘 다 실패 시 null(조용히 스킵)
|
|
12
12
|
export async function loadBreakingJson(tempDir) {
|
|
13
13
|
try {
|
|
14
14
|
const res = await fetch(BC_URL, { signal: AbortSignal.timeout(3000) });
|
|
@@ -25,7 +25,7 @@ export async function loadBreakingJson(tempDir) {
|
|
|
25
25
|
// opts:
|
|
26
26
|
// cwd - 통합 대상 루트 (기존 version.yml에서 현재 템플릿 버전 읽음)
|
|
27
27
|
// tempDir - clone된 템플릿 (번들 폴백용)
|
|
28
|
-
// templateVersion - 설치하려는 템플릿 버전 (
|
|
28
|
+
// templateVersion - 설치하려는 템플릿 버전 (고정 DEFAULT_VERSION 아님 — 설계 D2)
|
|
29
29
|
// askYesNo - async(message, defaultYes)→bool. null이면 비대화형: 경고만 출력 후 진행
|
|
30
30
|
// loader - 테스트 주입용 (기본 loadBreakingJson)
|
|
31
31
|
// onItems - (#493) 통과 구간 항목 콜백 ({current, target, critical, warnings}) —
|
|
@@ -34,7 +34,7 @@ export async function runBreakingCheck({ cwd, tempDir, templateVersion, askYesNo
|
|
|
34
34
|
const vy = join(cwd, "version.yml");
|
|
35
35
|
if (!existsSync(vy)) return true; // 신규 통합 — 비교 기준 없음
|
|
36
36
|
const { templateVersion: current } = parseExisting(readFileSync(vy, "utf8"));
|
|
37
|
-
if (!current) return true; // 템플릿 메타 없음(unknown) —
|
|
37
|
+
if (!current) return true; // 템플릿 메타 없음(unknown) — 스킵
|
|
38
38
|
|
|
39
39
|
const json = await loader(tempDir);
|
|
40
40
|
if (!json) return true;
|
|
@@ -72,11 +72,11 @@ export async function runBreakingCheck({ cwd, tempDir, templateVersion, askYesNo
|
|
|
72
72
|
|
|
73
73
|
if (critical.length > 0) {
|
|
74
74
|
if (askYesNo) {
|
|
75
|
-
// 대화형: 명시 확인 없으면 중단 (기본 N
|
|
75
|
+
// 대화형: 명시 확인 없으면 중단 (기본 N)
|
|
76
76
|
const ok = await askYesNo(t("breaking.askContinue"), false);
|
|
77
77
|
if (ok !== true) return false;
|
|
78
78
|
} else {
|
|
79
|
-
// 비대화형(--force): 게이트로 CI를 죽이지 않고 경고 후 진행 (
|
|
79
|
+
// 비대화형(--force): 게이트로 CI를 죽이지 않고 경고 후 진행 (CI 친화)
|
|
80
80
|
e(t("breaking.nonInteractive"));
|
|
81
81
|
}
|
|
82
82
|
}
|
package/src/core/breaking.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 버전 비교: v 접두 제거, 3자리 숫자 비교, 누락 자리=0
|
|
2
2
|
export function compareVersions(a, b) {
|
|
3
3
|
const parse = (v) => String(v).replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
4
4
|
const pa = parse(a), pb = parse(b);
|
|
@@ -11,7 +11,7 @@ export function compareVersions(a, b) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// breaking-changes.json에서 current < ver <= target 범위 항목 수집.
|
|
14
|
-
// ⚠️
|
|
14
|
+
// ⚠️ target은 하드코딩 1.3.14가 아니라 실제 templateVersion을 넘긴다 (D2).
|
|
15
15
|
// _ 로 시작하는 키(메타) 제외. severity critical / 그 외(warning).
|
|
16
16
|
export function collectBreaking(json, current, target) {
|
|
17
17
|
const critical = [], warnings = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// .coderabbit.yaml 복사 (
|
|
1
|
+
// .coderabbit.yaml 복사 (force 경로).
|
|
2
2
|
// SP2-B는 force/비대화형 경로만 구현. 대화형 덮어쓰기/건너뛰기 메뉴는 SP2-C.
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { exists, copyFileSync } from "../fsutil.js";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// .gitignore 보장 (
|
|
1
|
+
// .gitignore 보장 (항목 정규화 > 누락 확인 > 추가).
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
|
|
5
5
|
const REQUIRED_ENTRIES = ["/.idea", "/.claude/settings.local.json"];
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// 항목 정규화: 주석 제거·트림·앞 / 제거·앞 ./ 제거·뒤 / 제거. 빈값이면 원본.
|
|
8
8
|
export function normalizeGitignoreEntry(entry) {
|
|
9
9
|
let e = String(entry);
|
|
10
10
|
e = e.replace(/#.*$/, ""); // 주석 제거
|
|
@@ -43,7 +43,7 @@ export function ensureGitignore(targetRoot = ".") {
|
|
|
43
43
|
const toAdd = REQUIRED_ENTRIES.filter((e) => !entryExists(e, content));
|
|
44
44
|
if (toAdd.length === 0) return { created: false, added: [] };
|
|
45
45
|
|
|
46
|
-
// 파일 끝에 개행 없으면 추가
|
|
46
|
+
// 파일 끝에 개행 없으면 추가
|
|
47
47
|
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
48
48
|
content += "\n";
|
|
49
49
|
content += "# ====================================================================\n";
|
package/src/core/copy/readme.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// README 버전 섹션
|
|
1
|
+
// README 버전 섹션 추가.
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, readFileSync, appendFileSync } from "node:fs";
|
|
4
4
|
|
|
@@ -15,8 +15,7 @@ export function addVersionSectionToReadme(version, targetRoot = ".") {
|
|
|
15
15
|
if (content.includes(MARKER)) return "skip-marker";
|
|
16
16
|
if (VERSION_LINE_RE.test(content)) return "skip-version-line";
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
// (원본 파일이 개행으로 끝난다는 전제는 .sh와 동일 — heredoc은 원본 끝에 그대로 붙는다.)
|
|
18
|
+
// append 본문은 빈 줄 + "---"로 시작 (원본 파일이 개행으로 끝난다는 전제).
|
|
20
19
|
const section =
|
|
21
20
|
"\n" +
|
|
22
21
|
"---\n" +
|
package/src/core/copy/simple.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// 단순 복사 함수 (무조건 덮어쓰기류)
|
|
2
|
-
// 실측: template_integrator.sh 3818, 3845, 3872, 3895, 4114.
|
|
1
|
+
// 단순 복사 함수 (무조건 덮어쓰기류): scripts, config, issue, discussion, setup guide.
|
|
3
2
|
import { join, basename } from "node:path";
|
|
4
3
|
import { chmodSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { PATHS } from "../paths.js";
|
|
@@ -16,6 +15,9 @@ export function copyScripts(tempDir, targetRoot = ".") {
|
|
|
16
15
|
"changelog_manager.py",
|
|
17
16
|
"truncate_release_notes.sh", "truncate_release_notes.py",
|
|
18
17
|
"issue_helper.py",
|
|
18
|
+
// GitHub REST 호출 공통 (#914). issue_helper·changelog_notice·pr_summary_comment·dispatch_downstream·
|
|
19
|
+
// close_issues_on_release·store_deploy_config 가 import 하므로 빠지면 그 워크플로가 모두 죽는다.
|
|
20
|
+
"github_api.py",
|
|
19
21
|
// 릴리스 워크플로우가 PAT 없이 머지한 뒤 후속 워크플로우를 깨울 때 호출 (#551).
|
|
20
22
|
"dispatch_downstream.py",
|
|
21
23
|
// AI PR SUMMARY 워크플로우가 요약 댓글을 작성·갱신할 때 호출 (#553).
|
package/src/core/copy/util.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// util 모듈 복사 (
|
|
1
|
+
// util 모듈 복사 (force 경로).
|
|
2
2
|
// tempDir/.github/util/<type>/ 있으면 (force) .github/util/<type>/로 전체 복사, 모듈 수 카운트.
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { readdirSync } from "node:fs";
|
|
@@ -15,7 +15,7 @@ export function copyUtilModules(tempDir, type, { force = false } = {}, targetRoo
|
|
|
15
15
|
const dst = join(targetRoot, ".github", "util", type);
|
|
16
16
|
copyDirSync(src, dst);
|
|
17
17
|
|
|
18
|
-
// 하위 디렉토리 개수 = 모듈 수
|
|
18
|
+
// 하위 디렉토리 개수 = 모듈 수
|
|
19
19
|
// `_` 로 시작하는 폴더는 모듈이 아니라 모듈들이 공유하는 자산이다
|
|
20
20
|
// (예: flutter/_shared — 마법사 3종 공통 CSS/JS). 개수에서 제외한다.
|
|
21
21
|
let moduleCount = 0;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// 워크플로우 복사 엔진.
|
|
2
|
-
// 파일 안의 ".sh ... 등가" 표기는 그 삭제된 스크립트를 가리킨다 — 줄 번호는 더 이상 찾을 수 없다.
|
|
1
|
+
// 워크플로우 복사 엔진.
|
|
3
2
|
//
|
|
4
3
|
// 충돌(기존 파일과 다름)의 처리 결정은 hooks.decisions(Map<파일명, 'skip'|'backup'|'template'>)로 받는다.
|
|
5
4
|
// - 실제 마법사(commands/interactive.js)는 listWorkflowConflicts 로 충돌 목록을 얻어 직접 Map 을 만든다.
|
|
@@ -35,7 +34,7 @@ export function onlyTriggersOnDevelop(content) {
|
|
|
35
34
|
return branches.length > 0 && filtered >= needFilter && branches.every((b) => b === "develop");
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
// 한 파일에 env 치환을 적용해 대상 파일을
|
|
37
|
+
// 한 파일에 env 치환을 적용해 대상 파일을 갱신.
|
|
39
38
|
// values/useDefaults: env 계획(promptEnvPlan) 결과 — 미지정이면 기본값 경로(현행 force 동작).
|
|
40
39
|
// trace(#494): 치환 1건당 env.substituted 이벤트 (before/after 포함 — 치환 불일치류 버그의 즉시 발견 근거).
|
|
41
40
|
function configureEnv(targetPath, filename, { type, projectPath = ".", repoName = "", resolvers = {}, collectAsks = null, values = new Map(), useDefaults = true, trace = null }) {
|
|
@@ -173,11 +172,8 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
173
172
|
Object.defineProperty(counters, "onSkip", { value: onSkip, enumerable: false });
|
|
174
173
|
|
|
175
174
|
// (1) common — 타입별과 동일한 판정·보호를 받는다 (#560).
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// 프로젝트마다 뒤에 붙일 일이 다른 워크플로우는 common에 있어도 사용자가 고쳐 쓸 수밖에
|
|
179
|
-
// 없다. 고치지 않고는 쓸 수 없는 파일을, 고치면 백업도 없이 날아가는 규칙으로 관리하고
|
|
180
|
-
// 있었다. common/deploy조차 .bak을 남기는데 본체만 아무것도 남기지 않았다.
|
|
175
|
+
// 릴리스 파이프라인처럼 프로젝트마다 뒤에 붙일 일이 다른 워크플로우는 common 이어도 사용자가 고쳐 쓴다.
|
|
176
|
+
// 그래서 수정본을 무조건 덮어쓰지 않고 타입별과 같은 충돌 판정·백업을 거친다.
|
|
181
177
|
const commonDir = join(projectTypesDir, "common");
|
|
182
178
|
if (exists(commonDir)) {
|
|
183
179
|
const commonEnv = envOptsFor("common");
|
|
@@ -319,13 +315,13 @@ function recordBaseline(workflowsDir, targetRoot, counters, previous, templateVe
|
|
|
319
315
|
}
|
|
320
316
|
}
|
|
321
317
|
|
|
322
|
-
// changed(기존에 있고 내용이 바뀐) 파일 1개를 결정에 따라 처리 (
|
|
318
|
+
// changed(기존에 있고 내용이 바뀐) 파일 1개를 결정에 따라 처리 (3지선: 백업 교체·템플릿 사본·유지).
|
|
323
319
|
// 'skip'(기본): 기존 유지. 'backup': 기존→.bak 후 교체. 'template': 기존 유지 + 새 버전을 .template.yaml로.
|
|
324
320
|
function applyDecision(decision, srcDir, workflowsDir, filename, counters, trace = null, type = "") {
|
|
325
321
|
const src = join(srcDir, filename);
|
|
326
322
|
const dst = join(workflowsDir, filename);
|
|
327
323
|
if (decision === "backup") {
|
|
328
|
-
//
|
|
324
|
+
// 백업 교체: 기존을 .bak으로 백업 후 새 버전으로 교체
|
|
329
325
|
renameSync(dst, dst + ".bak");
|
|
330
326
|
copyFileSync(src, dst);
|
|
331
327
|
counters.copied++;
|
|
@@ -334,14 +330,14 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, trace
|
|
|
334
330
|
return;
|
|
335
331
|
}
|
|
336
332
|
if (decision === "template") {
|
|
337
|
-
//
|
|
333
|
+
// 템플릿 사본: `<이름>.template.yaml` — .yaml만 strip (.yml은 그대로 뒤에 붙음)
|
|
338
334
|
const templateName = (filename.endsWith(".yaml") ? filename.slice(0, -".yaml".length) : filename) + ".template.yaml";
|
|
339
|
-
copyFileSync(src, join(workflowsDir, templateName)); //
|
|
335
|
+
copyFileSync(src, join(workflowsDir, templateName)); // 기존 .template.yaml 덮어씀
|
|
340
336
|
counters.templateAdded++;
|
|
341
337
|
trace?.event("copy", "template-added", templateName, { original: filename });
|
|
342
338
|
return;
|
|
343
339
|
}
|
|
344
|
-
counters.skipped++; // 'skip'/미지정/ESC → 기존 유지 (
|
|
340
|
+
counters.skipped++; // 'skip'/미지정/ESC → 기존 유지 (force 기본)
|
|
345
341
|
// #654 — 새 템플릿을 incoming에 남기고 diff 줄 수를 detail로 함께 기록
|
|
346
342
|
const inc = counters.onSkip?.(srcDir, filename, type) ?? {};
|
|
347
343
|
trace?.event("copy", "skipped-conflict", filename, {
|
package/src/core/detect-fs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 실 파일시스템 프로젝트
|
|
1
|
+
// 실 파일시스템 프로젝트 감지.
|
|
2
2
|
// SP2-A detect.js 순수 함수를 fs/git으로 구동한다.
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join, basename } from "node:path";
|
|
@@ -18,7 +18,7 @@ function gitOut(root, args) {
|
|
|
18
18
|
} catch { return ""; }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// 확장자 스캔용 파일 목록 (
|
|
21
|
+
// 확장자 스캔용 파일 목록 (depth≤3, vendor 폴더 프루닝).
|
|
22
22
|
const SCAN_PRUNE = new Set([
|
|
23
23
|
"node_modules", ".git", "build", "dist", ".dart_tool",
|
|
24
24
|
"android", "ios", ".gradle", "venv", ".venv", "__pycache__",
|
|
@@ -45,7 +45,7 @@ export function listScanFiles(root, maxDepth = 3) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// 타입 감지 — version.yml의 project_types 최우선(source of truth), 없으면 마커 스캔.
|
|
48
|
-
// 마커도 전혀 없으면(basic 폴백) 확장자 빈도 스캔으로 2차 추천 (
|
|
48
|
+
// 마커도 전혀 없으면(basic 폴백) 확장자 빈도 스캔으로 2차 추천 (#458).
|
|
49
49
|
export function detectTypes(root) {
|
|
50
50
|
const vy = join(root, "version.yml");
|
|
51
51
|
if (existsSync(vy)) {
|
|
@@ -64,7 +64,7 @@ export function detectTypes(root) {
|
|
|
64
64
|
return byMarkers;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// 버전
|
|
67
|
+
// 버전 감지. jq 유무는 command 존재로 판정.
|
|
68
68
|
export function detectVersion(root, { hasJq } = {}) {
|
|
69
69
|
const read = readFile(root);
|
|
70
70
|
const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
|
|
@@ -103,7 +103,7 @@ function hasCommand(cmd) {
|
|
|
103
103
|
} catch { return false; }
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// Spring application*.yml 탐색
|
|
106
|
+
// Spring application*.yml 탐색
|
|
107
107
|
// find {base} -path "*/src/main/resources/application*.yml" | head -1 의 fs 재귀 구현.
|
|
108
108
|
// 반환: root 기준 상대경로 (예: "server/src/main/resources/application.yml") 또는 "".
|
|
109
109
|
export function findSpringAppYml(root, base = ".") {
|
|
@@ -130,7 +130,7 @@ export function findSpringAppYml(root, base = ".") {
|
|
|
130
130
|
return hit;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// 실 resolver 세트 생성
|
|
133
|
+
// 실 resolver 4종 세트 생성 — index/interactive 공용.
|
|
134
134
|
// paths: Map<type, path> (모노레포 경로).
|
|
135
135
|
export function makeResolvers(root, repoName, paths) {
|
|
136
136
|
const springBase = (t) => paths.get(t || "spring") || paths.get("spring") || ".";
|