project-auto-wizard 0.15.0 → 0.15.1
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 +1 -1
- package/README.ko.md +1 -1
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/payload/config/breaking-changes.json +31 -5
- package/payload/config/wizard-prompts.yml +0 -4
- package/payload/scripts/messages.py +156 -26
- package/payload/scripts/version_manager.py +69 -40
- package/payload/version.yml.template +2 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +3 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +15 -1
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +61 -12
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +46 -10
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +40 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +24 -8
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +29 -10
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +57 -23
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +155 -72
- package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +49 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +162 -62
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +184 -87
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +38 -15
- package/payload/workflows/go/PROJECT-GO-CI.yaml +14 -5
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +375 -120
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +9 -4
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +12 -4
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +375 -120
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +9 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +38 -9
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +19 -5
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +71 -35
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +9 -6
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +7 -4
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +425 -144
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +7 -4
- package/src/cli/args.js +17 -7
- package/src/commands/doctor.js +11 -0
- package/src/commands/dry-run.js +9 -1
- package/src/commands/full.js +5 -2
- package/src/commands/interactive.js +4 -1
- package/src/commands/status.js +14 -1
- package/src/core/copy/simple.js +31 -0
- package/src/core/copy/workflows.js +2 -2
- package/src/core/deploy-style.js +1 -1
- package/src/core/detect.js +3 -2
- package/src/core/types.js +15 -8
- package/src/core/version-yml.js +52 -6
- package/src/i18n/catalog/en/cli.js +1 -0
- package/src/i18n/catalog/en/commands.js +8 -0
- package/src/i18n/catalog/en/core2.js +3 -0
- package/src/i18n/catalog/ko/cli.js +1 -0
- package/src/i18n/catalog/ko/commands.js +8 -0
- package/src/i18n/catalog/ko/core2.js +3 -0
- package/src/index.js +4 -1
- package/src/ui/env-plan.js +1 -1
- package/src/ui/prompts.js +7 -3
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +0 -241
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +0 -330
|
@@ -132,6 +132,7 @@ jobs:
|
|
|
132
132
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
133
133
|
DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
|
|
134
134
|
run: |
|
|
135
|
+
[ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
|
|
135
136
|
m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
|
|
136
137
|
MISSING=""
|
|
137
138
|
for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
|
|
@@ -172,6 +173,7 @@ jobs:
|
|
|
172
173
|
# write it to a file in the resources directory
|
|
173
174
|
- name: Generate application-prod.yml
|
|
174
175
|
run: |
|
|
176
|
+
[ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
|
|
175
177
|
m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
|
|
176
178
|
mkdir -p ${{ env.APPLICATION_YML_DIR }}
|
|
177
179
|
|
|
@@ -187,10 +189,10 @@ jobs:
|
|
|
187
189
|
run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
|
|
188
190
|
|
|
189
191
|
- name: Set up Docker Buildx
|
|
190
|
-
uses: docker/setup-buildx-action@
|
|
192
|
+
uses: docker/setup-buildx-action@v4
|
|
191
193
|
|
|
192
194
|
- name: Log in to DockerHub
|
|
193
|
-
uses: docker/login-action@
|
|
195
|
+
uses: docker/login-action@v4
|
|
194
196
|
with:
|
|
195
197
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
196
198
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
@@ -205,7 +207,7 @@ jobs:
|
|
|
205
207
|
|
|
206
208
|
# Use the branch name as the image tag to manage images per environment
|
|
207
209
|
- name: Build and push Docker image
|
|
208
|
-
uses: docker/build-push-action@
|
|
210
|
+
uses: docker/build-push-action@v7
|
|
209
211
|
with:
|
|
210
212
|
context: ${{ env.PROJECT_PATH }}
|
|
211
213
|
file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
|
|
@@ -238,6 +240,7 @@ jobs:
|
|
|
238
240
|
|
|
239
241
|
- name: Load messages
|
|
240
242
|
run: |
|
|
243
|
+
[ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
|
|
241
244
|
MSGS="$(python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" dump wf_spring_deploy. \
|
|
242
245
|
| python3 -c 'import json, shlex, sys; d = json.load(sys.stdin); print("\n".join("M_%s=%s" % (k.replace(".", "_"), shlex.quote(v)) for k, v in d.items()))' \
|
|
243
246
|
| base64 | tr -d '\n')"
|
|
@@ -429,7 +432,7 @@ jobs:
|
|
|
429
432
|
m simple_summary_branch branch="$BRANCH"
|
|
430
433
|
m simple_summary_container name="$CONTAINER_NAME"
|
|
431
434
|
m simple_summary_port port="$PORT"
|
|
432
|
-
m simple_summary_time time="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
435
|
+
m simple_summary_time time="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
|
433
436
|
echo ""
|
|
434
437
|
m simple_access_url url="http://${{ secrets.SERVER_HOST }}:${PORT}"
|
|
435
438
|
|
package/src/cli/args.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { VALID_TYPES, VALID_MODES } from "../context.js";
|
|
3
3
|
import { DEPLOY_STYLES, isDeployStyle, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
4
4
|
import { isValidBranchName } from "../core/branches.js";
|
|
5
|
-
import { TYPES } from "../core/types.js";
|
|
5
|
+
import { TYPES, canonicalTypeId } from "../core/types.js";
|
|
6
6
|
import { CliError } from "../core/errors.js";
|
|
7
7
|
import { normalizePath, isRepoRelativePath } from "../core/paths.js";
|
|
8
8
|
import { t, DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES, isSupportedLanguage, normalizeLanguage } from "../i18n/index.js";
|
|
@@ -108,12 +108,15 @@ export function parseArgs(argv) {
|
|
|
108
108
|
for (let name of csv.split(",")) {
|
|
109
109
|
name = name.replace(/\s/g, "");
|
|
110
110
|
if (name === "") continue;
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
// The alias is resolved here, before validation and dedup (--type react,next is just react).
|
|
112
|
+
// Only registry ids are valid, so an unsupported name is reported as typed.
|
|
113
|
+
const id = canonicalTypeId(name);
|
|
114
|
+
if (seen.has(id)) continue; // dedup
|
|
115
|
+
if (!VALID_TYPES.includes(id)) {
|
|
113
116
|
throw new CliError(t("cli.args.typeUnsupported", { type: name, valid: VALID_TYPES.join(" ") }));
|
|
114
117
|
}
|
|
115
|
-
seen.add(
|
|
116
|
-
types.push(
|
|
118
|
+
seen.add(id);
|
|
119
|
+
types.push(id);
|
|
117
120
|
}
|
|
118
121
|
if (types.length === 0) throw new CliError(t("cli.args.typeEmpty"));
|
|
119
122
|
result.types = types;
|
|
@@ -193,20 +196,27 @@ export function parseArgs(argv) {
|
|
|
193
196
|
// "flutter=app,react=client" -> Map<type, normalizedPath>. Validates types (invalid -> throw).
|
|
194
197
|
export function parsePathsCsv(csv) {
|
|
195
198
|
const map = new Map();
|
|
199
|
+
const spelled = new Map(); // type -> name as typed, to tell an alias clash from a plain repeat
|
|
196
200
|
if (!csv) return map;
|
|
197
201
|
for (const pair of csv.split(",")) {
|
|
198
202
|
if (pair.trim() === "") continue;
|
|
199
203
|
const eq = pair.indexOf("=");
|
|
200
|
-
const
|
|
204
|
+
const typeRaw = (eq >= 0 ? pair.slice(0, eq) : pair).replace(/\s/g, "");
|
|
205
|
+
const type = canonicalTypeId(typeRaw);
|
|
201
206
|
const rawPath = eq >= 0 ? pair.slice(eq + 1) : "";
|
|
202
207
|
if (!VALID_TYPES.includes(type)) {
|
|
203
|
-
throw new CliError(t("cli.args.pathsTypeUnsupported", { type }));
|
|
208
|
+
throw new CliError(t("cli.args.pathsTypeUnsupported", { type: typeRaw }));
|
|
204
209
|
}
|
|
205
210
|
const path = normalizePath(rawPath);
|
|
206
211
|
if (!isRepoRelativePath(path)) {
|
|
207
212
|
throw new CliError(t("cli.args.pathsRelativeOnly", { pair: `${type}=${rawPath.trim()}` }));
|
|
208
213
|
}
|
|
214
|
+
// An alias and its canonical name in one list (react=a,next=b) would silently drop a folder; refuse it instead.
|
|
215
|
+
if (map.has(type) && spelled.get(type) !== typeRaw && map.get(type) !== path) {
|
|
216
|
+
throw new CliError(t("cli.args.pathsDuplicateType", { type }));
|
|
217
|
+
}
|
|
209
218
|
map.set(type, path);
|
|
219
|
+
spelled.set(type, typeRaw);
|
|
210
220
|
}
|
|
211
221
|
return map;
|
|
212
222
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -14,6 +14,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { A, paint, colorEnabled, visualWidth } from "../ui/ansi.js";
|
|
16
16
|
import { PATHS } from "../core/paths.js";
|
|
17
|
+
import { findMissingScripts } from "../core/copy/simple.js";
|
|
18
|
+
import { resolvePayloadRoot } from "../core/assets.js";
|
|
17
19
|
import { parseExisting } from "../core/version-yml.js";
|
|
18
20
|
import { hooksFor } from "../core/types.js";
|
|
19
21
|
import { t } from "../i18n/index.js";
|
|
@@ -44,6 +46,15 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
44
46
|
// Per-type local file checks (e.g. Flutter store deploy files): only local files are read, so this runs regardless of the gh lookup results.
|
|
45
47
|
if (installed) {
|
|
46
48
|
const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
|
|
49
|
+
const missing = findMissingScripts(resolvePayloadRoot(), cwd);
|
|
50
|
+
if (missing.length) {
|
|
51
|
+
add({
|
|
52
|
+
name: t("cmd.doctor.scripts.name"), label: t("cmd.doctor.scripts.label"), purpose: t("cmd.doctor.scripts.purpose"), status: "WARN",
|
|
53
|
+
value: t("cmd.doctor.scripts.missing", { files: missing.join(", ") }),
|
|
54
|
+
impact: [t("cmd.doctor.scripts.impact")],
|
|
55
|
+
actions: [t("cmd.doctor.scripts.action")],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
47
58
|
for (const { hook } of hooksFor(existing.types, "doctorChecks")) {
|
|
48
59
|
for (const item of hook(cwd, existing, { docs: DOC })) add(item);
|
|
49
60
|
}
|
package/src/commands/dry-run.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PATHS } from "../core/paths.js";
|
|
|
6
6
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
7
7
|
import { planTypeAppFiles } from "../core/copy/app-files.js";
|
|
8
8
|
import { planUninstall } from "./uninstall.js";
|
|
9
|
-
import { renderVersionYml, parseExisting, sameIgnoringTimestamps } from "../core/version-yml.js";
|
|
9
|
+
import { renderVersionYml, parseExisting, sameIgnoringTimestamps, droppedPathLines } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
12
|
import { planScripts } from "../core/copy/simple.js";
|
|
@@ -33,6 +33,11 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
|
33
33
|
return { existed: existingRaw !== null, changed: existingRaw === null || !sameIgnoringTimestamps(existingRaw, wouldBe) };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function existingDroppedLines(targetRoot, paths) {
|
|
37
|
+
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
38
|
+
return existsSync(vyPath) ? droppedPathLines(parseExisting(readFileSync(vyPath, "utf8")).droppedPaths, paths) : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
// mode: "full" | "uninstall". Read-only: writes no file.
|
|
37
42
|
export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
38
43
|
if (mode === "uninstall") {
|
|
@@ -54,6 +59,8 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
|
54
59
|
// Flutter store deploy files (Fastfile, ExportOptions.plist): existing files are kept, not overwritten.
|
|
55
60
|
flutterApp: planTypeAppFiles(context, payloadRoot, targetRoot),
|
|
56
61
|
versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
|
|
62
|
+
// Folders the rewrite would drop from version.yml (same type listed under two names with different folders).
|
|
63
|
+
droppedPathLines: existingDroppedLines(targetRoot, context.paths),
|
|
57
64
|
// Files the real install also changes; scripts that overwrite existing files in particular must be shown in advance.
|
|
58
65
|
scripts: planScripts(payloadRoot, targetRoot),
|
|
59
66
|
readme: planVersionSection(targetRoot),
|
|
@@ -130,6 +137,7 @@ export function printDryRun(plan) {
|
|
|
130
137
|
// For types with a deploy block (spring etc.) say the preview may differ from the real install.
|
|
131
138
|
lines.push(t("cmd.dryRun.versionYml.note"));
|
|
132
139
|
}
|
|
140
|
+
if (plan.droppedPathLines?.length) lines.push(...plan.droppedPathLines.map((l) => `⚠️ ${l}`));
|
|
133
141
|
if (plan.scripts) {
|
|
134
142
|
const mark = { create: "+", overwrite: "~", unchanged: "=" };
|
|
135
143
|
const note = {
|
package/src/commands/full.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
6
6
|
import { writeText, findUnwritable } from "../core/fsutil.js";
|
|
7
7
|
import { CliError } from "../core/errors.js";
|
|
8
8
|
import { PATHS } from "../core/paths.js";
|
|
9
|
-
import { renderVersionYml, parseExisting, sameIgnoringTimestamps } from "../core/version-yml.js";
|
|
9
|
+
import { renderVersionYml, parseExisting, sameIgnoringTimestamps, droppedPathLines } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
12
|
import { addVersionSectionToReadme, README_STATUS_LABEL } from "../core/copy/readme.js";
|
|
@@ -70,7 +70,10 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
70
70
|
// Preserve unknown top-level fields of an existing version.yml when regenerating it.
|
|
71
71
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
72
72
|
const prevVy = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
73
|
-
const
|
|
73
|
+
const prevParsed = prevVy != null ? parseExisting(prevVy) : null;
|
|
74
|
+
const extraTopLevel = prevParsed ? prevParsed.extraTopLevel : [];
|
|
75
|
+
// Folders folded into one type entry are not managed after this rewrite; leave a trace in the install log.
|
|
76
|
+
for (const line of droppedPathLines(prevParsed?.droppedPaths, paths)) log.warn("version", "paths", line);
|
|
74
77
|
|
|
75
78
|
// 2. Generate version.yml (render payload/version.yml.template; full regeneration)
|
|
76
79
|
// A re-run where only the date lines differ does not rewrite it (idempotent).
|
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "../core/assets.js";
|
|
8
8
|
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectMarkers } from "../core/detect-fs.js";
|
|
9
|
-
import { parseExisting } from "../core/version-yml.js";
|
|
9
|
+
import { parseExisting, droppedPathLines } from "../core/version-yml.js";
|
|
10
10
|
import { pickReleaseOptions, resolveReleaseOptions } from "../core/release-options.js";
|
|
11
11
|
import { runBreakingCheck } from "../core/breaking-check.js";
|
|
12
12
|
import { resolveProjectPaths } from "../core/paths-resolve.js";
|
|
@@ -294,6 +294,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
294
294
|
for (const ty of types) if (ty !== "basic" && !paths.has(ty)) paths.set(ty, existing?.paths.get(ty) || ".");
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
// Announce merged folders only now: the message must follow the folders actually chosen, not the saved ones.
|
|
298
|
+
if (existing?.droppedPaths?.length) io.note?.(droppedPathLines(existing.droppedPaths, paths).join("\n"), t("core.versionYml.pathMergedTitle"));
|
|
299
|
+
|
|
297
300
|
// In a monorepo the version/build-number files live inside the type folder - detect there now that paths are settled.
|
|
298
301
|
if (versionAutoDetected && [...paths.values()].some((p) => p && p !== ".")) {
|
|
299
302
|
version = detectVersion(cwd, { types, paths, warn: () => {} });
|
package/src/commands/status.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// status command: read-only install state check. No network access (local file comparison only).
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { parseExisting } from "../core/version-yml.js";
|
|
4
|
+
import { parseExisting, droppedPathLines } from "../core/version-yml.js";
|
|
5
5
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
6
6
|
import { makeResolvers, detectRepoName, detectDefaultBranch } from "../core/detect-fs.js";
|
|
7
7
|
import { PATHS } from "../core/paths.js";
|
|
8
8
|
import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
9
|
+
import { findMissingScripts } from "../core/copy/simple.js";
|
|
9
10
|
import { findStaleWorkflows } from "../core/removal-plan.js";
|
|
10
11
|
import { readBaseline } from "../core/baseline.js";
|
|
11
12
|
import { hooksFor, mergeHookResults } from "../core/types.js";
|
|
@@ -56,6 +57,9 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
56
57
|
removed: plan.removed.map((f) => f.filename), // deleted by the user and not restored
|
|
57
58
|
},
|
|
58
59
|
staleFiles: stale,
|
|
60
|
+
// Installed scripts the workflows need but the repo no longer has; the workflow comparison above cannot see them.
|
|
61
|
+
missingScripts: findMissingScripts(payloadRoot, targetRoot),
|
|
62
|
+
droppedPaths: existing.droppedPaths,
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -90,6 +94,15 @@ export function printStatus(status) {
|
|
|
90
94
|
for (const f of status.staleFiles) lines.push(` - ${f}`);
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
if (status.missingScripts?.length) {
|
|
98
|
+
lines.push("", t("cmd.status.missingScripts", { n: status.missingScripts.length, dir: PATHS.scriptsDir }));
|
|
99
|
+
for (const f of status.missingScripts) lines.push(` - ${f}`);
|
|
100
|
+
lines.push(t("cmd.status.missingScriptsFix"));
|
|
101
|
+
}
|
|
102
|
+
if (status.droppedPaths?.length) {
|
|
103
|
+
lines.push("", ...droppedPathLines(status.droppedPaths).map((l) => `⚠️ ${l}`));
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
// What an update would do. Installs without a baseline are all zeros, so nothing is printed.
|
|
94
107
|
const b = status.buckets || { autoUpdatable: [], localKept: [], conflicts: [], removed: [] };
|
|
95
108
|
if (b.autoUpdatable.length || b.localKept.length || b.conflicts.length || b.removed.length) {
|
package/src/core/copy/simple.js
CHANGED
|
@@ -24,6 +24,37 @@ export function planScripts(payloadRoot, targetRoot = ".") {
|
|
|
24
24
|
return out;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Script -> bundled scripts it imports (`import x` / `from x import ...`), read from the payload so the
|
|
28
|
+
// dependency table cannot drift from the code. Only names in SCRIPT_NAMES count; stdlib imports are ignored.
|
|
29
|
+
function scriptImports(payloadRoot, name) {
|
|
30
|
+
let src = "";
|
|
31
|
+
try { src = readFileSync(join(payloadRoot, PAYLOAD.scriptsDir, name), "utf8"); } catch { return []; }
|
|
32
|
+
const mods = new Set();
|
|
33
|
+
for (const m of src.matchAll(/^[ \t]*(?:from[ \t]+(\w+)[ \t]+import\b|import[ \t]+([\w, \t]+))/gm)) {
|
|
34
|
+
if (m[1]) mods.add(m[1]);
|
|
35
|
+
else for (const x of m[2].split(",")) mods.add(x.trim().split(/\s+/)[0]);
|
|
36
|
+
}
|
|
37
|
+
return SCRIPT_NAMES.filter((n) => n !== name && mods.has(n.replace(/\.py$/, "")));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Bundled scripts the installed workflows call but the repo no longer has (deleted by hand, never copied).
|
|
41
|
+
// Only scripts an installed workflow actually references count: some (truncate_release_notes.py) belong to
|
|
42
|
+
// workflows of one type only. Scripts those import (messages.py, issue_helper.py, ...) are required too,
|
|
43
|
+
// transitively, since the called script fails at import time without them.
|
|
44
|
+
export function findMissingScripts(payloadRoot, targetRoot = ".") {
|
|
45
|
+
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
46
|
+
let text = "";
|
|
47
|
+
try {
|
|
48
|
+
for (const f of readdirSync(wfDir)) if (/\.ya?ml$/.test(f)) text += `${readFileSync(join(wfDir, f), "utf8")}\n`;
|
|
49
|
+
} catch { return []; }
|
|
50
|
+
const required = new Set(SCRIPT_NAMES.filter((n) => text.includes(n)));
|
|
51
|
+
const queue = [...required];
|
|
52
|
+
while (queue.length) {
|
|
53
|
+
for (const dep of scriptImports(payloadRoot, queue.pop())) if (!required.has(dep)) { required.add(dep); queue.push(dep); }
|
|
54
|
+
}
|
|
55
|
+
return SCRIPT_NAMES.filter((n) => required.has(n) && exists(join(payloadRoot, PAYLOAD.scriptsDir, n)) && !exists(join(targetRoot, PATHS.scriptsDir, n)));
|
|
56
|
+
}
|
|
57
|
+
|
|
27
58
|
// Scripts are always overwritten with the payload version (+chmod, meaningless but harmless on Windows).
|
|
28
59
|
// User-edited scripts are overwritten too, so per-file results are returned for the caller to log.
|
|
29
60
|
export function copyScripts(payloadRoot, targetRoot = ".") {
|
|
@@ -48,7 +48,7 @@ export function readSavedDeployValues(targetRoot = ".") {
|
|
|
48
48
|
// and status/dry-run never see different file sets. When flutterStore is not an array (null = undecided,
|
|
49
49
|
// non-interactive default) no store filter is applied. It always returns a function even without filters,
|
|
50
50
|
// because processDir cannot take null.
|
|
51
|
-
// Some types (go
|
|
51
|
+
// Some types (go, python, react) keep server deploy workflows directly in the type root, so the deploy style
|
|
52
52
|
// filter is always applied. `available` (set of payload file names) lets a type without the chosen style fall back
|
|
53
53
|
// to the single server deploy.
|
|
54
54
|
export function buildTypeRootFilter(type, deployStyle, flutterStore, available = null) {
|
|
@@ -64,7 +64,7 @@ export function buildTypeRootFilter(type, deployStyle, flutterStore, available =
|
|
|
64
64
|
export function typeWorkflowSources(type, payloadRoot, { deployStyle, flutterStore = null, available = null }) {
|
|
65
65
|
const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
|
|
66
66
|
const sources = [];
|
|
67
|
-
// Some types (go
|
|
67
|
+
// Some types (go, python, react) keep server deploy workflows directly in the type root, so filter the root by deploy style too.
|
|
68
68
|
if (exists(typeDir)) sources.push({ srcDir: typeDir, type, filter: buildTypeRootFilter(type, deployStyle, flutterStore, available) });
|
|
69
69
|
// "No deploy" excludes the whole folder
|
|
70
70
|
if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
|
package/src/core/deploy-style.js
CHANGED
|
@@ -31,7 +31,7 @@ export const isDeployWorkflow = (filename) => DEPLOY_STYLES.some((s) => filename
|
|
|
31
31
|
|
|
32
32
|
const PREVIEW_SUFFIX = "-PR-PREVIEW.yaml";
|
|
33
33
|
|
|
34
|
-
// All workflows that deploy to a server: CD bodies, single
|
|
34
|
+
// All workflows that deploy to a server: CD bodies, the single CD (react) and PR preview.
|
|
35
35
|
// "No deploy" removes all of them identically for every type. Removing only some would leave server Secret requirements behind.
|
|
36
36
|
export const isServerDeployWorkflow = (filename) =>
|
|
37
37
|
isDeployWorkflow(filename) || SINGLE_SERVER_CD_FILES.has(filename) || filename.endsWith(PREVIEW_SUFFIX);
|
package/src/core/detect.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// package.json classification: compares dependency "keys" exactly. A raw substring match would misdetect an
|
|
7
7
|
// export script or exponential-backoff as expo, a web app using react-native-web as react-native, and
|
|
8
|
-
// "
|
|
8
|
+
// "react" in keywords as react. Input is the raw package.json string. Evaluation order is the registry's detectOrder.
|
|
9
9
|
export function classifyPackageText(raw) {
|
|
10
10
|
let pkg;
|
|
11
11
|
try { pkg = JSON.parse(String(raw || "")); } catch { return PACKAGE_FALLBACK_TYPE; }
|
|
@@ -15,7 +15,8 @@ export function classifyPackageText(raw) {
|
|
|
15
15
|
const d = pkg[field];
|
|
16
16
|
if (d && typeof d === "object") for (const k of Object.keys(d)) deps.add(k);
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
const hasDep = (t) => [t.packageDep, ...(t.extraPackageDeps || [])].some((k) => deps.has(k));
|
|
19
|
+
return PACKAGE_DETECTED_TYPES.find(hasDep)?.id ?? PACKAGE_FALLBACK_TYPE;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
// Convenience: when given a parsed object, re-serialize it and apply the rule above.
|
package/src/core/types.js
CHANGED
|
@@ -11,6 +11,8 @@ import { flutterHooks } from "./flutter-hooks.js";
|
|
|
11
11
|
// "package": detected by a package.json dependency key (packageDep)
|
|
12
12
|
// "package-fallback": package.json exists but none of the above matched
|
|
13
13
|
// absent: not auto-detected (basic is the result when nothing was detected).
|
|
14
|
+
// extraPackageDeps Other dependency keys that also classify a project as this type (e.g. next -> react).
|
|
15
|
+
// displayName Human-readable name shown next to the id in interactive choices (optional).
|
|
14
16
|
// detectOrder Evaluation order within the same detectBy. The earlier rule wins (an expo app also has a react-native dependency).
|
|
15
17
|
// versionSources File keys to read the version from at install (sources in detect.js); read first in this order for the primary type.
|
|
16
18
|
// buildNumberSource File key to read the mobile build number from (detectBuildNumberFromFiles in detect.js).
|
|
@@ -51,16 +53,11 @@ export const TYPES = [
|
|
|
51
53
|
hooks: flutterHooks,
|
|
52
54
|
},
|
|
53
55
|
{
|
|
54
|
-
|
|
55
|
-
markers: ["package.json"],
|
|
56
|
-
detectBy: "package", packageDep: "next", detectOrder: 3,
|
|
57
|
-
versionSources: ["packageJson"],
|
|
58
|
-
singleServerCd: "PROJECT-NEXT-CICD.yaml",
|
|
59
|
-
},
|
|
60
|
-
{
|
|
56
|
+
// Next.js projects are React projects: same package.json version handling and the same CI/CD workflows.
|
|
61
57
|
id: "react",
|
|
58
|
+
displayName: "React / Next.js",
|
|
62
59
|
markers: ["package.json"],
|
|
63
|
-
detectBy: "package", packageDep: "react", detectOrder:
|
|
60
|
+
detectBy: "package", packageDep: "react", extraPackageDeps: ["next"], detectOrder: 3,
|
|
64
61
|
versionSources: ["packageJson"],
|
|
65
62
|
singleServerCd: "PROJECT-REACT-CICD.yaml",
|
|
66
63
|
},
|
|
@@ -109,6 +106,16 @@ export const TYPES = [
|
|
|
109
106
|
// Type used when nothing was detected
|
|
110
107
|
export const FALLBACK_TYPE = "basic";
|
|
111
108
|
|
|
109
|
+
// Names that were once separate types and are now accepted as an alias of another one.
|
|
110
|
+
// Every input that can carry a type name (--type, --paths, version.yml) goes through canonicalTypeId(),
|
|
111
|
+
// so the rest of the code only ever sees registry ids and version.yml is rewritten with the canonical name.
|
|
112
|
+
export const TYPE_ALIASES = Object.freeze({ next: "react" });
|
|
113
|
+
|
|
114
|
+
export const canonicalTypeId = (name) => TYPE_ALIASES[name] ?? name;
|
|
115
|
+
|
|
116
|
+
// Canonical ids of a type list with duplicates removed (react,next -> react), keeping the first occurrence's position.
|
|
117
|
+
export const canonicalTypeIds = (names) => [...new Set(names.map(canonicalTypeId))];
|
|
118
|
+
|
|
112
119
|
const BY_ID = new Map(TYPES.map((t) => [t.id, t]));
|
|
113
120
|
|
|
114
121
|
// Unknown types give undefined; callers decide their own defaults.
|
package/src/core/version-yml.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_DEPLOY_STYLE } from "./deploy-style.js";
|
|
2
2
|
import { escapeYamlDoubleQuoted } from "./wizard-env.js";
|
|
3
|
-
import { hooksFor, mergeHookResults, allHookValues } from "./types.js";
|
|
3
|
+
import { hooksFor, mergeHookResults, allHookValues, canonicalTypeId, canonicalTypeIds } from "./types.js";
|
|
4
|
+
import { normalizePath } from "./paths.js";
|
|
4
5
|
import { DEFAULT_LANGUAGE, isSupportedLanguage, normalizeLanguage } from "../i18n/languages.js";
|
|
5
6
|
// Aliased: `t` is used as a local variable name (type, trimmed line) throughout this file.
|
|
6
7
|
import { t as tr } from "../i18n/index.js";
|
|
@@ -88,6 +89,21 @@ export function parseTemplateOptions(content) {
|
|
|
88
89
|
return out;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// User-facing lines for entries parseExisting() folded away: what is kept, what drops out, and how to keep the other folder.
|
|
93
|
+
// finalPaths (Map<type, folder>) is the folder set the run will actually write; without it the saved winner is assumed.
|
|
94
|
+
// The message follows the real outcome, so an explicit --paths choice is never told the opposite.
|
|
95
|
+
export function droppedPathLines(droppedPaths = [], finalPaths = null) {
|
|
96
|
+
return droppedPaths.flatMap((d) => {
|
|
97
|
+
const final = finalPaths?.get(d.type) ?? d.kept;
|
|
98
|
+
const same = (x) => normalizePath(x) === normalizePath(final);
|
|
99
|
+
const lost = same(d.kept) ? d.path : same(d.path) ? d.kept : `${d.kept}, ${d.path}`;
|
|
100
|
+
const lines = [tr("core.versionYml.pathMerged", { keptName: d.keptName, kept: d.kept, name: d.name, path: d.path, type: d.type, final, lost })];
|
|
101
|
+
// The re-run hint only makes sense when the saved winner is what stays.
|
|
102
|
+
if (same(d.kept)) lines.push(tr("core.versionYml.pathMergedHint", { type: d.type, path: d.path }));
|
|
103
|
+
return lines;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
// Extract values from an existing version.yml (line-based, avoids false hits on comment lines).
|
|
92
108
|
export function parseExisting(content) {
|
|
93
109
|
const text = String(content || "");
|
|
@@ -107,21 +123,45 @@ export function parseExisting(content) {
|
|
|
107
123
|
// project_types: ["a","b"]
|
|
108
124
|
const typesRaw = line(/^project_types:\s*(\[[^\]]*\])/);
|
|
109
125
|
let types = [];
|
|
110
|
-
|
|
126
|
+
// Old names (next) are read as their current type so every later step, including the rewrite, only sees canonical ids.
|
|
127
|
+
if (typesRaw) types = canonicalTypeIds([...typesRaw.matchAll(/"([^"]+)"/g)].map((m) => m[1]));
|
|
111
128
|
// language: "en" - only supported values count; a hand-edited unknown value is treated as unset
|
|
112
129
|
const langRaw = normalizeLanguage((line(/^language:\s*(.+)/) || "").replace(/\s+#.*$/, "").replace(/["']/g, ""));
|
|
113
130
|
const language = isSupportedLanguage(langRaw) ? langRaw : null;
|
|
114
131
|
// project_paths block: ` type: "path"`
|
|
115
132
|
const paths = new Map();
|
|
133
|
+
// Entries folded into another entry of the same canonical type with a different folder (react: client + next: web).
|
|
134
|
+
// A type holds one folder, so these are lost on rewrite; callers surface them instead of dropping them silently.
|
|
135
|
+
const droppedPaths = [];
|
|
136
|
+
const pathNames = new Map(); // canonical id -> the name the kept folder was written under
|
|
116
137
|
let inPaths = false;
|
|
117
138
|
for (const l of text.split("\n")) {
|
|
118
139
|
if (/^project_paths:/.test(l)) { inPaths = true; continue; }
|
|
119
140
|
if (inPaths) {
|
|
120
141
|
const m = l.match(/^\s+([a-z-]+):\s*"([^"]*)"/);
|
|
121
|
-
|
|
142
|
+
// The first entry wins when an old name and its current name are both present.
|
|
143
|
+
if (m) {
|
|
144
|
+
const id = canonicalTypeId(m[1]);
|
|
145
|
+
if (!paths.has(id)) { paths.set(id, m[2]); pathNames.set(id, m[1]); }
|
|
146
|
+
else if (normalizePath(paths.get(id)) !== normalizePath(m[2])) droppedPaths.push({ type: id, keptName: pathNames.get(id), kept: paths.get(id), name: m[1], path: m[2] });
|
|
147
|
+
}
|
|
122
148
|
else if (/^\S/.test(l)) inPaths = false; // end of indentation -> end of block
|
|
123
149
|
}
|
|
124
150
|
}
|
|
151
|
+
// A name listed in project_types without a project_paths entry lives at the repo root, so when it shares a
|
|
152
|
+
// canonical type with a name that has a folder, the root is the folder that drops out.
|
|
153
|
+
if (typesRaw) {
|
|
154
|
+
const names = [...new Set([...typesRaw.matchAll(/"([^"]+)"/g)].map((m) => m[1]))];
|
|
155
|
+
const written = new Set(text.split("\n").map((l) => l.match(/^\s+([a-z-]+):\s*"/)?.[1]).filter(Boolean));
|
|
156
|
+
for (const id of new Set(names.map(canonicalTypeId))) {
|
|
157
|
+
const group = names.filter((n) => canonicalTypeId(n) === id);
|
|
158
|
+
if (group.length < 2 || !paths.has(id)) continue;
|
|
159
|
+
for (const n of group) {
|
|
160
|
+
if (written.has(n) || normalizePath(paths.get(id)) === ".") continue;
|
|
161
|
+
droppedPaths.push({ type: id, keptName: pathNames.get(id), kept: paths.get(id), name: n, path: "." });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
125
165
|
// version inside the template: block
|
|
126
166
|
let templateVersion = "";
|
|
127
167
|
let inTemplate = false;
|
|
@@ -138,7 +178,7 @@ export function parseExisting(content) {
|
|
|
138
178
|
// metadata.template.branches - main/develop/mode (to skip re-asking in update mode)
|
|
139
179
|
const branches = parseTemplateBranches(text);
|
|
140
180
|
return {
|
|
141
|
-
version, versionCode, types, language, paths, templateVersion, options, branches,
|
|
181
|
+
version, versionCode, types, language, paths, droppedPaths, templateVersion, options, branches,
|
|
142
182
|
deploy: parseDeployBlock(text), extraTopLevel: parseExtraTopLevel(text),
|
|
143
183
|
};
|
|
144
184
|
}
|
|
@@ -157,9 +197,15 @@ export function parseDeployBlock(content) {
|
|
|
157
197
|
if (!inDeploy) continue;
|
|
158
198
|
if (/^\S/.test(l)) { inDeploy = false; continue; } // next top-level key -> end of block
|
|
159
199
|
const t = l.match(/^ {2}([a-z][a-z-]*):\s*(?:#.*)?$/);
|
|
160
|
-
if (t) {
|
|
200
|
+
if (t) {
|
|
201
|
+
// An old type name shares the block of its current name; keys already present there win.
|
|
202
|
+
const id = canonicalTypeId(t[1]);
|
|
203
|
+
current = out.get(id) ?? new Map();
|
|
204
|
+
out.set(id, current);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
161
207
|
const kv = l.match(/^ {4}([A-Za-z_][A-Za-z0-9_]*):\s*"((?:[^"\\]|\\.)*)"/);
|
|
162
|
-
if (kv && current) current.set(kv[1], kv[2].replace(/\\(["\\])/g, "$1"));
|
|
208
|
+
if (kv && current && !current.has(kv[1])) current.set(kv[1], kv[2].replace(/\\(["\\])/g, "$1"));
|
|
163
209
|
}
|
|
164
210
|
return out;
|
|
165
211
|
}
|
|
@@ -20,6 +20,7 @@ export default {
|
|
|
20
20
|
"cli.args.unknownOption": "Unknown option: {option}",
|
|
21
21
|
"cli.args.modeUnsupported": "Unsupported mode: '{mode}'\nSupported modes: interactive full uninstall status doctor",
|
|
22
22
|
"cli.args.pathsTypeUnsupported": "Unsupported type in --paths: '{type}'",
|
|
23
|
+
"cli.args.pathsDuplicateType": "--paths lists '{type}' twice with different folders (next is the old name of react); keep one entry",
|
|
23
24
|
"cli.args.pathsRelativeOnly": "--paths accepts only paths relative to the repo: '{pair}'",
|
|
24
25
|
"cli.index.purgePrompt": "To run purge, type this repo name exactly: {repoName}\n> ",
|
|
25
26
|
"cli.index.dryRunNeedsMode": "Use --dry-run together with --mode <full|uninstall> (it is not supported in interactive mode).",
|
|
@@ -12,6 +12,8 @@ export default {
|
|
|
12
12
|
"cmd.status.modified": "Workflow files modified by the user ({n}):",
|
|
13
13
|
"cmd.status.unmodified": "All workflow files match their install-time defaults (no modifications).",
|
|
14
14
|
"cmd.status.stale": "Old workflows not in the current version ({n}; cleaned up on the next update):",
|
|
15
|
+
"cmd.status.missingScripts": "Warning: {n} installed script(s) missing from {dir}/ (workflows call them and will fail at run time):",
|
|
16
|
+
"cmd.status.missingScriptsFix": " Restore them with: npx project-auto-wizard --mode full --force",
|
|
15
17
|
"cmd.status.bucketsTitle": "If you update now:",
|
|
16
18
|
"cmd.status.bucketAuto": " Upstream changed - auto-applicable {n}",
|
|
17
19
|
"cmd.status.bucketLocalKept": " Edited by me - upstream unchanged {n} (kept)",
|
|
@@ -24,6 +26,12 @@ export default {
|
|
|
24
26
|
"cmd.doctor.install.purpose": "Whether the wizard is installed in this folder",
|
|
25
27
|
"cmd.doctor.install.value": "version.yml found",
|
|
26
28
|
"cmd.doctor.install.note": "Not installed in this folder yet; you can install now.",
|
|
29
|
+
"cmd.doctor.scripts.name": "Installed scripts",
|
|
30
|
+
"cmd.doctor.scripts.label": "Installed scripts",
|
|
31
|
+
"cmd.doctor.scripts.purpose": "Scripts the installed workflows call",
|
|
32
|
+
"cmd.doctor.scripts.missing": "Missing from .github/scripts/: {files}",
|
|
33
|
+
"cmd.doctor.scripts.impact": "Workflow steps that call them stop with a clear error (messages.py is missing) instead of carrying on.",
|
|
34
|
+
"cmd.doctor.scripts.action": "Restore them with: npx project-auto-wizard --mode full --force",
|
|
27
35
|
"cmd.doctor.gh.purpose": "For reading repo settings",
|
|
28
36
|
"cmd.doctor.gh.notFound": "The gh CLI was not found.",
|
|
29
37
|
"cmd.doctor.gh.notFoundImpact": "Repo permissions and secrets cannot be read, so all remote checks below are skipped.",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// English messages: core area (part 2).
|
|
2
2
|
export default {
|
|
3
3
|
"core.breakingCheck.confirm": "Have you reviewed the breaking changes above and want to continue?",
|
|
4
|
+
"core.versionYml.pathMergedTitle": "One folder per type",
|
|
5
|
+
"core.versionYml.pathMerged": "version.yml lists '{keptName}' (folder {kept}) and '{name}' (folder {path}) separately, but they are now the same type and only one folder per type is managed. Keeping {type}={final}; {lost} drops out of CI and version sync.",
|
|
6
|
+
"core.versionYml.pathMergedHint": "To manage {path} instead, rerun with --paths {type}={path}; to keep both, add a hand-written CI workflow for the dropped folder.",
|
|
4
7
|
"core.breakingCheck.nonInteractive": "⚠️ There are CRITICAL breaking changes - continuing because this is a non-interactive run. Be sure to review the notes above.",
|
|
5
8
|
"core.logger.error.nameExhausted": "All log file names are in use: {base}",
|
|
6
9
|
"core.logger.warn.startFailed": "Could not start the run log: {message}",
|
|
@@ -20,6 +20,7 @@ export default {
|
|
|
20
20
|
"cli.args.unknownOption": "알 수 없는 옵션: {option}",
|
|
21
21
|
"cli.args.modeUnsupported": "지원하지 않는 모드: '{mode}'\n지원 모드: interactive full uninstall status doctor",
|
|
22
22
|
"cli.args.pathsTypeUnsupported": "--paths에 지원하지 않는 타입: '{type}'",
|
|
23
|
+
"cli.args.pathsDuplicateType": "--paths에 '{type}'이 서로 다른 폴더로 두 번 지정됨 (next는 react의 이전 이름입니다): 한 항목만 남기세요",
|
|
23
24
|
"cli.args.pathsRelativeOnly": "--paths는 레포 안의 상대경로만 지정할 수 있습니다: '{pair}'",
|
|
24
25
|
"cli.index.purgePrompt": "purge를 실행하려면 정확히 이 레포명을 입력하세요: {repoName}\n> ",
|
|
25
26
|
"cli.index.dryRunNeedsMode": "--dry-run은 --mode <full|uninstall>와 함께 사용하세요 (대화형 모드에서는 지원하지 않습니다).",
|
|
@@ -12,6 +12,8 @@ export default {
|
|
|
12
12
|
"cmd.status.modified": "사용자가 수정한 워크플로우 파일 ({n}개):",
|
|
13
13
|
"cmd.status.unmodified": "모든 워크플로우 파일이 설치 시점 기본값과 동일합니다 (수정 없음).",
|
|
14
14
|
"cmd.status.stale": "현재 버전에 없는 이전 워크플로우 ({n}개 — 다음 업데이트에서 정리됨):",
|
|
15
|
+
"cmd.status.missingScripts": "경고: 설치된 스크립트 {n}개가 {dir}/ 에 없습니다 (워크플로우가 호출하므로 실행 시 실패합니다):",
|
|
16
|
+
"cmd.status.missingScriptsFix": " 복구: npx project-auto-wizard --mode full --force",
|
|
15
17
|
"cmd.status.bucketsTitle": "지금 업데이트하면:",
|
|
16
18
|
"cmd.status.bucketAuto": " 업스트림 변경 — 자동 적용 가능 {n}",
|
|
17
19
|
"cmd.status.bucketLocalKept": " 내가 수정 · 업스트림 그대로 {n} (유지)",
|
|
@@ -24,6 +26,12 @@ export default {
|
|
|
24
26
|
"cmd.doctor.install.purpose": "이 폴더의 마법사 설치 여부",
|
|
25
27
|
"cmd.doctor.install.value": "version.yml 있음",
|
|
26
28
|
"cmd.doctor.install.note": "이 폴더엔 아직 설치되지 않았습니다 — 지금 설치하면 됩니다.",
|
|
29
|
+
"cmd.doctor.scripts.name": "설치된 스크립트",
|
|
30
|
+
"cmd.doctor.scripts.label": "설치된 스크립트",
|
|
31
|
+
"cmd.doctor.scripts.purpose": "설치된 워크플로우가 호출하는 스크립트",
|
|
32
|
+
"cmd.doctor.scripts.missing": ".github/scripts/ 에 없음: {files}",
|
|
33
|
+
"cmd.doctor.scripts.impact": "이 스크립트를 호출하는 스텝은 누락을 알리는 오류(messages.py is missing)로 명확히 실패합니다.",
|
|
34
|
+
"cmd.doctor.scripts.action": "복구: npx project-auto-wizard --mode full --force",
|
|
27
35
|
"cmd.doctor.gh.purpose": "레포 설정 조회용",
|
|
28
36
|
"cmd.doctor.gh.notFound": "gh CLI를 찾을 수 없습니다.",
|
|
29
37
|
"cmd.doctor.gh.notFoundImpact": "레포 권한·secret 설정을 조회할 수 없어 아래 원격 점검을 모두 건너뜁니다.",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// 한국어 메시지: core 영역 (2부).
|
|
2
2
|
export default {
|
|
3
3
|
"core.breakingCheck.confirm": "위 호환성 변경을 확인했고 계속 진행할까요?",
|
|
4
|
+
"core.versionYml.pathMergedTitle": "타입당 폴더 하나",
|
|
5
|
+
"core.versionYml.pathMerged": "version.yml 에 '{keptName}'(폴더 {kept})과 '{name}'(폴더 {path})이 따로 적혀 있지만 이제 같은 타입이라 타입당 폴더 하나만 관리합니다. {type}={final} 를 유지하고, {lost} 는 CI·버전 동기화에서 빠집니다.",
|
|
6
|
+
"core.versionYml.pathMergedHint": "{path} 를 대신 관리하려면 --paths {type}={path} 로 다시 실행하세요. 둘 다 유지하려면 빠진 폴더용 CI 워크플로우를 직접 추가하세요.",
|
|
4
7
|
"core.breakingCheck.nonInteractive": "⚠️ CRITICAL 호환성 변경이 있습니다 — 비대화형 실행이라 계속 진행합니다. 위 내용을 꼭 확인하세요.",
|
|
5
8
|
"core.logger.error.nameExhausted": "로그 파일 이름이 모두 사용 중입니다: {base}",
|
|
6
9
|
"core.logger.warn.startFailed": "실행 로그를 시작하지 못했습니다: {message}",
|
package/src/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { hooksFor, mergeHookResults } from "./core/types.js";
|
|
|
14
14
|
import { PATHS } from "./core/paths.js";
|
|
15
15
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "./core/assets.js";
|
|
16
16
|
import { detectTypes, detectDefaultBranch, detectRepoName, makeResolvers, detectMarkers } from "./core/detect-fs.js";
|
|
17
|
-
import { parseExisting } from "./core/version-yml.js";
|
|
17
|
+
import { parseExisting, droppedPathLines } from "./core/version-yml.js";
|
|
18
18
|
import { resolveReleaseOptions } from "./core/release-options.js";
|
|
19
19
|
import { runBreakingCheck } from "./core/breaking-check.js";
|
|
20
20
|
import { resolveProjectPaths } from "./core/paths-resolve.js";
|
|
@@ -316,6 +316,9 @@ async function runInner(argv, {
|
|
|
316
316
|
throw e;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
// Warn against the final folders (an explicit --paths may keep the other one). The dry-run preview prints its own notice.
|
|
320
|
+
if (!opts.dryRun) for (const line of droppedPathLines(existing?.droppedPaths, paths)) console.error(`⚠️ ${line}`);
|
|
321
|
+
|
|
319
322
|
// version: existing version.yml first (SSoT; prevents overwriting on re-run) -> CLI value -> file detection.
|
|
320
323
|
// Non-interactive, so the fallback notice uses the CLI wording (--project-version) as is.
|
|
321
324
|
// Detect after paths are confirmed so a monorepo subfolder's version and build number are read.
|
package/src/ui/env-plan.js
CHANGED
|
@@ -59,7 +59,7 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
|
|
|
59
59
|
const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
|
|
60
60
|
if (!exists(typeDir)) continue;
|
|
61
61
|
// Same folder layout as the copy engine: type root + server-deploy (only when deploy is not "none").
|
|
62
|
-
// Types such as go
|
|
62
|
+
// Types such as go, python and react keep server deploy workflows directly in the type root; they go
|
|
63
63
|
// through the same deploy-style filter - with "no deploy", CD/PR-preview ask keys (DEPLOY_PORT, SSH_AUTH_METHOD, ...) are not asked.
|
|
64
64
|
// Flutter also filters out deselected store workflows (PLAYSTORE, TESTFLIGHT) so the question scope equals the install scope.
|
|
65
65
|
units.push([type, typeDir, buildTypeRootFilter(type, deployStyle, flutterStore, available)]);
|