project-auto-wizard 0.13.2 → 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/baseline.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Install-time baseline - the reference point that tells an update "who changed this".
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// base
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Why it is needed: isUnchanged() compares the payload (theirs) and the installed copy (ours) 2-way.
|
|
4
|
+
// Without a base, upstream changing a single character drops a file the user never touched into
|
|
5
|
+
// "changed", so the only choices are "skip everything (no updates)" or "back up everything
|
|
6
|
+
// (all user edits lost)".
|
|
7
7
|
//
|
|
8
|
-
//
|
|
8
|
+
// Only hashes are kept, not file copies - the goal is classification, not automatic merging.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// - installed :
|
|
13
|
-
// - rendered :
|
|
10
|
+
// Why two hashes: for files with user values substituted from env, the disk content and the
|
|
11
|
+
// "payload rendered with defaults" differ from the start, so one hash cannot answer both questions.
|
|
12
|
+
// - installed : what we wrote to disk at install time -> "did the user touch it since"
|
|
13
|
+
// - rendered : the payload rendered with defaults at that time -> "did upstream change it since"
|
|
14
14
|
//
|
|
15
|
-
// installed
|
|
16
|
-
// "
|
|
15
|
+
// installed is filled only for files we actually wrote. Recording a user-edited file as installed
|
|
16
|
+
// would claim "we wrote this", and the next update would silently overwrite it.
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -26,14 +26,14 @@ export function sha256(text) {
|
|
|
26
26
|
return "sha256:" + createHash("sha256").update(String(text), "utf8").digest("hex");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Flutter
|
|
30
|
-
//
|
|
29
|
+
// Hash of Flutter app files (Fastfile, ExportOptions.plist) - normalized to LF first so a file that
|
|
30
|
+
// differs only by checkout line endings (autocrlf) is not treated as "modified by the user".
|
|
31
31
|
export function appFileHash(text) {
|
|
32
32
|
return sha256(String(text).replace(/\r\n/g, "\n"));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
35
|
+
// null when missing or corrupt - callers fall back to "base unknown" (we never silently use an empty
|
|
36
|
+
// baseline, which could be misread as "everything was deleted" rather than "no record").
|
|
37
37
|
export function readBaseline(targetRoot = ".") {
|
|
38
38
|
const p = join(targetRoot, BASELINE_PATH);
|
|
39
39
|
if (!existsSync(p)) return null;
|
|
@@ -42,27 +42,27 @@ export function readBaseline(targetRoot = ".") {
|
|
|
42
42
|
if (!data || typeof data !== "object" || typeof data.files !== "object" || data.files === null) return null;
|
|
43
43
|
return data;
|
|
44
44
|
} catch {
|
|
45
|
-
return null; //
|
|
45
|
+
return null; // A corrupt baseline is treated as missing - it must not block the update
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// entries: Map<filename, {installed?:string|null, rendered?:string|null}>
|
|
50
|
-
// appFiles: Map
|
|
51
|
-
// "
|
|
52
|
-
//
|
|
50
|
+
// appFiles: Map<repo-relative path, appFileHash> - Flutter app files newly created this run. It is what lets a
|
|
51
|
+
// full removal delete only "files the wizard created and the user did not touch". files is keyed by workflow filename, so they are not mixed.
|
|
52
|
+
// The existing baseline is merged in - files not touched this run keep their reference point.
|
|
53
53
|
export function writeBaseline(targetRoot, { templateVersion, installedAt, entries, previous = null, appFiles = new Map() }) {
|
|
54
54
|
const files = { ...(previous?.files || {}) };
|
|
55
55
|
for (const [filename, entry] of entries) {
|
|
56
56
|
const prev = files[filename] || {};
|
|
57
57
|
files[filename] = {
|
|
58
|
-
// installed
|
|
58
|
+
// installed is updated only when actually written this run. Files kept (skipped) retain the old reference point.
|
|
59
59
|
installed: entry.installed ?? prev.installed ?? null,
|
|
60
|
-
// rendered
|
|
60
|
+
// Without rendered (a file that did not take the upstream change due to a conflict) the old reference point is kept.
|
|
61
61
|
rendered: entry.rendered ?? prev.rendered ?? null,
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
const apps = { ...(previous?.appFiles || {}), ...Object.fromEntries(appFiles) };
|
|
65
|
-
//
|
|
65
|
+
// A re-run that changes no reference point keeps the install time too - a file that changes on every run is not idempotent.
|
|
66
66
|
const unchanged = previous && previous.templateVersion === (templateVersion || "unknown")
|
|
67
67
|
&& JSON.stringify(previous.files) === JSON.stringify(files)
|
|
68
68
|
&& JSON.stringify(previous.appFiles || {}) === JSON.stringify(apps);
|
|
@@ -76,14 +76,14 @@ export function writeBaseline(targetRoot, { templateVersion, installedAt, entrie
|
|
|
76
76
|
return out;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// baseline
|
|
80
|
-
//
|
|
81
|
-
// candidates: payload
|
|
79
|
+
// In the baseline but missing on disk = deleted by the user.
|
|
80
|
+
// A side benefit of this design is that no separate deletion-history file is needed.
|
|
81
|
+
// candidates: filenames the payload intends to install this run (other baseline entries are ignored)
|
|
82
82
|
export function detectRemoved(baseline, candidates, workflowsDir) {
|
|
83
83
|
if (!baseline) return [];
|
|
84
84
|
const removed = [];
|
|
85
85
|
for (const filename of candidates) {
|
|
86
|
-
if (!baseline.files[filename]) continue; //
|
|
86
|
+
if (!baseline.files[filename]) continue; // A file we never installed - no basis for a judgement
|
|
87
87
|
if (existsSync(join(workflowsDir, filename))) continue;
|
|
88
88
|
removed.push(filename);
|
|
89
89
|
}
|
package/src/core/branches.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
// on: push: branches:
|
|
3
|
-
// {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}}
|
|
4
|
-
// main === develop
|
|
1
|
+
// Branch configuration: main/develop branches and the pr-flow / trunk-based modes.
|
|
2
|
+
// on: push: branches: is a static YAML value, so the wizard asks for (or takes via flags) the release/develop
|
|
3
|
+
// branches and substitutes the {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} placeholders (the substitution itself lives in branding.js).
|
|
4
|
+
// When main === develop it is trunk-based mode, and RELEASE-PUBLISH is installed alone.
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
6
7
|
|
|
7
|
-
//
|
|
8
|
+
// Default exec: runs a git command. Returns {code, stdout, stderr}. Tests inject a mock.
|
|
8
9
|
export function defaultExec(cmd, args, { cwd } = {}) {
|
|
9
10
|
return new Promise((resolve) => {
|
|
10
11
|
execFile(cmd, args, { cwd, windowsHide: true }, (err, stdout, stderr) => {
|
|
@@ -13,22 +14,23 @@ export function defaultExec(cmd, args, { cwd } = {}) {
|
|
|
13
14
|
});
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
//
|
|
17
|
-
// git
|
|
17
|
+
// Detection: remote branch list from local git (no network, based on the origin/* refs local knows).
|
|
18
|
+
// Empty list when git is missing or this is not a repo (falls back to the prompt-default path).
|
|
18
19
|
export async function detectRemoteBranches(cwd, exec = defaultExec) {
|
|
19
20
|
const r = await exec("git", ["branch", "-r", "--format=%(refname:short)"], { cwd });
|
|
20
21
|
if (r.code !== 0) return [];
|
|
21
22
|
return r.stdout
|
|
22
23
|
.split(/\r?\n/)
|
|
23
24
|
.map((s) => s.trim())
|
|
24
|
-
.filter((s) => s && !s.includes("->")) // "origin/HEAD -> origin/main"
|
|
25
|
+
.filter((s) => s && !s.includes("->")) // exclude "origin/HEAD -> origin/main"
|
|
25
26
|
.map((s) => s.replace(/^origin\//, ""))
|
|
26
27
|
.filter((s, i, a) => a.indexOf(s) === i);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
30
|
+
// Branch name validation: on top of the git check-ref-format --branch rules, shell special characters such as
|
|
31
|
+
// whitespace, parentheses and quotes are rejected because the value is substituted verbatim into workflow YAML and
|
|
32
|
+
// shell commands. If a detection-failure value like "(unknown)" from an empty remote, or a whitespace-only input,
|
|
33
|
+
// were written into the trigger, the workflow would never run.
|
|
32
34
|
export function isValidBranchName(name) {
|
|
33
35
|
if (typeof name !== "string" || name === "") return false;
|
|
34
36
|
if (/[\s~^:?*[\\\x00-\x1f\x7f()"'`$;&|<>!{}]/.test(name)) return false;
|
|
@@ -38,23 +40,23 @@ export function isValidBranchName(name) {
|
|
|
38
40
|
return !name.split("/").some((part) => part.startsWith("."));
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
//
|
|
42
|
-
// develop
|
|
43
|
+
// Notice for when the remote has no branches at all (empty remote, no origin) or develop creation was skipped.
|
|
44
|
+
// Without develop the pr-flow workflows are never triggered, so this must not pass silently.
|
|
43
45
|
export function developMissingNotice({ main, develop }) {
|
|
44
|
-
return
|
|
46
|
+
return t("core.branches.developMissing", { main, develop });
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
+
// Decision (pure function): flags/answers to the final configuration.
|
|
50
|
+
// Priority: explicit values (mainBranch/developBranch), then detected default, then hard fallback (main/develop).
|
|
49
51
|
export function resolveBranchConfig({ mainBranch = "", developBranch = "", defaultBranch = "" } = {}) {
|
|
50
52
|
const main = mainBranch || defaultBranch || "main";
|
|
51
53
|
const develop = developBranch || "develop";
|
|
52
54
|
return { main, develop, mode: main === develop ? "trunk-based" : "pr-flow" };
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
57
|
+
// Ordering for the branch selection prompt. def goes first, then priority (main/develop) in that order, at the
|
|
58
|
+
// front of the list. The rest keep remoteBranches' original order (alphabetical as given by git).
|
|
59
|
+
// Pure function: the remoteBranches original is not modified.
|
|
58
60
|
export function sortBranchesForSelection(remoteBranches, def, priority = ["main", "develop"]) {
|
|
59
61
|
const priorityOrder = [def, ...priority].filter((b, i, arr) => arr.indexOf(b) === i);
|
|
60
62
|
const inPriority = priorityOrder.filter((b) => remoteBranches.includes(b));
|
|
@@ -62,27 +64,27 @@ export function sortBranchesForSelection(remoteBranches, def, priority = ["main"
|
|
|
62
64
|
return [...inPriority, ...rest];
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
//
|
|
66
|
-
// confirm: async(message)
|
|
67
|
-
// exec
|
|
67
|
+
// Creation: when develop is missing on the remote, create it from the current HEAD and push.
|
|
68
|
+
// confirm: async(message)->bool, the interactive confirmation. When null (--force) it is created without asking.
|
|
69
|
+
// exec is injectable (test mock). Returns { created, pushed?, skipped? }.
|
|
68
70
|
export async function ensureDevelopBranch({ develop, remoteBranches = [], confirm = null, cwd, exec = defaultExec, log = null }) {
|
|
69
71
|
if (remoteBranches.includes(develop)) return { created: false };
|
|
70
72
|
|
|
71
73
|
if (confirm) {
|
|
72
|
-
const ok = await confirm(
|
|
74
|
+
const ok = await confirm(t("core.branches.confirmCreate", { develop }));
|
|
73
75
|
if (ok !== true) return { created: false, skipped: true };
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
const br = await exec("git", ["branch", develop], { cwd });
|
|
77
79
|
if (br.code !== 0) {
|
|
78
|
-
//
|
|
79
|
-
log?.(
|
|
80
|
+
// e.g. it already exists locally: just try the push
|
|
81
|
+
log?.(t("core.branches.localSkipped", { develop, reason: (br.stderr || "").trim() || t("core.branches.alreadyExists") }));
|
|
80
82
|
}
|
|
81
83
|
const push = await exec("git", ["push", "-u", "origin", develop], { cwd });
|
|
82
84
|
if (push.code !== 0) {
|
|
83
|
-
log?.(
|
|
85
|
+
log?.(t("core.branches.pushFailed", { develop }));
|
|
84
86
|
return { created: true, pushed: false };
|
|
85
87
|
}
|
|
86
|
-
log?.(
|
|
88
|
+
log?.(t("core.branches.created", { develop }));
|
|
87
89
|
return { created: true, pushed: true };
|
|
88
90
|
}
|
package/src/core/branding.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Branch placeholder substitution pipeline.
|
|
2
|
+
// Replaces {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} in payload workflows with the real branch names at install time.
|
|
3
|
+
// WARNING: the GitHub Actions expression `${{ ... }}` is not our token, so a leading `$` excludes it (lookbehind).
|
|
4
|
+
// Throws when an unknown {{TOKEN}} remains after substitution: a copy-integrity guard (catches typos and omissions early).
|
|
5
|
+
import { t } from "../i18n/index.js";
|
|
5
6
|
|
|
6
7
|
const TOKEN_RE = /(?<!\$)\{\{([A-Z][A-Z0-9_]*)\}\}/g;
|
|
7
8
|
|
|
8
|
-
// substitute(text, {main, develop})
|
|
9
|
+
// substitute(text, {main, develop}): substitutes branch tokens. Throws when an unknown token is found.
|
|
9
10
|
export function substitute(text, { main, develop }) {
|
|
10
11
|
const map = { MAIN_BRANCH: main, DEVELOP_BRANCH: develop };
|
|
11
12
|
return String(text).replace(TOKEN_RE, (_, name) => {
|
|
12
13
|
if (name in map) return map[name];
|
|
13
|
-
throw new Error(
|
|
14
|
+
throw new Error(t("core.branding.unknownPlaceholder", { name }));
|
|
14
15
|
});
|
|
15
16
|
}
|
|
@@ -1,32 +1,33 @@
|
|
|
1
|
-
// breaking-changes
|
|
2
|
-
// collectBreaking(
|
|
1
|
+
// Wiring for the breaking-changes confirmation flow.
|
|
2
|
+
// collectBreaking (pure comparison) is in breaking.js - this module handles loading (bundled copy), display and the confirmation gate.
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { collectBreaking } from "./breaking.js";
|
|
5
|
+
import { collectBreaking, localizedField } from "./breaking.js";
|
|
6
6
|
import { parseExisting } from "./version-yml.js";
|
|
7
|
+
import { t } from "../i18n/index.js";
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// Reads only the payload/config copy bundled with the package. Reading remote (main) would make results
|
|
10
|
+
// differ per run even for the same package version, and add a network request during install. null on read failure (silent skip).
|
|
10
11
|
export function loadBreakingJson(payloadRoot) {
|
|
11
12
|
try {
|
|
12
13
|
const p = join(payloadRoot, "config", "breaking-changes.json");
|
|
13
14
|
if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
|
|
14
|
-
} catch { /*
|
|
15
|
+
} catch { /* bundle read failed - skip */ }
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
//
|
|
19
|
+
// Returns: true = proceed, false = user cancelled.
|
|
19
20
|
// opts:
|
|
20
|
-
// cwd -
|
|
21
|
-
// payloadRoot -
|
|
22
|
-
// templateVersion -
|
|
23
|
-
// askYesNo - async(message, defaultYes)
|
|
24
|
-
// loader -
|
|
21
|
+
// cwd - integration target root (current template version is read from the existing version.yml)
|
|
22
|
+
// payloadRoot - package payload/ root (where the bundled breaking-changes.json lives)
|
|
23
|
+
// templateVersion - template version being installed (the actual package version, not a fixed value)
|
|
24
|
+
// askYesNo - async(message, defaultYes)->bool. null = non-interactive: print the warning and proceed
|
|
25
|
+
// loader - injected by tests (default loadBreakingJson)
|
|
25
26
|
export async function runBreakingCheck({ cwd, payloadRoot, templateVersion, askYesNo = null, loader = loadBreakingJson }) {
|
|
26
27
|
const vy = join(cwd, "version.yml");
|
|
27
|
-
if (!existsSync(vy)) return true; //
|
|
28
|
+
if (!existsSync(vy)) return true; // New integration - nothing to compare against
|
|
28
29
|
const { templateVersion: current, types } = parseExisting(readFileSync(vy, "utf8"));
|
|
29
|
-
if (!current) return true; //
|
|
30
|
+
if (!current) return true; // No template metadata (unknown) - nothing to compare against, skip
|
|
30
31
|
|
|
31
32
|
const json = await loader(payloadRoot);
|
|
32
33
|
if (!json) return true;
|
|
@@ -34,26 +35,26 @@ export async function runBreakingCheck({ cwd, payloadRoot, templateVersion, askY
|
|
|
34
35
|
const { critical, warnings } = collectBreaking(json, current, templateVersion, types);
|
|
35
36
|
if (critical.length === 0 && warnings.length === 0) return true;
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
+
// Box display
|
|
38
39
|
const e = (s = "") => process.stderr.write(s + "\n");
|
|
39
40
|
e("");
|
|
40
41
|
e("╔══════════════════════════════════════════════════════════════════╗");
|
|
41
42
|
e(`║ ⚠️ BREAKING CHANGES (v${current} → v${templateVersion})`);
|
|
42
43
|
e("╠══════════════════════════════════════════════════════════════════╣");
|
|
43
|
-
for (const c of critical) { e("║"); e(`║ [CRITICAL] ${c.version} - ${c
|
|
44
|
-
for (const w of warnings) { e("║"); e(`║ [WARNING] ${w.version} - ${w
|
|
44
|
+
for (const c of critical) { e("║"); e(`║ [CRITICAL] ${c.version} - ${localizedField(c, "title")}`); e(`║ → ${localizedField(c, "message")}`); }
|
|
45
|
+
for (const w of warnings) { e("║"); e(`║ [WARNING] ${w.version} - ${localizedField(w, "title")}`); e(`║ → ${localizedField(w, "message")}`); }
|
|
45
46
|
e("║");
|
|
46
47
|
e("╚══════════════════════════════════════════════════════════════════╝");
|
|
47
48
|
e("");
|
|
48
49
|
|
|
49
50
|
if (critical.length > 0) {
|
|
50
51
|
if (askYesNo) {
|
|
51
|
-
//
|
|
52
|
-
const ok = await askYesNo("
|
|
52
|
+
// Interactive: abort without explicit confirmation (default N)
|
|
53
|
+
const ok = await askYesNo(t("core.breakingCheck.confirm"), false);
|
|
53
54
|
if (ok !== true) return false;
|
|
54
55
|
} else {
|
|
55
|
-
//
|
|
56
|
-
e("
|
|
56
|
+
// Non-interactive (--force): do not kill CI with a gate, warn and proceed (CI friendly)
|
|
57
|
+
e(t("core.breakingCheck.nonInteractive"));
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
return true;
|
package/src/core/breaking.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { getLanguage } from "../i18n/index.js";
|
|
2
|
+
|
|
3
|
+
// Version comparison: strips the v prefix, compares 3 numeric parts, missing part = 0
|
|
2
4
|
export function compareVersions(a, b) {
|
|
3
5
|
const parse = (v) => String(v).replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
4
6
|
const pa = parse(a), pb = parse(b);
|
|
@@ -10,19 +12,24 @@ export function compareVersions(a, b) {
|
|
|
10
12
|
return 0;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
//
|
|
15
|
+
// Text of an entry in the given language: `title_ko` / `message_ko` when present, otherwise the plain (English) field.
|
|
16
|
+
export function localizedField(entry, field, lang = getLanguage()) {
|
|
17
|
+
return entry?.[`${field}_${lang}`] || entry?.[field] || "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Collect entries from breaking-changes.json with current < ver <= target.
|
|
21
|
+
// target is the actual templateVersion, not a fixed value - so every notice up to the version being installed is caught.
|
|
22
|
+
// Keys starting with _ (metadata) are skipped. severity critical / anything else (warning).
|
|
23
|
+
// A version key's value is an entry object or an array of entry objects (several notices in one release).
|
|
24
|
+
// types: installed project types. When an entry has types it is shown only if one overlaps - a
|
|
25
|
+
// Flutter-only warning on a spring repo would bury the notices that really matter. Unknown types (empty array) shows everything.
|
|
19
26
|
export function collectBreaking(json, current, target, types = []) {
|
|
20
27
|
const critical = [], warnings = [];
|
|
21
28
|
const relevant = (entry) => !Array.isArray(entry?.types) || !types.length || entry.types.some((t) => types.includes(t));
|
|
22
29
|
for (const [ver, value] of Object.entries(json || {})) {
|
|
23
30
|
if (ver.startsWith("_")) continue;
|
|
24
31
|
if (compareVersions(current, ver) < 0 && compareVersions(ver, target) <= 0) {
|
|
25
|
-
//
|
|
32
|
+
// Several notices in one release are registered as an array - JSON keys (versions) are unique, so an object holds only one.
|
|
26
33
|
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
27
34
|
if (!relevant(entry)) continue;
|
|
28
35
|
const rec = { version: ver, ...entry };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Installs type-specific app files (user-owned, created only when missing) by running the type hooks
|
|
2
|
+
// (planAppFiles/copyAppFiles) and merging their results.
|
|
3
|
+
// Which files are targeted is decided by the hooks, so this module knows no type names.
|
|
3
4
|
import { hooksFor, typeInfo } from "../types.js";
|
|
4
5
|
|
|
5
|
-
//
|
|
6
|
+
// Read-only, used by dry-run. Returns { created:[], kept:[] }
|
|
6
7
|
export function planTypeAppFiles(context, payloadRoot, targetRoot = ".") {
|
|
7
8
|
const result = { created: [], kept: [] };
|
|
8
9
|
for (const { hook } of hooksFor(context.types || [], "planAppFiles")) {
|
|
@@ -13,7 +14,7 @@ export function planTypeAppFiles(context, payloadRoot, targetRoot = ".") {
|
|
|
13
14
|
return result;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
// onFile(tag, action, relPath):
|
|
17
|
+
// onFile(tag, action, relPath): log callback invoked per file. Returns { created:[], kept:[] }
|
|
17
18
|
export function copyTypeAppFiles(context, payloadRoot, targetRoot = ".", onFile = () => {}) {
|
|
18
19
|
const result = { created: [], kept: [] };
|
|
19
20
|
for (const { id, hook } of hooksFor(context.types || [], "copyAppFiles")) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Flutter
|
|
2
|
-
//
|
|
3
|
-
// 3-way
|
|
4
|
-
//
|
|
1
|
+
// Installs Flutter app-owned files (fastlane, ExportOptions.plist).
|
|
2
|
+
// Unlike workflows these are files the user fills in, so they are created only when missing: never
|
|
3
|
+
// overwritten and no baseline 3-way applied (existing user files are not overwritten by convention).
|
|
4
|
+
// Propagating upstream changes to existing users is out of scope.
|
|
5
5
|
import { join, posix } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { copyFileSync } from "../fsutil.js";
|
|
@@ -9,8 +9,8 @@ import { storeAppFilesFor } from "../flutter-options.js";
|
|
|
9
9
|
|
|
10
10
|
const FLUTTER_APP_DIR = "flutter-app"; // payload/flutter-app/
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
12
|
+
// List of (reported relative path, source, destination) for the files handled in this run.
|
|
13
|
+
// Only when the Flutter type is present, limited to the selected platforms (context.flutterStore, null = both).
|
|
14
14
|
function flutterAppTargets(context, payloadRoot, targetRoot) {
|
|
15
15
|
const { types = [], paths = new Map(), flutterStore = null } = context;
|
|
16
16
|
if (!types.includes("flutter")) return [];
|
|
@@ -22,7 +22,7 @@ function flutterAppTargets(context, payloadRoot, targetRoot) {
|
|
|
22
22
|
}));
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// Read-only, used by status/dry-run. Does not read the source payload.
|
|
26
26
|
export function planFlutterAppFiles(context, payloadRoot, targetRoot = ".") {
|
|
27
27
|
const result = { created: [], kept: [] };
|
|
28
28
|
for (const { reported, dst } of flutterAppTargets(context, payloadRoot, targetRoot)) {
|
|
@@ -35,7 +35,7 @@ export function copyFlutterAppFiles(context, payloadRoot, targetRoot = ".") {
|
|
|
35
35
|
const result = { created: [], kept: [] };
|
|
36
36
|
for (const { reported, src, dst } of flutterAppTargets(context, payloadRoot, targetRoot)) {
|
|
37
37
|
if (existsSync(dst)) { result.kept.push(reported); continue; }
|
|
38
|
-
copyFileSync(src, dst); //
|
|
38
|
+
copyFileSync(src, dst); // creates parent directories and copies bytes as-is (preserves CRLF)
|
|
39
39
|
result.created.push(reported);
|
|
40
40
|
}
|
|
41
41
|
return result;
|