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,37 +1,38 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Per-type project path detection and confirmation. In a monorepo, decides which folder holds each
|
|
2
|
+
// type's version file using a 5-step priority.
|
|
3
3
|
//
|
|
4
|
-
// io
|
|
4
|
+
// io injection contract (same as the readline-engine signatures):
|
|
5
5
|
// io.select({message, options:[{value,label}]}) → value | CANCEL(symbol)
|
|
6
6
|
// io.text({message, defaultValue}) → string | CANCEL
|
|
7
7
|
// io.confirm({message, initialValue}) → bool | CANCEL
|
|
8
|
-
// io.log(line) →
|
|
8
|
+
// io.log(line) → notice output (stderr when absent)
|
|
9
9
|
import { existsSync, readdirSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { markerForType as baseMarkerForType, resolveMarker } from "./detect.js";
|
|
12
12
|
import { TYPES, typeInfo } from "./types.js";
|
|
13
13
|
import { normalizePath, isRepoRelativePath } from "./paths.js";
|
|
14
14
|
import { CliError } from "./errors.js";
|
|
15
|
+
import { t as tr } from "../i18n/index.js";
|
|
15
16
|
|
|
16
|
-
//
|
|
17
|
+
// Cancel (ESC) is the CANCEL symbol (the engine aborts on Ctrl+C with an exception); judged only by being a symbol, without importing ui (avoids a core-to-ui back reference).
|
|
17
18
|
const isCancel = (v) => typeof v === "symbol";
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
// detect.js
|
|
20
|
+
// A type's representative marker file name.
|
|
21
|
+
// detect.js returns package.json by default for unknown types, but path search must tell marker-less types (basic etc.) apart with an empty string, hence the wrapper.
|
|
21
22
|
const KNOWN_MARKER_TYPES = new Set(TYPES.filter((t) => t.markers.length).map((t) => t.id));
|
|
22
23
|
export function markerForType(type) {
|
|
23
24
|
return KNOWN_MARKER_TYPES.has(type) ? baseMarkerForType(type) : "";
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
-
// (
|
|
27
|
+
// Returns the marker file name that really exists in a directory: the fs-driven version of resolveMarker.
|
|
28
|
+
// (including secondary markers: spring build.gradle/.kts/pom.xml, python pyproject/setup.py/requirements.txt)
|
|
28
29
|
export function existingMarkerInDir(type, dir) {
|
|
29
|
-
if (!markerForType(type)) return ""; //
|
|
30
|
+
if (!markerForType(type)) return ""; // empty string for unknown types
|
|
30
31
|
return resolveMarker(type, (n) => existsSync(join(dir, n)));
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
//
|
|
34
|
-
// find
|
|
34
|
+
// Recursive file search with maxdepth 3: collects the relative "directory" path of matching files (root is ".").
|
|
35
|
+
// find's maxdepth counts file path components (./a/b/f = depth 3), so it is computed the same way.
|
|
35
36
|
function walkFindDirs(root, { prune, match, maxDepth = 3 }) {
|
|
36
37
|
const hits = [];
|
|
37
38
|
const walk = (rel, depth) => {
|
|
@@ -41,37 +42,37 @@ function walkFindDirs(root, { prune, match, maxDepth = 3 }) {
|
|
|
41
42
|
const childDepth = depth + 1;
|
|
42
43
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
43
44
|
if (e.isDirectory()) {
|
|
44
|
-
// prune
|
|
45
|
+
// prune folders are excluded along with everything below them
|
|
45
46
|
if (prune.has(e.name)) continue;
|
|
46
|
-
//
|
|
47
|
+
// descend only when child files fall within depth <= maxDepth
|
|
47
48
|
if (childDepth < maxDepth) walk(childRel, childDepth);
|
|
48
|
-
// childDepth === maxDepth
|
|
49
|
-
else if (childDepth === maxDepth) { /*
|
|
49
|
+
// files inside a directory at childDepth === maxDepth are at depth maxDepth+1, which find does not see
|
|
50
|
+
else if (childDepth === maxDepth) { /* files count only up to maxDepth; no need to descend into directories */ }
|
|
50
51
|
} else if (childDepth <= maxDepth && match(e.name)) {
|
|
51
52
|
hits.push(rel === "" ? "." : rel);
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
};
|
|
55
56
|
walk("", 0);
|
|
56
|
-
return [...new Set(hits)].sort(); //
|
|
57
|
+
return [...new Set(hits)].sort(); // dedupe + sort
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
60
|
+
// Marker-file candidate search per type.
|
|
61
|
+
// Returns an array of candidate directory relative paths (root is ".").
|
|
61
62
|
export function findTypePathCandidates(root, type) {
|
|
62
|
-
//
|
|
63
|
-
// version_manager
|
|
64
|
-
//
|
|
63
|
+
// -- Spring multi-module: the settings.gradle(.kts) folder collapses to the module root --
|
|
64
|
+
// version_manager updates every build.gradle beneath that folder, so submodules are not expanded.
|
|
65
|
+
// The settings.gradle under android/ (Flutter/RN) is not spring, so it is pruned.
|
|
65
66
|
if (type === "spring") {
|
|
66
67
|
const mm = walkFindDirs(root, {
|
|
67
68
|
prune: new Set(["node_modules", ".git", "build", "dist", ".gradle", "android", "ios"]),
|
|
68
69
|
match: (n) => n === "settings.gradle" || n === "settings.gradle.kts",
|
|
69
70
|
});
|
|
70
71
|
if (mm.length) return mm;
|
|
71
|
-
// settings.gradle
|
|
72
|
+
// no settings.gradle: single module, fall back to build.gradle below
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
//
|
|
75
|
+
// The marker order in the registry is the priority (representative file first).
|
|
75
76
|
const names = typeInfo(type)?.markers;
|
|
76
77
|
if (!names?.length) return [];
|
|
77
78
|
|
|
@@ -79,7 +80,7 @@ export function findTypePathCandidates(root, type) {
|
|
|
79
80
|
"node_modules", ".git", "build", "dist", ".dart_tool", "android", "ios",
|
|
80
81
|
".gradle", "venv", ".venv", "__pycache__",
|
|
81
82
|
]);
|
|
82
|
-
//
|
|
83
|
+
// If found with a higher-priority marker, use only that
|
|
83
84
|
let found = [];
|
|
84
85
|
for (const n of names) {
|
|
85
86
|
found = walkFindDirs(root, { prune, match: (name) => name === n });
|
|
@@ -88,43 +89,43 @@ export function findTypePathCandidates(root, type) {
|
|
|
88
89
|
|
|
89
90
|
return found.filter((d) => {
|
|
90
91
|
if (type === "flutter") {
|
|
91
|
-
// example/
|
|
92
|
+
// exclude example/ and require a sibling lib/ to avoid false positives
|
|
92
93
|
if (d.includes("example")) return false;
|
|
93
94
|
const libDir = d === "." ? join(root, "lib") : join(root, d, "lib");
|
|
94
95
|
if (!existsSync(libDir)) return false;
|
|
95
96
|
}
|
|
96
97
|
if (type === "spring") {
|
|
97
|
-
// Flutter/RN
|
|
98
|
+
// exclude the false positive of Flutter/RN's android/build.gradle
|
|
98
99
|
if (d.includes("android")) return false;
|
|
99
100
|
}
|
|
100
101
|
return true;
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
//
|
|
105
|
-
// (5
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
105
|
+
// Detects and confirms the paths of all selected types into a Map<type,path>
|
|
106
|
+
// (5-step priority).
|
|
107
|
+
// 1. already in paths (--paths): kept
|
|
108
|
+
// 2. marker exists at the root: "." automatically
|
|
109
|
+
// 3. existingPaths (value saved in version.yml)
|
|
110
|
+
// 4. candidate scan
|
|
111
|
+
// 5. branch - non-interactive: existing value, then a single candidate, then error / interactive: confirm, select, enter manually
|
|
111
112
|
export async function resolveProjectPaths({
|
|
112
113
|
root, types = [], paths = new Map(), existingPaths = new Map(),
|
|
113
114
|
force = false, tty = true, io = {},
|
|
114
115
|
}) {
|
|
115
116
|
const say = io.log || ((m) => process.stderr.write(`${m}\n`));
|
|
116
|
-
const result = new Map(paths); // --paths
|
|
117
|
-
const targets = types.filter((t) => t !== "basic"); // basic
|
|
117
|
+
const result = new Map(paths); // keep the pre-set --paths values (the caller's Map is left unchanged)
|
|
118
|
+
const targets = types.filter((t) => t !== "basic"); // basic needs no path
|
|
118
119
|
if (targets.length === 0) return result;
|
|
119
120
|
|
|
120
121
|
const total = targets.length;
|
|
121
|
-
//
|
|
122
|
+
// -- Intro notice (detection result + what will happen) --
|
|
122
123
|
say("");
|
|
123
|
-
if (total > 1) say(
|
|
124
|
-
else say(
|
|
124
|
+
if (total > 1) say(tr("core.paths.intro.multi", { total }));
|
|
125
|
+
else say(tr("core.paths.intro.single", { type: targets[0] }));
|
|
125
126
|
for (const t of targets) say(` • ${t.padEnd(8)} → ${existingMarkerInDir(t, root)}`);
|
|
126
127
|
say("");
|
|
127
|
-
say(
|
|
128
|
+
say(tr("core.paths.intro.rootDef"));
|
|
128
129
|
say("");
|
|
129
130
|
|
|
130
131
|
let idx = 0;
|
|
@@ -132,133 +133,134 @@ export async function resolveProjectPaths({
|
|
|
132
133
|
idx += 1;
|
|
133
134
|
const prog = `[${idx}/${total}]`;
|
|
134
135
|
|
|
135
|
-
//
|
|
136
|
+
// 1. already given via --paths etc.: highest priority
|
|
136
137
|
if (result.get(t)) {
|
|
137
138
|
const p = result.get(t);
|
|
138
139
|
if (!existsSync(join(root, p))) {
|
|
139
|
-
throw new CliError(
|
|
140
|
+
throw new CliError(tr("core.paths.err.pathMissing", { type: t, path: p }));
|
|
140
141
|
}
|
|
141
|
-
say(
|
|
142
|
-
//
|
|
142
|
+
say(tr("core.paths.say.explicit", { type: t, path: p }));
|
|
143
|
+
// Not blocking (some setups work without the marker); this only flags a typo that pointed to the wrong folder.
|
|
143
144
|
const pm = existingMarkerInDir(t, join(root, p));
|
|
144
145
|
if (pm && !existsSync(join(root, p, pm))) {
|
|
145
|
-
say(
|
|
146
|
+
say(tr("core.paths.warn.noMarker", { path: p, type: t, marker: pm }));
|
|
146
147
|
}
|
|
147
148
|
continue;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
//
|
|
151
|
+
// 2. marker exists at the root: "." confirmed automatically (including secondary markers)
|
|
151
152
|
const rootMarker = existingMarkerInDir(t, root);
|
|
152
153
|
if (rootMarker && existsSync(join(root, rootMarker))) {
|
|
153
154
|
result.set(t, ".");
|
|
154
|
-
say(
|
|
155
|
+
say(tr("core.paths.say.rootMarker", { type: t, marker: rootMarker }));
|
|
155
156
|
continue;
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
//
|
|
159
|
+
// 3. value saved in the existing version.yml: default suggestion
|
|
159
160
|
const existing = existingPaths.get(t) || "";
|
|
160
161
|
|
|
161
|
-
//
|
|
162
|
+
// 4. candidate search
|
|
162
163
|
const candidates = findTypePathCandidates(root, t);
|
|
163
164
|
let chosen = "";
|
|
164
165
|
|
|
165
|
-
//
|
|
166
|
+
// -- 5a. non-interactive (--force or no TTY; root fallback intentionally excluded) --
|
|
166
167
|
if (force || !tty) {
|
|
167
168
|
if (existing) {
|
|
168
169
|
chosen = existing;
|
|
169
|
-
say(
|
|
170
|
+
say(tr("core.paths.say.keepExisting", { type: t, path: chosen }));
|
|
170
171
|
} else if (candidates.length === 1) {
|
|
171
172
|
chosen = candidates[0];
|
|
172
|
-
say(
|
|
173
|
+
say(tr("core.paths.say.auto", { type: t, path: chosen }));
|
|
173
174
|
} else if (candidates.length === 0) {
|
|
174
|
-
throw new CliError(
|
|
175
|
+
throw new CliError(tr("core.paths.err.none", { type: t }));
|
|
175
176
|
} else {
|
|
176
|
-
throw new CliError(
|
|
177
|
+
throw new CliError(tr("core.paths.err.ambiguous", { type: t, count: candidates.length, list: candidates.join(", ") }));
|
|
177
178
|
}
|
|
178
179
|
result.set(t, chosen);
|
|
179
180
|
continue;
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
//
|
|
183
|
+
// -- 5b. interactive: branch by candidate count --
|
|
183
184
|
if (candidates.length === 1) {
|
|
184
185
|
const cand = candidates[0];
|
|
185
186
|
const candMarker = existingMarkerInDir(t, cand === "." ? root : join(root, cand));
|
|
186
187
|
const candFull = cand === "." ? candMarker : `${cand}/${candMarker}`;
|
|
187
188
|
say("");
|
|
188
|
-
say(
|
|
189
|
-
say(
|
|
190
|
-
// '
|
|
189
|
+
say(tr("core.paths.found.one", { prog, type: t, marker: candMarker }));
|
|
190
|
+
say(tr("core.paths.found.location", { file: candFull }));
|
|
191
|
+
// On 'No'/cancel chosen stays unset, falling through to the manual-entry loop below
|
|
191
192
|
const ok = await io.confirm({
|
|
192
|
-
message:
|
|
193
|
+
message: tr("core.paths.confirm.candidate", { type: t, cand, file: candFull }),
|
|
193
194
|
initialValue: true,
|
|
194
195
|
});
|
|
195
196
|
if (ok === true) chosen = cand;
|
|
196
197
|
} else if (candidates.length > 1) {
|
|
197
198
|
say("");
|
|
198
|
-
say(
|
|
199
|
-
//
|
|
199
|
+
say(tr("core.paths.found.many", { prog, type: t, count: candidates.length }));
|
|
200
|
+
// Candidates + a 'manual entry' menu: the value itself is the display text (avoids exposing a sentinel)
|
|
201
|
+
const manual = tr("core.paths.manualInput");
|
|
200
202
|
const options = candidates.map((c) => ({
|
|
201
203
|
value: c,
|
|
202
204
|
label: `${c} (${existingMarkerInDir(t, c === "." ? root : join(root, c))})`,
|
|
203
205
|
}));
|
|
204
|
-
options.push({ value:
|
|
205
|
-
const sel = await io.select({ message:
|
|
206
|
-
// ESC(
|
|
207
|
-
if (!isCancel(sel) && sel != null && sel !==
|
|
206
|
+
options.push({ value: manual, label: manual });
|
|
207
|
+
const sel = await io.select({ message: tr("core.paths.select.root", { type: t }), options });
|
|
208
|
+
// ESC (cancel) also falls back to manual entry
|
|
209
|
+
if (!isCancel(sel) && sel != null && sel !== manual) chosen = sel;
|
|
208
210
|
} else {
|
|
209
211
|
say("");
|
|
210
|
-
say(
|
|
212
|
+
say(tr("core.paths.found.none", { prog, type: t }));
|
|
211
213
|
}
|
|
212
214
|
|
|
213
|
-
//
|
|
215
|
+
// -- Manual-entry loop (when still undecided above) --
|
|
214
216
|
while (!chosen) {
|
|
215
217
|
const hintMarker = existingMarkerInDir(t, root);
|
|
216
|
-
let prompt =
|
|
217
|
-
if (existing) prompt +=
|
|
218
|
+
let prompt = tr("core.paths.prompt.base", { type: t, marker: hintMarker });
|
|
219
|
+
if (existing) prompt += tr("core.paths.prompt.current", { existing });
|
|
218
220
|
prompt += "): ";
|
|
219
221
|
let input = await io.text({ message: prompt, defaultValue: "" });
|
|
220
|
-
if (isCancel(input) || input == null) input = ""; // ESC
|
|
222
|
+
if (isCancel(input) || input == null) input = ""; // ESC becomes empty (falls back below)
|
|
221
223
|
input = String(input).trim();
|
|
222
|
-
//
|
|
224
|
+
// Empty means the existing value or the root; decided before normalizePath
|
|
223
225
|
input = input === "" ? (existing || ".") : normalizePath(input);
|
|
224
226
|
if (!isRepoRelativePath(input)) {
|
|
225
|
-
say(
|
|
227
|
+
say(tr("core.paths.warn.outsideRepo", { input }));
|
|
226
228
|
continue;
|
|
227
229
|
}
|
|
228
|
-
//
|
|
230
|
+
// Validation: confirm a marker exists at the entered path (including secondary markers)
|
|
229
231
|
const m = existingMarkerInDir(t, input === "." ? root : join(root, input));
|
|
230
232
|
if (m && existsSync(join(root, input === "." ? "" : input, m))) {
|
|
231
233
|
chosen = input;
|
|
232
234
|
} else {
|
|
233
|
-
say(
|
|
234
|
-
//
|
|
235
|
-
// (
|
|
236
|
-
//
|
|
237
|
-
const forceOk = await io.confirm({ message: "
|
|
235
|
+
say(tr("core.paths.warn.noFile", { input, marker: m }));
|
|
236
|
+
// Default (Enter) and ESC keep this path as is. With 'No' as the default, a type whose marker does not exist yet
|
|
237
|
+
// (e.g. added before the project is created) could not be exited with Enter alone.
|
|
238
|
+
// To enter it again, explicitly choose 'No'. The path can be fixed later in version.yml project_paths.
|
|
239
|
+
const forceOk = await io.confirm({ message: tr("core.paths.confirm.useAnyway"), initialValue: true });
|
|
238
240
|
if (forceOk !== false) chosen = input;
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
result.set(t, chosen);
|
|
243
|
-
say(
|
|
245
|
+
say(tr("core.paths.say.chosen", { type: t, chosen }));
|
|
244
246
|
}
|
|
245
247
|
|
|
246
|
-
//
|
|
248
|
+
// -- Summary + duplicate-marker-file warning --
|
|
247
249
|
say("");
|
|
248
|
-
say("
|
|
249
|
-
const fileToTypes = new Map(); //
|
|
250
|
+
say(tr("core.paths.summary.title"));
|
|
251
|
+
const fileToTypes = new Map(); // marker file relative path to the types using that file
|
|
250
252
|
for (const [pt, pp] of result) {
|
|
251
253
|
const m = existingMarkerInDir(pt, pp === "." ? root : join(root, pp));
|
|
252
254
|
const file = pp === "." ? m : `${pp}/${m}`;
|
|
253
|
-
say(
|
|
255
|
+
say(tr("core.paths.summary.row", { type: pt, file }));
|
|
254
256
|
if (!fileToTypes.has(file)) fileToTypes.set(file, []);
|
|
255
257
|
fileToTypes.get(file).push(pt);
|
|
256
258
|
}
|
|
257
259
|
for (const [file, ts] of fileToTypes) {
|
|
258
260
|
if (ts.length > 1) {
|
|
259
|
-
//
|
|
260
|
-
say(
|
|
261
|
-
say("
|
|
261
|
+
// Idempotent behavior, so only a warning rather than a block
|
|
262
|
+
say(tr("core.paths.dup.warn", { file, types: ts.join(" ") }));
|
|
263
|
+
say(tr("core.paths.dup.note"));
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
say("");
|
package/src/core/paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Path constants: install target (user repo) paths plus the payload's internal layout.
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
export const PATHS = {
|
|
@@ -7,15 +7,15 @@ export const PATHS = {
|
|
|
7
7
|
scriptsDir: ".github/scripts",
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
// payload/
|
|
10
|
+
// Layout inside payload/ (payload is the single source of truth)
|
|
11
11
|
export const PAYLOAD = {
|
|
12
12
|
workflowsDir: "workflows", // payload/workflows/{common,spring,flutter,...}
|
|
13
13
|
scriptsDir: "scripts", // payload/scripts/*.py
|
|
14
|
-
configDir: "config", // payload/config/wizard-prompts.yml
|
|
14
|
+
configDir: "config", // payload/config/wizard-prompts.yml etc. (for the wizard runtime)
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
// Source workflow folders of one type [directly under the type, under server-deploy]; callers check existence.
|
|
18
|
+
// Install, conflict scan, preview and deploy-style detection must all see the same folder layout so their results do not diverge.
|
|
19
19
|
export function typeWorkflowDirs(payloadRoot, type) {
|
|
20
20
|
const typeDir = join(payloadRoot, PAYLOAD.workflowsDir, type);
|
|
21
21
|
return [typeDir, join(typeDir, "server-deploy")];
|
|
@@ -24,17 +24,17 @@ export function typeWorkflowDirs(payloadRoot, type) {
|
|
|
24
24
|
export const WORKFLOW_PREFIX = "PROJECT";
|
|
25
25
|
export const WORKFLOW_COMMON_PREFIX = "PROJECT-COMMON";
|
|
26
26
|
|
|
27
|
-
//
|
|
27
|
+
// Path normalization: strips surrounding whitespace, converts \ to /, removes a trailing / and a leading ./, empty becomes "."
|
|
28
28
|
export function normalizePath(p) {
|
|
29
29
|
let s = String(p).trim();
|
|
30
30
|
s = s.replace(/\\/g, "/");
|
|
31
|
-
s = s.replace(/\/+$/, ""); //
|
|
32
|
-
s = s.replace(/^\.\//, ""); //
|
|
31
|
+
s = s.replace(/\/+$/, ""); // trailing /
|
|
32
|
+
s = s.replace(/^\.\//, ""); // leading ./
|
|
33
33
|
return s === "" ? "." : s;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// Whether a normalized path stays inside the repo (relative, no '..'). A path pointing outside would create the
|
|
37
|
+
// installed files (fastlane etc.) in a neighboring repo, and the workflow paths filter would not work either.
|
|
38
38
|
export function isRepoRelativePath(p) {
|
|
39
39
|
if (/^\//.test(p) || /^[A-Za-z]:/.test(p)) return false;
|
|
40
40
|
return !p.split("/").includes("..");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Value-decision rules for the release options (semver_auto, copilot_ai), shared by non-interactive (index.js) and interactive (interactive.js) modes.
|
|
2
|
+
// Priority: explicit value (CLI flag, prompt answer), then the value saved in version.yml, then the default.
|
|
3
|
+
// Keeping separate copies per path would install the same repo with different settings depending on how it was run.
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Reflects only explicit and saved values. null means not decided yet (interactive mode uses it to decide whether to ask).
|
|
6
6
|
export function pickReleaseOptions(explicit = {}, existing = null) {
|
|
7
7
|
return {
|
|
8
8
|
semverAuto: explicit.semverAuto ?? existing?.options?.semverAuto ?? null,
|
|
@@ -10,10 +10,10 @@ export function pickReleaseOptions(explicit = {}, existing = null) {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
// semver_auto:
|
|
15
|
-
//
|
|
16
|
-
// copilot_ai:
|
|
13
|
+
// Final values: fills undecided options with defaults.
|
|
14
|
+
// semver_auto: true only for fresh installs. If an existing install whose version.yml lacked the key (installed
|
|
15
|
+
// before the feature existed) were switched on silently, one ambiguous commit could promote a major bump, so it stays false.
|
|
16
|
+
// copilot_ai: an opt-in that consumes AI credits, so it is false for both new and existing installs unless stated.
|
|
17
17
|
export function resolveReleaseOptions(explicit = {}, existing = null) {
|
|
18
18
|
const { semverAuto, copilotAi } = pickReleaseOptions(explicit, existing);
|
|
19
19
|
return {
|
package/src/core/removal-exec.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// uninstall
|
|
2
|
-
//
|
|
1
|
+
// Shared removal executor for uninstall and purge - the two commands only turn options into a removal plan,
|
|
2
|
+
// and the actual deletion and logging happen here in one place. Target detection is done by removal-plan.js (read-only).
|
|
3
3
|
import { join, posix } from "node:path";
|
|
4
4
|
import { readdirSync, rmdirSync } from "node:fs";
|
|
5
5
|
import { PATHS } from "./paths.js";
|
|
@@ -7,9 +7,10 @@ import { remove } from "./fsutil.js";
|
|
|
7
7
|
import { removeVersionSectionFromReadme } from "./copy/readme.js";
|
|
8
8
|
import { removeAutoAddedEntriesFromGitignore } from "./copy/gitignore.js";
|
|
9
9
|
import { logRemovals } from "./logger.js";
|
|
10
|
+
import { t } from "../i18n/index.js";
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
12
|
+
// After deleting files, clean up the parent folders that became empty, up to just below the repo root (fastlane/ etc. made by the wizard).
|
|
13
|
+
// It stops at the first non-empty folder, so folders still holding user files are left alone.
|
|
13
14
|
export function pruneEmptyDirs(targetRoot, relDir) {
|
|
14
15
|
let rel = relDir;
|
|
15
16
|
while (rel && rel !== "." && rel !== "/") {
|
|
@@ -22,8 +23,8 @@ export function pruneEmptyDirs(targetRoot, relDir) {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
// Flutter
|
|
26
|
-
//
|
|
26
|
+
// Remove Flutter app files - only unmodified ones enter the plan, so delete them as-is and tidy empty folders.
|
|
27
|
+
// Logs are written all at once after deletion finishes, so entries to record are collected in done.
|
|
27
28
|
export function removeAppFiles(targetRoot, appFiles, done = []) {
|
|
28
29
|
for (const rel of appFiles) {
|
|
29
30
|
remove(join(targetRoot, rel));
|
|
@@ -32,17 +33,17 @@ export function removeAppFiles(targetRoot, appFiles, done = []) {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
36
|
+
// Clean up .github/workflows and .github/scripts (and .github) that became empty after removing installed files.
|
|
37
|
+
// Only when something was deleted from that folder this run - folders that were empty to begin with are not touched.
|
|
37
38
|
export function pruneInstallDirs(targetRoot, plan) {
|
|
38
39
|
if (plan.workflows.length || plan.baseline?.length) pruneEmptyDirs(targetRoot, PATHS.workflowsDir);
|
|
39
40
|
if (plan.scripts.length) pruneEmptyDirs(targetRoot, PATHS.scriptsDir);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
// plan: { workflows, scripts, appFiles, baseline, versionYml, readme, gitignore, changelog }
|
|
43
|
-
// options.logOrder:
|
|
44
|
-
// options.gitignoreDetail(status): .gitignore
|
|
45
|
-
//
|
|
43
|
+
// plan: { workflows, scripts, appFiles, baseline, versionYml, readme, gitignore, changelog } - each is a "to delete" flag/list.
|
|
44
|
+
// options.logOrder: record order of the trailing entries (version, readme, gitignore, changelog) - each command keeps its existing order.
|
|
45
|
+
// options.gitignoreDetail(status): the .gitignore log text. Returning null means nothing is recorded.
|
|
46
|
+
// Returns: what was actually removed - readme/gitignore may be safely given up (skip-*) even if the plan targeted them, so the actual result overrides.
|
|
46
47
|
export function executeRemoval(targetRoot, plan, { logOrder, gitignoreDetail }) {
|
|
47
48
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
48
49
|
const done = [];
|
|
@@ -58,7 +59,7 @@ export function executeRemoval(targetRoot, plan, { logOrder, gitignoreDetail })
|
|
|
58
59
|
const gitignore = gitignoreStatus === "removed" || gitignoreStatus === "file-deleted";
|
|
59
60
|
if (plan.versionYml) { remove(join(targetRoot, PATHS.versionFile)); entries.version.push(["remove", "version", PATHS.versionFile]); }
|
|
60
61
|
for (const f of plan.changelog || []) { remove(join(targetRoot, f)); entries.changelog.push(["remove", "changelog", f]); }
|
|
61
|
-
if (readme) entries.readme.push(["remove", "readme", "
|
|
62
|
+
if (readme) entries.readme.push(["remove", "readme", t("core.removalExec.readmeSection")]);
|
|
62
63
|
const detail = gitignoreStatus && gitignoreDetail(gitignoreStatus);
|
|
63
64
|
if (detail) entries.gitignore.push(["remove", "gitignore", detail]);
|
|
64
65
|
for (const k of logOrder) done.push(...entries[k]);
|