project-auto-wizard 0.1.14 → 0.1.16
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/package.json +1 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +1 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +1 -0
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +1 -0
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +1 -0
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +1 -0
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml +1 -0
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +1 -0
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +1 -0
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +1 -0
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +1 -0
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +1 -0
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +1 -0
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +1 -0
- package/payload/workflows/spring/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +1 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +1 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +1 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +1 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +1 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +1 -0
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +1 -0
- package/src/cli/args.js +30 -9
- package/src/commands/dry-run.js +8 -4
- package/src/commands/full.js +7 -2
- package/src/commands/revert.js +35 -11
- package/src/commands/version.js +7 -1
- package/src/commands/workflows.js +4 -1
- package/src/core/copy/gitignore.js +34 -9
- package/src/core/copy/workflows.js +24 -15
- package/src/core/paths-resolve.js +11 -6
- package/src/core/version-yml.js +39 -4
- package/src/core/wizard-env.js +10 -1
- package/src/index.js +11 -5
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx project-auto-wizard
|
|
|
16
16
|
[](package.json)
|
|
17
17
|
|
|
18
18
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
19
|
-
## 최신 버전 : v0.1.
|
|
19
|
+
## 최신 버전 : v0.1.14 (2026-08-04)
|
|
20
20
|
|
|
21
21
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
22
22
|
|
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -32,6 +32,7 @@ export function parseArgs(argv) {
|
|
|
32
32
|
keepScripts: false,
|
|
33
33
|
};
|
|
34
34
|
const args = [...argv];
|
|
35
|
+
const seenFlags = new Set(); // L7: --nexus류 상호 모순 플래그 검증용
|
|
35
36
|
while (args.length > 0) {
|
|
36
37
|
const a = args.shift();
|
|
37
38
|
switch (a) {
|
|
@@ -74,15 +75,35 @@ export function parseArgs(argv) {
|
|
|
74
75
|
case "--keep-changelog": result.keepChangelog = true; break;
|
|
75
76
|
case "--keep-workflows": result.keepWorkflows = true; break;
|
|
76
77
|
case "--keep-scripts": result.keepScripts = true; break;
|
|
77
|
-
case "--nexus":
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
case "--no-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
case "--nexus":
|
|
79
|
+
if (seenFlags.has("--no-nexus")) throw new CliError("--nexus와 --no-nexus는 동시에 지정할 수 없습니다");
|
|
80
|
+
seenFlags.add("--nexus"); result.includeNexus = true; break;
|
|
81
|
+
case "--no-nexus":
|
|
82
|
+
if (seenFlags.has("--nexus")) throw new CliError("--nexus와 --no-nexus는 동시에 지정할 수 없습니다");
|
|
83
|
+
seenFlags.add("--no-nexus"); result.includeNexus = false; break;
|
|
84
|
+
case "--secret-backup":
|
|
85
|
+
if (seenFlags.has("--no-secret-backup")) throw new CliError("--secret-backup과 --no-secret-backup은 동시에 지정할 수 없습니다");
|
|
86
|
+
seenFlags.add("--secret-backup"); result.includeSecretBackup = true; break;
|
|
87
|
+
case "--no-secret-backup":
|
|
88
|
+
if (seenFlags.has("--secret-backup")) throw new CliError("--secret-backup과 --no-secret-backup은 동시에 지정할 수 없습니다");
|
|
89
|
+
seenFlags.add("--no-secret-backup"); result.includeSecretBackup = false; break;
|
|
90
|
+
case "--semver-auto":
|
|
91
|
+
if (seenFlags.has("--no-semver-auto")) throw new CliError("--semver-auto와 --no-semver-auto는 동시에 지정할 수 없습니다");
|
|
92
|
+
seenFlags.add("--semver-auto"); result.includeSemverAuto = true; break;
|
|
93
|
+
case "--no-semver-auto":
|
|
94
|
+
if (seenFlags.has("--semver-auto")) throw new CliError("--semver-auto와 --no-semver-auto는 동시에 지정할 수 없습니다");
|
|
95
|
+
seenFlags.add("--no-semver-auto"); result.includeSemverAuto = false; break;
|
|
83
96
|
case "--paths": result.pathsCsv = args.shift() ?? ""; break;
|
|
84
|
-
case "--main-branch":
|
|
85
|
-
|
|
97
|
+
case "--main-branch": {
|
|
98
|
+
const v = args.shift();
|
|
99
|
+
if (!v) throw new CliError("--main-branch에 빈 값을 지정할 수 없습니다");
|
|
100
|
+
result.mainBranch = v; break;
|
|
101
|
+
}
|
|
102
|
+
case "--develop-branch": {
|
|
103
|
+
const v = args.shift();
|
|
104
|
+
if (!v) throw new CliError("--develop-branch에 빈 값을 지정할 수 없습니다");
|
|
105
|
+
result.developBranch = v; break;
|
|
106
|
+
}
|
|
86
107
|
case "-h": case "--help": result.help = true; break;
|
|
87
108
|
default:
|
|
88
109
|
throw new CliError(`알 수 없는 옵션: ${a}`);
|
|
@@ -114,7 +135,7 @@ export function parsePathsCsv(csv) {
|
|
|
114
135
|
for (const pair of csv.split(",")) {
|
|
115
136
|
if (pair.trim() === "") continue;
|
|
116
137
|
const eq = pair.indexOf("=");
|
|
117
|
-
const type = (eq >= 0 ? pair.slice(0, eq) : pair).
|
|
138
|
+
const type = (eq >= 0 ? pair.slice(0, eq) : pair).replace(/\s/g, "");
|
|
118
139
|
const rawPath = eq >= 0 ? pair.slice(eq + 1) : "";
|
|
119
140
|
if (!VALID_TYPES.includes(type)) {
|
|
120
141
|
throw new CliError(`--paths에 지원하지 않는 타입: '${type}'`);
|
package/src/commands/dry-run.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PATHS } from "../core/paths.js";
|
|
|
6
6
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
7
7
|
import { planRevert } from "./revert.js";
|
|
8
8
|
import { planUninstall } from "./uninstall.js";
|
|
9
|
-
import { buildVersionYml } from "../core/version-yml.js";
|
|
9
|
+
import { buildVersionYml, parseExisting } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { markerForType } from "../core/detect.js";
|
|
12
12
|
|
|
@@ -17,18 +17,22 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
|
17
17
|
includeSemverAuto } = context;
|
|
18
18
|
const pathMarkers = new Map();
|
|
19
19
|
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
20
|
+
|
|
21
|
+
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
22
|
+
const existingRaw = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
23
|
+
const extraTopLevel = existingRaw !== null ? parseExisting(existingRaw).extraTopLevel : [];
|
|
24
|
+
|
|
20
25
|
const wouldBe = buildVersionYml({
|
|
21
26
|
templateText: readVersionYmlTemplate(payloadRoot),
|
|
22
27
|
version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
|
|
28
|
+
extraTopLevel,
|
|
23
29
|
templateOptions: {
|
|
24
30
|
templateVersion, includeNexus, includeSecretBackup,
|
|
25
31
|
includeSemverAuto: includeSemverAuto !== false,
|
|
26
32
|
optionsDate: today,
|
|
27
33
|
},
|
|
28
34
|
});
|
|
29
|
-
|
|
30
|
-
const existing = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
31
|
-
return { existed: existing !== null, changed: existing !== wouldBe };
|
|
35
|
+
return { existed: existingRaw !== null, changed: existingRaw !== wouldBe };
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
// mode: "full" | "version" | "workflows" | "revert" | "uninstall". 읽기 전용 — 아무 파일도 쓰지 않는다.
|
package/src/commands/full.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다 — issue #7.
|
|
4
4
|
// (원본의 util/issue/discussion/setup-guide/config 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
7
|
import { writeText } from "../core/fsutil.js";
|
|
7
8
|
import { PATHS } from "../core/paths.js";
|
|
8
|
-
import { buildVersionYml } from "../core/version-yml.js";
|
|
9
|
+
import { buildVersionYml, parseExisting } from "../core/version-yml.js";
|
|
9
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
10
11
|
import { markerForType } from "../core/detect.js";
|
|
11
12
|
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
@@ -31,12 +32,16 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
31
32
|
const wfCounters = copyWorkflows(context, payloadRoot, targetRoot, hooks);
|
|
32
33
|
const deployValues = wfCounters.deployValues || new Map(); // Map<type, Map<key,value>>
|
|
33
34
|
|
|
35
|
+
// 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다 (issue #20 M8).
|
|
36
|
+
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
37
|
+
const extraTopLevel = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")).extraTopLevel : [];
|
|
38
|
+
|
|
34
39
|
// 2. version.yml 생성 (payload/version.yml.template 렌더링 — 전체 재생성 전략 D4)
|
|
35
40
|
writeText(join(targetRoot, PATHS.versionFile),
|
|
36
41
|
buildVersionYml({
|
|
37
42
|
templateText: readVersionYmlTemplate(payloadRoot),
|
|
38
43
|
version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
|
|
39
|
-
deployValues,
|
|
44
|
+
deployValues, extraTopLevel,
|
|
40
45
|
templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
|
|
41
46
|
}));
|
|
42
47
|
|
package/src/commands/revert.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
// revert 모드 — payload 유래
|
|
2
|
-
// 원칙: payload에 존재하는 파일명과 정확히 일치하는
|
|
1
|
+
// revert 모드 — payload 유래 파일 + 마커로 식별되는 파일을 제거 (DESIGN-SPEC §4 되돌리기).
|
|
2
|
+
// 원칙: (a) 현재 payload에 존재하는 파일명과 정확히 일치하는 것, (b) 설치된 워크플로우 파일 중
|
|
3
|
+
// 마법사 관리 마커(MANAGED_WORKFLOW_MARKER)로 시작하는 것 — 이 둘의 합집합을 제거 대상으로 삼는다.
|
|
4
|
+
// (a)만으로는 과거 버전에서 설치된 뒤 이후 릴리스에서 payload 파일명이 바뀌거나 삭제된 파일을
|
|
5
|
+
// 인식하지 못했다 (issue #20 L12 원래 버그). 반대로 (b)만으로는 이 수정 이전에 설치되어 마커가
|
|
6
|
+
// 없는(그러나 파일명은 여전히 현재 payload와 일치하는) 기존 설치 전체를 인식하지 못하는 회귀가
|
|
7
|
+
// 생긴다 — 그래서 두 방식을 합집합으로 병행한다. 마커는 이 수정 이후 배포되는 payload 템플릿부터
|
|
8
|
+
// 포함되므로, (b) 경로가 실제로 새로 잡아내는 것은 "이름이 바뀌거나 삭제된, 마커가 있는" 파일뿐이다.
|
|
3
9
|
// 사용자가 직접 만든 워크플로우·version.yml·README·.gitignore는 건드리지 않는다
|
|
4
10
|
// (version.yml은 사용자 버전 데이터 — 제거 대상이 아니라 산출물이다).
|
|
5
11
|
import { join } from "node:path";
|
|
6
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
12
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
13
|
import { PATHS, PAYLOAD } from "../core/paths.js";
|
|
8
14
|
import { remove } from "../core/fsutil.js";
|
|
9
15
|
|
|
16
|
+
// payload/workflows/**/*.yaml 첫 줄에 심어둔 고정 마커 — 이 값이 바뀌면 과거 설치분과의 매칭이 끊긴다.
|
|
17
|
+
export const MANAGED_WORKFLOW_MARKER = "# project-auto-wizard:managed-workflow";
|
|
18
|
+
|
|
10
19
|
// payload/workflows/** 전체(하위 폴더 포함)의 yaml 파일명 집합.
|
|
11
20
|
function payloadWorkflowNames(payloadRoot) {
|
|
12
21
|
const names = new Set();
|
|
@@ -18,26 +27,41 @@ function payloadWorkflowNames(payloadRoot) {
|
|
|
18
27
|
return names;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
//
|
|
30
|
+
// 설치된 워크플로우 디렉토리(평면 구조)에서 관리 마커로 시작하는 파일명 집합.
|
|
31
|
+
// .bak/.template.yaml 백업본도 원본 텍스트를 그대로 복사한 것이라 마커를 그대로 갖고 있어
|
|
32
|
+
// 확장자나 파일명 유추 없이 동일한 방식으로 인식된다 (issue #20 L12).
|
|
33
|
+
function markedWorkflowNames(wfDir) {
|
|
34
|
+
const names = new Set();
|
|
35
|
+
if (!existsSync(wfDir)) return names;
|
|
36
|
+
for (const entry of readdirSync(wfDir, { withFileTypes: true })) {
|
|
37
|
+
if (!entry.isFile()) continue;
|
|
38
|
+
const firstLine = readFileSync(join(wfDir, entry.name), "utf8").split("\n", 1)[0].replace(/\r$/, "");
|
|
39
|
+
if (firstLine === MANAGED_WORKFLOW_MARKER) names.add(entry.name);
|
|
40
|
+
}
|
|
41
|
+
return names;
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
// 아무것도 지우지 않는 순수 함수 — --dry-run과 status류 기능에서 재사용.
|
|
23
45
|
export function planRevert(payloadRoot, targetRoot = ".") {
|
|
24
|
-
const removedWf =
|
|
46
|
+
const removedWf = new Set();
|
|
25
47
|
const removedScripts = [];
|
|
26
48
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
27
|
-
const names = payloadWorkflowNames(payloadRoot);
|
|
28
49
|
if (existsSync(wfDir)) {
|
|
29
|
-
|
|
50
|
+
// (a) 현재 payload와 파일명이 일치하는 것 — 기존 동작 그대로(마커 유무 무관, 기존 설치 회귀 방지).
|
|
51
|
+
for (const name of payloadWorkflowNames(payloadRoot)) {
|
|
30
52
|
const p = join(wfDir, name);
|
|
31
|
-
if (existsSync(p)) removedWf.
|
|
53
|
+
if (existsSync(p)) removedWf.add(name);
|
|
32
54
|
const templateName = (name.endsWith(".yaml") ? name.slice(0, -".yaml".length) : name) + ".template.yaml";
|
|
33
|
-
if (existsSync(join(wfDir, templateName))) removedWf.
|
|
34
|
-
if (existsSync(p + ".bak")) removedWf.
|
|
55
|
+
if (existsSync(join(wfDir, templateName))) removedWf.add(templateName);
|
|
56
|
+
if (existsSync(p + ".bak")) removedWf.add(name + ".bak");
|
|
35
57
|
}
|
|
58
|
+
// (b) 관리 마커로 시작하는 것 — payload에서 이름이 바뀌거나 삭제된 파일도 인식 (issue #20 L12).
|
|
59
|
+
for (const name of markedWorkflowNames(wfDir)) removedWf.add(name);
|
|
36
60
|
}
|
|
37
61
|
for (const s of ["version_manager.py", "changelog_manager.py"]) {
|
|
38
62
|
if (existsSync(join(targetRoot, PATHS.scriptsDir, s))) removedScripts.push(s);
|
|
39
63
|
}
|
|
40
|
-
return { workflows: removedWf, scripts: removedScripts };
|
|
64
|
+
return { workflows: [...removedWf], scripts: removedScripts };
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
// 반환: { workflows: [...제거된 파일명], scripts: [...] } — planRevert와 동일한 형태.
|
package/src/commands/version.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// (워크플로우를 복사하지 않으므로 충돌 백업 부산물이 생길 수 없다 — gitignore 갱신 대상 없음, issue #7.
|
|
4
4
|
// util·issue·setup-guide는 스코프 제외.)
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
7
|
import { writeText } from "../core/fsutil.js";
|
|
7
8
|
import { PATHS } from "../core/paths.js";
|
|
8
|
-
import { buildVersionYml } from "../core/version-yml.js";
|
|
9
|
+
import { buildVersionYml, parseExisting } from "../core/version-yml.js";
|
|
9
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
10
11
|
import { markerForType } from "../core/detect.js";
|
|
11
12
|
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
@@ -20,10 +21,15 @@ export function runVersion(context, payloadRoot, targetRoot = ".") {
|
|
|
20
21
|
const pathMarkers = new Map();
|
|
21
22
|
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
22
23
|
|
|
24
|
+
// 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다 (issue #20 M8).
|
|
25
|
+
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
26
|
+
const extraTopLevel = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")).extraTopLevel : [];
|
|
27
|
+
|
|
23
28
|
writeText(join(targetRoot, PATHS.versionFile),
|
|
24
29
|
buildVersionYml({
|
|
25
30
|
templateText: readVersionYmlTemplate(payloadRoot),
|
|
26
31
|
version, types, paths, pathMarkers, branch, branches: context.branches, versionCode, now, today,
|
|
32
|
+
extraTopLevel,
|
|
27
33
|
templateOptions: { templateVersion, includeNexus, includeSecretBackup, includeSemverAuto: includeSemverAuto !== false, optionsDate: today },
|
|
28
34
|
}));
|
|
29
35
|
addVersionSectionToReadme(version, targetRoot);
|
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
7
7
|
import { PATHS } from "../core/paths.js";
|
|
8
8
|
import { copyWorkflows } from "../core/copy/workflows.js";
|
|
9
9
|
import { copyScripts } from "../core/copy/simple.js";
|
|
10
|
+
import { escapeYamlDoubleQuoted } from "../core/wizard-env.js";
|
|
10
11
|
|
|
11
12
|
export function runWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
12
13
|
const wf = copyWorkflows(context, payloadRoot, targetRoot, hooks);
|
|
@@ -42,7 +43,9 @@ export function upsertDeployBlock(content, deployValues) {
|
|
|
42
43
|
text += `\ndeploy: # 마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)\n`;
|
|
43
44
|
for (const t of deployTypes) {
|
|
44
45
|
text += ` ${t}:\n`;
|
|
45
|
-
|
|
46
|
+
// 동일한 이스케이프를 재사용 — deploy 값도 @wizard ask 값과 같은 경로로 들어오므로
|
|
47
|
+
// 큰따옴표가 섞이면 YAML이 깨진다 (issue #20 L9, 세 번째 지점).
|
|
48
|
+
for (const [k, v] of deployValues.get(t)) text += ` ${k}: "${escapeYamlDoubleQuoted(v)}"\n`;
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
return text;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// .gitignore 보장 — 마법사 자신이 만드는 충돌 백업 부산물(*.bak, *.template.yaml)만 대상으로 한다.
|
|
2
2
|
// 마법사가 설치하는 것과 무관한 개인 개발환경 설정(IDE 등)은 마법사 책임 범위 밖 — issue #7.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// 배너 블록은 종료 마커(BANNER_END)로 범위가 명확히 구분되어 있어, 그 안에 사용자가 다른 줄을
|
|
4
|
+
// 끼워넣어도 REQUIRED_ENTRIES만 정확히 개별 제거하고 나머지는 보존한다 (issue #20 M7).
|
|
5
|
+
// 주의(의도된 트레이드오프): 이 수정 이전 버전으로 설치되어 종료 마커가 없는 배너가 이미 있는
|
|
6
|
+
// 레포에서는, 배너 직후 REQUIRED_ENTRIES가 연속으로 이어지는 동안만 제거하는 구 방식으로
|
|
7
|
+
// 폴백한다 — issue #7이 "이미 설치된 레포의 .gitignore는 소급 처리하지 않고 사용자 판단에
|
|
8
|
+
// 맡긴다"고 명시하므로 별도 마이그레이션 로직을 추가하지 않는다.
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
10
11
|
|
|
@@ -57,19 +58,28 @@ export function ensureGitignore(targetRoot = ".") {
|
|
|
57
58
|
content += "# project-auto-wizard: Auto-added entries\n";
|
|
58
59
|
content += "# ====================================================================\n";
|
|
59
60
|
for (const e of toAdd) content += e + "\n";
|
|
61
|
+
content += BANNER_END;
|
|
60
62
|
writeFileSync(p, content);
|
|
61
63
|
return { created: false, added: toAdd };
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// ensureGitignore가 기존 파일에 배너 블록을 추가할 때 항상 이 정확한 시퀀스로 시작한다
|
|
65
|
-
// (빈 줄 하나 + 3줄 배너)
|
|
66
|
-
//
|
|
67
|
+
// (빈 줄 하나 + 3줄 배너), 그리고 REQUIRED_ENTRIES 뒤에 BANNER_END로 끝난다. 배너~BANNER_END
|
|
68
|
+
// 범위 안에서 REQUIRED_ENTRIES와 일치하는 줄만 개별 제거하고, 사용자가 그 사이/뒤에 추가한
|
|
69
|
+
// 줄은 절대 건드리지 않는다 (issue #20 M7).
|
|
67
70
|
const BANNER =
|
|
68
71
|
"\n" +
|
|
69
72
|
"# ====================================================================\n" +
|
|
70
73
|
"# project-auto-wizard: Auto-added entries\n" +
|
|
71
74
|
"# ====================================================================\n";
|
|
72
75
|
|
|
76
|
+
// REQUIRED_ENTRIES 뒤에 오는 종료 마커 — 배너 블록의 "끝"을 명확히 구분해, 그 사이에 사용자가
|
|
77
|
+
// 다른 줄을 끼워넣어도 removeAutoAddedEntriesFromGitignore가 정확한 범위 안에서 개별 제거할 수
|
|
78
|
+
// 있게 한다 (issue #20 M7). ensureGitignore가 기존 파일에 배너를 추가할 때만 함께 쓴다
|
|
79
|
+
// (신규 파일 생성 케이스는 NEW_FILE_CONTENT를 통째로 쓰고 종료 마커는 쓰지 않는다 —
|
|
80
|
+
// startsWith 전체 prefix 매칭이라 별도 마커가 필요 없다).
|
|
81
|
+
const BANNER_END = "# ==== project-auto-wizard: end of auto-added entries ====\n";
|
|
82
|
+
|
|
73
83
|
// .gitignore에 마법사가 추가한 항목이 있는지 확인 (체크리스트 노출 판단용).
|
|
74
84
|
// 두 케이스: (1) 원래 없던 파일을 통째로(또는 그 뒤에 사용자가 이어 쓴 형태로) 새로 만든 경우
|
|
75
85
|
// (2) 기존 파일에 배너 블록을 붙인 경우.
|
|
@@ -97,11 +107,26 @@ export function removeAutoAddedEntriesFromGitignore(targetRoot = ".") {
|
|
|
97
107
|
return "removed";
|
|
98
108
|
}
|
|
99
109
|
|
|
100
|
-
// 기존 파일에 배너 블록이 붙은
|
|
101
|
-
// 동안만 제거하고, 그 뒤(사용자가 나중에 추가한 항목)는 그대로 둔다.
|
|
110
|
+
// 기존 파일에 배너 블록이 붙은 경우.
|
|
102
111
|
const idx = content.indexOf(BANNER);
|
|
103
112
|
if (idx === -1) return "skip-not-found";
|
|
104
113
|
const afterBanner = idx + BANNER.length;
|
|
114
|
+
|
|
115
|
+
// 종료 마커가 있으면(이 수정 이후 설치분) 그 범위 안에서 REQUIRED_ENTRIES만 개별적으로
|
|
116
|
+
// 제거하고, 사용자가 그 사이에 끼워넣은 줄은 순서·연속 여부와 무관하게 그대로 보존한다
|
|
117
|
+
// (issue #20 M7).
|
|
118
|
+
const endIdx = content.indexOf(BANNER_END, afterBanner);
|
|
119
|
+
if (endIdx !== -1) {
|
|
120
|
+
const region = content.slice(afterBanner, endIdx).split("\n");
|
|
121
|
+
const kept = region.filter((line) =>
|
|
122
|
+
!REQUIRED_ENTRIES.some((e) => normalizeGitignoreEntry(line) === normalizeGitignoreEntry(e)));
|
|
123
|
+
const afterEndMarker = content.slice(endIdx + BANNER_END.length);
|
|
124
|
+
writeFileSync(p, content.slice(0, idx) + kept.join("\n") + afterEndMarker);
|
|
125
|
+
return "removed";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 종료 마커가 없는 구버전 설치(이 수정 이전) — 배너 직후 REQUIRED_ENTRIES가 연속으로
|
|
129
|
+
// 이어지는 동안만 제거하는 기존 방식으로 폴백한다 (issue #7과 동일한 소급 미처리 원칙).
|
|
105
130
|
const lines = content.slice(afterBanner).split("\n");
|
|
106
131
|
let consumed = 0;
|
|
107
132
|
for (const line of lines) {
|
|
@@ -74,24 +74,21 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
74
74
|
// values/useDefaults는 치환 경로에서만 의미 (isUnchanged는 내부에서 useDefaults:true 강제 — 가상 비교 무손상)
|
|
75
75
|
const envOptsFor = (type) => ({ type, projectPath: paths.get(type) || ".", repoName, resolvers, values: envValues, useDefaults: envUseDefaults });
|
|
76
76
|
|
|
77
|
-
// (1) common — unchanged면 스킵,
|
|
77
|
+
// (1) common — 타입별 워크플로우와 동일한 3지선(README 계약, issue #20 H3): unchanged면 스킵,
|
|
78
|
+
// changed면 decisions Map 결정에 따름(미지정 시 skip — 기존 사용자 수정 보존).
|
|
78
79
|
// trunk-based 모드는 VERSION-CONTROL·AUTO-CHANGELOG 미설치 (RELEASE-PUBLISH 단독).
|
|
79
80
|
const branchMode = context.branches?.mode || "pr-flow";
|
|
80
81
|
const commonDir = join(projectTypesDir, "common");
|
|
81
82
|
if (exists(commonDir)) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (existsSync(dst) && isUnchanged(body, readFileSync(dst, "utf8"), envOptsFor("common"))) {
|
|
88
|
-
counters.skipped++;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
writeText(dst, body);
|
|
83
|
+
const notExcluded = (filename) => !(branchMode === "trunk-based" && TRUNK_BASED_EXCLUDED.has(filename));
|
|
84
|
+
const { newFiles, unchanged, changed } = classify(commonDir, workflowsDir, envOptsFor("common"), srcText);
|
|
85
|
+
counters.skipped += unchanged.filter(notExcluded).length;
|
|
86
|
+
for (const f of newFiles.filter(notExcluded)) {
|
|
87
|
+
writeText(join(workflowsDir, f), srcText(join(commonDir, f)));
|
|
92
88
|
counters.copied++;
|
|
93
|
-
counters.copiedFiles.push(
|
|
89
|
+
counters.copiedFiles.push(f);
|
|
94
90
|
}
|
|
91
|
+
for (const f of changed.filter(notExcluded)) applyDecision(decisions.get(f), commonDir, workflowsDir, f, counters, srcText);
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
// (2~4) 타입별
|
|
@@ -144,12 +141,24 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcTe
|
|
|
144
141
|
|
|
145
142
|
// 대상 워크플로우 디렉토리에서 changed(충돌) 파일 목록만 뽑는다 — copyWorkflowsInteractive의 사전 조사용.
|
|
146
143
|
// copyWorkflows 본체와 동일한 classify 기준을 써야 결정 Map이 실제 처리 대상과 1:1로 맞는다.
|
|
144
|
+
// common도 타입별과 동일하게 스캔한다 (issue #20 H3 — 이전에는 common 충돌이 질문조차 되지 않았다).
|
|
147
145
|
export function listWorkflowConflicts(context, payloadRoot, targetRoot = ".") {
|
|
148
146
|
const { types = [], paths = new Map(), includeNexus = false, repoName = "", resolvers = {} } = context;
|
|
149
147
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
150
148
|
const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
151
149
|
const srcText = makeSrcText(context.branches || null);
|
|
152
|
-
const
|
|
150
|
+
const branchMode = context.branches?.mode || "pr-flow";
|
|
151
|
+
const conflicts = []; // [{ filename, type }] — 엔진 처리 순서와 동일 (common → 타입 순회 → 직하위 → server-deploy)
|
|
152
|
+
|
|
153
|
+
const commonDir = join(projectTypesDir, "common");
|
|
154
|
+
if (exists(commonDir)) {
|
|
155
|
+
const envOpts = { type: "common", projectPath: ".", repoName, resolvers };
|
|
156
|
+
for (const f of classify(commonDir, workflowsDir, envOpts, srcText).changed) {
|
|
157
|
+
if (branchMode === "trunk-based" && TRUNK_BASED_EXCLUDED.has(f)) continue;
|
|
158
|
+
conflicts.push({ filename: f, type: "common" });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
153
162
|
for (const type of types) {
|
|
154
163
|
const envOpts = { type, projectPath: paths.get(type) || ".", repoName, resolvers };
|
|
155
164
|
const typeDir = join(projectTypesDir, type);
|
|
@@ -248,8 +257,8 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
// 전체 워크플로우 분류(common + 타입별 + server-deploy + nexus opt-in) — status/dry-run 공용.
|
|
251
|
-
//
|
|
252
|
-
//
|
|
260
|
+
// changed뿐 아니라 newFiles/unchanged까지 전부 반환한다는 점이 listWorkflowConflicts(changed만
|
|
261
|
+
// 반환)와 다르다(읽기 전용 — 실제로 아무 파일도 쓰지 않는다).
|
|
253
262
|
export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
254
263
|
const { types = [], paths = new Map(), includeNexus = false, includeSecretBackup = false, repoName = "", resolvers = {} } = context;
|
|
255
264
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync, readdirSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { markerForType as baseMarkerForType, extraMarkers } from "./detect.js";
|
|
13
|
-
import { normalizePath } from "../cli/args.js";
|
|
13
|
+
import { normalizePath, CliError } from "../cli/args.js";
|
|
14
14
|
|
|
15
15
|
// 취소(ESC/Ctrl+C)는 CANCEL 심볼 — ui를 import하지 않고 심볼 여부로만 판정 (core→ui 역참조 방지)
|
|
16
16
|
const isCancel = (v) => typeof v === "symbol";
|
|
@@ -119,7 +119,7 @@ export function findTypePathCandidates(root, type) {
|
|
|
119
119
|
// ② 루트에 마커 존재 → "." 자동
|
|
120
120
|
// ③ existingPaths(version.yml 저장값)
|
|
121
121
|
// ④ 후보 스캔
|
|
122
|
-
// ⑤ 분기 — 비대화형: 기존값→후보1
|
|
122
|
+
// ⑤ 분기 — 비대화형: 기존값→후보1개→에러 / 대화형: 확인·선택·직접입력
|
|
123
123
|
export async function resolveProjectPaths({
|
|
124
124
|
root, types = [], paths = new Map(), existingPaths = new Map(),
|
|
125
125
|
force = false, tty = true, io = {},
|
|
@@ -146,7 +146,11 @@ export async function resolveProjectPaths({
|
|
|
146
146
|
|
|
147
147
|
// ① --paths 등으로 이미 지정됨 → 최우선 (.sh L1441~1446)
|
|
148
148
|
if (result.get(t)) {
|
|
149
|
-
|
|
149
|
+
const p = result.get(t);
|
|
150
|
+
if (!existsSync(join(root, p))) {
|
|
151
|
+
throw new CliError(`--paths로 지정한 경로가 존재하지 않습니다: '${t}=${p}'`);
|
|
152
|
+
}
|
|
153
|
+
say(` ${t} → ${p} (--paths 지정)`);
|
|
150
154
|
continue;
|
|
151
155
|
}
|
|
152
156
|
|
|
@@ -165,7 +169,7 @@ export async function resolveProjectPaths({
|
|
|
165
169
|
const candidates = findTypePathCandidates(root, t);
|
|
166
170
|
let chosen = "";
|
|
167
171
|
|
|
168
|
-
// ── ⑤-a 비대화형 (--force 또는 TTY 없음, .sh L1476~1489) ──
|
|
172
|
+
// ── ⑤-a 비대화형 (--force 또는 TTY 없음, .sh L1476~1489 — M4: root 폴백 의도적 불포함) ──
|
|
169
173
|
if (force || !tty) {
|
|
170
174
|
if (existing) {
|
|
171
175
|
chosen = existing;
|
|
@@ -173,9 +177,10 @@ export async function resolveProjectPaths({
|
|
|
173
177
|
} else if (candidates.length === 1) {
|
|
174
178
|
chosen = candidates[0];
|
|
175
179
|
say(` ${t} → ${chosen} (자동 감지)`);
|
|
180
|
+
} else if (candidates.length === 0) {
|
|
181
|
+
throw new CliError(`${t}: 프로젝트 경로를 찾지 못했습니다. --paths "${t}=경로"로 직접 지정하세요.`);
|
|
176
182
|
} else {
|
|
177
|
-
|
|
178
|
-
say(` ⚠️ ${t} → 후보 ${candidates.length}개로 자동 확정 불가, 루트(.)로 기록 (--paths "${t}=경로"로 지정 가능)`);
|
|
183
|
+
throw new CliError(`${t}: 경로 후보가 ${candidates.length}개로 모호합니다(${candidates.join(", ")}). --paths "${t}=경로"로 직접 지정하세요.`);
|
|
179
184
|
}
|
|
180
185
|
result.set(t, chosen);
|
|
181
186
|
continue;
|
package/src/core/version-yml.js
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
|
+
import { escapeYamlDoubleQuoted } from "./wizard-env.js";
|
|
2
|
+
|
|
1
3
|
// version.yml 파싱·생성 (.sh create_version_yml 등가, 전체 재생성 전략 D4).
|
|
2
4
|
// ⚠️ YAML 재직렬화 금지 — 주석이 데이터.
|
|
3
5
|
// 레이아웃 단일 진실 = payload/version.yml.template (호출부가 templateText로 주입).
|
|
4
6
|
|
|
7
|
+
// version.yml.template이 아는 최상위 키 — 이 밖의 최상위 키는 사용자가 직접 추가한 것으로 간주한다.
|
|
8
|
+
const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
9
|
+
"version", "version_code", "project_types", "project_type", "project_paths", "metadata", "deploy",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
// 최상위 레벨의 알려지지 않은 필드(사용자가 직접 추가한 임의 필드)를 원본 그대로 보존한다
|
|
13
|
+
// (issue #20 M8). 각 알려지지 않은 최상위 키부터 다음 최상위 키 직전까지를 통째로 한 블록으로
|
|
14
|
+
// 캡처한다(중첩 구조가 있어도 유효한 YAML로 남기기 위함). metadata/project_paths/deploy처럼
|
|
15
|
+
// 이 모듈이 이미 아는 블록 "내부"의 알려지지 않은 하위 키는 대상이 아니다(범위 밖 — issue #20 결정).
|
|
16
|
+
export function parseExtraTopLevel(content) {
|
|
17
|
+
const blocks = [];
|
|
18
|
+
let current = null;
|
|
19
|
+
for (const line of String(content || "").split("\n")) {
|
|
20
|
+
// 최상위 키는 하이픈을 포함할 수 있다(YAML 관례) — issue #20 M8 리뷰에서 지적된 놓침 방지.
|
|
21
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):/);
|
|
22
|
+
if (m) {
|
|
23
|
+
if (current) blocks.push(current.join("\n"));
|
|
24
|
+
current = KNOWN_TOP_LEVEL_KEYS.has(m[1]) ? null : [line];
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (current) current.push(line);
|
|
28
|
+
}
|
|
29
|
+
if (current) blocks.push(current.join("\n"));
|
|
30
|
+
return blocks;
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
// metadata.template.options 상태머신 파싱 (.sh read_template_options L2361~2416 등가).
|
|
6
34
|
// 반환: { nexus: bool|null, secretBackup: bool|null } — null=미기재.
|
|
7
35
|
// 구 synology·coderabbit 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다(파싱 에러 없음).
|
|
@@ -92,7 +120,7 @@ export function parseExisting(content) {
|
|
|
92
120
|
const options = parseTemplateOptions(text);
|
|
93
121
|
// metadata.template.branches — main/develop/mode (업데이트 모드 재질문 생략용)
|
|
94
122
|
const branches = parseTemplateBranches(text);
|
|
95
|
-
return { version, versionCode, types, paths, templateVersion, options, branches };
|
|
123
|
+
return { version, versionCode, types, paths, templateVersion, options, branches, extraTopLevel: parseExtraTopLevel(text) };
|
|
96
124
|
}
|
|
97
125
|
|
|
98
126
|
// metadata.template.branches 블록 파싱. 셋 다 있어야 유효 — 아니면 null.
|
|
@@ -120,7 +148,8 @@ export function parseTemplateBranches(content) {
|
|
|
120
148
|
|
|
121
149
|
// version.yml 전체 생성 — payload/version.yml.template 렌더링.
|
|
122
150
|
// opts: { templateText, version, types:[], primaryType?, paths:Map, pathMarkers?:Map,
|
|
123
|
-
// branch, branches?, versionCode, now, today, templateOptions?, deployValues
|
|
151
|
+
// branch, branches?, versionCode, now, today, templateOptions?, deployValues?,
|
|
152
|
+
// extraTopLevel?:string[] } ← 기존 version.yml의 알려지지 않은 최상위 필드 보존 (issue #20 M8)
|
|
124
153
|
// templateText = payload/version.yml.template 원문 (readVersionYmlTemplate — 필수)
|
|
125
154
|
// now = "YYYY-MM-DD HH:MM:SS" (UTC) — 결정성 위해 주입 / today = "YYYY-MM-DD"
|
|
126
155
|
// branches = { main, develop, mode } (resolveBranchConfig 결과. 없으면 branch 기반 기본값)
|
|
@@ -129,7 +158,7 @@ export function parseTemplateBranches(content) {
|
|
|
129
158
|
export function buildVersionYml({
|
|
130
159
|
templateText, version, types = [], primaryType, paths = new Map(), pathMarkers = new Map(),
|
|
131
160
|
branch = "main", branches = null, versionCode = 1, now, today,
|
|
132
|
-
templateOptions = null, deployValues = new Map(),
|
|
161
|
+
templateOptions = null, deployValues = new Map(), extraTopLevel = [],
|
|
133
162
|
}) {
|
|
134
163
|
if (!templateText) throw new Error("version.yml.template 원문이 필요합니다 (payload/version.yml.template 누락?)");
|
|
135
164
|
const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(", ")}]` : `["basic"]`;
|
|
@@ -159,7 +188,9 @@ export function buildVersionYml({
|
|
|
159
188
|
const rows = ["", "deploy: # 마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)"];
|
|
160
189
|
for (const t of deployTypes) {
|
|
161
190
|
rows.push(` ${t}:`);
|
|
162
|
-
|
|
191
|
+
// 동일한 이스케이프를 재사용 — deploy 값도 @wizard ask 값과 같은 경로로 들어오므로
|
|
192
|
+
// 큰따옴표가 섞이면 setEnvLine과 동일하게 YAML이 깨진다 (issue #20 L9, 두 번째 지점).
|
|
193
|
+
for (const [k, v] of deployValues.get(t)) rows.push(` ${k}: "${escapeYamlDoubleQuoted(v)}"`);
|
|
163
194
|
}
|
|
164
195
|
deployBlock = rows.join("\n");
|
|
165
196
|
}
|
|
@@ -185,6 +216,10 @@ export function buildVersionYml({
|
|
|
185
216
|
}));
|
|
186
217
|
}
|
|
187
218
|
let text = out.join("\n");
|
|
219
|
+
if (extraTopLevel.length) {
|
|
220
|
+
if (!text.endsWith("\n")) text += "\n";
|
|
221
|
+
text += "\n" + extraTopLevel.join("\n\n");
|
|
222
|
+
}
|
|
188
223
|
if (!text.endsWith("\n")) text += "\n";
|
|
189
224
|
return text.replace(/\n{3,}$/, "\n"); // 말미 과잉 빈 줄 정리
|
|
190
225
|
}
|
package/src/core/wizard-env.js
CHANGED
|
@@ -16,6 +16,14 @@ export function parseWizardLine(line) {
|
|
|
16
16
|
return { indent: km[1], key: km[2], action: marker[1], arg: marker[2].trim() };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// YAML 큰따옴표 문자열 안에 안전하게 넣기 위한 이스케이프(백슬래시 우선 — 그래야 그 다음에 붙이는
|
|
20
|
+
// 큰따옴표 이스케이프가 깨지지 않는다). @wizard 치환(setEnvLine)과 version.yml의 deploy 블록
|
|
21
|
+
// (buildVersionYml, src/core/version-yml.js) 양쪽에서 재사용한다 — issue #20 L9는 두 지점 모두에서
|
|
22
|
+
// 발생하는 동일한 버그다.
|
|
23
|
+
export function escapeYamlDoubleQuoted(value) {
|
|
24
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
// .sh _wf_set_env 등가: `KEY: "..."` 따옴표 안 값 치환 + 그 줄 끝 `# @wizard ...` 주석 제거.
|
|
20
28
|
// 라인 하나에 대해 수행. value가 빈문자면 (.sh는 [ -n "$_val" ] 가드) 치환 스킵.
|
|
21
29
|
export function setEnvLine(line, key, value) {
|
|
@@ -23,10 +31,11 @@ export function setEnvLine(line, key, value) {
|
|
|
23
31
|
// CRLF 안전: 라인 끝 \r을 분리해 처리 후 복원 (autocrlf 프로젝트 대응)
|
|
24
32
|
const cr = line.endsWith("\r") ? "\r" : "";
|
|
25
33
|
const body = cr ? line.slice(0, -1) : line;
|
|
34
|
+
const escaped = escapeYamlDoubleQuoted(value);
|
|
26
35
|
// 값 치환: KEY: "기존값" → KEY: "value"
|
|
27
36
|
let out = body.replace(
|
|
28
37
|
new RegExp(`^(\\s*${key}:\\s*")[^"]*(")`),
|
|
29
|
-
(_m, p1, p2) => `${p1}${
|
|
38
|
+
(_m, p1, p2) => `${p1}${escaped}${p2}`,
|
|
30
39
|
);
|
|
31
40
|
// 그 줄 끝 # @wizard ... 주석 제거 (앞 공백째)
|
|
32
41
|
out = out.replace(/(\S)[^\S\r\n]*#[^\S\r\n]*@wizard[^\S\r\n].*$/, "$1");
|
package/src/index.js
CHANGED
|
@@ -240,11 +240,17 @@ export async function run(argv, {
|
|
|
240
240
|
const versionCode = existing?.versionCode ?? 1; // 기존 빌드번호 보존 (.sh L2208~2221)
|
|
241
241
|
const branch = detectDefaultBranch(cwd);
|
|
242
242
|
const repoName = detectRepoName(cwd);
|
|
243
|
-
// 경로 확정 (.sh resolve_project_paths 비대화형 경로 — --paths 우선 → 저장값 → 후보 1개 자동 →
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
// 경로 확정 (.sh resolve_project_paths 비대화형 경로 — --paths 우선 → 저장값 → 후보 1개 자동 → 에러)
|
|
244
|
+
let paths;
|
|
245
|
+
try {
|
|
246
|
+
paths = await resolveProjectPaths({
|
|
247
|
+
root: cwd, types, paths: parsePathsCsv(opts.pathsCsv),
|
|
248
|
+
existingPaths: existing?.paths ?? new Map(), force: true, tty: false, io: {},
|
|
249
|
+
});
|
|
250
|
+
} catch (e) {
|
|
251
|
+
if (e instanceof CliError) { console.error(e.message); return 1; }
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
248
254
|
|
|
249
255
|
// 브랜치 구성 (--main-branch/--develop-branch → version.yml 저장값 → 감지 default → main/develop)
|
|
250
256
|
const branches = resolveBranchConfig({
|