project-auto-wizard 0.13.0 → 0.13.1
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 +3 -1
- package/payload/scripts/version_manager.py +94 -19
- package/payload/version.yml.template +1 -0
- package/src/commands/doctor.js +9 -50
- package/src/commands/dry-run.js +5 -4
- package/src/commands/full.js +11 -8
- package/src/commands/install-settings.js +51 -0
- package/src/commands/interactive.js +16 -18
- package/src/commands/purge.js +8 -21
- package/src/commands/status.js +3 -12
- package/src/commands/uninstall.js +10 -55
- package/src/core/copy/app-files.js +28 -0
- package/src/core/copy/workflows.js +63 -84
- package/src/core/detect.js +49 -3
- package/src/core/flutter-doctor.js +49 -0
- package/src/core/flutter-hooks.js +29 -0
- package/src/core/removal-exec.js +67 -0
- package/src/core/types.js +23 -0
- package/src/index.js +19 -24
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Everything it installs is plain GitHub Actions in your own repository. No API ke
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
24
|
-
## Latest Version : v0.13.
|
|
24
|
+
## Latest Version : v0.13.1 (2026-09-30)
|
|
25
25
|
|
|
26
26
|
[Full release history](CHANGELOG.md)
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-auto-wizard",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "One command DevOps: npx wizard that installs GitHub-native Release Automation (optional GitHub Copilot AI summaries) into any project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"test": "npm run test:node && npm run test:py",
|
|
22
22
|
"test:node": "node --test --test-concurrency=1",
|
|
23
23
|
"test:py": "node scripts/run-py-tests.mjs",
|
|
24
|
+
"generate:workflows": "node scripts/generate-workflows.mjs",
|
|
25
|
+
"generate:workflows:check": "node scripts/generate-workflows.mjs --check",
|
|
24
26
|
"sync:dogfood": "node scripts/sync-dogfood.mjs",
|
|
25
27
|
"sync:dogfood:check": "node scripts/sync-dogfood.mjs --check"
|
|
26
28
|
},
|
|
@@ -436,6 +436,64 @@ def sync_maven(path_dir, new_version):
|
|
|
436
436
|
|
|
437
437
|
|
|
438
438
|
_GRADLE_VERSION_RE = re.compile(r"""^([ \t]*version[ \t]*=[ \t]*)(['"])([^'"\n]*)\2""", re.MULTILINE)
|
|
439
|
+
_GRADLE_SHARED_BLOCKS = ("allprojects", "subprojects")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _gradle_code(line, quote=""):
|
|
443
|
+
"""한 줄에서 주석을 떼고 따옴표 안 글자는 공백으로 바꾼다. url 'https://…'의 `//`나
|
|
444
|
+
문자열 속 중괄호가 블록 깊이 계산을 흐트러뜨리지 않게 한다. detect.js와 같은 규칙.
|
|
445
|
+
|
|
446
|
+
quote는 이 줄이 시작될 때 열려 있던 따옴표다. 삼중따옴표(\"\"\" ''')는 여러 줄에 걸치므로
|
|
447
|
+
(코드, 줄 끝에서 열려 있는 따옴표)를 돌려줘 다음 줄이 이어받게 한다. 한 줄짜리 따옴표는
|
|
448
|
+
줄 끝에서 닫힌 것으로 본다."""
|
|
449
|
+
out, i = [], 0
|
|
450
|
+
while i < len(line):
|
|
451
|
+
ch = line[i]
|
|
452
|
+
if quote:
|
|
453
|
+
if ch == "\\" and i + 1 < len(line):
|
|
454
|
+
out.append(" "); i += 2; continue
|
|
455
|
+
if line.startswith(quote, i):
|
|
456
|
+
out.append(quote); i += len(quote); quote = ""; continue
|
|
457
|
+
out.append(" ")
|
|
458
|
+
elif line.startswith(('"""', "'''"), i):
|
|
459
|
+
quote = line[i:i + 3]; out.append(quote); i += 3; continue
|
|
460
|
+
elif ch in "'\"":
|
|
461
|
+
quote = ch; out.append(ch)
|
|
462
|
+
elif line.startswith("//", i):
|
|
463
|
+
break
|
|
464
|
+
else:
|
|
465
|
+
out.append(ch)
|
|
466
|
+
i += 1
|
|
467
|
+
return "".join(out), quote if len(quote) == 3 else ""
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _gradle_version_matches(text):
|
|
471
|
+
"""프로젝트 버전 줄의 match 목록. 읽기와 동기화가 이 한 곳을 공유해 같은 줄만 다룬다.
|
|
472
|
+
|
|
473
|
+
들여쓰지 않은 `version =`이 있으면 그것만 쓴다. 없을 때만 allprojects/subprojects 블록 안의
|
|
474
|
+
들여쓴 줄을 인정한다 — `node { version = '20.11.0' }` 같은 플러그인 설정 블록을
|
|
475
|
+
프로젝트 버전으로 오인하면 버전이 뛰고 빌드 설정이 깨진다."""
|
|
476
|
+
top, shared = [], []
|
|
477
|
+
stack = [] # 열린 블록 이름. 줄 끝이 `{`로 끝나는 줄의 마지막 단어
|
|
478
|
+
quote = "" # 여러 줄 문자열 안이면 그 따옴표. 문자열 속 줄은 코드가 아니다
|
|
479
|
+
pos = 0
|
|
480
|
+
for line in text.splitlines(keepends=True):
|
|
481
|
+
m = None if quote else _GRADLE_VERSION_RE.match(line)
|
|
482
|
+
if m:
|
|
483
|
+
m = _GRADLE_VERSION_RE.match(text, pos)
|
|
484
|
+
if not m.group(1)[:1].isspace():
|
|
485
|
+
top.append(m)
|
|
486
|
+
elif any(b in _GRADLE_SHARED_BLOCKS for b in stack):
|
|
487
|
+
shared.append(m)
|
|
488
|
+
code, quote = _gradle_code(line, quote)
|
|
489
|
+
name = re.search(r"(\w+)\s*\{[^{}]*$", code)
|
|
490
|
+
for ch in code:
|
|
491
|
+
if ch == "{":
|
|
492
|
+
stack.append(name.group(1) if name else "")
|
|
493
|
+
elif ch == "}" and stack:
|
|
494
|
+
stack.pop()
|
|
495
|
+
pos += len(line)
|
|
496
|
+
return top or shared
|
|
439
497
|
|
|
440
498
|
|
|
441
499
|
def sync_spring(path_dir, new_version):
|
|
@@ -455,10 +513,13 @@ def sync_spring(path_dir, new_version):
|
|
|
455
513
|
text = read_file(gradle_file)
|
|
456
514
|
# 줄 시작의 `version =`만 프로젝트 버전이다. 앵커가 없으면 kotlin_version 같은
|
|
457
515
|
# 의존성 버전 변수까지 함께 바뀌어 빌드가 깨진다.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
516
|
+
matches = _gradle_version_matches(text)
|
|
517
|
+
new_text = text
|
|
518
|
+
# 뒤에서부터 바꿔 앞쪽 span이 밀리지 않게 한다.
|
|
519
|
+
for m in reversed(matches):
|
|
520
|
+
replaced = f"{m.group(1)}{m.group(2)}{_keep_snapshot(m.group(3), new_version)}{m.group(2)}"
|
|
521
|
+
new_text = new_text[:m.start()] + replaced + new_text[m.end():]
|
|
522
|
+
if not matches:
|
|
462
523
|
log(f"WARNING: spring: no `version = '...'` line in {gradle_file} — skipping")
|
|
463
524
|
continue
|
|
464
525
|
write_file(gradle_file, new_text)
|
|
@@ -586,21 +647,36 @@ def sync_python(path_dir, new_version):
|
|
|
586
647
|
log(f"updated: {target}")
|
|
587
648
|
|
|
588
649
|
|
|
650
|
+
_PLIST_VERSION_RE = re.compile(r'(<key>CFBundleShortVersionString</key>\s*<string>)([^<]*)(</string>)')
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _rn_app_plists(ios_dir):
|
|
654
|
+
"""앱 타깃의 Info.plist만 이름순으로. Pods·빌드 산출물·테스트 타깃은 우리 버전이 아니다.
|
|
655
|
+
읽기와 동기화가 같은 파일 집합을 보도록 한 곳에서 고른다."""
|
|
656
|
+
skip = {"Pods", "build"}
|
|
657
|
+
return [
|
|
658
|
+
p for p in sorted(ios_dir.glob("*/Info.plist"))
|
|
659
|
+
if p.parent.name not in skip and not p.parent.name.endswith("Tests")
|
|
660
|
+
]
|
|
661
|
+
|
|
662
|
+
|
|
589
663
|
def sync_react_native(path_dir, new_version):
|
|
590
664
|
ios_dir = Path(path_dir) / "ios"
|
|
591
665
|
found_plist = False
|
|
592
666
|
if ios_dir.is_dir():
|
|
593
|
-
for plist_file in ios_dir
|
|
667
|
+
for plist_file in _rn_app_plists(ios_dir):
|
|
594
668
|
text = read_file(plist_file)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
669
|
+
m = _PLIST_VERSION_RE.search(text)
|
|
670
|
+
if not m:
|
|
671
|
+
continue
|
|
672
|
+
# $(MARKETING_VERSION) 같은 빌드 변수 참조는 Xcode 설정이 원본이라 덮어쓰지 않는다.
|
|
673
|
+
if m.group(2).strip().startswith("$("):
|
|
674
|
+
log(f"skipped: {plist_file} — CFBundleShortVersionString references a build variable")
|
|
675
|
+
continue
|
|
676
|
+
new_text = _PLIST_VERSION_RE.sub(lambda mm: mm.group(1) + new_version + mm.group(3), text)
|
|
677
|
+
write_file(plist_file, new_text)
|
|
678
|
+
log(f"updated: {plist_file}")
|
|
679
|
+
found_plist = True
|
|
604
680
|
else:
|
|
605
681
|
log(f"WARNING: react-native: {ios_dir} not found — skipping")
|
|
606
682
|
|
|
@@ -691,7 +767,7 @@ def _read_spring(path_dir):
|
|
|
691
767
|
for name in ("build.gradle", "build.gradle.kts"):
|
|
692
768
|
p = Path(path_dir) / name
|
|
693
769
|
if p.is_file():
|
|
694
|
-
for m in
|
|
770
|
+
for m in _gradle_version_matches(read_file(p)):
|
|
695
771
|
version = core_version(m.group(3))
|
|
696
772
|
if version:
|
|
697
773
|
return version
|
|
@@ -725,10 +801,9 @@ def _read_react_native(path_dir):
|
|
|
725
801
|
$(MARKETING_VERSION) 참조나 템플릿 기본값 "1.0"처럼 x.y.z가 아닌 값은 건너뛴다."""
|
|
726
802
|
ios_dir = Path(path_dir) / "ios"
|
|
727
803
|
if ios_dir.is_dir():
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
version = core_version(m.group(1)) if m else None
|
|
804
|
+
for plist in _rn_app_plists(ios_dir):
|
|
805
|
+
m = _PLIST_VERSION_RE.search(read_file(plist))
|
|
806
|
+
version = core_version(m.group(2)) if m else None
|
|
732
807
|
if version:
|
|
733
808
|
return version
|
|
734
809
|
gradle_file = Path(path_dir) / "android" / "app" / "build.gradle"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
# - go: (none — git tag based, go.mod has no version field)
|
|
28
28
|
# - basic: version.yml only
|
|
29
29
|
# A file version like 1.2.0-rc.1 or 1.2.0+4 is read as 1.2.0 (version below is x.y.z only).
|
|
30
|
+
# Synced files are rewritten to x.y.z as well, so even `get` turns 2.0.0-beta.1 in package.json into 2.0.0.
|
|
30
31
|
# Gradle/Maven -SNAPSHOT is kept when the version is bumped (1.2.0-SNAPSHOT -> 1.2.1-SNAPSHOT).
|
|
31
32
|
# ===
|
|
32
33
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -11,12 +11,11 @@
|
|
|
11
11
|
// 않는다. 번역하면 설명은 읽히지만 정작 화면에서 그 항목을 찾지 못한다.
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
-
import { join
|
|
14
|
+
import { join } from "node:path";
|
|
15
15
|
import { A, paint, colorEnabled, visualWidth } from "../ui/ansi.js";
|
|
16
16
|
import { PATHS } from "../core/paths.js";
|
|
17
17
|
import { parseExisting } from "../core/version-yml.js";
|
|
18
|
-
import {
|
|
19
|
-
import { inferInstalledStores } from "../core/installed-stores.js";
|
|
18
|
+
import { hooksFor } from "../core/types.js";
|
|
20
19
|
|
|
21
20
|
const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
22
21
|
|
|
@@ -43,8 +42,13 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
43
42
|
note: ["이 폴더엔 아직 설치되지 않았습니다 — 지금 설치하면 됩니다."],
|
|
44
43
|
});
|
|
45
44
|
|
|
46
|
-
// Flutter 스토어 배포 파일
|
|
47
|
-
if (installed)
|
|
45
|
+
// 타입별 로컬 파일 점검(예: Flutter 스토어 배포 파일) — 로컬 파일만 보므로 gh 조회 결과와 무관하게 수행한다.
|
|
46
|
+
if (installed) {
|
|
47
|
+
const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
|
|
48
|
+
for (const { hook } of hooksFor(existing.types, "doctorChecks")) {
|
|
49
|
+
for (const item of hook(cwd, existing, { docs: DOC })) add(item);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
const ghVersion = exec("gh", ["--version"]);
|
|
50
54
|
if (ghVersion.error || ghVersion.status !== 0) {
|
|
@@ -179,51 +183,6 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
179
183
|
return results;
|
|
180
184
|
}
|
|
181
185
|
|
|
182
|
-
const PLATFORM_ROW_NAME = { android: "Flutter Android 배포 파일", ios: "Flutter iOS 배포 파일" };
|
|
183
|
-
const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g; // 감지 규칙은 ExportOptions.plist 템플릿·IOS-TESTFLIGHT 검증과 동일
|
|
184
|
-
const EXPORT_OPTIONS_REL = STORE_APP_FILES.ios.find((rel) => rel.endsWith("ExportOptions.plist"));
|
|
185
|
-
|
|
186
|
-
// Flutter 스토어 배포 진단 — 선택한 플랫폼의 필수 파일과 ExportOptions.plist 플레이스홀더.
|
|
187
|
-
// 스토어 시크릿 등록 여부는 이번 범위 밖이다. 반환: doctor 결과 행 배열.
|
|
188
|
-
function flutterStoreChecks(cwd) {
|
|
189
|
-
const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
|
|
190
|
-
if (!existing.types.includes("flutter")) return [];
|
|
191
|
-
const saved = existing.options.flutterStore == null ? null : parseStoreList(existing.options.flutterStore);
|
|
192
|
-
// 저장값 없는 기존 설치는 설치된 스토어 워크플로우로 추론한다 (interactive와 같은 규칙).
|
|
193
|
-
const stores = saved ?? inferInstalledStores(join(cwd, PATHS.workflowsDir));
|
|
194
|
-
const flutterRoot = existing.paths.get("flutter") || ".";
|
|
195
|
-
const rows = [];
|
|
196
|
-
|
|
197
|
-
for (const platform of STORE_PLATFORMS.filter((p) => stores.includes(p))) {
|
|
198
|
-
const files = storeAppFilesFor([platform]).map((rel) => posix.join(flutterRoot, rel));
|
|
199
|
-
const missing = files.filter((file) => !existsSync(join(cwd, file)));
|
|
200
|
-
const head = { name: PLATFORM_ROW_NAME[platform], purpose: "fastlane 스토어 업로드에 필요한 파일" };
|
|
201
|
-
rows.push(missing.length
|
|
202
|
-
? {
|
|
203
|
-
...head, status: "WARN", value: `없는 파일: ${missing.join(", ")}`,
|
|
204
|
-
impact: ["이 파일이 없으면 스토어 배포 워크플로우가 fastlane 단계에서 실패합니다."],
|
|
205
|
-
actions: ["마법사를 다시 실행하면 없는 파일만 새로 만들어 줍니다 (이미 있는 파일은 덮어쓰지 않음)"],
|
|
206
|
-
doc: DOC.flutterStore,
|
|
207
|
-
}
|
|
208
|
-
: { ...head, status: "OK", value: `${files.length}개 있음` });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const plistPath = posix.join(flutterRoot, EXPORT_OPTIONS_REL);
|
|
212
|
-
if (stores.includes("ios") && existsSync(join(cwd, plistPath))) {
|
|
213
|
-
const head = { name: "ExportOptions.plist", purpose: "iOS 서명·내보내기 설정" };
|
|
214
|
-
const left = [...new Set(readFileSync(join(cwd, plistPath), "utf8").match(PLACEHOLDER_RE) ?? [])];
|
|
215
|
-
rows.push(left.length
|
|
216
|
-
? {
|
|
217
|
-
...head, status: "WARN", value: `채워지지 않은 값: ${left.join(", ")}`,
|
|
218
|
-
impact: ["채우지 않으면 IOS-TESTFLIGHT 워크플로우가 ExportOptions.plist 검증 단계에서 중단됩니다."],
|
|
219
|
-
actions: [`${plistPath} 의 플레이스홀더를 실제 값(Team ID · 번들 ID · 프로비저닝 프로파일 이름)으로 바꾸세요`],
|
|
220
|
-
doc: DOC.flutterStore,
|
|
221
|
-
}
|
|
222
|
-
: { ...head, status: "OK", value: "플레이스홀더 없음" });
|
|
223
|
-
}
|
|
224
|
-
return rows;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
186
|
const asLines = (v) => (Array.isArray(v) ? v : v ? [String(v)] : []);
|
|
228
187
|
const headOf = (r) => `${r.label || r.name}${r.purpose ? ` — ${r.purpose}` : ""}`;
|
|
229
188
|
|
package/src/commands/dry-run.js
CHANGED
|
@@ -4,9 +4,9 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { PATHS } from "../core/paths.js";
|
|
6
6
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
7
|
-
import {
|
|
7
|
+
import { planTypeAppFiles } from "../core/copy/app-files.js";
|
|
8
8
|
import { planUninstall } from "./uninstall.js";
|
|
9
|
-
import { renderVersionYml, parseExisting } from "../core/version-yml.js";
|
|
9
|
+
import { renderVersionYml, parseExisting, sameIgnoringTimestamps } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
12
|
import { planScripts } from "../core/copy/simple.js";
|
|
@@ -28,7 +28,8 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
|
28
28
|
const wouldBe = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), {
|
|
29
29
|
pathMarkers, extraTopLevel,
|
|
30
30
|
});
|
|
31
|
-
|
|
31
|
+
// 실제 설치와 같은 기준으로 비교한다 — 시각 줄만 다르면 설치도 파일을 다시 쓰지 않는다.
|
|
32
|
+
return { existed: existingRaw !== null, changed: existingRaw === null || !sameIgnoringTimestamps(existingRaw, wouldBe) };
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// mode: "full" | "uninstall". 읽기 전용 — 아무 파일도 쓰지 않는다.
|
|
@@ -49,7 +50,7 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
|
49
50
|
cleanup,
|
|
50
51
|
gitignore: backedUp ? planGitignore(targetRoot) : null,
|
|
51
52
|
// Flutter 스토어 배포 파일(Fastfile·ExportOptions.plist) — 이미 있는 파일은 덮어쓰지 않고 유지한다.
|
|
52
|
-
flutterApp:
|
|
53
|
+
flutterApp: planTypeAppFiles(context, payloadRoot, targetRoot),
|
|
53
54
|
versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
|
|
54
55
|
// 실제 설치가 함께 바꾸는 파일 — 특히 기존 파일을 덮어쓰는 스크립트는 미리 보여줘야 한다.
|
|
55
56
|
scripts: planScripts(payloadRoot, targetRoot),
|
package/src/commands/full.js
CHANGED
|
@@ -12,12 +12,12 @@ import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
|
12
12
|
import { addVersionSectionToReadme, README_STATUS_LABEL } from "../core/copy/readme.js";
|
|
13
13
|
import { copyWorkflows, computeBaselineEntries, makeSrcText } from "../core/copy/workflows.js";
|
|
14
14
|
import { copyScripts, removeScriptBytecode } from "../core/copy/simple.js";
|
|
15
|
-
import {
|
|
15
|
+
import { copyTypeAppFiles } from "../core/copy/app-files.js";
|
|
16
16
|
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
17
17
|
import { readBaseline, writeBaseline, appFileHash } from "../core/baseline.js";
|
|
18
18
|
import { scanUnsubstituted, classifySecrets, narrowSecretsBySshAuth } from "../core/verify.js";
|
|
19
19
|
import { cleanupOtherDeployWorkflows, payloadWorkflowNames, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
20
|
-
import {
|
|
20
|
+
import { hooksFor } from "../core/types.js";
|
|
21
21
|
import { findStaleWorkflows, cleanupStaleWorkflows } from "../core/removal-plan.js";
|
|
22
22
|
import { log, maskValue } from "../core/logger.js";
|
|
23
23
|
|
|
@@ -62,9 +62,9 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
62
62
|
|
|
63
63
|
// 1-1. Flutter 앱 파일(fastlane·ExportOptions) — 사용자가 값을 채워 쓰는 파일이라 없을 때만 만든다.
|
|
64
64
|
// 워크플로우 복사가 끝난 뒤에 실행한다(워크플로우가 이 파일들을 전제로 돈다).
|
|
65
|
-
const flutterApp =
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
const flutterApp = copyTypeAppFiles(context, payloadRoot, targetRoot, (tag, action, f) => {
|
|
66
|
+
log.info(tag, action, action === "keep" ? `${f} (기존 파일 유지)` : f);
|
|
67
|
+
});
|
|
68
68
|
|
|
69
69
|
// 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다.
|
|
70
70
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
@@ -188,9 +188,12 @@ export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { j
|
|
|
188
188
|
const cleanup = cleanupOtherDeployWorkflows(workflowsDir, listDir(),
|
|
189
189
|
context.deployStyle || DEFAULT_DEPLOY_STYLE, baseline,
|
|
190
190
|
{ available: payloadWorkflowNames(payloadRoot), justWritten, dryRun });
|
|
191
|
-
const storeCleanup =
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
const storeCleanup = { removed: [], backedUp: [] };
|
|
192
|
+
for (const { hook } of hooksFor(context.types || [], "cleanupWorkflows")) {
|
|
193
|
+
const r = hook(workflowsDir, listDir(), context, baseline, { dryRun });
|
|
194
|
+
storeCleanup.removed.push(...r.removed);
|
|
195
|
+
storeCleanup.backedUp.push(...r.backedUp);
|
|
196
|
+
}
|
|
194
197
|
const staleCleanup = cleanupStaleWorkflows(targetRoot, findStaleWorkflows(payloadRoot, targetRoot, baseline), baseline, { dryRun });
|
|
195
198
|
return { cleanup, storeCleanup, staleCleanup };
|
|
196
199
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 설치 설정 해석 단계 — 비대화형(index.js)과 대화형(interactive.js)이 공유한다.
|
|
2
|
+
// 값을 채우는 방법(플래그·질문)은 경로마다 다르지만, 타입·버전·배포 방식·옵션을 확정하고
|
|
3
|
+
// 설치 컨텍스트로 조립하는 규칙은 한 곳에 둬야 한쪽만 고쳐 두 모드의 결과가 갈라지지 않는다.
|
|
4
|
+
import { detectVersion, detectBuildNumber } from "../core/detect-fs.js";
|
|
5
|
+
import { createContext } from "../context.js";
|
|
6
|
+
import { isDeployStyle, DEFAULT_DEPLOY_STYLE, hasServerDeployWorkflows, effectiveDeployStyle } from "../core/deploy-style.js";
|
|
7
|
+
|
|
8
|
+
// version.yml에 저장된 배포 방식 — 없거나 알 수 없는 값이면 "".
|
|
9
|
+
export const savedDeployStyle = (existing) =>
|
|
10
|
+
isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
|
|
11
|
+
|
|
12
|
+
// 배포 방식 — 명시값(플래그·질문 답변) → 저장값 → 기본값.
|
|
13
|
+
// 서버 배포 워크플로우가 없는 타입은 설치 결과에 영향이 없으므로 기록하지 않는다(null).
|
|
14
|
+
export function resolveDeployStyle({ payload, types, explicit = "", existing }) {
|
|
15
|
+
if (!hasServerDeployWorkflows(payload, types)) return null;
|
|
16
|
+
return explicit || savedDeployStyle(existing) || DEFAULT_DEPLOY_STYLE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 버전 — 기존 version.yml 최우선(재실행 시 덮어쓰기 방지) → 명시값 → 파일 감지.
|
|
20
|
+
// detectOpts는 감지 함수로 그대로 넘긴다(types·paths·warn·hint).
|
|
21
|
+
export function resolveVersion({ cwd, existing, explicit = "", ...detectOpts }) {
|
|
22
|
+
return existing?.version || explicit || detectVersion(cwd, detectOpts);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 빌드 번호 — 기존 값 보존, 신규 통합이면 프로젝트 파일에서 감지, 못 찾으면 1.
|
|
26
|
+
export function resolveVersionCode({ cwd, existing, types, paths }) {
|
|
27
|
+
return existing?.versionCode ?? detectBuildNumber(cwd, { types, paths }) ?? 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 확정된 값들을 설치 컨텍스트로 조립한다.
|
|
31
|
+
// deployStyle: resolveDeployStyle 결과(고른 값). 실제 설치되는 방식으로 바꿔 기록한다.
|
|
32
|
+
// flutterOptions: { envMode, stores, androidDeployMode, iosDeployMode } — 두 경로 모두 확정된 값.
|
|
33
|
+
// releaseOptions: { includeSemverAuto, includeCopilotAi } — resolveReleaseOptions 결과.
|
|
34
|
+
// extra: 경로별로만 쓰는 필드(envValues 등).
|
|
35
|
+
export function buildInstallContext({
|
|
36
|
+
payload, existing, templateVersion, types, deployStyle, flutterOptions, releaseOptions, ...rest
|
|
37
|
+
}) {
|
|
38
|
+
return createContext({
|
|
39
|
+
types,
|
|
40
|
+
...releaseOptions,
|
|
41
|
+
// 안내는 고른 값으로 하되, 기록은 실제로 설치되는 방식으로 한다.
|
|
42
|
+
deployStyle: effectiveDeployStyle(payload, types, deployStyle),
|
|
43
|
+
envMode: flutterOptions.envMode,
|
|
44
|
+
flutterStore: flutterOptions.stores,
|
|
45
|
+
androidDeployMode: flutterOptions.androidDeployMode,
|
|
46
|
+
iosDeployMode: flutterOptions.iosDeployMode,
|
|
47
|
+
previousTemplateVersion: existing?.templateVersion || "",
|
|
48
|
+
templateVersion,
|
|
49
|
+
...rest,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "../core/assets.js";
|
|
8
|
-
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers,
|
|
8
|
+
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectMarkers } from "../core/detect-fs.js";
|
|
9
9
|
import { parseExisting } from "../core/version-yml.js";
|
|
10
10
|
import { pickReleaseOptions, resolveReleaseOptions } from "../core/release-options.js";
|
|
11
11
|
import { runBreakingCheck } from "../core/breaking-check.js";
|
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
} from "../core/branches.js";
|
|
16
16
|
import { promptEnvPlan } from "../ui/env-plan.js";
|
|
17
17
|
import { surveyWorkflows } from "../core/copy/workflows.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { VALID_TYPES } from "../context.js";
|
|
19
|
+
import { savedDeployStyle, resolveDeployStyle, resolveVersion, resolveVersionCode, buildInstallContext } from "./install-settings.js";
|
|
20
|
+
import { isDeployStyle, DEFAULT_DEPLOY_STYLE, hasServerDeployWorkflows, hasNonstopWorkflows } from "../core/deploy-style.js";
|
|
20
21
|
import { PATHS } from "../core/paths.js";
|
|
21
22
|
import { resolveFlutterOptions, DEFAULT_DEPLOY_MODE, STORE_PLATFORMS } from "../core/flutter-options.js";
|
|
22
23
|
import { inferInstalledStores } from "../core/installed-stores.js";
|
|
@@ -83,8 +84,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
83
84
|
// 먼저 나와 앞선 질문에 대한 경고처럼 보였다. 안내 문구도 대화형용으로 바꾼다.
|
|
84
85
|
const detectWarnings = [];
|
|
85
86
|
let types = detectTypes(cwd);
|
|
86
|
-
let version = (
|
|
87
|
-
types,
|
|
87
|
+
let version = resolveVersion({
|
|
88
|
+
cwd, existing, types,
|
|
88
89
|
warn: (m) => detectWarnings.push(m),
|
|
89
90
|
hint: "다음 화면의 '수정하기 > 버전'에서 바로 고칠 수 있습니다.",
|
|
90
91
|
});
|
|
@@ -100,7 +101,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
100
101
|
let { semverAuto: includeSemverAuto, copilotAi: includeCopilotAi } = pickReleaseOptions(
|
|
101
102
|
{ semverAuto: baseCtx?.includeSemverAuto, copilotAi: baseCtx?.includeCopilotAi }, existing);
|
|
102
103
|
// 서버 배포 방식 — 저장값(version.yml)이 있으면 재질문하지 않는다 (semver_auto와 같은 규약).
|
|
103
|
-
let deployStyle =
|
|
104
|
+
let deployStyle = savedDeployStyle(existing);
|
|
104
105
|
const showOptional = mode === "full";
|
|
105
106
|
const realTty = process.stdout.isTTY === true;
|
|
106
107
|
|
|
@@ -295,7 +296,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
295
296
|
if (versionAutoDetected && [...paths.values()].some((p) => p && p !== ".")) {
|
|
296
297
|
version = detectVersion(cwd, { types, paths, warn: () => {} });
|
|
297
298
|
}
|
|
298
|
-
const versionCode =
|
|
299
|
+
const versionCode = resolveVersionCode({ cwd, existing, types, paths });
|
|
299
300
|
|
|
300
301
|
// @wizard env 계획 질문 (full/workflows만)
|
|
301
302
|
const resolvers = makeResolvers(cwd, repoName, paths, flutterOptions);
|
|
@@ -312,20 +313,17 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
const { now, today } = clock || utcNow();
|
|
315
|
-
const ctx =
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
const ctx = buildInstallContext({
|
|
317
|
+
payload, existing, templateVersion, types,
|
|
318
|
+
// 저장값이 무중단이어도 선택한 타입에 그 방식이 없으면 단일 서버 배포가 설치된다 — 설치된 방식을 기록한다.
|
|
319
|
+
deployStyle: resolveDeployStyle({ payload, types, explicit: deployStyle, existing }),
|
|
320
|
+
flutterOptions,
|
|
321
|
+
releaseOptions: { includeSemverAuto, includeCopilotAi },
|
|
322
|
+
mode, force: true, version, versionCode, branch, branches, paths,
|
|
323
|
+
repoName, resolvers, envValues, envUseDefaults, now, today,
|
|
320
324
|
// 설치 로그·완료 요약이 쓰는 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
321
325
|
markers, envAnswers, detectWarnings,
|
|
322
|
-
// 저장값이 무중단이어도 선택한 타입에 그 방식이 없으면 단일 서버 배포가 설치된다 — 설치된 방식을 기록한다.
|
|
323
|
-
deployStyle: hasServerDeploy() ? effectiveDeployStyle(payload, types, deployStyle || DEFAULT_DEPLOY_STYLE) : null,
|
|
324
|
-
envMode: flutterOptions.envMode, flutterStore: flutterOptions.stores,
|
|
325
|
-
androidDeployMode: flutterOptions.androidDeployMode, iosDeployMode: flutterOptions.iosDeployMode,
|
|
326
|
-
previousTemplateVersion: existing?.templateVersion || "",
|
|
327
326
|
});
|
|
328
|
-
ctx.templateVersion = templateVersion;
|
|
329
327
|
|
|
330
328
|
// 사용자가 답해야 하는 것만 묻는다. baseline 3-way가 자동으로 안전한 경우를
|
|
331
329
|
// 걸러내므로, 여기 오는 것은 (a) 양쪽이 다 바뀐 진짜 충돌과 (b) 사용자가 지운 파일뿐이다.
|
package/src/commands/purge.js
CHANGED
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { PATHS } from "../core/paths.js";
|
|
10
|
-
import { remove } from "../core/fsutil.js";
|
|
11
10
|
import { planRemoval, backupArtifacts } from "../core/removal-plan.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { logRemovals } from "../core/logger.js";
|
|
11
|
+
import { executeRemoval } from "../core/removal-exec.js";
|
|
12
|
+
import { hasVersionSection } from "../core/copy/readme.js";
|
|
13
|
+
import { hasAutoAddedEntries } from "../core/copy/gitignore.js";
|
|
16
14
|
|
|
17
15
|
const CHANGELOG_FILES = ["CHANGELOG.json", "CHANGELOG.md"];
|
|
18
16
|
|
|
@@ -87,22 +85,11 @@ export function printPurgePlan(plan, { dryRun = false } = {}) {
|
|
|
87
85
|
// 이론상 어긋나는 경우에도 printPurgeResult가 거짓으로 "제거됨"을 보고하지 않는다.
|
|
88
86
|
export function executePurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
89
87
|
const plan = planPurge(payloadRoot, targetRoot, keepFlags);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for (const p of plan.baseline || []) { remove(join(targetRoot, p)); done.push(["remove", "metadata", p]); }
|
|
96
|
-
pruneInstallDirs(targetRoot, plan);
|
|
97
|
-
if (plan.versionYml) { remove(join(targetRoot, PATHS.versionFile)); done.push(["remove", "version", PATHS.versionFile]); }
|
|
98
|
-
const readmeSection = plan.readmeSection && removeVersionSectionFromReadme(targetRoot) === "removed";
|
|
99
|
-
const gitignoreStatus = plan.gitignore ? removeAutoAddedEntriesFromGitignore(targetRoot) : null;
|
|
100
|
-
const gitignore = gitignoreStatus === "removed" || gitignoreStatus === "file-deleted";
|
|
101
|
-
if (gitignore) done.push(["remove", "gitignore", ".gitignore 자동 추가 항목"]);
|
|
102
|
-
for (const f of plan.changelog) { remove(join(targetRoot, f)); done.push(["remove", "changelog", f]); }
|
|
103
|
-
if (readmeSection) done.push(["remove", "readme", "README.md 버전 섹션"]);
|
|
104
|
-
logRemovals(targetRoot, done);
|
|
105
|
-
return { ...plan, readmeSection, gitignore };
|
|
88
|
+
const { readme, gitignore } = executeRemoval(targetRoot, { ...plan, readme: plan.readmeSection }, {
|
|
89
|
+
logOrder: ["version", "gitignore", "changelog", "readme"],
|
|
90
|
+
gitignoreDetail: (status) => (status === "removed" || status === "file-deleted" ? ".gitignore 자동 추가 항목" : null),
|
|
91
|
+
});
|
|
92
|
+
return { ...plan, readmeSection: readme, gitignore };
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
// 삭제 후 실제 제거된 목록 출력 — printPurgePlan과 완전히 동일한 형태(파일명 나열)로
|
package/src/commands/status.js
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveFlutterOptions } from "../core/flutter-options.js";
|
|
|
9
9
|
import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
10
10
|
import { findStaleWorkflows } from "../core/removal-plan.js";
|
|
11
11
|
import { readBaseline } from "../core/baseline.js";
|
|
12
|
+
import { hooksFor } from "../core/types.js";
|
|
12
13
|
|
|
13
14
|
// payloadRoot: 패키지 payload/ 루트. targetRoot: 상태를 확인할 대상 레포.
|
|
14
15
|
export function runStatus(payloadRoot, targetRoot = ".") {
|
|
@@ -77,9 +78,9 @@ export function printStatus(status) {
|
|
|
77
78
|
const boolLabel = (v) => (v === null ? "미설정(기본 false)" : v);
|
|
78
79
|
const semverAutoLabel = status.options.semverAuto === null ? "미설정(기본 false)" : status.options.semverAuto;
|
|
79
80
|
const copilotAiLabel = boolLabel(status.options.copilotAi ?? null);
|
|
80
|
-
const
|
|
81
|
+
const typeLabels = hooksFor(status.types, "statusLabels").map(({ hook }) => hook(status.options)).join("");
|
|
81
82
|
const deployLabel = status.options.deployStyle ? ` deploy_style=${status.options.deployStyle}` : "";
|
|
82
|
-
lines.push(`옵션 : semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${deployLabel}${
|
|
83
|
+
lines.push(`옵션 : semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${deployLabel}${typeLabels}`);
|
|
83
84
|
if (status.modifiedFiles.length) {
|
|
84
85
|
lines.push("", `사용자가 수정한 워크플로우 파일 (${status.modifiedFiles.length}개):`);
|
|
85
86
|
for (const f of status.modifiedFiles) lines.push(` - ${f}`);
|
|
@@ -104,13 +105,3 @@ export function printStatus(status) {
|
|
|
104
105
|
lines.push("");
|
|
105
106
|
console.log(lines.join("\n"));
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
-
// Flutter 타입일 때만 붙는 옵션. 저장값이 없으면 그 상태에서 실제로 적용되는 동작을 함께 알린다.
|
|
109
|
-
function flutterOptionLabels(options) {
|
|
110
|
-
return [
|
|
111
|
-
` env_mode=${options.envMode ?? "미설정(dotenv 유지)"}`,
|
|
112
|
-
` flutter_store=${options.flutterStore ?? "미설정(둘 다 설치)"}`,
|
|
113
|
-
` android_deploy_mode=${options.androidDeployMode ?? "미설정(store_only)"}`,
|
|
114
|
-
` ios_deploy_mode=${options.iosDeployMode ?? "미설정(store_only)"}`,
|
|
115
|
-
].join("");
|
|
116
|
-
}
|