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/core/detect.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { t as tr } from "../i18n/index.js";
|
|
1
2
|
import {
|
|
2
3
|
typeInfo, FALLBACK_TYPE, MARKER_DETECTED_TYPES, PACKAGE_DETECTED_TYPES, PACKAGE_FALLBACK_TYPE,
|
|
3
4
|
} from "./types.js";
|
|
4
5
|
|
|
5
|
-
// package.json
|
|
6
|
-
// exponential-backoff
|
|
7
|
-
// next
|
|
6
|
+
// package.json classification: compares dependency "keys" exactly. A raw substring match would misdetect an
|
|
7
|
+
// export script or exponential-backoff as expo, a web app using react-native-web as react-native, and
|
|
8
|
+
// "next" in keywords as next. Input is the raw package.json string. Evaluation order is the registry's detectOrder.
|
|
8
9
|
export function classifyPackageText(raw) {
|
|
9
10
|
let pkg;
|
|
10
11
|
try { pkg = JSON.parse(String(raw || "")); } catch { return PACKAGE_FALLBACK_TYPE; }
|
|
@@ -17,14 +18,14 @@ export function classifyPackageText(raw) {
|
|
|
17
18
|
return PACKAGE_DETECTED_TYPES.find((t) => deps.has(t.packageDep))?.id ?? PACKAGE_FALLBACK_TYPE;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
+
// Convenience: when given a parsed object, re-serialize it and apply the rule above.
|
|
21
22
|
export function classifyPackageJson(pkgOrRaw) {
|
|
22
23
|
const raw = typeof pkgOrRaw === "string" ? pkgOrRaw : JSON.stringify(pkgOrRaw || {});
|
|
23
24
|
return classifyPackageText(raw);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
-
// read(relpath)=>string|null
|
|
27
|
+
// Marker scan. has(relpath)=>bool is injected. node is not added when another type is present.
|
|
28
|
+
// read(relpath)=>string|null supplies the raw package.json passed to classifyPackageText.
|
|
28
29
|
export function detectTypesFromMarkers({ has, read }) {
|
|
29
30
|
const types = [];
|
|
30
31
|
for (const t of MARKER_DETECTED_TYPES) if (t.markers.some(has)) types.push(t.id);
|
|
@@ -36,25 +37,25 @@ export function detectTypesFromMarkers({ has, read }) {
|
|
|
36
37
|
return types.length ? [...new Set(types)] : [FALLBACK_TYPE];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
// 1.2.3-rc.1
|
|
40
|
-
// version.yml
|
|
41
|
-
//
|
|
40
|
+
// For prerelease/build metadata such as 1.2.3-rc.1, 1.2.3+7, 1.2.0-SNAPSHOT only the x.y.z core is used.
|
|
41
|
+
// version.yml accepts only x.y.z, so the core is more accurate than falling back to 0.0.1.
|
|
42
|
+
// Must match the release-time read (payload/scripts/version_manager.py core_version) so the value survives right after install.
|
|
42
43
|
function coreVersion(v) {
|
|
43
44
|
const m = String(v ?? "").trim().match(/^v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.+-]*)?$/);
|
|
44
45
|
return m ? m[1] : null;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
// setup
|
|
48
|
+
// setup(version="x.y.z") in setup.py. Other keys such as python_version are filtered out by the word boundary.
|
|
48
49
|
export function versionFromSetupPy(content) {
|
|
49
50
|
if (!content) return null;
|
|
50
51
|
const m = String(content).match(/(?<![\w.])version\s*=\s*["']([^"']+)["']/);
|
|
51
52
|
return m ? coreVersion(m[1]) : null;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
// React Native
|
|
55
|
-
// android/app/build.gradle)
|
|
56
|
-
// $(MARKETING_VERSION)
|
|
57
|
-
// list(relDir)=>string[]|null
|
|
55
|
+
// React Native app version: read from the files version_manager writes at release time (ios/<app>/Info.plist,
|
|
56
|
+
// android/app/build.gradle). package.json version is not a sync target, so using it as the source would drift.
|
|
57
|
+
// Values that are not x.y.z, such as a $(MARKETING_VERSION) reference or the template default "1.0", are skipped.
|
|
58
|
+
// list(relDir)=>string[]|null supplies the app folder names under ios (deep plists such as Pods are not inspected).
|
|
58
59
|
export function versionFromReactNative({ read, list }) {
|
|
59
60
|
for (const dir of [...(list?.("ios") || [])].sort()) {
|
|
60
61
|
const m = String(read(`ios/${dir}/Info.plist`) || "").match(/<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/);
|
|
@@ -65,12 +66,12 @@ export function versionFromReactNative({ read, list }) {
|
|
|
65
66
|
return m ? coreVersion(m[1]) : null;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
//
|
|
69
|
-
// package.json
|
|
70
|
-
// hint:
|
|
71
|
-
//
|
|
72
|
-
// types:
|
|
73
|
-
//
|
|
69
|
+
// Version detection: first success in order. read(relpath)=>string|null is injected.
|
|
70
|
+
// package.json is already parsed with Node JSON.parse, so it is always used regardless of whether jq is installed.
|
|
71
|
+
// hint: one line appended to the fallback warning saying how to fix it. Interactive mode and the CLI
|
|
72
|
+
// suggest different fixes, so the caller decides. The CLI wording is used when omitted.
|
|
73
|
+
// types: the primary type's (first entry) version file is read first. At release time version_manager compares
|
|
74
|
+
// the primary type's file with version.yml, so picking another type's version would make the version jump on the first release.
|
|
74
75
|
export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hint, types = [] }) {
|
|
75
76
|
const grab = (content, re) => {
|
|
76
77
|
for (const line of (content || "").split("\n")) {
|
|
@@ -79,17 +80,17 @@ export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hin
|
|
|
79
80
|
}
|
|
80
81
|
return null;
|
|
81
82
|
};
|
|
82
|
-
//
|
|
83
|
-
//
|
|
83
|
+
// Without a line-start anchor, dependency version variables such as ext.kotlin_version match first.
|
|
84
|
+
// Only quoted values are considered: that is the only form the release-time sync can rewrite.
|
|
84
85
|
const gradleRe = /^(\s*)version\s*=\s*(["'])([^"'\n]*)\2/;
|
|
85
|
-
//
|
|
86
|
-
// `node { version = '20.11.0' }`
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// quote
|
|
91
|
-
// { code, quote }
|
|
92
|
-
//
|
|
86
|
+
// Prefer a non-indented `version =`; only when none exists use an indented line inside an allprojects/subprojects block.
|
|
87
|
+
// If a plugin config block such as `node { version = '20.11.0' }` were read as the project version, the version would jump on the first release.
|
|
88
|
+
// Must follow the same rule as version_manager.py at release time.
|
|
89
|
+
// Strip comments and blank out characters inside quotes so the `//` in url 'https://...' or braces inside strings
|
|
90
|
+
// do not throw off the block-depth count. Must follow the same rule as version_manager.py.
|
|
91
|
+
// quote is the quote that was open when this line started. Triple quotes (""" ''') span several lines, so
|
|
92
|
+
// { code, quote } returns the quote still open at the end of the line for the next line to continue.
|
|
93
|
+
// A single-line quote is treated as closed at the end of the line.
|
|
93
94
|
const gradleCode = (line, quote) => {
|
|
94
95
|
let out = "";
|
|
95
96
|
for (let i = 0; i < line.length;) {
|
|
@@ -109,7 +110,7 @@ export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hin
|
|
|
109
110
|
};
|
|
110
111
|
const gradleVersion = (content) => {
|
|
111
112
|
const top = [], shared = [], stack = [];
|
|
112
|
-
let quote = ""; //
|
|
113
|
+
let quote = ""; // the open quote when inside a multi-line string; lines inside a string are not code
|
|
113
114
|
for (const line of (content || "").split("\n")) {
|
|
114
115
|
const m = quote ? null : line.match(gradleRe);
|
|
115
116
|
if (m) {
|
|
@@ -131,8 +132,8 @@ export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hin
|
|
|
131
132
|
const sources = {
|
|
132
133
|
packageJson: () => coreVersion(readJson?.("package.json")?.version),
|
|
133
134
|
appJson: () => coreVersion(readJson?.("app.json")?.expo?.version),
|
|
134
|
-
// Groovy DSL
|
|
135
|
-
// .kts
|
|
135
|
+
// Groovy DSL and Kotlin DSL share the same syntax (`version = "x.y.z"`), so they share the regex.
|
|
136
|
+
// Omitting .kts would reset every Kotlin DSL Spring project to 0.0.1.
|
|
136
137
|
gradle: () => gradleVersion(read("build.gradle")),
|
|
137
138
|
gradleKts: () => gradleVersion(read("build.gradle.kts")),
|
|
138
139
|
pom: () => versionFromPom(read("pom.xml")),
|
|
@@ -150,13 +151,13 @@ export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hin
|
|
|
150
151
|
if (v) return v;
|
|
151
152
|
}
|
|
152
153
|
if (gitTag) { const t = coreVersion(gitTag); if (t) return t; }
|
|
153
|
-
const tail = hint ?? "
|
|
154
|
-
warn?.(
|
|
154
|
+
const tail = hint ?? tr("core.detect.versionFallbackHint");
|
|
155
|
+
warn?.(tr("core.detect.versionFallbackWarn", { tail }));
|
|
155
156
|
return "0.0.1";
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
// pyproject.toml
|
|
159
|
-
// [project]
|
|
159
|
+
// Package version in pyproject.toml. `version =` in other sections such as [tool.*] is tool config,
|
|
160
|
+
// so it is read only inside the [project] and [tool.poetry] sections.
|
|
160
161
|
export function versionFromPyproject(content) {
|
|
161
162
|
if (!content) return null;
|
|
162
163
|
let section = "";
|
|
@@ -170,9 +171,9 @@ export function versionFromPyproject(content) {
|
|
|
170
171
|
return null;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
// Maven pom.xml
|
|
174
|
-
// <parent>(
|
|
175
|
-
//
|
|
174
|
+
// Maven pom.xml project version: only the <version> directly under <project> counts.
|
|
175
|
+
// Versions inside <parent> (Spring Boot BOM) or <dependencies> are not the project version, so depth tells them apart.
|
|
176
|
+
// Returns null when there is no project version (inherited from the parent); a dependency version is never picked instead.
|
|
176
177
|
export function versionFromPom(content) {
|
|
177
178
|
if (!content) return null;
|
|
178
179
|
const text = String(content);
|
|
@@ -197,42 +198,43 @@ export function versionFromPom(content) {
|
|
|
197
198
|
return null;
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
//
|
|
201
|
+
// A type's representative marker file. Types without markers (basic) and unknown types return package.json.
|
|
201
202
|
export function markerForType(type) {
|
|
202
203
|
return typeInfo(type)?.markers[0] || "package.json";
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
//
|
|
206
|
+
// Secondary markers besides the representative file (e.g. Spring's build.gradle.kts and pom.xml).
|
|
206
207
|
export function extraMarkers(type) {
|
|
207
208
|
return typeInfo(type)?.markers.slice(1) || [];
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
211
|
+
// The file actually used to detect that type. markerForType always returns one representative file per type,
|
|
212
|
+
// so even a repo with only build.gradle.kts printed "found build.gradle" and the file name disagreed with the
|
|
213
|
+
// path-confirmation screen in the same install log. has() picks the one that really exists.
|
|
214
|
+
// When no candidate exists (e.g. screens before detection) the representative file is used. When shown as
|
|
215
|
+
// "evidence" (fallback:false) an empty string is returned instead: attaching a file that does not exist for a
|
|
216
|
+
// manually chosen type as evidence would make it look detected.
|
|
215
217
|
export function resolveMarker(type, has, { fallback = true } = {}) {
|
|
216
218
|
const candidates = [markerForType(type), ...extraMarkers(type)];
|
|
217
219
|
return candidates.find(has) ?? (fallback ? candidates[0] : "");
|
|
218
220
|
}
|
|
219
221
|
|
|
220
|
-
//
|
|
221
|
-
// toolchain
|
|
222
|
-
//
|
|
223
|
-
//
|
|
222
|
+
// Build JDK detection: the deploy workflow's JAVA_VERSION default is fixed at 21, so a project with a
|
|
223
|
+
// different toolchain (e.g. 25) that just presses Enter ends up with a runner JDK mismatch and a broken build.
|
|
224
|
+
// Measured from project files the same way detectBuildNumberFromFiles reads the build number.
|
|
225
|
+
// Returns a major version string such as "21", or null when not found.
|
|
224
226
|
export function detectJdkFromFiles({ read }) {
|
|
225
227
|
const pick = (content, patterns) => {
|
|
226
228
|
if (!content) return null;
|
|
227
229
|
for (const re of patterns) {
|
|
228
230
|
const m = String(content).match(re);
|
|
229
|
-
// JavaVersion.VERSION_1_8
|
|
231
|
+
// Normalize the 1_8 notation (JavaVersion.VERSION_1_8) to 8.
|
|
230
232
|
if (m) return m[1] === "1_8" ? "8" : m[1].replace("1_", "");
|
|
231
233
|
}
|
|
232
234
|
return null;
|
|
233
235
|
};
|
|
234
236
|
const gradlePatterns = [
|
|
235
|
-
/JavaLanguageVersion\.of\((\d+)\)/, // toolchain (Gradle
|
|
237
|
+
/JavaLanguageVersion\.of\((\d+)\)/, // toolchain (recommended Gradle notation)
|
|
236
238
|
/JavaVersion\.VERSION_(\d+(?:_\d+)?)/, // sourceCompatibility = JavaVersion.VERSION_21
|
|
237
239
|
/(?:source|target)Compatibility\s*=?\s*["'](\d+)["']/, // sourceCompatibility = '17'
|
|
238
240
|
];
|
|
@@ -246,40 +248,40 @@ export function detectJdkFromFiles({ read }) {
|
|
|
246
248
|
return null;
|
|
247
249
|
}
|
|
248
250
|
|
|
249
|
-
//
|
|
251
|
+
// Map of the real marker file per type: built in one place so the detection log and install log cite the same evidence.
|
|
250
252
|
export function resolveMarkers(types = [], has) {
|
|
251
253
|
const out = new Map();
|
|
252
254
|
for (const t of types) {
|
|
253
255
|
if (t === "basic") continue;
|
|
254
|
-
//
|
|
256
|
+
// Only files that actually exist count as evidence: otherwise the type is left out of the map and screens/logs treat it as "manually chosen".
|
|
255
257
|
const found = resolveMarker(t, has, { fallback: false });
|
|
256
258
|
if (found) out.set(t, found);
|
|
257
259
|
}
|
|
258
260
|
return out;
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
// readJson(rel)=>object|null
|
|
263
|
+
// Build number detection: on a fresh integration, read the build number already recorded in
|
|
264
|
+
// pubspec.yaml/build.gradle/app.json so version_code is not always reset to 1. Only the first matching
|
|
265
|
+
// type in the types array is used (same types[0]=primary convention as the other detection logic).
|
|
266
|
+
// read(rel)=>string|null and readJson(rel)=>object|null are injected.
|
|
265
267
|
export function detectBuildNumberFromFiles({ types = [], read, readJson, warn }) {
|
|
266
268
|
const tryFlutter = () => {
|
|
267
269
|
const content = read("pubspec.yaml");
|
|
268
270
|
if (content == null) return null;
|
|
269
|
-
// 1.2.3-rc.1+4
|
|
271
|
+
// +N is the build number even with a prerelease such as 1.2.3-rc.1+4 (same rule as the release-time read).
|
|
270
272
|
const m = content.match(/^version:\s*\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\+(\d+)/m);
|
|
271
273
|
if (m) return parseInt(m[1], 10);
|
|
272
|
-
warn?.("
|
|
274
|
+
warn?.(tr("core.detect.buildNumber.pubspecMissing"));
|
|
273
275
|
return null;
|
|
274
276
|
};
|
|
275
277
|
const tryReactNative = () => {
|
|
276
278
|
const content = read("android/app/build.gradle");
|
|
277
279
|
if (content == null) return null;
|
|
278
|
-
//
|
|
279
|
-
// "// versionCode 2"
|
|
280
|
+
// Anchors + the m flag match only lines that are entirely "versionCode N", so a commented-out
|
|
281
|
+
// "// versionCode 2" or a versionCode reference in another block is not matched by mistake.
|
|
280
282
|
const m = content.match(/^\s*versionCode\s+(\d+)\s*$/m);
|
|
281
283
|
if (m) return parseInt(m[1], 10);
|
|
282
|
-
warn?.("
|
|
284
|
+
warn?.(tr("core.detect.buildNumber.gradleMissing"));
|
|
283
285
|
return null;
|
|
284
286
|
};
|
|
285
287
|
const tryExpo = () => {
|
|
@@ -287,7 +289,7 @@ export function detectBuildNumberFromFiles({ types = [], read, readJson, warn })
|
|
|
287
289
|
if (data == null) return null;
|
|
288
290
|
const code = data?.expo?.android?.versionCode;
|
|
289
291
|
if (Number.isInteger(code)) return code;
|
|
290
|
-
warn?.("
|
|
292
|
+
warn?.(tr("core.detect.buildNumber.expoMissing"));
|
|
291
293
|
return null;
|
|
292
294
|
};
|
|
293
295
|
const readers = { pubspec: tryFlutter, androidGradle: tryReactNative, expoAppJson: tryExpo };
|
package/src/core/errors.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
// core
|
|
1
|
+
// User input error - the caller (index.js) prints only the message, no stack, and exits 1.
|
|
2
|
+
// Core modules throw it too, so it lives in core rather than cli (avoids a core -> cli back-reference).
|
|
3
3
|
export class CliError extends Error {}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
// Flutter
|
|
1
|
+
// Flutter diagnostic checks, called by doctor through the type hook (doctorChecks).
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join, posix } from "node:path";
|
|
4
4
|
import { PATHS } from "./paths.js";
|
|
5
5
|
import { STORE_PLATFORMS, STORE_APP_FILES, parseStoreList, storeAppFilesFor } from "./flutter-options.js";
|
|
6
6
|
import { inferInstalledStores } from "./installed-stores.js";
|
|
7
|
+
import { t } from "../i18n/index.js";
|
|
7
8
|
|
|
8
|
-
const PLATFORM_ROW_NAME = { android: "
|
|
9
|
-
const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g; //
|
|
9
|
+
const PLATFORM_ROW_NAME = { android: "core.flutterDoctor.row.android", ios: "core.flutterDoctor.row.ios" }; // catalog keys, resolved lazily
|
|
10
|
+
const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g; // Same detection rule as the ExportOptions.plist template and the IOS-TESTFLIGHT validation
|
|
10
11
|
const EXPORT_OPTIONS_REL = STORE_APP_FILES.ios.find((rel) => rel.endsWith("ExportOptions.plist"));
|
|
11
12
|
|
|
12
|
-
// Flutter
|
|
13
|
-
//
|
|
13
|
+
// Flutter store deploy diagnostics: required files for the selected platforms and ExportOptions.plist placeholders.
|
|
14
|
+
// Whether store secrets are registered is out of scope. Returns an array of doctor result rows.
|
|
14
15
|
export function flutterStoreChecks(cwd, existing, { docs }) {
|
|
15
16
|
const saved = existing.options.flutterStore == null ? null : parseStoreList(existing.options.flutterStore);
|
|
16
|
-
//
|
|
17
|
+
// An existing install with no saved value is inferred from the installed store workflows (same rule as interactive).
|
|
17
18
|
const stores = saved ?? inferInstalledStores(join(cwd, PATHS.workflowsDir));
|
|
18
19
|
const flutterRoot = existing.paths.get("flutter") || ".";
|
|
19
20
|
const rows = [];
|
|
@@ -21,29 +22,29 @@ export function flutterStoreChecks(cwd, existing, { docs }) {
|
|
|
21
22
|
for (const platform of STORE_PLATFORMS.filter((p) => stores.includes(p))) {
|
|
22
23
|
const files = storeAppFilesFor([platform]).map((rel) => posix.join(flutterRoot, rel));
|
|
23
24
|
const missing = files.filter((file) => !existsSync(join(cwd, file)));
|
|
24
|
-
const head = { name: PLATFORM_ROW_NAME[platform], purpose: "
|
|
25
|
+
const head = { name: t(PLATFORM_ROW_NAME[platform]), purpose: t("core.flutterDoctor.purpose.storeFiles") };
|
|
25
26
|
rows.push(missing.length
|
|
26
27
|
? {
|
|
27
|
-
...head, status: "WARN", value:
|
|
28
|
-
impact: ["
|
|
29
|
-
actions: ["
|
|
28
|
+
...head, status: "WARN", value: t("core.flutterDoctor.value.missing", { files: missing.join(", ") }),
|
|
29
|
+
impact: [t("core.flutterDoctor.impact.missing")],
|
|
30
|
+
actions: [t("core.flutterDoctor.action.missing")],
|
|
30
31
|
doc: docs.flutterStore,
|
|
31
32
|
}
|
|
32
|
-
: { ...head, status: "OK", value:
|
|
33
|
+
: { ...head, status: "OK", value: t("core.flutterDoctor.value.present", { count: files.length }) });
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const plistPath = posix.join(flutterRoot, EXPORT_OPTIONS_REL);
|
|
36
37
|
if (stores.includes("ios") && existsSync(join(cwd, plistPath))) {
|
|
37
|
-
const head = { name: "ExportOptions.plist", purpose: "
|
|
38
|
+
const head = { name: "ExportOptions.plist", purpose: t("core.flutterDoctor.purpose.exportOptions") };
|
|
38
39
|
const left = [...new Set(readFileSync(join(cwd, plistPath), "utf8").match(PLACEHOLDER_RE) ?? [])];
|
|
39
40
|
rows.push(left.length
|
|
40
41
|
? {
|
|
41
|
-
...head, status: "WARN", value:
|
|
42
|
-
impact: ["
|
|
43
|
-
actions: [
|
|
42
|
+
...head, status: "WARN", value: t("core.flutterDoctor.value.unfilled", { left: left.join(", ") }),
|
|
43
|
+
impact: [t("core.flutterDoctor.impact.unfilled")],
|
|
44
|
+
actions: [t("core.flutterDoctor.action.unfilled", { path: plistPath })],
|
|
44
45
|
doc: docs.flutterStore,
|
|
45
46
|
}
|
|
46
|
-
: { ...head, status: "OK", value: "
|
|
47
|
+
: { ...head, status: "OK", value: t("core.flutterDoctor.value.noPlaceholder") });
|
|
47
48
|
}
|
|
48
49
|
return rows;
|
|
49
50
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Flutter
|
|
2
|
-
//
|
|
1
|
+
// Flutter type hooks: the implementations the flutter entry of the types.js registry points to.
|
|
2
|
+
// Common code does not know flutter directly and calls it only by hook name (see the types.js header for each hook's contract).
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import {
|
|
5
5
|
ENV_MODES, DEPLOY_MODES, DEFAULT_ENV_MODE, DEFAULT_DEPLOY_MODE, STORE_PLATFORMS, NO_STORE,
|
|
@@ -11,20 +11,21 @@ import { flutterStoreChecks } from "./flutter-doctor.js";
|
|
|
11
11
|
import { inferInstalledStores } from "./installed-stores.js";
|
|
12
12
|
import { escapeYamlDoubleQuoted } from "./wizard-env.js";
|
|
13
13
|
import { CliError } from "./errors.js";
|
|
14
|
+
import { t } from "../i18n/index.js";
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
+
// When there is no saved value, also states the behavior that actually applies in that state.
|
|
16
17
|
function statusLabels(options) {
|
|
17
18
|
return [
|
|
18
|
-
` env_mode=${options.envMode ?? "
|
|
19
|
-
` flutter_store=${options.flutterStore ?? "
|
|
20
|
-
` android_deploy_mode=${options.androidDeployMode ?? "
|
|
21
|
-
` ios_deploy_mode=${options.iosDeployMode ?? "
|
|
19
|
+
` env_mode=${options.envMode ?? t("core.flutterHooks.status.envModeUnset")}`,
|
|
20
|
+
` flutter_store=${options.flutterStore ?? t("core.flutterHooks.status.storeUnset")}`,
|
|
21
|
+
` android_deploy_mode=${options.androidDeployMode ?? t("core.flutterHooks.status.deployModeUnset")}`,
|
|
22
|
+
` ios_deploy_mode=${options.iosDeployMode ?? t("core.flutterHooks.status.deployModeUnset")}`,
|
|
22
23
|
].join("");
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// workflowsDir
|
|
26
|
+
// An existing Flutter install with no saved store value is inferred from the installed store workflows (same conclusion as interactive).
|
|
27
|
+
// Without inference the undecided state would be saved as "both", resurrecting workflows and fastlane files of a platform that was removed.
|
|
28
|
+
// Without workflowsDir (read-only paths such as status) no inference is made.
|
|
28
29
|
function resolveOptions({ opts = {}, existing = null, workflowsDir = null }) {
|
|
29
30
|
const inferredStores = workflowsDir && opts.flutterStore == null && existing?.types?.includes("flutter")
|
|
30
31
|
&& existing.options?.flutterStore == null && existsSync(workflowsDir)
|
|
@@ -38,35 +39,35 @@ function resolveOptions({ opts = {}, existing = null, workflowsDir = null }) {
|
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
// version.yml options
|
|
42
|
-
//
|
|
42
|
+
// Flutter block under version.yml options (6-space indent). Empty values are filled with the same values as the workflow template defaults,
|
|
43
|
+
// so the saved values do not diverge from what was actually installed. When stores is null (undecided) both are installed as today, so "android,ios" is recorded.
|
|
43
44
|
function versionOptionsBlock({ envMode, stores, androidDeployMode, iosDeployMode } = {}) {
|
|
44
45
|
const quote = (v) => `"${escapeYamlDoubleQuoted(v)}"`;
|
|
45
46
|
return [
|
|
46
|
-
` env_mode: ${quote(envMode || DEFAULT_ENV_MODE)} # ${ENV_MODES.join(" | ")} (
|
|
47
|
-
` flutter_store: ${quote(formatStoreList(stores ?? STORE_PLATFORMS))} # android | ios | android,ios | none (
|
|
48
|
-
` android_deploy_mode: ${quote(androidDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (
|
|
49
|
-
` ios_deploy_mode: ${quote(iosDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (
|
|
47
|
+
` env_mode: ${quote(envMode || DEFAULT_ENV_MODE)} # ${ENV_MODES.join(" | ")} ${t("core.flutterHooks.comment.envMode")}`,
|
|
48
|
+
` flutter_store: ${quote(formatStoreList(stores ?? STORE_PLATFORMS))} # android | ios | android,ios | none ${t("core.flutterHooks.comment.store")}`,
|
|
49
|
+
` android_deploy_mode: ${quote(androidDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} ${t("core.flutterHooks.comment.androidDeployMode")}`,
|
|
50
|
+
` ios_deploy_mode: ${quote(iosDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} ${t("core.flutterHooks.comment.iosDeployMode")}`,
|
|
50
51
|
].join("\n");
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
+
// Builds one flag validated against a value list; CliError when the value is outside the list.
|
|
54
55
|
const enumFlag = (flag, field, isValid, allowed) => ({
|
|
55
56
|
flag, field, initial: "",
|
|
56
57
|
parse(v) {
|
|
57
|
-
if (!isValid(v)) throw new CliError(
|
|
58
|
+
if (!isValid(v)) throw new CliError(t("core.flutterHooks.err.invalidFlag", { flag, value: v ?? t("core.flutterHooks.noValue"), allowed: allowed.join(" | ") }));
|
|
58
59
|
return v;
|
|
59
60
|
},
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
export const flutterHooks = {
|
|
63
64
|
resolveOptions,
|
|
64
|
-
//
|
|
65
|
+
// Decided options to install-context fields. The defaults are the "undecided" state used when there are no options.
|
|
65
66
|
contextDefaults: {
|
|
66
|
-
envMode: "", // "dart-define" | "dotenv". ""
|
|
67
|
-
flutterStore: null, //
|
|
68
|
-
androidDeployMode: "", // store_only | store_prepare | store_submit. ""
|
|
69
|
-
iosDeployMode: "", //
|
|
67
|
+
envMode: "", // "dart-define" | "dotenv". ""=undecided, template default (dart-define) applies
|
|
68
|
+
flutterStore: null, // store deploy targets, string[] (e.g. ["android","ios"]). null=undecided, both (current behavior)
|
|
69
|
+
androidDeployMode: "", // store_only | store_prepare | store_submit. ""=undecided, store_only
|
|
70
|
+
iosDeployMode: "", // same as above (iOS)
|
|
70
71
|
},
|
|
71
72
|
contextFields: (options) => ({
|
|
72
73
|
envMode: options.envMode,
|
|
@@ -78,21 +79,21 @@ export const flutterHooks = {
|
|
|
78
79
|
envMode, stores: flutterStore, androidDeployMode, iosDeployMode,
|
|
79
80
|
}),
|
|
80
81
|
versionOptionsBlock,
|
|
81
|
-
// version.yml
|
|
82
|
+
// Saved version.yml key to parsed-result field. The value is returned as the raw string; validity is resolveOptions' job.
|
|
82
83
|
savedOptionKeys: {
|
|
83
84
|
env_mode: "envMode", flutter_store: "flutterStore",
|
|
84
85
|
android_deploy_mode: "androidDeployMode", ios_deploy_mode: "iosDeployMode",
|
|
85
86
|
},
|
|
86
|
-
// CLI
|
|
87
|
+
// CLI flags: parsed-result field (opts) and value validation. initial is the not-given value.
|
|
87
88
|
cliFlags: [
|
|
88
89
|
enumFlag("--flutter-env-mode", "flutterEnvMode", isEnvMode, ENV_MODES),
|
|
89
90
|
{
|
|
90
91
|
flag: "--flutter-store", field: "flutterStore", initial: null,
|
|
91
92
|
parse(v) {
|
|
92
|
-
//
|
|
93
|
+
// parseStoreList sees an empty string as [], but "select no store" is accepted only as an explicit none.
|
|
93
94
|
const stores = v ? parseStoreList(v) : null;
|
|
94
95
|
if (stores === null) {
|
|
95
|
-
throw new CliError(
|
|
96
|
+
throw new CliError(t("core.flutterHooks.err.invalidFlag", { flag: "--flutter-store", value: v || t("core.flutterHooks.noValue"), allowed: [STORE_PLATFORMS.join(","), ...STORE_PLATFORMS, NO_STORE].join(" | ") }));
|
|
96
97
|
}
|
|
97
98
|
return stores;
|
|
98
99
|
},
|
|
@@ -100,20 +101,20 @@ export const flutterHooks = {
|
|
|
100
101
|
enumFlag("--android-deploy-mode", "androidDeployMode", isDeployMode, DEPLOY_MODES),
|
|
101
102
|
enumFlag("--ios-deploy-mode", "iosDeployMode", isDeployMode, DEPLOY_MODES),
|
|
102
103
|
],
|
|
103
|
-
//
|
|
104
|
+
// Warnings shown right after install: store_submit auto-submits a review on every main push, so it must not appear for a store that was not selected.
|
|
104
105
|
installNotices: ({ stores, androidDeployMode, iosDeployMode }) => [
|
|
105
106
|
(stores === null || stores.includes("android")) && deployModeWarning(androidDeployMode),
|
|
106
107
|
(stores === null || stores.includes("ios")) && deployModeWarning(iosDeployMode),
|
|
107
108
|
],
|
|
108
|
-
//
|
|
109
|
+
// Selected values to record in the install log, as [name, value] pairs
|
|
109
110
|
logChoices: (context) => {
|
|
110
|
-
const stores = Array.isArray(context.flutterStore) ? (context.flutterStore.join(",") || "
|
|
111
|
+
const stores = Array.isArray(context.flutterStore) ? (context.flutterStore.join(",") || t("core.flutterHooks.log.storesNone")) : t("core.flutterHooks.log.storesUndecided");
|
|
111
112
|
return [["flutter",
|
|
112
113
|
`env=${context.envMode || "-"} stores=${stores} android=${context.androidDeployMode || "-"} ios=${context.iosDeployMode || "-"}`]];
|
|
113
114
|
},
|
|
114
|
-
//
|
|
115
|
+
// The store workflow filter applies only when the store target is an array (null=undecided, install everything).
|
|
115
116
|
workflowFilter: ({ flutterStore }) => (Array.isArray(flutterStore) ? storeWorkflowFilter(flutterStore) : null),
|
|
116
|
-
//
|
|
117
|
+
// Cleans up deselected store workflows; when the target is undecided (null) nothing is deleted.
|
|
117
118
|
cleanupWorkflows: (workflowsDir, installed, context, baseline, opts) => (Array.isArray(context.flutterStore)
|
|
118
119
|
? cleanupDeselectedStoreWorkflows(workflowsDir, installed, context.flutterStore, baseline, opts)
|
|
119
120
|
: { removed: [], backedUp: [] }),
|