project-auto-wizard 0.13.3 → 0.15.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.ja.md +2 -0
- package/README.ko.md +2 -0
- package/README.md +3 -1
- package/README.zh-CN.md +2 -0
- 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 +304 -265
- package/payload/scripts/issue_helper.py +54 -50
- package/payload/scripts/messages.py +2585 -0
- package/payload/scripts/truncate_release_notes.py +7 -6
- package/payload/scripts/version_manager.py +110 -107
- package/payload/version.yml.template +2 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +23 -16
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +106 -63
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +55 -45
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +90 -72
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +59 -45
- 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 +90 -89
- package/src/commands/dry-run.js +64 -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 +102 -36
- 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 +188 -0
- package/src/i18n/catalog/en/copy.js +20 -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 +188 -0
- package/src/i18n/catalog/ko/copy.js +20 -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 +132 -97
- 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 +90 -89
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,63 +125,65 @@ 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
|
|
|
183
179
|
return results;
|
|
184
180
|
}
|
|
185
181
|
|
|
182
|
+
// Exit code for the CLI: 0 when every item is OK/INFO, 1 when any WARN or FAIL item was found.
|
|
183
|
+
// Warnings and errors are not split: both mean "something the workflows rely on needs attention",
|
|
184
|
+
// and a script that wants to tolerate warnings can ignore the code (`|| true`).
|
|
185
|
+
export const doctorExitCode = (results) => (results.some((r) => r.status === "WARN" || r.status === "FAIL") ? 1 : 0);
|
|
186
|
+
|
|
186
187
|
const asLines = (v) => (Array.isArray(v) ? v : v ? [String(v)] : []);
|
|
187
188
|
const headOf = (r) => `${r.label || r.name}${r.purpose ? ` — ${r.purpose}` : ""}`;
|
|
188
189
|
|
|
@@ -192,9 +193,9 @@ export function printDoctorReport(results, { out = (s) => console.log(s), color
|
|
|
192
193
|
const problems = results.filter((r) => r.status === "WARN" || r.status === "FAIL");
|
|
193
194
|
const infos = results.filter((r) => r.status === "INFO");
|
|
194
195
|
|
|
195
|
-
const lines = ["", `${p("◆", A.cyan)} ${p("
|
|
196
|
+
const lines = ["", `${p("◆", A.cyan)} ${p(t("cmd.doctor.heading"), A.bold)} ${p("— project-auto-wizard doctor", A.dim)}`, ""];
|
|
196
197
|
|
|
197
|
-
//
|
|
198
|
+
// Healthy items: one line each. Values are aligned to the label width (double-width CJK counting is delegated to visualWidth).
|
|
198
199
|
if (oks.length) {
|
|
199
200
|
const width = Math.max(...oks.map((r) => visualWidth(headOf(r))));
|
|
200
201
|
for (const r of oks) {
|
|
@@ -205,18 +206,18 @@ export function printDoctorReport(results, { out = (s) => console.log(s), color
|
|
|
205
206
|
lines.push("");
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
//
|
|
209
|
+
// Problems: unfold as symptom -> impact -> action -> docs.
|
|
209
210
|
for (const r of problems) {
|
|
210
211
|
const fail = r.status === "FAIL";
|
|
211
212
|
lines.push(` ${p(fail ? "[✗]" : "[!]", fail ? A.red : A.yellow)} ${p(headOf(r), A.bold)}`);
|
|
212
213
|
if (r.value) lines.push(` ${p(fail ? "✗" : "✗", fail ? A.red : A.yellow)} ${r.value}`);
|
|
213
214
|
for (const l of asLines(r.impact)) lines.push(` ${p(l, A.dim)}`);
|
|
214
215
|
for (const a of asLines(r.actions)) lines.push(` ${p("→", A.cyan)} ${a}`);
|
|
215
|
-
if (r.doc) lines.push(` ${p("→", A.cyan)}
|
|
216
|
+
if (r.doc) lines.push(` ${p("→", A.cyan)} ${t("cmd.doctor.details")}${p(r.doc, A.dim)}`);
|
|
216
217
|
lines.push("");
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
//
|
|
220
|
+
// Notes: guidance that needs no action.
|
|
220
221
|
for (const r of infos) {
|
|
221
222
|
lines.push(` ${p("[i]", A.gray)} ${headOf(r)}`);
|
|
222
223
|
for (const l of asLines(r.note)) lines.push(` ${p(l, A.dim)}`);
|
|
@@ -227,15 +228,15 @@ export function printDoctorReport(results, { out = (s) => console.log(s), color
|
|
|
227
228
|
out(lines.join("\n"));
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
//
|
|
231
|
+
// Summary: states only the facts found instead of an "OK to install" verdict (same stance as flutter doctor's last line).
|
|
231
232
|
function summaryLine(problems, p) {
|
|
232
|
-
if (!problems.length) return ` ${p("✓", A.green)}
|
|
233
|
+
if (!problems.length) return ` ${p("✓", A.green)} ${t("cmd.doctor.summaryNone")}`;
|
|
233
234
|
const n = problems.length;
|
|
234
235
|
if (problems.some((r) => r.status === "FAIL")) {
|
|
235
|
-
return ` ${p("✗", A.red)} ${
|
|
236
|
+
return ` ${p("✗", A.red)} ${t("cmd.doctor.summaryFail", { n })}`;
|
|
236
237
|
}
|
|
237
238
|
return [
|
|
238
|
-
` ${p("!", A.yellow)} ${
|
|
239
|
-
`
|
|
239
|
+
` ${p("!", A.yellow)} ${t("cmd.doctor.summaryWarn", { n })}`,
|
|
240
|
+
` ${t("cmd.doctor.summaryWarnNote", { n })}`,
|
|
240
241
|
].join("\n");
|
|
241
242
|
}
|
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,93 @@ 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
|
-
|
|
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
|
+
"heading-updated": t("cmd.dryRun.readme.headingUpdated"),
|
|
148
|
+
}[plan.readme] || t("cmd.dryRun.readme.other", { status: plan.readme }));
|
|
128
149
|
}
|
|
129
150
|
if (plan.baselineExists !== undefined) {
|
|
130
|
-
lines.push(
|
|
151
|
+
lines.push(t(plan.baselineExists ? "cmd.dryRun.baseline.update" : "cmd.dryRun.baseline.create", { path: BASELINE_PATH }));
|
|
131
152
|
}
|
|
132
153
|
}
|
|
133
154
|
lines.push("");
|