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
package/src/commands/full.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// full
|
|
2
|
-
//
|
|
3
|
-
// gitignore
|
|
1
|
+
// full mode orchestrator: performs the actual install from the context settled by interactive/non-interactive mode.
|
|
2
|
+
// Copy order: workflows (+env substitution) -> flutter app files -> version.yml -> readme -> scripts -> gitignore (conditional)
|
|
3
|
+
// gitignore is updated only when conflict backup byproducts (.bak/.template.yaml) were actually created in this run.
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
6
6
|
import { writeText, findUnwritable } from "../core/fsutil.js";
|
|
@@ -20,33 +20,34 @@ import { cleanupOtherDeployWorkflows, payloadWorkflowNames, DEFAULT_DEPLOY_STYLE
|
|
|
20
20
|
import { hooksFor } from "../core/types.js";
|
|
21
21
|
import { findStaleWorkflows, cleanupStaleWorkflows } from "../core/removal-plan.js";
|
|
22
22
|
import { log, maskValue } from "../core/logger.js";
|
|
23
|
+
import { t } from "../i18n/index.js";
|
|
23
24
|
|
|
24
25
|
// context: { version, types, paths:Map, branch, versionCode,
|
|
25
26
|
// force, repoName, resolvers, now, today }
|
|
26
|
-
// payloadRoot:
|
|
27
|
+
// payloadRoot: the package's payload/ root. targetRoot: the install target.
|
|
27
28
|
export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
28
29
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
29
30
|
force = true, now, today, templateVersion = "unknown",
|
|
30
31
|
includeSemverAuto } = context;
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
+
// Check permissions before writing anything: stopping midway would leave a half-installed state.
|
|
33
34
|
const blocked = findUnwritable(targetRoot,
|
|
34
35
|
[".", PATHS.workflowsDir, PATHS.scriptsDir, ".github/.wizard"],
|
|
35
36
|
[PATHS.versionFile, "README.md", ".gitignore", ".github/.wizard/baseline.json"]);
|
|
36
37
|
if (blocked.length) {
|
|
37
|
-
throw new CliError(
|
|
38
|
+
throw new CliError(t("cmd.full.noWritePermission", { list: blocked.map((p) => ` - ${p}`).join("\n") }));
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
// project_paths
|
|
41
|
-
//
|
|
42
|
-
// version.yml
|
|
41
|
+
// Compute the project_paths markers.
|
|
42
|
+
// Use the file that actually exists in that folder, not the canonical marker name: writing "# build.gradle"
|
|
43
|
+
// into version.yml of a repo that only has build.gradle.kts would be the same kind of lie as in the detection log.
|
|
43
44
|
const pathMarkers = new Map();
|
|
44
|
-
for (const [
|
|
45
|
-
const marker = existingMarkerInDir(
|
|
46
|
-
pathMarkers.set(
|
|
47
|
-
//
|
|
45
|
+
for (const [type, p] of paths) {
|
|
46
|
+
const marker = existingMarkerInDir(type, join(targetRoot, p || "."));
|
|
47
|
+
pathMarkers.set(type, marker);
|
|
48
|
+
// If the file does not actually exist, the user picked the type by hand; citing the canonical file name as evidence would look like detection.
|
|
48
49
|
const found = marker && existsSync(join(targetRoot, p || ".", marker));
|
|
49
|
-
log.info("detect", "type",
|
|
50
|
+
log.info("detect", "type", t("cmd.full.log.typeEvidence", { type, evidence: found ? marker : t("cmd.full.log.userChosen") }));
|
|
50
51
|
}
|
|
51
52
|
log.info("detect", "version", `${version}${context.versionSource ? ` (${context.versionSource})` : ""}`);
|
|
52
53
|
log.info("detect", "branch", `${branch}${context.branches ? ` | main=${context.branches.main} develop=${context.branches.develop} mode=${context.branches.mode}` : ""}`);
|
|
@@ -55,63 +56,63 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
55
56
|
log.info("prompt", a.isDefault ? "default" : "answer", `${a.key}=${maskValue(a.key, a.value)}`);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// 1.
|
|
59
|
-
// hooks.decisions:
|
|
59
|
+
// 1. Copy workflows (+ env substitution) and collect the ask values for the deploy block.
|
|
60
|
+
// hooks.decisions: Map of interactive three-way conflict decisions (unset = skip, the current force behavior)
|
|
60
61
|
const wfCounters = copyWorkflows(context, payloadRoot, targetRoot, hooks);
|
|
61
62
|
const deployValues = wfCounters.deployValues || new Map(); // Map<type, Map<key,value>>
|
|
62
63
|
|
|
63
|
-
// 1-1. Flutter
|
|
64
|
-
//
|
|
64
|
+
// 1-1. Flutter app files (fastlane, ExportOptions): users fill these in, so create them only when missing.
|
|
65
|
+
// Runs after the workflow copy finishes (the workflows assume these files).
|
|
65
66
|
const flutterApp = copyTypeAppFiles(context, payloadRoot, targetRoot, (tag, action, f) => {
|
|
66
|
-
log.info(tag, action, action === "keep" ?
|
|
67
|
+
log.info(tag, action, action === "keep" ? t("cmd.full.log.keepExisting", { file: f }) : f);
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
//
|
|
70
|
+
// Preserve unknown top-level fields of an existing version.yml when regenerating it.
|
|
70
71
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
71
72
|
const prevVy = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
72
73
|
const extraTopLevel = prevVy != null ? parseExisting(prevVy).extraTopLevel : [];
|
|
73
74
|
|
|
74
|
-
// 2. version.yml
|
|
75
|
-
//
|
|
75
|
+
// 2. Generate version.yml (render payload/version.yml.template; full regeneration)
|
|
76
|
+
// A re-run where only the date lines differ does not rewrite it (idempotent).
|
|
76
77
|
const vyText = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel });
|
|
77
78
|
if (prevVy == null || !sameIgnoringTimestamps(prevVy, vyText)) {
|
|
78
79
|
writeText(vyPath, vyText);
|
|
79
80
|
log.info("version", "write", `version.yml (v${version}, code=${versionCode})`);
|
|
80
81
|
} else {
|
|
81
|
-
log.info("version", "skip",
|
|
82
|
+
log.info("version", "skip", t("cmd.full.log.versionUnchanged", { version, code: versionCode }));
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
// 3. README
|
|
85
|
+
// 3. README version section
|
|
85
86
|
const readme = addVersionSectionToReadme(version, targetRoot);
|
|
86
87
|
log.info("readme", readme === "added" ? "append" : "skip", README_STATUS_LABEL[readme] || readme);
|
|
87
88
|
|
|
88
|
-
// 4. scripts (payload/scripts/*.py
|
|
89
|
+
// 4. scripts (payload/scripts/*.py -> .github/scripts/): always overwritten, so also record that user edits are lost.
|
|
89
90
|
const scripts = copyScripts(payloadRoot, targetRoot);
|
|
90
91
|
for (const { name, action } of scripts) {
|
|
91
|
-
log.info("script", action, `${PATHS.scriptsDir}/${name}${action === "overwrite" ? "
|
|
92
|
+
log.info("script", action, `${PATHS.scriptsDir}/${name}${action === "overwrite" ? t("cmd.full.log.scriptOverwritten") : ""}`);
|
|
92
93
|
}
|
|
93
94
|
const bytecodeRemoved = removeScriptBytecode(targetRoot);
|
|
94
|
-
for (const rel of bytecodeRemoved) log.info("script", "remove",
|
|
95
|
+
for (const rel of bytecodeRemoved) log.info("script", "remove", t("cmd.full.log.bytecodeRemoved", { file: rel }));
|
|
95
96
|
|
|
96
|
-
// 5. gitignore
|
|
97
|
-
//
|
|
97
|
+
// 5. gitignore: updated only when conflict handling actually created .bak or .template.yaml files.
|
|
98
|
+
// Installs without conflicts (most first installs) do not touch .gitignore at all.
|
|
98
99
|
const gitignoreUpdated0 = wfCounters.backupAdded > 0 || wfCounters.templateAdded > 0;
|
|
99
100
|
|
|
100
|
-
// 6.
|
|
101
|
-
//
|
|
101
|
+
// 6. Clean up the previous deploy style: reinstalling with a different style would leave the old CD and deploy twice.
|
|
102
|
+
// Now, while the old baseline is still alive, is the only time we can judge "did the user touch it?".
|
|
102
103
|
const previousBaseline = readBaseline(targetRoot);
|
|
103
104
|
const { cleanup, storeCleanup, staleCleanup } = cleanupWorkflows(context, payloadRoot, targetRoot, previousBaseline,
|
|
104
105
|
{ justWritten: wfCounters.copiedFiles || [] });
|
|
105
|
-
//
|
|
106
|
+
// Drop removed files' reference points from the baseline too, or the next run mistakes them for "deleted by the user".
|
|
106
107
|
for (const r of [cleanup, storeCleanup, staleCleanup]) {
|
|
107
108
|
for (const f of [...r.removed, ...r.backedUp]) delete previousBaseline?.files?.[f];
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
for (const f of cleanup.removed || []) log.info("cleanup", "remove", `${f} (
|
|
111
|
+
for (const f of cleanup.removed || []) log.info("cleanup", "remove", `${f} (${t("cmd.dryRun.reason.cleanup")})`);
|
|
111
112
|
for (const f of cleanup.backedUp || []) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
112
|
-
for (const f of storeCleanup.removed) log.info("cleanup", "remove", `${f} (
|
|
113
|
+
for (const f of storeCleanup.removed) log.info("cleanup", "remove", `${f} (${t("cmd.dryRun.reason.storeCleanup")})`);
|
|
113
114
|
for (const f of storeCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
114
|
-
for (const f of staleCleanup.removed) log.info("cleanup", "remove", `${f} (
|
|
115
|
+
for (const f of staleCleanup.removed) log.info("cleanup", "remove", `${f} (${t("cmd.dryRun.reason.staleCleanup")})`);
|
|
115
116
|
for (const f of staleCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
116
117
|
const cleanupCount = (key) => [cleanup, storeCleanup, staleCleanup].reduce((n, r) => n + r[key].length, 0);
|
|
117
118
|
|
|
@@ -120,13 +121,13 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
120
121
|
if (gitignoreUpdated) {
|
|
121
122
|
const gi = ensureGitignore(targetRoot);
|
|
122
123
|
log.info("gitignore", gi.created ? "create" : gi.added.length ? "append" : "skip",
|
|
123
|
-
gi.added.length ? `.gitignore += ${gi.added.join(", ")}` : ".
|
|
124
|
+
gi.added.length ? `.gitignore += ${gi.added.join(", ")}` : t("cmd.full.log.gitignoreExisting"));
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
// 7. baseline
|
|
127
|
-
// env
|
|
128
|
-
//
|
|
129
|
-
//
|
|
127
|
+
// 7. Write the baseline: the reference point that tells "who changed it" on the next update.
|
|
128
|
+
// Hash only after env substitution is fully done so the on-disk content is final; that is why it is
|
|
129
|
+
// recorded here rather than inside copyWorkflows.
|
|
130
|
+
// The existing baseline is merged, so reference points for files not touched this time are not lost.
|
|
130
131
|
writeBaseline(targetRoot, {
|
|
131
132
|
templateVersion,
|
|
132
133
|
installedAt: now || today || "",
|
|
@@ -136,52 +137,53 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
136
137
|
makeSrcText(context.branches || null, context.deployStyle || ""),
|
|
137
138
|
deployValues),
|
|
138
139
|
previous: previousBaseline,
|
|
139
|
-
//
|
|
140
|
+
// Record only newly created Flutter app files; existing user files (kept) are not candidates for full removal.
|
|
140
141
|
appFiles: new Map(flutterApp.created.map((rel) => [rel, appFileHash(readFileSync(join(targetRoot, rel), "utf8"))])),
|
|
141
142
|
});
|
|
142
143
|
|
|
143
|
-
// 8.
|
|
144
|
-
//
|
|
145
|
-
//
|
|
144
|
+
// 8. Post-install verification: re-read what was actually written to disk.
|
|
145
|
+
// Whether unsubstituted placeholders remain, and which Secrets the workflows need to run.
|
|
146
|
+
// It does not fail the install; the goal is to report facts and leave the judgment to the user.
|
|
146
147
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
147
148
|
const managed = [...(wfCounters.baselineTargets || new Map()).keys()];
|
|
148
149
|
const unresolved = scanUnsubstituted(wfDir, managed);
|
|
149
|
-
//
|
|
150
|
+
// Secrets that have a default or where either of two is enough are reported separately from the required ones.
|
|
150
151
|
const secretSets = classifySecrets(wfDir, managed);
|
|
151
152
|
const secrets = narrowSecretsBySshAuth(secretSets.required, firstDeployValue(deployValues, "SSH_AUTH_METHOD"));
|
|
152
153
|
const optionalSecrets = secretSets.optional;
|
|
153
154
|
|
|
154
|
-
// 9.
|
|
155
|
+
// 9. Summary: append a result block at the end of the file so the outcome is visible from the tail alone.
|
|
155
156
|
for (const u of unresolved) log.warn("verify", "unresolved", `${u.filename}:${u.line} ${u.token}`);
|
|
156
157
|
for (const [name, users] of secrets) log.info("verify", "secret", `${name} ← ${users.join(", ")}`);
|
|
157
|
-
for (const [name, users] of optionalSecrets) log.info("verify", "secret-opt",
|
|
158
|
-
// copiedFiles
|
|
158
|
+
for (const [name, users] of optionalSecrets) log.info("verify", "secret-opt", t("cmd.full.log.secretOptional", { name, users: users.join(", ") }));
|
|
159
|
+
// copiedFiles also contains the auto-updated ones; without excluding them the same file is counted as both installed and auto-updated.
|
|
159
160
|
const autoUpdated = new Set(wfCounters.autoUpdated || []);
|
|
160
161
|
log.summary([
|
|
161
|
-
["
|
|
162
|
-
["
|
|
163
|
-
["
|
|
164
|
-
["
|
|
165
|
-
["
|
|
166
|
-
//
|
|
167
|
-
["
|
|
168
|
-
["
|
|
169
|
-
["
|
|
170
|
-
["
|
|
162
|
+
[t("cmd.full.summary.installed"), t("cmd.full.summary.installedValue", { n: (wfCounters.copiedFiles || []).filter((f) => !autoUpdated.has(f)).length })],
|
|
163
|
+
[t("cmd.full.summary.autoUpdated"), t("cmd.full.summary.autoUpdatedValue", { n: autoUpdated.size })],
|
|
164
|
+
[t("cmd.full.summary.kept"), t("cmd.full.summary.keptValue", { n: (wfCounters.keptLocal || []).length })],
|
|
165
|
+
[t("cmd.full.summary.unchanged"), t("cmd.full.summary.unchangedValue", { n: (wfCounters.unchangedFiles || []).length })],
|
|
166
|
+
[t("cmd.full.summary.backup"), t("cmd.full.summary.backupValue", { n: wfCounters.backupAdded || 0 })],
|
|
167
|
+
// Deploy style changes, store deselection and old-workflow cleanup also delete files or move them to .bak; counted separately from conflict backups.
|
|
168
|
+
[t("cmd.full.summary.cleanup"), t("cmd.full.summary.cleanupValue", { removed: cleanupCount("removed"), backedUp: cleanupCount("backedUp") })],
|
|
169
|
+
[t("cmd.full.summary.unresolved"), t(unresolved.length ? "cmd.full.summary.unresolvedActionValue" : "cmd.full.summary.unresolvedValue", { n: unresolved.length })],
|
|
170
|
+
[t("cmd.full.summary.secrets"), t("cmd.full.summary.secretsValue", { n: secrets.size })],
|
|
171
|
+
[t("cmd.full.summary.result"), unresolved.length ? t("cmd.full.summary.resultWarn", { n: unresolved.length }) : "OK"],
|
|
171
172
|
]);
|
|
172
173
|
|
|
173
174
|
return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup, storeCleanup, staleCleanup, flutterApp, readme, scripts, bytecodeRemoved, optionalSecrets };
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
//
|
|
177
|
-
// 6
|
|
178
|
-
// 6-1
|
|
179
|
-
//
|
|
180
|
-
// Fastfile
|
|
181
|
-
// 6-2
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
177
|
+
// Three workflow cleanups: the real install (runFull) and the --dry-run preview use the same decisions.
|
|
178
|
+
// 6 Deploy style cleanup: reinstalling with a different style would leave the old CD and deploy twice.
|
|
179
|
+
// 6-1 Deselected store workflow cleanup: reinstalling with fewer store targets would let the old workflows
|
|
180
|
+
// keep running on every main push. Nothing is deleted when the selection is undecided (null) or Flutter is absent.
|
|
181
|
+
// Fastfile and ExportOptions belong to the user and are not handled here.
|
|
182
|
+
// 6-2 Cleanup of old workflows that were renamed or dropped from the payload.
|
|
183
|
+
// The rules are the same for all (delete if unmodified, .bak if modified). The three sets never share file
|
|
184
|
+
// names, so the outcome is independent of order.
|
|
185
|
+
// Pass the pre-cleanup baseline: it is the only reference that can judge "did the user touch it?".
|
|
186
|
+
// With dryRun it only decides and leaves the files alone.
|
|
185
187
|
export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { justWritten = [], dryRun = false } = {}) {
|
|
186
188
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
187
189
|
const listDir = () => (existsSync(workflowsDir) ? readdirSync(workflowsDir) : []);
|
|
@@ -198,8 +200,8 @@ export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { j
|
|
|
198
200
|
return { cleanup, storeCleanup, staleCleanup };
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
+
// Record the choices that shaped the install result (deploy style, auto bump, Copilot, type-specific options).
|
|
204
|
+
// Values picked interactively converge here too, so "why was it installed like this?" can later be traced from the log alone.
|
|
203
205
|
function logChoices(context, types) {
|
|
204
206
|
if (context.deployStyle) log.info("option", "deploy", context.deployStyle);
|
|
205
207
|
if (context.includeSemverAuto != null) log.info("option", "semver", context.includeSemverAuto ? "on" : "off");
|
|
@@ -209,25 +211,25 @@ function logChoices(context, types) {
|
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
//
|
|
213
|
-
// interactive:
|
|
214
|
+
// Facts to report separately from the completion summary: if a non-interactive run passes silently, users believe they received the whole update.
|
|
215
|
+
// interactive: in interactive mode the user picked the conflict outcome, so omit the conflict notice and report only the cleanup result.
|
|
214
216
|
export function postInstallNotices(result, { interactive = false } = {}) {
|
|
215
217
|
const lines = [];
|
|
216
218
|
const stale = result?.staleCleanup || { removed: [], backedUp: [] };
|
|
217
219
|
if (stale.removed.length || stale.backedUp.length) {
|
|
218
|
-
lines.push(
|
|
219
|
-
for (const f of stale.removed) lines.push(
|
|
220
|
-
for (const f of stale.backedUp) lines.push(
|
|
220
|
+
lines.push(t("cmd.full.notice.staleHeading"));
|
|
221
|
+
for (const f of stale.removed) lines.push(t("cmd.full.notice.staleRemoved", { file: f }));
|
|
222
|
+
for (const f of stale.backedUp) lines.push(t("cmd.full.notice.staleBackedUp", { file: f }));
|
|
221
223
|
}
|
|
222
224
|
const kept = interactive ? [] : result?.workflows?.conflictKept || [];
|
|
223
225
|
if (kept.length) {
|
|
224
|
-
lines.push(
|
|
225
|
-
lines.push(
|
|
226
|
+
lines.push(t("cmd.full.notice.conflictKept", { n: kept.length, files: kept.join(", ") }));
|
|
227
|
+
lines.push(t("cmd.full.notice.conflictHint"));
|
|
226
228
|
}
|
|
227
229
|
return lines;
|
|
228
230
|
}
|
|
229
231
|
|
|
230
|
-
// deployValues
|
|
232
|
+
// deployValues is Map<type, Map<key,value>>; used when only the first value is needed regardless of type.
|
|
231
233
|
function firstDeployValue(deployValues, key) {
|
|
232
234
|
for (const [, asks] of deployValues) {
|
|
233
235
|
const v = asks.get(key);
|
|
@@ -1,45 +1,46 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Install settings resolution step, shared by non-interactive (index.js) and interactive (interactive.js) mode.
|
|
2
|
+
// How values are filled in (flags vs. questions) differs per path, but the rules that settle type, version,
|
|
3
|
+
// deploy style and options and assemble the install context belong in one place, so fixing only one side
|
|
4
|
+
// cannot make the two modes diverge.
|
|
4
5
|
import { detectVersion, detectBuildNumber } from "../core/detect-fs.js";
|
|
5
6
|
import { createContext } from "../context.js";
|
|
6
7
|
import { mergeHookResults } from "../core/types.js";
|
|
7
8
|
import { isDeployStyle, DEFAULT_DEPLOY_STYLE, hasServerDeployWorkflows, effectiveDeployStyle } from "../core/deploy-style.js";
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
+
// Deploy style saved in version.yml; "" when missing or unknown.
|
|
10
11
|
export const savedDeployStyle = (existing) =>
|
|
11
12
|
isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
|
|
12
13
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
14
|
+
// Deploy style: explicit value (flag or question answer) -> saved value -> default.
|
|
15
|
+
// Types without server deploy workflows do not affect the install result, so nothing is recorded (null).
|
|
15
16
|
export function resolveDeployStyle({ payload, types, explicit = "", existing }) {
|
|
16
17
|
if (!hasServerDeployWorkflows(payload, types)) return null;
|
|
17
18
|
return explicit || savedDeployStyle(existing) || DEFAULT_DEPLOY_STYLE;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
-
// detectOpts
|
|
21
|
+
// Version: existing version.yml first (prevents overwriting on re-run) -> explicit value -> file detection.
|
|
22
|
+
// detectOpts is passed to the detection function as is (types, paths, warn, hint).
|
|
22
23
|
export function resolveVersion({ cwd, existing, explicit = "", ...detectOpts }) {
|
|
23
24
|
return existing?.version || explicit || detectVersion(cwd, detectOpts);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
+
// Build number: keep the existing value; for a new install detect it from the project files; 1 if not found.
|
|
27
28
|
export function resolveVersionCode({ cwd, existing, types, paths }) {
|
|
28
29
|
return existing?.versionCode ?? detectBuildNumber(cwd, { types, paths }) ?? 1;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
-
// deployStyle: resolveDeployStyle
|
|
33
|
-
// typeOptions:
|
|
34
|
-
// releaseOptions: { includeSemverAuto, includeCopilotAi }
|
|
35
|
-
// extra:
|
|
32
|
+
// Assemble the settled values into the install context.
|
|
33
|
+
// deployStyle: result of resolveDeployStyle (the chosen value); recorded as the style actually installed.
|
|
34
|
+
// typeOptions: type-specific options settled by the type hook (resolveOptions), settled on both paths; the type hook also converts them into context fields.
|
|
35
|
+
// releaseOptions: { includeSemverAuto, includeCopilotAi }, the resolveReleaseOptions result.
|
|
36
|
+
// extra: fields used by one path only (envValues etc.).
|
|
36
37
|
export function buildInstallContext({
|
|
37
38
|
payload, existing, templateVersion, types, deployStyle, typeOptions, releaseOptions, ...rest
|
|
38
39
|
}) {
|
|
39
40
|
return createContext({
|
|
40
41
|
types,
|
|
41
42
|
...releaseOptions,
|
|
42
|
-
//
|
|
43
|
+
// Guidance uses the chosen value, but the record uses the style actually installed.
|
|
43
44
|
deployStyle: effectiveDeployStyle(payload, types, deployStyle),
|
|
44
45
|
...mergeHookResults(types, "contextFields", typeOptions),
|
|
45
46
|
previousTemplateVersion: existing?.templateVersion || "",
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// envMode
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Flutter option questions of the interactive wizard - env mode, store deploy targets, deploy mode.
|
|
2
|
+
// State is passed around as an immutable { envMode, stores, androidDeployMode, iosDeployMode } object.
|
|
3
|
+
// "" for envMode/*DeployMode and null for stores mean undecided (an empty stores array means "no store deploy", already decided).
|
|
4
|
+
// Already-decided values are not asked again (convention: saved version.yml values skip the question). ESC (cancel) always means default:
|
|
5
|
+
// the default for a first-time question, the current value while editing.
|
|
6
6
|
import {
|
|
7
7
|
isEnvMode, isDeployMode, parseStoreList, STORE_PLATFORMS, DEFAULT_DEPLOY_MODE,
|
|
8
8
|
} from "../core/flutter-options.js";
|
|
9
9
|
import { deployModeWarning } from "../ui/prompts.js";
|
|
10
|
+
import { t } from "../i18n/index.js";
|
|
10
11
|
|
|
11
12
|
export const FLUTTER_EDIT_ITEMS = new Set(["envMode", "flutterStore", "deployMode"]);
|
|
12
13
|
|
|
13
14
|
const DEPLOY_MODE_KEY = { android: "androidDeployMode", ios: "iosDeployMode" };
|
|
14
15
|
|
|
15
|
-
// STORE_PLATFORMS
|
|
16
|
+
// Sort by STORE_PLATFORMS order and drop unknown values - keeps the per-platform question order fixed.
|
|
16
17
|
const normalizeStores = (picked) => STORE_PLATFORMS.filter((platform) => picked.includes(platform));
|
|
17
18
|
|
|
18
|
-
//
|
|
19
|
+
// Move the saved values of the existing version.yml into state. Invalid values are treated as not saved and asked again.
|
|
19
20
|
export function savedFlutterState(existing) {
|
|
20
21
|
const options = existing?.options ?? {};
|
|
21
22
|
return {
|
|
@@ -28,13 +29,13 @@ export function savedFlutterState(existing) {
|
|
|
28
29
|
|
|
29
30
|
async function askDeployMode(io, platform, initialValue) {
|
|
30
31
|
const picked = await io.selectDeployMode({ platform, initialValue });
|
|
31
|
-
const mode = isDeployMode(picked) ? picked : initialValue; // ESC =
|
|
32
|
+
const mode = isDeployMode(picked) ? picked : initialValue; // ESC = default
|
|
32
33
|
const warning = deployModeWarning(mode);
|
|
33
|
-
if (warning) io.note?.(warning, "
|
|
34
|
+
if (warning) io.note?.(warning, t("interactive.flutter.deployModeTitle"));
|
|
34
35
|
return mode;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
+
// Ask only for chosen platforms that have no deploy mode yet.
|
|
38
39
|
async function askUnsetDeployModes(io, state) {
|
|
39
40
|
const next = { ...state };
|
|
40
41
|
for (const platform of state.stores ?? []) {
|
|
@@ -44,23 +45,23 @@ async function askUnsetDeployModes(io, state) {
|
|
|
44
45
|
return next;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
//
|
|
48
|
-
// envModeDefault
|
|
49
|
-
// inferredStores
|
|
48
|
+
// Ask only the options that are not decided yet.
|
|
49
|
+
// envModeDefault - dart-define for new installs / dotenv for existing installs (preserves behaviour)
|
|
50
|
+
// inferredStores - stores an existing install without saved values already used (new installs get all, same as the CLI default)
|
|
50
51
|
export async function askUnsetFlutterOptions(io, state, { envModeDefault, inferredStores }) {
|
|
51
52
|
const next = { ...state };
|
|
52
53
|
if (!next.envMode) {
|
|
53
54
|
const picked = await io.selectEnvMode({ initialValue: envModeDefault });
|
|
54
|
-
next.envMode = isEnvMode(picked) ? picked : envModeDefault; // ESC =
|
|
55
|
+
next.envMode = isEnvMode(picked) ? picked : envModeDefault; // ESC = default
|
|
55
56
|
}
|
|
56
57
|
if (next.stores === null) {
|
|
57
58
|
const picked = await io.selectFlutterStores({ initialValues: inferredStores });
|
|
58
|
-
next.stores = Array.isArray(picked) ? normalizeStores(picked) : inferredStores; // ESC =
|
|
59
|
+
next.stores = Array.isArray(picked) ? normalizeStores(picked) : inferredStores; // ESC = initial selection
|
|
59
60
|
}
|
|
60
61
|
return askUnsetDeployModes(io, next);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
+
// Re-ask one item chosen in the edit menu. The current value is the initial selection and ESC keeps it.
|
|
64
65
|
export async function editFlutterOption(io, what, state, envModeDefault) {
|
|
65
66
|
if (what === "envMode") {
|
|
66
67
|
const picked = await io.selectEnvMode({ initialValue: state.envMode || envModeDefault });
|
|
@@ -70,18 +71,18 @@ export async function editFlutterOption(io, what, state, envModeDefault) {
|
|
|
70
71
|
const picked = await io.selectFlutterStores({ initialValues: state.stores ?? [] });
|
|
71
72
|
if (!Array.isArray(picked)) return state;
|
|
72
73
|
const stores = normalizeStores(picked);
|
|
73
|
-
//
|
|
74
|
+
// Reset the deploy mode of deselected platforms - otherwise the old value survives re-selection and is never asked again.
|
|
74
75
|
const reset = { ...state, stores };
|
|
75
76
|
for (const platform of STORE_PLATFORMS) {
|
|
76
77
|
if (!stores.includes(platform)) reset[DEPLOY_MODE_KEY[platform]] = "";
|
|
77
78
|
}
|
|
78
|
-
//
|
|
79
|
+
// Ask the deploy mode only for platforms newly added or just reset - modes of already-decided platforms are kept.
|
|
79
80
|
return askUnsetDeployModes(io, reset);
|
|
80
81
|
}
|
|
81
82
|
if (what === "deployMode") {
|
|
82
83
|
const stores = state.stores ?? [];
|
|
83
84
|
if (!stores.length) {
|
|
84
|
-
io.note?.("
|
|
85
|
+
io.note?.(t("interactive.flutter.storeFirst"), t("interactive.flutter.deployModeTitle"));
|
|
85
86
|
return state;
|
|
86
87
|
}
|
|
87
88
|
const next = { ...state };
|