project-auto-wizard 0.13.3 → 0.14.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 +1 -1
- package/bin/project-auto-wizard.js +18 -4
- package/package.json +2 -2
- package/payload/config/breaking-changes.json +28 -14
- package/payload/config/wizard-prompts.yml +102 -46
- package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
- package/payload/flutter-app/ios/ExportOptions.plist +8 -8
- package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
- package/payload/scripts/changelog_manager.py +296 -257
- package/payload/scripts/issue_helper.py +54 -50
- package/payload/scripts/messages.py +2279 -0
- package/payload/scripts/truncate_release_notes.py +7 -6
- package/payload/scripts/version_manager.py +64 -61
- package/payload/version.yml.template +2 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
- package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
- package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
- package/src/cli/args.js +98 -52
- package/src/cli/help.js +8 -41
- package/src/commands/doctor.js +85 -89
- package/src/commands/dry-run.js +63 -43
- package/src/commands/full.js +79 -77
- package/src/commands/install-settings.js +16 -15
- package/src/commands/interactive-flutter.js +20 -19
- package/src/commands/interactive.js +135 -132
- package/src/commands/purge.js +47 -42
- package/src/commands/status.js +36 -36
- package/src/commands/uninstall.js +43 -41
- package/src/context.js +12 -11
- package/src/core/assets.js +12 -11
- package/src/core/baseline.js +27 -27
- package/src/core/branches.js +29 -27
- package/src/core/branding.js +7 -6
- package/src/core/breaking-check.js +22 -21
- package/src/core/breaking.js +15 -8
- package/src/core/copy/app-files.js +5 -4
- package/src/core/copy/flutter-app.js +8 -8
- package/src/core/copy/gitignore.js +58 -50
- package/src/core/copy/readme.js +45 -35
- package/src/core/copy/simple.js +16 -15
- package/src/core/copy/workflows.js +131 -124
- package/src/core/deploy-style.js +51 -48
- package/src/core/detect-fs.js +57 -57
- package/src/core/detect.js +67 -65
- package/src/core/errors.js +2 -2
- package/src/core/flutter-doctor.js +17 -16
- package/src/core/flutter-hooks.js +33 -32
- package/src/core/flutter-options.js +34 -33
- package/src/core/fsutil.js +12 -12
- package/src/core/installed-stores.js +4 -4
- package/src/core/logger.js +42 -40
- package/src/core/paths-resolve.js +88 -86
- package/src/core/paths.js +10 -10
- package/src/core/release-options.js +8 -8
- package/src/core/removal-exec.js +14 -13
- package/src/core/removal-plan.js +37 -37
- package/src/core/types.js +47 -47
- package/src/core/verify.js +44 -38
- package/src/core/version-yml.js +76 -68
- package/src/core/wizard-env.js +46 -46
- package/src/core/wizard-labels.js +63 -41
- package/src/i18n/catalog/en/cli.js +50 -0
- package/src/i18n/catalog/en/commands.js +187 -0
- package/src/i18n/catalog/en/copy.js +19 -0
- package/src/i18n/catalog/en/core.js +74 -0
- package/src/i18n/catalog/en/core2.js +15 -0
- package/src/i18n/catalog/en/ui.js +221 -0
- package/src/i18n/catalog/en.js +12 -0
- package/src/i18n/catalog/index.js +5 -0
- package/src/i18n/catalog/ko/cli.js +50 -0
- package/src/i18n/catalog/ko/commands.js +187 -0
- package/src/i18n/catalog/ko/copy.js +19 -0
- package/src/i18n/catalog/ko/core.js +74 -0
- package/src/i18n/catalog/ko/core2.js +15 -0
- package/src/i18n/catalog/ko/ui.js +221 -0
- package/src/i18n/catalog/ko.js +10 -0
- package/src/i18n/index.js +44 -0
- package/src/i18n/languages.js +9 -0
- package/src/index.js +128 -94
- package/src/ui/ansi.js +7 -7
- package/src/ui/banner.js +7 -6
- package/src/ui/env-plan.js +82 -81
- package/src/ui/prompts.js +97 -122
- package/src/ui/readline-engine.js +59 -58
- package/src/ui/status-cards.js +37 -31
- package/src/ui/summary.js +89 -88
package/src/commands/doctor.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// doctor
|
|
1
|
+
// doctor command: local environment diagnosis (read-only, rule-based). Delegates to the gh CLI to check remote state.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// `Android toolchain - develop for Android devices
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
3
|
+
// Output design, borrowed from the `flutter doctor` pattern.
|
|
4
|
+
// 1. Each item label carries its purpose ("what this setting is for"). `WORKFLOW_PAT` alone does not tell
|
|
5
|
+
// what it does in the release flow, the same reason flutter prints
|
|
6
|
+
// `Android toolchain - develop for Android devices`.
|
|
7
|
+
// 2. The tool never judges "OK to install / not OK". It only states the facts found (N problems) and
|
|
8
|
+
// leaves it to the user whether they matter.
|
|
9
|
+
// 3. Only problem items unfold into 4 levels (symptom -> impact -> action -> docs); healthy items collapse to one line.
|
|
10
|
+
// 4. Strings actually shown in the GitHub settings UI ("Read and write permissions", etc.) are not
|
|
11
|
+
// translated: a translation reads well but then the item cannot be found on the real screen.
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
@@ -16,33 +16,32 @@ 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
18
|
import { hooksFor } from "../core/types.js";
|
|
19
|
+
import { t } from "../i18n/index.js";
|
|
19
20
|
|
|
20
21
|
const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
23
|
+
// Resolution guide links. They point at English HTML anchors inside the docs site pages: auto-generated
|
|
24
|
+
// anchors from non-Latin headings get URL-encoded and become unreadable in a terminal.
|
|
25
|
+
// Getters so the link follows the output language (the localized page path comes from the catalog).
|
|
25
26
|
export const DOCS_SITE_URL = "https://twin-fang.github.io/project-auto-wizard";
|
|
26
27
|
export const DOC = {
|
|
27
|
-
postInstall
|
|
28
|
-
flutterStore
|
|
28
|
+
get postInstall() { return `${DOCS_SITE_URL}${t("cmd.doctor.docPath.postInstall")}`; },
|
|
29
|
+
get flutterStore() { return `${DOCS_SITE_URL}${t("cmd.doctor.docPath.flutterStore")}`; },
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
32
33
|
const results = [];
|
|
33
|
-
// name
|
|
34
|
+
// name is the item identifier (for tests/programmatic reference), label is for display. Falls back to name when label is omitted.
|
|
34
35
|
const add = (item) => { results.push({ actions: [], ...item }); return results; };
|
|
35
36
|
|
|
36
37
|
const installed = existsSync(join(cwd, "version.yml"));
|
|
37
|
-
//
|
|
38
|
+
// Not installed is not a problem: running doctor "before" installing is the normal usage path.
|
|
39
|
+
const installItem = { name: t("cmd.doctor.install.name"), label: t("cmd.doctor.install.label"), purpose: t("cmd.doctor.install.purpose") };
|
|
38
40
|
add(installed
|
|
39
|
-
? {
|
|
40
|
-
: {
|
|
41
|
-
name: "설치 여부", label: "설치 상태", purpose: "이 폴더의 마법사 설치 여부", status: "INFO",
|
|
42
|
-
note: ["이 폴더엔 아직 설치되지 않았습니다 — 지금 설치하면 됩니다."],
|
|
43
|
-
});
|
|
41
|
+
? { ...installItem, status: "OK", value: t("cmd.doctor.install.value") }
|
|
42
|
+
: { ...installItem, status: "INFO", note: [t("cmd.doctor.install.note")] });
|
|
44
43
|
|
|
45
|
-
//
|
|
44
|
+
// Per-type local file checks (e.g. Flutter store deploy files): only local files are read, so this runs regardless of the gh lookup results.
|
|
46
45
|
if (installed) {
|
|
47
46
|
const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
|
|
48
47
|
for (const { hook } of hooksFor(existing.types, "doctorChecks")) {
|
|
@@ -53,41 +52,41 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
53
52
|
const ghVersion = exec("gh", ["--version"]);
|
|
54
53
|
if (ghVersion.error || ghVersion.status !== 0) {
|
|
55
54
|
add({
|
|
56
|
-
name: "gh CLI", purpose: "
|
|
57
|
-
value: "gh
|
|
58
|
-
impact: ["
|
|
59
|
-
actions: ["
|
|
55
|
+
name: "gh CLI", purpose: t("cmd.doctor.gh.purpose"), status: "WARN",
|
|
56
|
+
value: t("cmd.doctor.gh.notFound"),
|
|
57
|
+
impact: [t("cmd.doctor.gh.notFoundImpact")],
|
|
58
|
+
actions: [t("cmd.doctor.gh.notFoundAction")],
|
|
60
59
|
});
|
|
61
60
|
return results;
|
|
62
61
|
}
|
|
63
62
|
add({
|
|
64
|
-
name: "gh CLI", purpose: "
|
|
65
|
-
value: (ghVersion.stdout || "").split("\n")[0] || "
|
|
63
|
+
name: "gh CLI", purpose: t("cmd.doctor.gh.purpose"), status: "OK",
|
|
64
|
+
value: (ghVersion.stdout || "").split("\n")[0] || t("cmd.doctor.gh.installed"),
|
|
66
65
|
});
|
|
67
66
|
|
|
68
67
|
const auth = exec("gh", ["auth", "status"]);
|
|
69
68
|
const authOk = !auth.error && auth.status === 0;
|
|
70
69
|
if (!authOk) {
|
|
71
70
|
add({
|
|
72
|
-
name: "
|
|
73
|
-
value: "
|
|
74
|
-
impact: ["
|
|
75
|
-
actions: ["
|
|
71
|
+
name: t("cmd.doctor.auth.name"), label: t("cmd.doctor.auth.label"), purpose: t("cmd.doctor.auth.purpose"), status: "FAIL",
|
|
72
|
+
value: t("cmd.doctor.auth.notLoggedIn"),
|
|
73
|
+
impact: [t("cmd.doctor.auth.impact")],
|
|
74
|
+
actions: [t("cmd.doctor.auth.action")],
|
|
76
75
|
});
|
|
77
76
|
return results;
|
|
78
77
|
}
|
|
79
|
-
add({ name: "
|
|
78
|
+
add({ name: t("cmd.doctor.auth.name"), label: t("cmd.doctor.auth.label"), purpose: t("cmd.doctor.auth.purpose"), status: "OK", value: t("cmd.doctor.auth.ok") });
|
|
80
79
|
|
|
81
80
|
const remote = exec("git", ["-C", cwd, "remote", "get-url", "origin"]);
|
|
82
81
|
const url = remote.status === 0 ? (remote.stdout || "").trim() : "";
|
|
83
|
-
//
|
|
82
|
+
// Repo names may contain dots (user.github.io, next.js); strip only the trailing .git.
|
|
84
83
|
const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(\.git)?\/?$/);
|
|
85
84
|
if (!match) {
|
|
86
85
|
add({
|
|
87
|
-
name: "
|
|
88
|
-
value: "
|
|
89
|
-
impact: ["
|
|
90
|
-
actions: ["
|
|
86
|
+
name: t("cmd.doctor.remote.name"), purpose: t("cmd.doctor.remote.purpose"), status: "WARN",
|
|
87
|
+
value: t("cmd.doctor.remote.notFound"),
|
|
88
|
+
impact: [t("cmd.doctor.remote.impact")],
|
|
89
|
+
actions: [t("cmd.doctor.remote.action")],
|
|
91
90
|
});
|
|
92
91
|
return results;
|
|
93
92
|
}
|
|
@@ -95,30 +94,30 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
95
94
|
|
|
96
95
|
const perm = exec("gh", ["api", `repos/${owner}/${repo}/actions/permissions/workflow`, "--jq", ".default_workflow_permissions"]);
|
|
97
96
|
const permValue = (perm.stdout || "").trim();
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// tests/node/payload-workflow-permissions.test.js)
|
|
101
|
-
// read
|
|
102
|
-
const PERM_PURPOSE = "
|
|
97
|
+
// This value is the "default applied when a workflow omits permissions", not an upper bound.
|
|
98
|
+
// Every workflow the wizard installs declares its own permissions (regression guard:
|
|
99
|
+
// tests/node/payload-workflow-permissions.test.js), so it works even with read; this repo itself is
|
|
100
|
+
// read and VERSION-CONTROL still succeeds in pushing the version commit, which is the proof.
|
|
101
|
+
const PERM_PURPOSE = t("cmd.doctor.perm.purpose");
|
|
103
102
|
if (perm.status !== 0) {
|
|
104
|
-
//
|
|
103
|
+
// A failed lookup is not a misdiagnosis but a real lack of information; the action is to ask an admin.
|
|
105
104
|
add({
|
|
106
105
|
name: "Workflow permissions", purpose: PERM_PURPOSE, status: "WARN",
|
|
107
|
-
value: "
|
|
108
|
-
impact: ["
|
|
109
|
-
actions: ["
|
|
106
|
+
value: t("cmd.doctor.perm.lookupFailed"),
|
|
107
|
+
impact: [t("cmd.doctor.adminImpact")],
|
|
108
|
+
actions: [t("cmd.doctor.perm.action")],
|
|
110
109
|
doc: DOC.postInstall,
|
|
111
110
|
});
|
|
112
111
|
} else if (permValue === "write") {
|
|
113
112
|
add({ name: "Workflow permissions", purpose: PERM_PURPOSE, status: "OK", value: "Read and write" });
|
|
114
113
|
} else {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
114
|
+
// No action is needed, so this is INFO rather than WARN. A warning would announce a nonexistent problem
|
|
115
|
+
// and push users toward an unnecessary permission increase, against the least-privilege principle.
|
|
117
116
|
add({
|
|
118
117
|
name: "Workflow permissions", purpose: PERM_PURPOSE, status: "INFO",
|
|
119
118
|
note: [
|
|
120
|
-
|
|
121
|
-
"
|
|
119
|
+
t("cmd.doctor.perm.infoCurrent", { value: permValue || t("cmd.doctor.perm.unknown") }),
|
|
120
|
+
t("cmd.doctor.perm.infoAdvice"),
|
|
122
121
|
],
|
|
123
122
|
});
|
|
124
123
|
}
|
|
@@ -126,57 +125,54 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
126
125
|
const secrets = exec("gh", ["secret", "list", "--repo", `${owner}/${repo}`]);
|
|
127
126
|
const hasPat = secrets.status === 0 && (secrets.stdout || "").split("\n").some((l) => l.startsWith("WORKFLOW_PAT"));
|
|
128
127
|
add(hasPat
|
|
129
|
-
? { name: "WORKFLOW_PAT secret", label: "WORKFLOW_PAT", purpose: "
|
|
128
|
+
? { name: "WORKFLOW_PAT secret", label: "WORKFLOW_PAT", purpose: t("cmd.doctor.pat.purpose"), status: "OK", value: t("cmd.doctor.pat.registered") }
|
|
130
129
|
: {
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
name: "WORKFLOW_PAT secret", label: "WORKFLOW_PAT", purpose: "
|
|
135
|
-
note: [
|
|
136
|
-
"secret이 없어도 폴백이 자동으로 이어받아 태그·Release 발행과 main 배포 워크플로우 실행까지 진행됩니다 — 실제 병합 후 최대 ~20초 정도 더 걸릴 뿐입니다.",
|
|
137
|
-
"속도를 더 원한다면 PAT을 등록할 수 있습니다 — 반드시 개인 계정이 아닌 조직 bot/machine 계정으로 발급하세요 (scopes: repo, workflow).",
|
|
138
|
-
"등록: 레포 Settings → Secrets and variables → Actions → New repository secret · 이름은 WORKFLOW_PAT",
|
|
139
|
-
],
|
|
130
|
+
// No action is needed, so this is INFO rather than WARN, for the same reason as the Workflow permissions
|
|
131
|
+
// item above: the fallback (wait-for-merge-and-trigger-release / Trigger NPM-PUBLISH) carries the
|
|
132
|
+
// pipeline through to the end with GITHUB_TOKEN alone, so a nonexistent problem is not raised as a warning.
|
|
133
|
+
name: "WORKFLOW_PAT secret", label: "WORKFLOW_PAT", purpose: t("cmd.doctor.pat.purpose"), status: "INFO",
|
|
134
|
+
note: [t("cmd.doctor.pat.note1"), t("cmd.doctor.pat.note2"), t("cmd.doctor.pat.note3")],
|
|
140
135
|
});
|
|
141
136
|
|
|
142
137
|
const mergeSettings = exec("gh", ["api", `repos/${owner}/${repo}`, "--jq", ".allow_merge_commit"]);
|
|
143
138
|
const automergeOk = mergeSettings.status === 0 && mergeSettings.stdout.trim() === "true";
|
|
139
|
+
const mergeItem = { name: t("cmd.doctor.merge.name"), label: t("cmd.doctor.merge.label"), purpose: t("cmd.doctor.merge.purpose") };
|
|
144
140
|
if (mergeSettings.status !== 0) {
|
|
145
141
|
add({
|
|
146
|
-
|
|
147
|
-
value: "
|
|
148
|
-
impact: ["
|
|
149
|
-
actions: ["
|
|
142
|
+
...mergeItem, status: "WARN",
|
|
143
|
+
value: t("cmd.doctor.merge.lookupFailed"),
|
|
144
|
+
impact: [t("cmd.doctor.adminImpact")],
|
|
145
|
+
actions: [t("cmd.doctor.merge.lookupAction")],
|
|
150
146
|
doc: DOC.postInstall,
|
|
151
147
|
});
|
|
152
148
|
} else if (automergeOk) {
|
|
153
|
-
add({
|
|
149
|
+
add({ ...mergeItem, status: "OK", value: t("cmd.doctor.merge.allowed") });
|
|
154
150
|
} else {
|
|
155
151
|
add({
|
|
156
|
-
|
|
157
|
-
value: "
|
|
158
|
-
impact: ["
|
|
152
|
+
...mergeItem, status: "WARN",
|
|
153
|
+
value: t("cmd.doctor.merge.disabled"),
|
|
154
|
+
impact: [t("cmd.doctor.merge.impact")],
|
|
159
155
|
actions: [
|
|
160
|
-
"
|
|
161
|
-
"
|
|
156
|
+
t("cmd.doctor.merge.action1"),
|
|
157
|
+
t("cmd.doctor.merge.action2"),
|
|
162
158
|
],
|
|
163
159
|
doc: DOC.postInstall,
|
|
164
160
|
});
|
|
165
161
|
}
|
|
166
162
|
|
|
167
|
-
//
|
|
163
|
+
// Show the actual setting: telling a user who turned it on that it is off would misrepresent the current state.
|
|
168
164
|
const copilotAi = installed ? parseExisting(readFileSync(join(cwd, "version.yml"), "utf8")).options.copilotAi : null;
|
|
169
165
|
const copilotState = copilotAi === true
|
|
170
|
-
? "
|
|
166
|
+
? t("cmd.doctor.copilot.on")
|
|
171
167
|
: copilotAi === false
|
|
172
|
-
? "
|
|
173
|
-
: "
|
|
168
|
+
? t("cmd.doctor.copilot.off")
|
|
169
|
+
: t("cmd.doctor.copilot.default");
|
|
174
170
|
add({
|
|
175
|
-
name: "
|
|
171
|
+
name: t("cmd.doctor.copilot.name"), label: t("cmd.doctor.copilot.name"), purpose: t("cmd.doctor.copilot.purpose"), status: "INFO",
|
|
176
172
|
note: [
|
|
177
173
|
copilotState,
|
|
178
|
-
|
|
179
|
-
"
|
|
174
|
+
t(copilotAi === true ? "cmd.doctor.copilot.creditsOn" : "cmd.doctor.copilot.creditsOff"),
|
|
175
|
+
t("cmd.doctor.copilot.fallback"),
|
|
180
176
|
],
|
|
181
177
|
});
|
|
182
178
|
|
|
@@ -192,9 +188,9 @@ export function printDoctorReport(results, { out = (s) => console.log(s), color
|
|
|
192
188
|
const problems = results.filter((r) => r.status === "WARN" || r.status === "FAIL");
|
|
193
189
|
const infos = results.filter((r) => r.status === "INFO");
|
|
194
190
|
|
|
195
|
-
const lines = ["", `${p("◆", A.cyan)} ${p("
|
|
191
|
+
const lines = ["", `${p("◆", A.cyan)} ${p(t("cmd.doctor.heading"), A.bold)} ${p("— project-auto-wizard doctor", A.dim)}`, ""];
|
|
196
192
|
|
|
197
|
-
//
|
|
193
|
+
// Healthy items: one line each. Values are aligned to the label width (double-width CJK counting is delegated to visualWidth).
|
|
198
194
|
if (oks.length) {
|
|
199
195
|
const width = Math.max(...oks.map((r) => visualWidth(headOf(r))));
|
|
200
196
|
for (const r of oks) {
|
|
@@ -205,18 +201,18 @@ export function printDoctorReport(results, { out = (s) => console.log(s), color
|
|
|
205
201
|
lines.push("");
|
|
206
202
|
}
|
|
207
203
|
|
|
208
|
-
//
|
|
204
|
+
// Problems: unfold as symptom -> impact -> action -> docs.
|
|
209
205
|
for (const r of problems) {
|
|
210
206
|
const fail = r.status === "FAIL";
|
|
211
207
|
lines.push(` ${p(fail ? "[✗]" : "[!]", fail ? A.red : A.yellow)} ${p(headOf(r), A.bold)}`);
|
|
212
208
|
if (r.value) lines.push(` ${p(fail ? "✗" : "✗", fail ? A.red : A.yellow)} ${r.value}`);
|
|
213
209
|
for (const l of asLines(r.impact)) lines.push(` ${p(l, A.dim)}`);
|
|
214
210
|
for (const a of asLines(r.actions)) lines.push(` ${p("→", A.cyan)} ${a}`);
|
|
215
|
-
if (r.doc) lines.push(` ${p("→", A.cyan)}
|
|
211
|
+
if (r.doc) lines.push(` ${p("→", A.cyan)} ${t("cmd.doctor.details")}${p(r.doc, A.dim)}`);
|
|
216
212
|
lines.push("");
|
|
217
213
|
}
|
|
218
214
|
|
|
219
|
-
//
|
|
215
|
+
// Notes: guidance that needs no action.
|
|
220
216
|
for (const r of infos) {
|
|
221
217
|
lines.push(` ${p("[i]", A.gray)} ${headOf(r)}`);
|
|
222
218
|
for (const l of asLines(r.note)) lines.push(` ${p(l, A.dim)}`);
|
|
@@ -227,15 +223,15 @@ export function printDoctorReport(results, { out = (s) => console.log(s), color
|
|
|
227
223
|
out(lines.join("\n"));
|
|
228
224
|
}
|
|
229
225
|
|
|
230
|
-
//
|
|
226
|
+
// Summary: states only the facts found instead of an "OK to install" verdict (same stance as flutter doctor's last line).
|
|
231
227
|
function summaryLine(problems, p) {
|
|
232
|
-
if (!problems.length) return ` ${p("✓", A.green)}
|
|
228
|
+
if (!problems.length) return ` ${p("✓", A.green)} ${t("cmd.doctor.summaryNone")}`;
|
|
233
229
|
const n = problems.length;
|
|
234
230
|
if (problems.some((r) => r.status === "FAIL")) {
|
|
235
|
-
return ` ${p("✗", A.red)} ${
|
|
231
|
+
return ` ${p("✗", A.red)} ${t("cmd.doctor.summaryFail", { n })}`;
|
|
236
232
|
}
|
|
237
233
|
return [
|
|
238
|
-
` ${p("!", A.yellow)} ${
|
|
239
|
-
`
|
|
234
|
+
` ${p("!", A.yellow)} ${t("cmd.doctor.summaryWarn", { n })}`,
|
|
235
|
+
` ${t("cmd.doctor.summaryWarnNote", { n })}`,
|
|
240
236
|
].join("\n");
|
|
241
237
|
}
|
package/src/commands/dry-run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// --dry-run
|
|
2
|
-
// full/uninstall
|
|
1
|
+
// --dry-run preview: computes what would change without writing any file.
|
|
2
|
+
// Supports the full/uninstall modes (partial-install and revert modes were removed).
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { PATHS } from "../core/paths.js";
|
|
@@ -14,6 +14,7 @@ import { planVersionSection } from "../core/copy/readme.js";
|
|
|
14
14
|
import { BASELINE_PATH, readBaseline } from "../core/baseline.js";
|
|
15
15
|
import { cleanupWorkflows } from "./full.js";
|
|
16
16
|
import { planGitignore } from "../core/copy/gitignore.js";
|
|
17
|
+
import { t } from "../i18n/index.js";
|
|
17
18
|
|
|
18
19
|
function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
19
20
|
const { paths = new Map() } = context;
|
|
@@ -24,35 +25,36 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
|
24
25
|
const existingRaw = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
25
26
|
const extraTopLevel = existingRaw !== null ? parseExisting(existingRaw).extraTopLevel : [];
|
|
26
27
|
|
|
27
|
-
//
|
|
28
|
+
// Use the same render function as the real install; assembling separately drifts the preview whenever an option is added.
|
|
28
29
|
const wouldBe = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), {
|
|
29
30
|
pathMarkers, extraTopLevel,
|
|
30
31
|
});
|
|
31
|
-
//
|
|
32
|
+
// Compare by the same rule as the real install: if only the timestamp lines differ, the install does not rewrite the file either.
|
|
32
33
|
return { existed: existingRaw !== null, changed: existingRaw === null || !sameIgnoringTimestamps(existingRaw, wouldBe) };
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
// mode: "full" | "uninstall".
|
|
36
|
+
// mode: "full" | "uninstall". Read-only: writes no file.
|
|
36
37
|
export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
37
38
|
if (mode === "uninstall") {
|
|
38
39
|
return { mode, uninstall: planUninstall(payloadRoot, targetRoot, context.uninstallSelection) };
|
|
39
40
|
}
|
|
40
41
|
const workflows = planWorkflows(context, payloadRoot, targetRoot);
|
|
41
|
-
//
|
|
42
|
-
//
|
|
42
|
+
// Run the same cleanup decision as the real run without touching files: files deleted or moved to .bak by a
|
|
43
|
+
// deploy style change or a store deselection must not be missing from the preview. Files a non-interactive
|
|
44
|
+
// run writes anew are the new and auto-updated ones.
|
|
43
45
|
const justWritten = [...workflows.newFiles, ...workflows.upstreamOnly].map((f) => f.filename);
|
|
44
46
|
const cleanup = cleanupWorkflows(context, payloadRoot, targetRoot, readBaseline(targetRoot), { justWritten, dryRun: true });
|
|
45
|
-
// .bak
|
|
47
|
+
// When a .bak appears, the real run ensures a backup-file entry in .gitignore. Non-interactive conflicts keep the existing file, so no .bak is made.
|
|
46
48
|
const backedUp = Object.values(cleanup).some((r) => r.backedUp.length > 0);
|
|
47
49
|
return {
|
|
48
50
|
mode,
|
|
49
51
|
workflows,
|
|
50
52
|
cleanup,
|
|
51
53
|
gitignore: backedUp ? planGitignore(targetRoot) : null,
|
|
52
|
-
// Flutter
|
|
54
|
+
// Flutter store deploy files (Fastfile, ExportOptions.plist): existing files are kept, not overwritten.
|
|
53
55
|
flutterApp: planTypeAppFiles(context, payloadRoot, targetRoot),
|
|
54
56
|
versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
|
|
55
|
-
//
|
|
57
|
+
// Files the real install also changes; scripts that overwrite existing files in particular must be shown in advance.
|
|
56
58
|
scripts: planScripts(payloadRoot, targetRoot),
|
|
57
59
|
readme: planVersionSection(targetRoot),
|
|
58
60
|
baselineExists: existsSync(join(targetRoot, BASELINE_PATH)),
|
|
@@ -60,74 +62,92 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export function printDryRun(plan) {
|
|
63
|
-
const lines = ["",
|
|
65
|
+
const lines = ["", t("cmd.dryRun.header", { mode: plan.mode }), ""];
|
|
64
66
|
if (plan.mode === "uninstall") {
|
|
65
67
|
const u = plan.uninstall;
|
|
66
|
-
lines.push(
|
|
68
|
+
lines.push(t("cmd.dryRun.uninstall.workflows", { n: u.workflows.length }));
|
|
67
69
|
for (const f of u.workflows) lines.push(` - ${f}`);
|
|
68
|
-
lines.push(
|
|
70
|
+
lines.push(t("cmd.dryRun.uninstall.scripts", { n: u.scripts.length }));
|
|
69
71
|
for (const f of u.scripts) lines.push(` - ${f}`);
|
|
70
|
-
for (const f of u.appFiles || []) lines.push(
|
|
71
|
-
if (u.readme) lines.push("
|
|
72
|
-
if (u.gitignore) lines.push("
|
|
73
|
-
if (u.versionYml) lines.push("
|
|
72
|
+
for (const f of u.appFiles || []) lines.push(t("cmd.dryRun.uninstall.appFile", { file: f }));
|
|
73
|
+
if (u.readme) lines.push(t("cmd.dryRun.uninstall.readme"));
|
|
74
|
+
if (u.gitignore) lines.push(t("cmd.dryRun.uninstall.gitignore"));
|
|
75
|
+
if (u.versionYml) lines.push(t("cmd.dryRun.uninstall.versionYml"));
|
|
74
76
|
} else {
|
|
75
77
|
if (plan.workflows) {
|
|
76
78
|
const w = plan.workflows;
|
|
77
|
-
lines.push(
|
|
79
|
+
lines.push(t("cmd.dryRun.full.newFiles", { n: w.newFiles.length }));
|
|
78
80
|
for (const f of w.newFiles) lines.push(` + ${f.filename} [${f.type}]`);
|
|
79
|
-
lines.push(
|
|
81
|
+
lines.push(t("cmd.dryRun.full.changed", { n: w.changed.length }));
|
|
80
82
|
for (const f of w.changed) lines.push(` ~ ${f.filename} [${f.type}]`);
|
|
81
|
-
lines.push(
|
|
83
|
+
lines.push(t("cmd.dryRun.full.unchanged", { n: w.unchanged.length }));
|
|
84
|
+
// Buckets that exist only with a baseline. The real run acts on each of them, so the preview lists them too
|
|
85
|
+
// (only when non-empty, so installs without a baseline print exactly as before).
|
|
86
|
+
if (w.upstreamOnly?.length) {
|
|
87
|
+
lines.push(t("cmd.dryRun.full.autoUpdated", { n: w.upstreamOnly.length }));
|
|
88
|
+
for (const f of w.upstreamOnly) lines.push(` ~ ${f.filename} [${f.type}]`);
|
|
89
|
+
}
|
|
90
|
+
if (w.localOnly?.length) {
|
|
91
|
+
lines.push(t("cmd.dryRun.full.localKept", { n: w.localOnly.length }));
|
|
92
|
+
for (const f of w.localOnly) lines.push(` = ${f.filename} [${f.type}]`);
|
|
93
|
+
}
|
|
94
|
+
if (w.removed?.length) {
|
|
95
|
+
lines.push(t("cmd.dryRun.full.removed", { n: w.removed.length }));
|
|
96
|
+
for (const f of w.removed) lines.push(` - ${f.filename} [${f.type}]`);
|
|
97
|
+
}
|
|
82
98
|
}
|
|
83
99
|
if (plan.cleanup) {
|
|
84
|
-
const reasons = {
|
|
100
|
+
const reasons = {
|
|
101
|
+
cleanup: t("cmd.dryRun.reason.cleanup"), storeCleanup: t("cmd.dryRun.reason.storeCleanup"), staleCleanup: t("cmd.dryRun.reason.staleCleanup"),
|
|
102
|
+
};
|
|
85
103
|
const removed = [];
|
|
86
104
|
const backedUp = [];
|
|
87
105
|
for (const [key, reason] of Object.entries(reasons)) {
|
|
88
|
-
for (const f of plan.cleanup[key]?.removed || []) removed.push(
|
|
89
|
-
for (const f of plan.cleanup[key]?.backedUp || []) backedUp.push(
|
|
106
|
+
for (const f of plan.cleanup[key]?.removed || []) removed.push(t("cmd.dryRun.cleanup.removedLine", { file: f, reason }));
|
|
107
|
+
for (const f of plan.cleanup[key]?.backedUp || []) backedUp.push(t("cmd.dryRun.cleanup.backedUpLine", { file: f, reason }));
|
|
90
108
|
}
|
|
91
|
-
if (removed.length) lines.push(
|
|
92
|
-
if (backedUp.length) lines.push(
|
|
109
|
+
if (removed.length) lines.push(t("cmd.dryRun.cleanup.removedHeading", { n: removed.length }), ...removed);
|
|
110
|
+
if (backedUp.length) lines.push(t("cmd.dryRun.cleanup.backedUpHeading", { n: backedUp.length }), ...backedUp);
|
|
93
111
|
}
|
|
94
112
|
if (plan.gitignore) {
|
|
95
113
|
const g = plan.gitignore;
|
|
96
114
|
lines.push(g.created
|
|
97
|
-
?
|
|
98
|
-
: g.added.length ?
|
|
115
|
+
? t("cmd.dryRun.gitignore.created", { added: g.added.join(", ") })
|
|
116
|
+
: g.added.length ? t("cmd.dryRun.gitignore.append", { added: g.added.join(", ") }) : t("cmd.dryRun.gitignore.unchanged"));
|
|
99
117
|
}
|
|
100
118
|
if (plan.flutterApp && (plan.flutterApp.created.length || plan.flutterApp.kept.length)) {
|
|
101
119
|
const { created, kept } = plan.flutterApp;
|
|
102
|
-
lines.push(
|
|
120
|
+
lines.push(t("cmd.dryRun.flutter.created", { n: created.length }));
|
|
103
121
|
for (const f of created) lines.push(` + ${f}`);
|
|
104
|
-
lines.push(
|
|
105
|
-
for (const f of kept) lines.push(
|
|
122
|
+
lines.push(t("cmd.dryRun.flutter.kept", { n: kept.length }));
|
|
123
|
+
for (const f of kept) lines.push(t("cmd.dryRun.flutter.keptLine", { file: f }));
|
|
106
124
|
}
|
|
107
125
|
if (plan.versionYml) {
|
|
108
126
|
lines.push(plan.versionYml.existed
|
|
109
|
-
? (plan.versionYml.changed ? "
|
|
110
|
-
: "
|
|
111
|
-
// dry-run
|
|
112
|
-
//
|
|
113
|
-
lines.push("
|
|
127
|
+
? (plan.versionYml.changed ? t("cmd.dryRun.versionYml.update") : t("cmd.dryRun.versionYml.unchanged"))
|
|
128
|
+
: t("cmd.dryRun.versionYml.create"));
|
|
129
|
+
// dry-run runs read-only without prompts, so it cannot compute @wizard ask deploy settings.
|
|
130
|
+
// For types with a deploy block (spring etc.) say the preview may differ from the real install.
|
|
131
|
+
lines.push(t("cmd.dryRun.versionYml.note"));
|
|
114
132
|
}
|
|
115
133
|
if (plan.scripts) {
|
|
116
134
|
const mark = { create: "+", overwrite: "~", unchanged: "=" };
|
|
117
|
-
const note = {
|
|
118
|
-
|
|
135
|
+
const note = {
|
|
136
|
+
create: t("cmd.dryRun.scripts.create"), overwrite: t("cmd.dryRun.scripts.overwrite"), unchanged: t("cmd.dryRun.scripts.unchanged"),
|
|
137
|
+
};
|
|
138
|
+
lines.push(t("cmd.dryRun.scripts.heading", { dir: PATHS.scriptsDir, n: plan.scripts.length }));
|
|
119
139
|
for (const s of plan.scripts) lines.push(` ${mark[s.action]} ${s.name} (${note[s.action]})`);
|
|
120
140
|
}
|
|
121
141
|
if (plan.readme) {
|
|
122
142
|
lines.push({
|
|
123
|
-
added: "
|
|
124
|
-
"skip-no-readme": "
|
|
125
|
-
"skip-marker": "
|
|
126
|
-
"skip-version-line": "
|
|
127
|
-
}[plan.readme] ||
|
|
143
|
+
added: t("cmd.dryRun.readme.added"),
|
|
144
|
+
"skip-no-readme": t("cmd.dryRun.readme.skipNoReadme"),
|
|
145
|
+
"skip-marker": t("cmd.dryRun.readme.skipMarker"),
|
|
146
|
+
"skip-version-line": t("cmd.dryRun.readme.skipVersionLine"),
|
|
147
|
+
}[plan.readme] || t("cmd.dryRun.readme.other", { status: plan.readme }));
|
|
128
148
|
}
|
|
129
149
|
if (plan.baselineExists !== undefined) {
|
|
130
|
-
lines.push(
|
|
150
|
+
lines.push(t(plan.baselineExists ? "cmd.dryRun.baseline.update" : "cmd.dryRun.baseline.create", { path: BASELINE_PATH }));
|
|
131
151
|
}
|
|
132
152
|
}
|
|
133
153
|
lines.push("");
|