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
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
// .gitignore
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
1
|
+
// .gitignore guarantee: covers only the conflict backup by-products the wizard itself creates
|
|
2
|
+
// (*.bak, *.template.yaml). Personal dev-environment settings (IDE etc.) unrelated to what the wizard
|
|
3
|
+
// installs are outside its responsibility.
|
|
4
|
+
// The banner block is clearly delimited by the end marker (BANNER_END), so even if the user inserts other
|
|
5
|
+
// lines inside it, only the REQUIRED_ENTRIES are removed individually and everything else is preserved.
|
|
6
|
+
// Note (intended trade-off): in repos installed before this change, where the banner has no end marker,
|
|
7
|
+
// we fall back to the old approach of removing only while REQUIRED_ENTRIES follow the banner consecutively.
|
|
8
|
+
// An earlier decision states that .gitignore of already installed repos is not retroactively processed and is
|
|
9
|
+
// left to the user, so no separate migration logic is added.
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
12
|
+
import { t, SUPPORTED_LANGUAGES } from "../../i18n/index.js";
|
|
11
13
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
14
|
+
// Personal dev-environment entries (/.idea etc.) are outside the wizard's responsibility, so they are excluded.
|
|
15
|
+
// Only the by-products the wizard's own conflict handling (workflows.js backup/template decisions)
|
|
16
|
+
// actually creates are gitignore targets.
|
|
15
17
|
const REQUIRED_ENTRIES = ["*.bak", "*.template.yaml"];
|
|
16
18
|
|
|
17
|
-
//
|
|
19
|
+
// Normalizes an entry: strip comment, trim, strip leading /, leading ./ and trailing /. Returns the original if empty.
|
|
18
20
|
export function normalizeGitignoreEntry(entry) {
|
|
19
21
|
let e = String(entry);
|
|
20
|
-
e = e.replace(/#.*$/, ""); //
|
|
21
|
-
e = e.trim(); //
|
|
22
|
-
e = e.replace(/^\//, ""); //
|
|
23
|
-
e = e.replace(/^\.\//, ""); //
|
|
24
|
-
e = e.replace(/\/$/, ""); //
|
|
22
|
+
e = e.replace(/#.*$/, ""); // strip comment
|
|
23
|
+
e = e.trim(); // surrounding whitespace
|
|
24
|
+
e = e.replace(/^\//, ""); // leading /
|
|
25
|
+
e = e.replace(/^\.\//, ""); // leading ./
|
|
26
|
+
e = e.replace(/\/$/, ""); // trailing /
|
|
25
27
|
return e === "" ? String(entry) : e;
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -35,13 +37,18 @@ function entryExists(target, content) {
|
|
|
35
37
|
return false;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const
|
|
39
|
-
"
|
|
40
|
+
const newFileContent = (lang) =>
|
|
41
|
+
t("copy.gitignore.newFileHeader", {}, lang) + "\n" +
|
|
40
42
|
"*.bak\n" +
|
|
41
43
|
"*.template.yaml\n";
|
|
44
|
+
// The header comment depends on the language it was written in, so a file created in one language must
|
|
45
|
+
// still be recognized under another: check every supported language's variant.
|
|
46
|
+
const NEW_FILE_CONTENT = () => newFileContent(undefined);
|
|
47
|
+
const matchingNewFileContent = (content) =>
|
|
48
|
+
[NEW_FILE_CONTENT(), ...SUPPORTED_LANGUAGES.map(newFileContent)].find((c) => content.startsWith(c)) ?? null;
|
|
42
49
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
50
|
+
// Only decides without writing, so the real update and the --dry-run preview share one decision.
|
|
51
|
+
// Returns: {created, added:[...]}
|
|
45
52
|
export function planGitignore(targetRoot = ".") {
|
|
46
53
|
const p = join(targetRoot, ".gitignore");
|
|
47
54
|
if (!existsSync(p)) return { created: true, added: REQUIRED_ENTRIES.slice() };
|
|
@@ -49,21 +56,21 @@ export function planGitignore(targetRoot = ".") {
|
|
|
49
56
|
return { created: false, added: REQUIRED_ENTRIES.filter((e) => !entryExists(e, content)) };
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
//
|
|
59
|
+
// Returns: {created, added:[...]}
|
|
53
60
|
export function ensureGitignore(targetRoot = ".") {
|
|
54
61
|
const p = join(targetRoot, ".gitignore");
|
|
55
62
|
const plan = planGitignore(targetRoot);
|
|
56
63
|
if (plan.created) {
|
|
57
|
-
writeFileSync(p, NEW_FILE_CONTENT);
|
|
64
|
+
writeFileSync(p, NEW_FILE_CONTENT());
|
|
58
65
|
return plan;
|
|
59
66
|
}
|
|
60
67
|
const toAdd = plan.added;
|
|
61
68
|
if (toAdd.length === 0) return plan;
|
|
62
69
|
let content = readFileSync(p, "utf8");
|
|
63
70
|
|
|
64
|
-
// BANNER
|
|
65
|
-
//
|
|
66
|
-
// (
|
|
71
|
+
// BANNER starts with "\n". If the file lacks a trailing newline that "\n" terminates the last line;
|
|
72
|
+
// otherwise it becomes one blank line. Either way, cutting up to BANNER on removal restores the original text.
|
|
73
|
+
// (Adding a newline separately would leave a trailing newline that was not there originally.)
|
|
67
74
|
content += BANNER;
|
|
68
75
|
for (const e of toAdd) content += e + "\n";
|
|
69
76
|
content += BANNER_END;
|
|
@@ -71,42 +78,44 @@ export function ensureGitignore(targetRoot = ".") {
|
|
|
71
78
|
return { created: false, added: toAdd };
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
// ensureGitignore
|
|
75
|
-
// ("\n" + 3
|
|
76
|
-
//
|
|
77
|
-
//
|
|
81
|
+
// When ensureGitignore adds a banner block to an existing file it always starts with this exact sequence
|
|
82
|
+
// ("\n" + 3-line banner) and ends with BANNER_END after REQUIRED_ENTRIES. Only lines matching
|
|
83
|
+
// REQUIRED_ENTRIES inside the banner..BANNER_END range are removed individually; lines the user added
|
|
84
|
+
// between or after them are never touched.
|
|
85
|
+
// The banner text is English in every language, so detection needs no language handling.
|
|
78
86
|
const BANNER =
|
|
79
87
|
"\n" +
|
|
80
88
|
"# ====================================================================\n" +
|
|
81
89
|
"# project-auto-wizard: Auto-added entries\n" +
|
|
82
90
|
"# ====================================================================\n";
|
|
83
91
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// (
|
|
88
|
-
// startsWith
|
|
92
|
+
// End marker after REQUIRED_ENTRIES: it clearly delimits the "end" of the banner block so that
|
|
93
|
+
// removeAutoAddedEntriesFromGitignore can remove entries individually within the exact range even if the user
|
|
94
|
+
// inserted other lines between them. Used only when ensureGitignore adds a banner to an existing file
|
|
95
|
+
// (the new-file case writes NEW_FILE_CONTENT as a whole and no end marker; it is matched
|
|
96
|
+
// by the full startsWith prefix, so no separate marker is needed).
|
|
89
97
|
const BANNER_END = "# ==== project-auto-wizard: end of auto-added entries ====\n";
|
|
90
98
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// (2)
|
|
99
|
+
// Checks whether the wizard added entries to .gitignore (to decide on showing the checklist).
|
|
100
|
+
// Two cases: (1) a file that did not exist was created as a whole (or with user content appended after it)
|
|
101
|
+
// (2) a banner block was attached to an existing file.
|
|
94
102
|
export function hasAutoAddedEntries(targetRoot = ".") {
|
|
95
103
|
const p = join(targetRoot, ".gitignore");
|
|
96
104
|
if (!existsSync(p)) return false;
|
|
97
105
|
const content = readFileSync(p, "utf8");
|
|
98
|
-
return content
|
|
106
|
+
return matchingNewFileContent(content) !== null || content.includes(BANNER);
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
//
|
|
109
|
+
// Returns: 'removed' | 'file-deleted' | 'skip-no-gitignore' | 'skip-not-found'
|
|
102
110
|
export function removeAutoAddedEntriesFromGitignore(targetRoot = ".") {
|
|
103
111
|
const p = join(targetRoot, ".gitignore");
|
|
104
112
|
if (!existsSync(p)) return "skip-no-gitignore";
|
|
105
113
|
const content = readFileSync(p, "utf8");
|
|
106
114
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
// The file did not exist and the wizard created it as a whole: keep only what the user appended after it.
|
|
116
|
+
const created = matchingNewFileContent(content);
|
|
117
|
+
if (created !== null) {
|
|
118
|
+
const remainder = content.slice(created.length);
|
|
110
119
|
if (remainder === "") {
|
|
111
120
|
rmSync(p);
|
|
112
121
|
return "file-deleted";
|
|
@@ -115,14 +124,13 @@ export function removeAutoAddedEntriesFromGitignore(targetRoot = ".") {
|
|
|
115
124
|
return "removed";
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
//
|
|
127
|
+
// A banner block was attached to an existing file.
|
|
119
128
|
const idx = content.indexOf(BANNER);
|
|
120
129
|
if (idx === -1) return "skip-not-found";
|
|
121
130
|
const afterBanner = idx + BANNER.length;
|
|
122
131
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//.
|
|
132
|
+
// If the end marker exists (installed after this change), remove only REQUIRED_ENTRIES individually
|
|
133
|
+
// within that range and keep any lines the user inserted between them, regardless of order or adjacency.
|
|
126
134
|
const endIdx = content.indexOf(BANNER_END, afterBanner);
|
|
127
135
|
if (endIdx !== -1) {
|
|
128
136
|
const region = content.slice(afterBanner, endIdx).split("\n");
|
|
@@ -133,14 +141,14 @@ export function removeAutoAddedEntriesFromGitignore(targetRoot = ".") {
|
|
|
133
141
|
return "removed";
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
//
|
|
137
|
-
//
|
|
144
|
+
// Old install without an end marker (before this change): fall back to the old approach of removing only
|
|
145
|
+
// while REQUIRED_ENTRIES follow the banner consecutively (same no-retroactive-processing principle).
|
|
138
146
|
const lines = content.slice(afterBanner).split("\n");
|
|
139
147
|
let consumed = 0;
|
|
140
148
|
for (const line of lines) {
|
|
141
149
|
const isKnownEntry = REQUIRED_ENTRIES.some((e) => normalizeGitignoreEntry(line) === normalizeGitignoreEntry(e));
|
|
142
150
|
if (!isKnownEntry) break;
|
|
143
|
-
consumed += line.length + 1; // +1:
|
|
151
|
+
consumed += line.length + 1; // +1: the "\n" swallowed by split
|
|
144
152
|
}
|
|
145
153
|
writeFileSync(p, content.slice(0, idx) + content.slice(afterBanner + consumed));
|
|
146
154
|
return "removed";
|
package/src/core/copy/readme.js
CHANGED
|
@@ -1,26 +1,80 @@
|
|
|
1
|
-
// README
|
|
1
|
+
// Appends the README version section.
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { t, SUPPORTED_LANGUAGES } from "../../i18n/index.js";
|
|
4
5
|
|
|
5
6
|
export const MARKER = "<!-- AUTO-VERSION-SECTION";
|
|
6
|
-
// ##
|
|
7
|
-
|
|
7
|
+
// "## <latest version|latest-version|current/recent version|Version|version> : vX.Y.Z", where the Korean words are written as
|
|
8
|
+
// \u escapes so this file stays free of Hangul (case-insensitive)
|
|
9
|
+
const VERSION_LINE_RE = /##\s*(\uCD5C\uC2E0\s*\uBC84\uC804|\uCD5C\uC2E0\uBC84\uC804|(?:latest|current|recent)[\s-]*version|Version|\uBC84\uC804)\s*:\s*v[0-9]+\.[0-9]+\.[0-9]+/i;
|
|
8
10
|
|
|
9
|
-
//
|
|
11
|
+
// Descriptions for the run log: a bare status code forces later readers to look up its meaning.
|
|
12
|
+
// Getters keep the text lazy so it follows the language resolved at runtime.
|
|
10
13
|
export const README_STATUS_LABEL = {
|
|
11
|
-
added
|
|
12
|
-
"skip-no-readme"
|
|
13
|
-
"skip-marker"
|
|
14
|
-
"skip-version-line"
|
|
14
|
+
get added() { return t("copy.readme.status.added"); },
|
|
15
|
+
get "skip-no-readme"() { return t("copy.readme.status.skipNoReadme"); },
|
|
16
|
+
get "skip-marker"() { return t("copy.readme.status.skipMarker"); },
|
|
17
|
+
get "skip-version-line"() { return t("copy.readme.status.skipVersionLine"); },
|
|
18
|
+
get "heading-updated"() { return t("copy.readme.status.headingUpdated"); },
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
// Bundled default heading text ("Latest Version", ...) without the "## " prefix and version part, per language.
|
|
22
|
+
const defaultHeading = (lang) => t("copy.readme.versionHeading", { version: "0" }, lang).replace(/^##\s*/, "").replace(/\s*:\s*v0$/, "");
|
|
23
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
|
|
25
|
+
// Text of the history link line in a given language (without the trailing newline).
|
|
26
|
+
const historyLink = (lang) => t("copy.readme.historyLink", {}, lang);
|
|
27
|
+
|
|
28
|
+
// Rewrites the wizard-made block (marker line, heading line, blank line, history link line) that was written
|
|
29
|
+
// in another language so it matches the current one. Only text that exactly equals another language's bundled
|
|
30
|
+
// default is replaced: a heading or link the user edited is left alone, and so is anything not directly
|
|
31
|
+
// under the marker (code-block examples, a README without the marker).
|
|
32
|
+
// Returns the new content, or null when nothing needs to change.
|
|
33
|
+
function refreshBlockLanguage(content) {
|
|
34
|
+
const currentHeading = defaultHeading();
|
|
35
|
+
const currentLink = historyLink();
|
|
36
|
+
const markerRe = new RegExp("^" + escapeRe(MARKER_LINE.trimEnd()) + "[ \\t]*\\r?\\n", "gm");
|
|
37
|
+
let out = "";
|
|
38
|
+
let last = 0;
|
|
39
|
+
let changed = false;
|
|
40
|
+
for (let m = markerRe.exec(content); m; m = markerRe.exec(content)) {
|
|
41
|
+
// An odd number of fence lines before the marker means it sits inside a code block (an example).
|
|
42
|
+
if ((content.slice(0, m.index).match(/^[ \t]*(```|~~~)/gm) || []).length % 2 === 1) continue;
|
|
43
|
+
const start = m.index + m[0].length;
|
|
44
|
+
let rest = content.slice(start);
|
|
45
|
+
let head = "";
|
|
46
|
+
// Heading line directly under the marker.
|
|
47
|
+
for (const lang of SUPPORTED_LANGUAGES) {
|
|
48
|
+
const heading = defaultHeading(lang);
|
|
49
|
+
if (heading === currentHeading) continue;
|
|
50
|
+
const hm = new RegExp("^## " + escapeRe(heading) + " : (?=v[0-9])").exec(rest);
|
|
51
|
+
if (hm) { head = "## " + currentHeading + " : "; rest = rest.slice(hm[0].length); changed = true; break; }
|
|
52
|
+
}
|
|
53
|
+
out += content.slice(last, start) + head;
|
|
54
|
+
last = start + (content.slice(start).length - rest.length);
|
|
55
|
+
// History link line: heading line, one blank line, then the link.
|
|
56
|
+
const lm = /^([^\r\n]*\r?\n\r?\n)([^\r\n]*)/.exec(rest);
|
|
57
|
+
if (lm) {
|
|
58
|
+
const linkStart = last + lm[1].length;
|
|
59
|
+
const isStale = SUPPORTED_LANGUAGES.some((lang) => historyLink(lang) !== currentLink && lm[2] === historyLink(lang));
|
|
60
|
+
if (isStale) {
|
|
61
|
+
out += content.slice(last, linkStart) + currentLink;
|
|
62
|
+
last = linkStart + lm[2].length;
|
|
63
|
+
changed = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return changed ? out + content.slice(last) : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Skip when README.md is missing, or when a marker or version line exists. Otherwise append to the end.
|
|
71
|
+
// Returns: 'skip-no-readme' | 'heading-updated' | 'skip-marker' | 'skip-version-line' | 'added'
|
|
72
|
+
// Only decides without writing, so the real append and the --dry-run preview share one decision.
|
|
20
73
|
export function planVersionSection(targetRoot = ".") {
|
|
21
74
|
const p = join(targetRoot, "README.md");
|
|
22
75
|
if (!existsSync(p)) return "skip-no-readme";
|
|
23
76
|
const content = readFileSync(p, "utf8");
|
|
77
|
+
if (refreshBlockLanguage(content) !== null) return "heading-updated";
|
|
24
78
|
if (content.includes(MARKER)) return "skip-marker";
|
|
25
79
|
if (VERSION_LINE_RE.test(content)) return "skip-version-line";
|
|
26
80
|
return "added";
|
|
@@ -28,39 +82,46 @@ export function planVersionSection(targetRoot = ".") {
|
|
|
28
82
|
|
|
29
83
|
export function addVersionSectionToReadme(version, targetRoot = ".") {
|
|
30
84
|
const status = planVersionSection(targetRoot);
|
|
31
|
-
if (status !== "added") return status;
|
|
32
85
|
const p = join(targetRoot, "README.md");
|
|
86
|
+
if (status === "heading-updated") {
|
|
87
|
+
// Swap only the bundled default wording; version text after the colon stays as the workflow wrote it.
|
|
88
|
+
writeFileSync(p, refreshBlockLanguage(readFileSync(p, "utf8")));
|
|
89
|
+
return status;
|
|
90
|
+
}
|
|
91
|
+
if (status !== "added") return status;
|
|
33
92
|
const content = readFileSync(p, "utf8");
|
|
34
93
|
|
|
35
|
-
//
|
|
94
|
+
// The appended body starts with "\n---\n..." to leave a blank line between the existing text and the rule.
|
|
36
95
|
const section =
|
|
37
96
|
"\n" +
|
|
38
97
|
"---\n" +
|
|
39
98
|
"\n" +
|
|
40
99
|
"<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->\n" +
|
|
41
|
-
|
|
100
|
+
t("copy.readme.versionHeading", { version }) + "\n" +
|
|
42
101
|
"\n" +
|
|
43
|
-
"
|
|
44
|
-
//
|
|
45
|
-
//
|
|
102
|
+
t("copy.readme.historyLink") + "\n";
|
|
103
|
+
// Appending directly to a README without a trailing newline puts "---" right after the last line,
|
|
104
|
+
// and Markdown turns that line into a heading (Setext h2), so terminate the line first.
|
|
46
105
|
const lead = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
47
106
|
appendFileSync(p, lead + section);
|
|
48
107
|
return "added";
|
|
49
108
|
}
|
|
50
109
|
|
|
51
|
-
// addVersionSectionToReadme
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
110
|
+
// Prefix/suffix sequence that addVersionSectionToReadme always appends at the very end of the file.
|
|
111
|
+
// If the prefix (SECTION_PREFIX) is present it is a "block" appended by the wizard. The block always ends
|
|
112
|
+
// with the SECTION_TAIL line, so cut only up to that point; cutting "to the end of the file" would delete
|
|
113
|
+
// content the user appended later (a license section etc.).
|
|
55
114
|
const SECTION_PREFIX = "\n---\n\n" + MARKER;
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
115
|
+
// The tail text depends on the language it was installed in, so a section written in one language must
|
|
116
|
+
// still be removable under another: candidates from every supported language are matched.
|
|
117
|
+
const sectionTails = () => [...new Set(SUPPORTED_LANGUAGES.map((l) => t("copy.readme.historyLink", {}, l) + "\n"))];
|
|
118
|
+
// For a README that already had the user's own version line at install time (addVersionSectionToReadme
|
|
119
|
+
// skipped it as 'skip-version-line'), PROJECT-COMMON-README-VERSION-UPDATE.yaml (the installed CI) inserts
|
|
120
|
+
// only a marker comment line above that version line, without the '---' separator. The version line itself
|
|
121
|
+
// is user-owned, so only the marker comment line is removed in that case.
|
|
61
122
|
const MARKER_LINE = "<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->\n";
|
|
62
123
|
|
|
63
|
-
//
|
|
124
|
+
// Checks whether the wizard (or the installed CI) left traces in README.md (to decide on showing the checklist).
|
|
64
125
|
export function hasVersionSection(targetRoot = ".") {
|
|
65
126
|
const p = join(targetRoot, "README.md");
|
|
66
127
|
if (!existsSync(p)) return false;
|
|
@@ -68,7 +129,7 @@ export function hasVersionSection(targetRoot = ".") {
|
|
|
68
129
|
return content.includes(SECTION_PREFIX) || content.includes(MARKER_LINE);
|
|
69
130
|
}
|
|
70
131
|
|
|
71
|
-
//
|
|
132
|
+
// Returns: 'removed' | 'skip-no-readme' | 'skip-no-marker' | 'skip-unexpected-format'
|
|
72
133
|
export function removeVersionSectionFromReadme(targetRoot = ".") {
|
|
73
134
|
const p = join(targetRoot, "README.md");
|
|
74
135
|
if (!existsSync(p)) return "skip-no-readme";
|
|
@@ -76,22 +137,27 @@ export function removeVersionSectionFromReadme(targetRoot = ".") {
|
|
|
76
137
|
|
|
77
138
|
const idx = content.indexOf(SECTION_PREFIX);
|
|
78
139
|
if (idx !== -1) {
|
|
79
|
-
//
|
|
80
|
-
// tail
|
|
81
|
-
// (
|
|
82
|
-
//
|
|
83
|
-
//
|
|
140
|
+
// Whole block appended by the wizard: cut only up to the SECTION_TAIL line and keep everything after it.
|
|
141
|
+
// If the tail is missing (the user deleted that line), or is found too far away for a wizard block
|
|
142
|
+
// (the user copied the same text elsewhere in the document), user content in between could be deleted,
|
|
143
|
+
// and we cannot be sure where the "wizard section" ends, so give up safely.
|
|
144
|
+
// The wizard block stays within a few hundred bytes even with a long version string.
|
|
84
145
|
const MAX_SECTION_LENGTH = 300;
|
|
85
|
-
|
|
146
|
+
let tailIdx = -1;
|
|
147
|
+
let tailLen = 0;
|
|
148
|
+
for (const tail of sectionTails()) {
|
|
149
|
+
const i = content.indexOf(tail, idx);
|
|
150
|
+
if (i !== -1 && (tailIdx === -1 || i < tailIdx)) { tailIdx = i; tailLen = tail.length; }
|
|
151
|
+
}
|
|
86
152
|
if (tailIdx === -1 || tailIdx > idx + MAX_SECTION_LENGTH) return "skip-unexpected-format";
|
|
87
|
-
const cutEnd = tailIdx +
|
|
153
|
+
const cutEnd = tailIdx + tailLen;
|
|
88
154
|
writeFileSync(p, content.slice(0, idx) + content.slice(cutEnd));
|
|
89
155
|
return "removed";
|
|
90
156
|
}
|
|
91
157
|
|
|
92
158
|
const markerIdx = content.indexOf(MARKER_LINE);
|
|
93
159
|
if (markerIdx !== -1) {
|
|
94
|
-
// CI
|
|
160
|
+
// The CI inserted only the marker above the user's existing version line; leave the version line alone.
|
|
95
161
|
writeFileSync(p, content.slice(0, markerIdx) + content.slice(markerIdx + MARKER_LINE.length));
|
|
96
162
|
return "removed";
|
|
97
163
|
}
|
package/src/core/copy/simple.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Simple copy functions (unconditional overwrite).
|
|
2
|
+
// Single source of truth: scripts in payload/scripts/*.py are installed to .github/scripts/ in the user repo.
|
|
3
|
+
// All workflows call this path (python3 .github/scripts/*.py), so a missing script breaks the install at runtime.
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { chmodSync, readFileSync, readdirSync, rmSync, rmdirSync } from "node:fs";
|
|
6
6
|
import { PATHS, PAYLOAD } from "../paths.js";
|
|
7
7
|
import { exists, copyFileSync } from "../fsutil.js";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// messages.py is the shared message catalog the other scripts and the workflows import/call.
|
|
10
|
+
export const SCRIPT_NAMES = ["version_manager.py", "changelog_manager.py", "truncate_release_notes.py", "issue_helper.py", "messages.py"];
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
12
|
+
// Computes what will happen per file (writes nothing), so the real copy and --dry-run share one decision.
|
|
13
|
+
// Returns: [{ name, action: "create" | "overwrite" | "unchanged" }]
|
|
13
14
|
export function planScripts(payloadRoot, targetRoot = ".") {
|
|
14
15
|
const out = [];
|
|
15
16
|
for (const name of SCRIPT_NAMES) {
|
|
@@ -23,22 +24,22 @@ export function planScripts(payloadRoot, targetRoot = ".") {
|
|
|
23
24
|
return out;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
27
|
+
// Scripts are always overwritten with the payload version (+chmod, meaningless but harmless on Windows).
|
|
28
|
+
// User-edited scripts are overwritten too, so per-file results are returned for the caller to log.
|
|
28
29
|
export function copyScripts(payloadRoot, targetRoot = ".") {
|
|
29
30
|
const results = planScripts(payloadRoot, targetRoot);
|
|
30
31
|
for (const { name } of results) {
|
|
31
32
|
const dst = join(targetRoot, PATHS.scriptsDir, name);
|
|
32
33
|
copyFileSync(join(payloadRoot, PAYLOAD.scriptsDir, name), dst);
|
|
33
|
-
try { chmodSync(dst, 0o755); } catch { /*
|
|
34
|
+
try { chmodSync(dst, 0o755); } catch { /* chmod is meaningless on Windows etc. */ }
|
|
34
35
|
}
|
|
35
36
|
return results;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
39
|
+
// Older workflow versions committed bytecode left by script imports into bot commits.
|
|
40
|
+
// Generation is now prevented, but already committed pyc files do not vanish on update, so they are removed here.
|
|
41
|
+
// Scope is only the pyc files in the wizard scripts folder and its __pycache__; no other path is touched.
|
|
42
|
+
// Returns paths (relative to targetRoot) of files to remove (removed); the user commits the deletion with the installed files.
|
|
42
43
|
export function planScriptBytecode(targetRoot = ".") {
|
|
43
44
|
const base = join(targetRoot, PATHS.scriptsDir);
|
|
44
45
|
const list = (rel) => {
|
|
@@ -55,8 +56,8 @@ export function removeScriptBytecode(targetRoot = ".") {
|
|
|
55
56
|
const removed = planScriptBytecode(targetRoot);
|
|
56
57
|
for (const rel of removed) rmSync(join(targetRoot, rel), { force: true });
|
|
57
58
|
if (removed.some((rel) => rel.includes("/__pycache__/"))) {
|
|
58
|
-
//
|
|
59
|
-
try { rmdirSync(join(targetRoot, PATHS.scriptsDir, "__pycache__")); } catch { /*
|
|
59
|
+
// remove only when empty; leave it if other files exist
|
|
60
|
+
try { rmdirSync(join(targetRoot, PATHS.scriptsDir, "__pycache__")); } catch { /* not empty */ }
|
|
60
61
|
}
|
|
61
62
|
return removed;
|
|
62
63
|
}
|