project-auto-wizard 0.15.0 โ†’ 0.16.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.
Files changed (65) hide show
  1. package/README.ja.md +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +1 -1
  5. package/package.json +1 -1
  6. package/payload/config/breaking-changes.json +31 -5
  7. package/payload/config/wizard-prompts.yml +0 -4
  8. package/payload/scripts/messages.py +162 -26
  9. package/payload/scripts/version_manager.py +69 -40
  10. package/payload/version.yml.template +6 -2
  11. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +3 -0
  12. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +64 -1
  13. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +61 -12
  14. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +46 -10
  15. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +40 -11
  16. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +24 -8
  17. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +29 -10
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +57 -23
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +155 -72
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +49 -20
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +162 -62
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +184 -87
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +38 -15
  24. package/payload/workflows/go/PROJECT-GO-CI.yaml +14 -5
  25. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +375 -120
  26. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +9 -4
  27. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +12 -4
  28. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +375 -120
  29. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +9 -4
  30. package/payload/workflows/react/PROJECT-REACT-CI.yaml +38 -9
  31. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +19 -5
  32. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +71 -35
  33. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +9 -6
  34. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +7 -4
  35. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +425 -144
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +7 -4
  37. package/src/cli/args.js +32 -22
  38. package/src/commands/doctor.js +20 -0
  39. package/src/commands/dry-run.js +9 -1
  40. package/src/commands/full.js +9 -4
  41. package/src/commands/interactive.js +29 -30
  42. package/src/commands/status.js +19 -5
  43. package/src/context.js +4 -2
  44. package/src/core/copy/simple.js +31 -0
  45. package/src/core/copy/workflows.js +2 -2
  46. package/src/core/deploy-style.js +1 -1
  47. package/src/core/detect.js +3 -2
  48. package/src/core/options.js +88 -0
  49. package/src/core/release-options.js +6 -23
  50. package/src/core/types.js +15 -8
  51. package/src/core/version-yml.js +68 -26
  52. package/src/i18n/catalog/en/cli.js +3 -1
  53. package/src/i18n/catalog/en/commands.js +15 -0
  54. package/src/i18n/catalog/en/core2.js +3 -0
  55. package/src/i18n/catalog/en/ui.js +1 -0
  56. package/src/i18n/catalog/ko/cli.js +3 -1
  57. package/src/i18n/catalog/ko/commands.js +15 -0
  58. package/src/i18n/catalog/ko/core2.js +3 -0
  59. package/src/i18n/catalog/ko/ui.js +1 -0
  60. package/src/index.js +7 -3
  61. package/src/ui/env-plan.js +1 -1
  62. package/src/ui/prompts.js +9 -5
  63. package/src/ui/status-cards.js +2 -2
  64. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +0 -241
  65. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +0 -330
@@ -0,0 +1,88 @@
1
+ // Option registry - one declaration per boolean release option; CLI flags, value resolution, version.yml parse/render
2
+ // and status/log lines are all derived from it (same idea as core/types.js for project types).
3
+ // Adding an option means adding one entry here; see ADDING-AN-OPTION.md.
4
+ //
5
+ // default : value on a new install when neither a flag nor a saved value exists
6
+ // legacyDefault : value on an existing install (version.yml present) whose file lacks this key
7
+ // semver_auto - false: an install from before the feature must not silently turn on and let one ambiguous commit bump major
8
+ // copilot_ai - false: opt-in because it consumes AI Credits
9
+ // release_automerge - true for both: automerge is what every existing install already does, so a missing key means ON
10
+ // conflictKey : i18n key of the error thrown when --<flag> and --no-<flag> are both given (catalog lives in src/i18n)
11
+ // logLabel : label used by the install log line ("option <logLabel> on|off")
12
+ // ask : present only for options the interactive wizard asks about / lists in the edit menu
13
+ // { questionKey, initial, menuLabelKey, summaryKey, cardIcon, cardLabelKey } (all i18n keys except initial/cardIcon)
14
+ // Keys only - this module never imports the i18n layer, so core stays free of UI dependencies.
15
+ export const OPTIONS = Object.freeze([
16
+ {
17
+ key: "semver_auto", name: "semverAuto", ctxField: "includeSemverAuto",
18
+ flag: "semver-auto", default: true, legacyDefault: false,
19
+ conflictKey: "cli.args.semverConflict", logLabel: "semver",
20
+ ask: {
21
+ questionKey: "interactive.question.semverAuto", initial: true,
22
+ menuLabelKey: "ui.prompts.edit.semverAuto", summaryKey: "interactive.summary.semverAuto",
23
+ cardIcon: "๐Ÿ”ข", cardLabelKey: "ui.status-cards.card.autoBump",
24
+ },
25
+ },
26
+ {
27
+ key: "copilot_ai", name: "copilotAi", ctxField: "includeCopilotAi",
28
+ flag: "copilot", default: false, legacyDefault: false,
29
+ conflictKey: "cli.args.copilotConflict", logLabel: "copilot",
30
+ ask: {
31
+ questionKey: "interactive.question.copilotAi", initial: false,
32
+ menuLabelKey: "ui.prompts.edit.copilotAi", summaryKey: "interactive.summary.copilotAi",
33
+ cardIcon: "๐Ÿค–", cardLabelKey: "ui.status-cards.card.copilot",
34
+ },
35
+ },
36
+ {
37
+ // No `ask`: the wizard never asks about it - the flag or version.yml is the control (default ON).
38
+ key: "release_automerge", name: "releaseAutomerge", ctxField: "includeReleaseAutomerge",
39
+ flag: "release-automerge", default: true, legacyDefault: true,
40
+ conflictKey: "cli.args.releaseAutomergeConflict", logLabel: "release-automerge",
41
+ },
42
+ ]);
43
+
44
+ export const optionByKey = (key) => OPTIONS.find((o) => o.key === key);
45
+
46
+ // version.yml template variable name, e.g. semver_auto -> OPT_SEMVER_AUTO
47
+ export const optionVar = (o) => `OPT_${o.key.toUpperCase()}`;
48
+
49
+ // Context fields (includeX) -> explicit-value map (name -> boolean|null)
50
+ export function explicitFromContext(ctx = {}) {
51
+ return Object.fromEntries(OPTIONS.map((o) => [o.name, ctx[o.ctxField] ?? null]));
52
+ }
53
+
54
+ // The option fields of a context/opts object only (what the interactive wizard receives as its starting values)
55
+ export function optionContextFields(ctx = {}) {
56
+ return Object.fromEntries(OPTIONS.map((o) => [o.ctxField, ctx[o.ctxField]]));
57
+ }
58
+
59
+ // Context defaults: null = not decided yet
60
+ export function defaultContextFields() {
61
+ return Object.fromEntries(OPTIONS.map((o) => [o.ctxField, null]));
62
+ }
63
+
64
+ // Only explicit and saved values are applied - null means undecided (interactive mode uses it to decide whether to ask).
65
+ export function pickOptionValues(explicit = {}, existing = null) {
66
+ return Object.fromEntries(
67
+ OPTIONS.map((o) => [o.name, explicit[o.name] ?? existing?.options?.[o.name] ?? null]),
68
+ );
69
+ }
70
+
71
+ // Final values keyed by context field - undecided options get default (new install) or legacyDefault (existing install).
72
+ export function resolveOptionValues(explicit = {}, existing = null) {
73
+ const picked = pickOptionValues(explicit, existing);
74
+ return Object.fromEntries(
75
+ OPTIONS.map((o) => {
76
+ const v = picked[o.name];
77
+ return [o.ctxField, v === null ? (existing ? o.legacyDefault : o.default) : v === true];
78
+ }),
79
+ );
80
+ }
81
+
82
+ // Values for rendering version.yml: a context field that is unset (null/undefined) falls back to the option's default.
83
+ export function renderValues(ctx = {}) {
84
+ return Object.fromEntries(OPTIONS.map((o) => [o.ctxField, (ctx[o.ctxField] ?? o.default) === true]));
85
+ }
86
+
87
+ // Options the interactive wizard asks about (those declaring `ask`).
88
+ export const askableOptions = () => OPTIONS.filter((o) => o.ask);
@@ -1,23 +1,6 @@
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
-
5
- // Reflects only explicit and saved values. null means not decided yet (interactive mode uses it to decide whether to ask).
6
- export function pickReleaseOptions(explicit = {}, existing = null) {
7
- return {
8
- semverAuto: explicit.semverAuto ?? existing?.options?.semverAuto ?? null,
9
- copilotAi: explicit.copilotAi ?? existing?.options?.copilotAi ?? null,
10
- };
11
- }
12
-
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
- export function resolveReleaseOptions(explicit = {}, existing = null) {
18
- const { semverAuto, copilotAi } = pickReleaseOptions(explicit, existing);
19
- return {
20
- includeSemverAuto: semverAuto === null ? !existing : semverAuto !== false,
21
- includeCopilotAi: copilotAi === true,
22
- };
23
- }
1
+ // Value-decision rules for the release options (semver_auto, copilot_ai, ...), shared by non-interactive (index.js) and
2
+ // interactive (interactive.js) modes. The rules themselves live in the option registry (core/options.js); this file keeps
3
+ // the existing import path and names.
4
+ // Priority: explicit value (CLI flag / prompt answer) -> saved value in version.yml -> default. Keeping one implementation
5
+ // stops the same repo from being installed with different settings depending on how the wizard was run.
6
+ export { pickOptionValues as pickReleaseOptions, resolveOptionValues as resolveReleaseOptions } from "./options.js";
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
- id: "next",
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: 4,
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.
@@ -1,6 +1,8 @@
1
1
  import { DEFAULT_DEPLOY_STYLE } from "./deploy-style.js";
2
+ import { OPTIONS, optionVar, renderValues } from "./options.js";
2
3
  import { escapeYamlDoubleQuoted } from "./wizard-env.js";
3
- import { hooksFor, mergeHookResults, allHookValues } from "./types.js";
4
+ import { hooksFor, mergeHookResults, allHookValues, canonicalTypeId, canonicalTypeIds } from "./types.js";
5
+ import { normalizePath } from "./paths.js";
4
6
  import { DEFAULT_LANGUAGE, isSupportedLanguage, normalizeLanguage } from "../i18n/languages.js";
5
7
  // Aliased: `t` is used as a local variable name (type, trimmed line) throughout this file.
6
8
  import { t as tr } from "../i18n/index.js";
@@ -40,15 +42,17 @@ export function parseExtraTopLevel(content) {
40
42
 
41
43
  // Type-specific option key -> returned field (type hook savedOptionKeys). The value is returned as the raw string; validity is up to the type hook's resolveOptions.
42
44
  const TYPE_OPTION_KEYS = allHookValues("savedOptionKeys");
45
+ const OPTION_BY_KEY = new Map(OPTIONS.map((o) => [o.key, o]));
46
+ const OPTION_LINE = new RegExp(`^\\s+(${OPTIONS.map((o) => o.key).join("|")}):\\s*(.+)`);
43
47
  const TYPE_OPTION_LINE = new RegExp(`^\\s+(${Object.keys(TYPE_OPTION_KEYS).join("|")}):\\s*(.+)`);
44
48
 
45
49
  // State-machine parse of metadata.template.options.
46
- // Returns: { semverAuto: bool|null, copilotAi: bool|null, deployStyle: string|null,
50
+ // Returns: { <one bool|null per registry option, e.g. semverAuto/copilotAi>, deployStyle: string|null,
47
51
  // type-specific option fields (e.g. envMode/flutterStore/androidDeployMode/iosDeployMode): string|null } - null = not written.
48
52
  // Other keys such as the old synology/coderabbit ones hit no branch and are naturally ignored (no parse error).
49
53
  export function parseTemplateOptions(content) {
50
54
  const out = {
51
- semverAuto: null, copilotAi: null, deployStyle: null,
55
+ ...Object.fromEntries(OPTIONS.map((o) => [o.name, null])), deployStyle: null,
52
56
  ...Object.fromEntries(Object.values(TYPE_OPTION_KEYS).map((field) => [field, null])),
53
57
  };
54
58
  // Value normalization: strip quotes + trim
@@ -65,18 +69,12 @@ export function parseTemplateOptions(content) {
65
69
  if (m) { const v = strip(m[1]); if (v) out.deployStyle = v; continue; }
66
70
  m = line.match(TYPE_OPTION_LINE);
67
71
  if (m) { const v = strip(m[2]); if (v) out[TYPE_OPTION_KEYS[m[1]]] = v; continue; }
68
- m = line.match(/^\s+semver_auto:\s*(.+)/);
69
- if (m) {
70
- const v = strip(m[1]);
71
- if (v === "true") out.semverAuto = true;
72
- if (v === "false") out.semverAuto = false;
73
- continue;
74
- }
75
- m = line.match(/^\s+copilot_ai:\s*(.+)/);
76
- if (m) {
77
- const v = strip(m[1]);
78
- if (v === "true") out.copilotAi = true;
79
- if (v === "false") out.copilotAi = false;
72
+ // Boolean options come from the registry; a value other than true/false is left null (= not written).
73
+ const optionLine = line.match(OPTION_LINE);
74
+ if (optionLine) {
75
+ const v = strip(optionLine[2]);
76
+ if (v === "true") out[OPTION_BY_KEY.get(optionLine[1]).name] = true;
77
+ if (v === "false") out[OPTION_BY_KEY.get(optionLine[1]).name] = false;
80
78
  continue;
81
79
  }
82
80
  // another key indented 0-4 spaces -> end of the options section
@@ -88,6 +86,21 @@ export function parseTemplateOptions(content) {
88
86
  return out;
89
87
  }
90
88
 
89
+ // User-facing lines for entries parseExisting() folded away: what is kept, what drops out, and how to keep the other folder.
90
+ // finalPaths (Map<type, folder>) is the folder set the run will actually write; without it the saved winner is assumed.
91
+ // The message follows the real outcome, so an explicit --paths choice is never told the opposite.
92
+ export function droppedPathLines(droppedPaths = [], finalPaths = null) {
93
+ return droppedPaths.flatMap((d) => {
94
+ const final = finalPaths?.get(d.type) ?? d.kept;
95
+ const same = (x) => normalizePath(x) === normalizePath(final);
96
+ const lost = same(d.kept) ? d.path : same(d.path) ? d.kept : `${d.kept}, ${d.path}`;
97
+ const lines = [tr("core.versionYml.pathMerged", { keptName: d.keptName, kept: d.kept, name: d.name, path: d.path, type: d.type, final, lost })];
98
+ // The re-run hint only makes sense when the saved winner is what stays.
99
+ if (same(d.kept)) lines.push(tr("core.versionYml.pathMergedHint", { type: d.type, path: d.path }));
100
+ return lines;
101
+ });
102
+ }
103
+
91
104
  // Extract values from an existing version.yml (line-based, avoids false hits on comment lines).
92
105
  export function parseExisting(content) {
93
106
  const text = String(content || "");
@@ -107,21 +120,45 @@ export function parseExisting(content) {
107
120
  // project_types: ["a","b"]
108
121
  const typesRaw = line(/^project_types:\s*(\[[^\]]*\])/);
109
122
  let types = [];
110
- if (typesRaw) types = [...typesRaw.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
123
+ // Old names (next) are read as their current type so every later step, including the rewrite, only sees canonical ids.
124
+ if (typesRaw) types = canonicalTypeIds([...typesRaw.matchAll(/"([^"]+)"/g)].map((m) => m[1]));
111
125
  // language: "en" - only supported values count; a hand-edited unknown value is treated as unset
112
126
  const langRaw = normalizeLanguage((line(/^language:\s*(.+)/) || "").replace(/\s+#.*$/, "").replace(/["']/g, ""));
113
127
  const language = isSupportedLanguage(langRaw) ? langRaw : null;
114
128
  // project_paths block: ` type: "path"`
115
129
  const paths = new Map();
130
+ // Entries folded into another entry of the same canonical type with a different folder (react: client + next: web).
131
+ // A type holds one folder, so these are lost on rewrite; callers surface them instead of dropping them silently.
132
+ const droppedPaths = [];
133
+ const pathNames = new Map(); // canonical id -> the name the kept folder was written under
116
134
  let inPaths = false;
117
135
  for (const l of text.split("\n")) {
118
136
  if (/^project_paths:/.test(l)) { inPaths = true; continue; }
119
137
  if (inPaths) {
120
138
  const m = l.match(/^\s+([a-z-]+):\s*"([^"]*)"/);
121
- if (m) paths.set(m[1], m[2]);
139
+ // The first entry wins when an old name and its current name are both present.
140
+ if (m) {
141
+ const id = canonicalTypeId(m[1]);
142
+ if (!paths.has(id)) { paths.set(id, m[2]); pathNames.set(id, m[1]); }
143
+ 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] });
144
+ }
122
145
  else if (/^\S/.test(l)) inPaths = false; // end of indentation -> end of block
123
146
  }
124
147
  }
148
+ // A name listed in project_types without a project_paths entry lives at the repo root, so when it shares a
149
+ // canonical type with a name that has a folder, the root is the folder that drops out.
150
+ if (typesRaw) {
151
+ const names = [...new Set([...typesRaw.matchAll(/"([^"]+)"/g)].map((m) => m[1]))];
152
+ const written = new Set(text.split("\n").map((l) => l.match(/^\s+([a-z-]+):\s*"/)?.[1]).filter(Boolean));
153
+ for (const id of new Set(names.map(canonicalTypeId))) {
154
+ const group = names.filter((n) => canonicalTypeId(n) === id);
155
+ if (group.length < 2 || !paths.has(id)) continue;
156
+ for (const n of group) {
157
+ if (written.has(n) || normalizePath(paths.get(id)) === ".") continue;
158
+ droppedPaths.push({ type: id, keptName: pathNames.get(id), kept: paths.get(id), name: n, path: "." });
159
+ }
160
+ }
161
+ }
125
162
  // version inside the template: block
126
163
  let templateVersion = "";
127
164
  let inTemplate = false;
@@ -138,7 +175,7 @@ export function parseExisting(content) {
138
175
  // metadata.template.branches - main/develop/mode (to skip re-asking in update mode)
139
176
  const branches = parseTemplateBranches(text);
140
177
  return {
141
- version, versionCode, types, language, paths, templateVersion, options, branches,
178
+ version, versionCode, types, language, paths, droppedPaths, templateVersion, options, branches,
142
179
  deploy: parseDeployBlock(text), extraTopLevel: parseExtraTopLevel(text),
143
180
  };
144
181
  }
@@ -157,9 +194,15 @@ export function parseDeployBlock(content) {
157
194
  if (!inDeploy) continue;
158
195
  if (/^\S/.test(l)) { inDeploy = false; continue; } // next top-level key -> end of block
159
196
  const t = l.match(/^ {2}([a-z][a-z-]*):\s*(?:#.*)?$/);
160
- if (t) { current = new Map(); out.set(t[1], current); continue; }
197
+ if (t) {
198
+ // An old type name shares the block of its current name; keys already present there win.
199
+ const id = canonicalTypeId(t[1]);
200
+ current = out.get(id) ?? new Map();
201
+ out.set(id, current);
202
+ continue;
203
+ }
161
204
  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"));
205
+ if (kv && current && !current.has(kv[1])) current.set(kv[1], kv[2].replace(/\\(["\\])/g, "$1"));
163
206
  }
164
207
  return out;
165
208
  }
@@ -210,8 +253,9 @@ export function buildVersionYml({
210
253
  const b = branches || { main: branch || "main", develop: "develop", mode: "pr-flow" };
211
254
  const {
212
255
  templateVersion = "unknown",
213
- includeSemverAuto = true, includeCopilotAi = false, deployStyle = "", optionsDate = today,
256
+ deployStyle = "", optionsDate = today,
214
257
  } = templateOptions || {};
258
+ const optionValues = renderValues(templateOptions || {});
215
259
 
216
260
  // project_paths block (full-line token {{PROJECT_PATHS}} - line removed when absent)
217
261
  let pathsBlock = "";
@@ -249,8 +293,7 @@ export function buildVersionYml({
249
293
  NOW: now, TODAY: today || optionsDate, DEFAULT_BRANCH: branch,
250
294
  TEMPLATE_VERSION: templateVersion,
251
295
  MAIN_BRANCH: b.main, DEVELOP_BRANCH: b.develop, BRANCH_MODE: b.mode,
252
- OPT_SEMVER_AUTO: String(includeSemverAuto),
253
- OPT_COPILOT_AI: String(includeCopilotAi),
296
+ ...Object.fromEntries(OPTIONS.map((o) => [optionVar(o), String(optionValues[o.ctxField])])),
254
297
  OPT_DEPLOY_STYLE: String(deployStyle || ""),
255
298
  };
256
299
 
@@ -289,15 +332,14 @@ export function sameIgnoringTimestamps(a, b) {
289
332
  export function renderVersionYml(context, templateText, { pathMarkers, deployValues = new Map(), extraTopLevel = [] }) {
290
333
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
291
334
  now, today, templateVersion = "unknown", branches = null, language,
292
- includeSemverAuto, includeCopilotAi, deployStyle } = context;
335
+ deployStyle } = context;
293
336
  return buildVersionYml({
294
337
  templateText, version, types, paths, pathMarkers, branch, branches, versionCode, now, today,
295
338
  deployValues, extraTopLevel, language,
296
339
  typeOptions: mergeHookResults(types, "optionsFromContext", context),
297
340
  templateOptions: {
298
341
  templateVersion,
299
- includeSemverAuto: includeSemverAuto !== false,
300
- includeCopilotAi: includeCopilotAi === true,
342
+ ...renderValues(context),
301
343
  // null = a type with no server deploy workflow, so the deploy style is meaningless (record omitted)
302
344
  deployStyle: deployStyle === null ? null : (deployStyle || DEFAULT_DEPLOY_STYLE),
303
345
  optionsDate: today,
@@ -5,7 +5,7 @@ export default {
5
5
  "cli.lang.missing": "--lang requires a value. Supported: {supported}",
6
6
  "cli.lang.help": "Message language: {supported} (default: en)",
7
7
  "cli.bin.nodeVersion": "Node.js 20.12 or later is required (current: {current})",
8
- "cli.help.text": "project-auto-wizard โ€” One command DevOps: GitHub-native release automation setup wizard\n\nUsage:\n npx project-auto-wizard [options]\n\nOptions:\n -m, --mode MODE Unified mode (full | uninstall | status | doctor)\n Default: interactive. full = install and update\n uninstall = full removal (interactive checklist; with --force, opt in via --purge-*)\n status = check install state and drift (read-only). doctor = environment diagnosis (read-only)\n -t, --type CSV Project types as csv (e.g. spring,react,python)\n Supported: {types}\n --project-version V Initial version of the target (e.g. 1.0.0). Auto-detected when omitted\n --paths \"t=p,...\" Per-type project paths (monorepo). e.g. flutter=app,react=client\n --main-branch B Release branch (default: detected default branch)\n --develop-branch B Development branch (default: develop). Same as the release branch = trunk-based mode\n --deploy-style STYLE Server deploy style: simple | nginx | traefik | none (default: simple)\n --flutter-env-mode MODE Flutter env var mode: dart-define | dotenv (default: dart-define for new installs; existing installs keep the saved value / dotenv)\n --flutter-store CSV Flutter store deploy targets: android,ios | android | ios | none (default: install both)\n --android-deploy-mode MODE Play Store deploy mode: store_only | store_prepare | store_submit (default: store_only)\n --ios-deploy-mode MODE iOS deploy mode: store_only | store_prepare | store_submit (default: store_only)\n --semver-auto / --no-semver-auto Auto major/minor/patch bump from commit types (default: enabled)\n --copilot / --no-copilot Generate the AI summary with Copilot (default: off; consumes GitHub Copilot AI Credits)\n --lang LANG {langHelp}\n --force Required for full; non-interactive removal for uninstall (skips all confirmations, uses defaults)\n --dry-run Preview what would change without touching any file (supported by full/uninstall)\n --purge-readme With --mode uninstall --force, also remove the README.md version section\n --purge-gitignore With --mode uninstall --force, also remove the auto-added .gitignore entries\n --purge-version With --mode uninstall --force, also remove version.yml\n -v, --version Print the project-auto-wizard version\n -h, --help Show this help\n\nExamples:\n npx project-auto-wizard --mode full --force --type spring,react\n npx project-auto-wizard --mode full --force --type flutter --paths \"flutter=app\"\n npx project-auto-wizard --mode status\n npx project-auto-wizard --mode doctor\n npx project-auto-wizard --mode full --force --type node --dry-run\n npx project-auto-wizard --mode uninstall --force --purge-readme --purge-gitignore --purge-version\n",
8
+ "cli.help.text": "project-auto-wizard โ€” One command DevOps: GitHub-native release automation setup wizard\n\nUsage:\n npx project-auto-wizard [options]\n\nOptions:\n -m, --mode MODE Unified mode (full | uninstall | status | doctor)\n Default: interactive. full = install and update\n uninstall = full removal (interactive checklist; with --force, opt in via --purge-*)\n status = check install state and drift (read-only). doctor = environment diagnosis (read-only)\n -t, --type CSV Project types as csv (e.g. spring,react,python)\n Supported: {types}\n --project-version V Initial version of the target (e.g. 1.0.0). Auto-detected when omitted\n --paths \"t=p,...\" Per-type project paths (monorepo). e.g. flutter=app,react=client\n --main-branch B Release branch (default: detected default branch)\n --develop-branch B Development branch (default: develop). Same as the release branch = trunk-based mode\n --deploy-style STYLE Server deploy style: simple | nginx | traefik | none (default: simple)\n --flutter-env-mode MODE Flutter env var mode: dart-define | dotenv (default: dart-define for new installs; existing installs keep the saved value / dotenv)\n --flutter-store CSV Flutter store deploy targets: android,ios | android | ios | none (default: install both)\n --android-deploy-mode MODE Play Store deploy mode: store_only | store_prepare | store_submit (default: store_only)\n --ios-deploy-mode MODE iOS deploy mode: store_only | store_prepare | store_submit (default: store_only)\n --semver-auto / --no-semver-auto Auto major/minor/patch bump from commit types (default: enabled)\n --copilot / --no-copilot Generate the AI summary with Copilot (default: off; consumes GitHub Copilot AI Credits)\n --release-automerge / --no-release-automerge Merge the release PR automatically (default: enabled; off = you merge it yourself)\n --lang LANG {langHelp}\n --force Required for full; non-interactive removal for uninstall (skips all confirmations, uses defaults)\n --dry-run Preview what would change without touching any file (supported by full/uninstall)\n --purge-readme With --mode uninstall --force, also remove the README.md version section\n --purge-gitignore With --mode uninstall --force, also remove the auto-added .gitignore entries\n --purge-version With --mode uninstall --force, also remove version.yml\n -v, --version Print the project-auto-wizard version\n -h, --help Show this help\n\nExamples:\n npx project-auto-wizard --mode full --force --type spring,react\n npx project-auto-wizard --mode full --force --type flutter --paths \"flutter=app\"\n npx project-auto-wizard --mode status\n npx project-auto-wizard --mode doctor\n npx project-auto-wizard --mode full --force --type node --dry-run\n npx project-auto-wizard --mode uninstall --force --purge-readme --purge-gitignore --purge-version\n",
9
9
  "cli.args.projectVersionInvalid": "Invalid --project-version value: '{raw}' (x.y.z format, e.g. 1.0.0)",
10
10
  "cli.args.typeUnsupported": "Unsupported type: '{type}'\nSupported types: {valid}",
11
11
  "cli.args.typeEmpty": "The --type argument is empty",
@@ -13,6 +13,7 @@ export default {
13
13
  "cli.args.deployStyleInvalid": "Invalid --deploy-style value: {value} ({choices})",
14
14
  "cli.args.semverConflict": "--semver-auto and --no-semver-auto cannot be specified together",
15
15
  "cli.args.copilotConflict": "--copilot and --no-copilot cannot be specified together",
16
+ "cli.args.releaseAutomergeConflict": "--release-automerge and --no-release-automerge cannot be specified together",
16
17
  "cli.args.pathsEmpty": "The --paths argument is empty (e.g. --paths flutter=app,react=client)",
17
18
  "cli.args.branchEmpty": "{flag} cannot be empty",
18
19
  "cli.args.branchInvalid": "{flag} value is not a valid branch name: '{value}'",
@@ -20,6 +21,7 @@ export default {
20
21
  "cli.args.unknownOption": "Unknown option: {option}",
21
22
  "cli.args.modeUnsupported": "Unsupported mode: '{mode}'\nSupported modes: interactive full uninstall status doctor",
22
23
  "cli.args.pathsTypeUnsupported": "Unsupported type in --paths: '{type}'",
24
+ "cli.args.pathsDuplicateType": "--paths lists '{type}' twice with different folders (next is the old name of react); keep one entry",
23
25
  "cli.args.pathsRelativeOnly": "--paths accepts only paths relative to the repo: '{pair}'",
24
26
  "cli.index.purgePrompt": "To run purge, type this repo name exactly: {repoName}\n> ",
25
27
  "cli.index.dryRunNeedsMode": "Use --dry-run together with --mode <full|uninstall> (it is not supported in interactive mode).",
@@ -8,10 +8,13 @@ export default {
8
8
  "cmd.status.none": "(none)",
9
9
  "cmd.status.branchMode": "Branch mode : {mode} ({main} / {develop})",
10
10
  "cmd.status.unsetDefaultFalse": "unset (default false)",
11
+ "cmd.status.unsetDefaultTrue": "unset (default true)",
11
12
  "cmd.status.options": "Options : {value}",
12
13
  "cmd.status.modified": "Workflow files modified by the user ({n}):",
13
14
  "cmd.status.unmodified": "All workflow files match their install-time defaults (no modifications).",
14
15
  "cmd.status.stale": "Old workflows not in the current version ({n}; cleaned up on the next update):",
16
+ "cmd.status.missingScripts": "Warning: {n} installed script(s) missing from {dir}/ (workflows call them and will fail at run time):",
17
+ "cmd.status.missingScriptsFix": " Restore them with: npx project-auto-wizard --mode full --force",
15
18
  "cmd.status.bucketsTitle": "If you update now:",
16
19
  "cmd.status.bucketAuto": " Upstream changed - auto-applicable {n}",
17
20
  "cmd.status.bucketLocalKept": " Edited by me - upstream unchanged {n} (kept)",
@@ -24,6 +27,12 @@ export default {
24
27
  "cmd.doctor.install.purpose": "Whether the wizard is installed in this folder",
25
28
  "cmd.doctor.install.value": "version.yml found",
26
29
  "cmd.doctor.install.note": "Not installed in this folder yet; you can install now.",
30
+ "cmd.doctor.scripts.name": "Installed scripts",
31
+ "cmd.doctor.scripts.label": "Installed scripts",
32
+ "cmd.doctor.scripts.purpose": "Scripts the installed workflows call",
33
+ "cmd.doctor.scripts.missing": "Missing from .github/scripts/: {files}",
34
+ "cmd.doctor.scripts.impact": "Workflow steps that call them stop with a clear error (messages.py is missing) instead of carrying on.",
35
+ "cmd.doctor.scripts.action": "Restore them with: npx project-auto-wizard --mode full --force",
27
36
  "cmd.doctor.gh.purpose": "For reading repo settings",
28
37
  "cmd.doctor.gh.notFound": "The gh CLI was not found.",
29
38
  "cmd.doctor.gh.notFoundImpact": "Repo permissions and secrets cannot be read, so all remote checks below are skipped.",
@@ -70,6 +79,12 @@ export default {
70
79
  "cmd.doctor.copilot.purpose": "AI release note generation (optional)",
71
80
  "cmd.doctor.copilot.creditsOn": "GitHub Copilot AI Credits are consumed; organizations need the 'Allow use of Copilot CLI billed to the organization' policy.",
72
81
  "cmd.doctor.copilot.creditsOff": "Turning it on consumes GitHub Copilot AI Credits; organizations need the 'Allow use of Copilot CLI billed to the organization' policy.",
82
+ "cmd.doctor.automerge.name": "Release PR automerge",
83
+ "cmd.doctor.automerge.purpose": "Whether the release PR merges by itself",
84
+ "cmd.doctor.automerge.on": "It is on (release_automerge: true in version.yml, or the key is absent).",
85
+ "cmd.doctor.automerge.onHow": "To merge the release PR yourself, reinstall with --no-release-automerge or set release_automerge: false in version.yml.",
86
+ "cmd.doctor.automerge.off": "It is off (release_automerge: false in version.yml); the workflow prepares the release PR but does not merge it.",
87
+ "cmd.doctor.automerge.offHow": "Merge the release PR yourself with a merge commit (not squash or rebase) so the release still publishes; --release-automerge turns it back on.",
73
88
  "cmd.doctor.copilot.fallback": "When it is off or unavailable, it switches to the rule-based summary automatically, so you can leave it as is.",
74
89
  "cmd.doctor.heading": "Environment check",
75
90
  "cmd.doctor.details": "Details: ",
@@ -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
  "ui.status-cards.card.on": "on",
21
21
  "ui.status-cards.card.off": "off",
22
22
  "ui.status-cards.card.autoBump": "Auto-bump",
23
+ "ui.status-cards.card.copilot": "Copilot",
23
24
  "ui.status-cards.card.paths": "Paths",
24
25
  "ui.status-cards.kind.update": "{title} โ€” template {from} โ†’ {to}",
25
26
  "ui.status-cards.kind.updateTitle": "Update",
@@ -5,7 +5,7 @@ export default {
5
5
  "cli.lang.missing": "--lang ๊ฐ’์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ง€์› ์–ธ์–ด: {supported}",
6
6
  "cli.lang.help": "๋ฉ”์‹œ์ง€ ์–ธ์–ด: {supported} (๊ธฐ๋ณธ: en)",
7
7
  "cli.bin.nodeVersion": "Node.js 20.12 ์ด์ƒ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค (ํ˜„์žฌ: {current})",
8
- "cli.help.text": "project-auto-wizard โ€” One command DevOps: GitHub-native ๋ฆด๋ฆฌ์Šค ์ž๋™ํ™” ์„ค์น˜ ๋งˆ๋ฒ•์‚ฌ\n\n์‚ฌ์šฉ๋ฒ•:\n npx project-auto-wizard [์˜ต์…˜]\n\n์˜ต์…˜:\n -m, --mode MODE ํ†ตํ•ฉ ๋ชจ๋“œ (full | uninstall | status | doctor)\n ๊ธฐ๋ณธ: interactive (๋Œ€ํ™”ํ˜•). full = ์„ค์น˜ ๋ฐ ์—…๋ฐ์ดํŠธ\n uninstall = ์™„์ „ ์‚ญ์ œ(๋Œ€ํ™”ํ˜• ์ฒดํฌ๋ฆฌ์ŠคํŠธ, --force ์‹œ --purge-*๋กœ opt-in)\n status = ์„ค์น˜ ์ƒํƒœยท๋“œ๋ฆฌํ”„ํŠธ ํ™•์ธ(์ฝ๊ธฐ ์ „์šฉ). doctor = ํ™˜๊ฒฝ ์ง„๋‹จ(์ฝ๊ธฐ ์ „์šฉ)\n -t, --type CSV ํ”„๋กœ์ ํŠธ ํƒ€์ž… csv (์˜ˆ: spring,react,python)\n ์ง€์›: {types}\n --project-version V ํ†ตํ•ฉ ๋Œ€์ƒ์˜ ์ดˆ๊ธฐ ๋ฒ„์ „ (์˜ˆ: 1.0.0). ๋ฏธ์ง€์ • ์‹œ ์ž๋™ ๊ฐ์ง€\n --paths \"t=p,...\" ํƒ€์ž…๋ณ„ ํ”„๋กœ์ ํŠธ ๊ฒฝ๋กœ (๋ชจ๋…ธ๋ ˆํฌ). ์˜ˆ: flutter=app,react=client\n --main-branch B ๋ฆด๋ฆฌ์Šค ๋ธŒ๋žœ์น˜ (๊ธฐ๋ณธ: ๊ฐ์ง€๋œ default branch)\n --develop-branch B ๊ฐœ๋ฐœ ๋ธŒ๋žœ์น˜ (๊ธฐ๋ณธ: develop). ๋ฆด๋ฆฌ์Šค ๋ธŒ๋žœ์น˜์™€ ๊ฐ™์œผ๋ฉด trunk-based ๋ชจ๋“œ\n --deploy-style STYLE ์„œ๋ฒ„ ๋ฐฐํฌ ๋ฐฉ์‹: simple | nginx | traefik | none (๊ธฐ๋ณธ: simple)\n --flutter-env-mode MODE Flutter ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐฉ์‹: dart-define | dotenv (๊ธฐ๋ณธ: ์‹ ๊ทœ ์„ค์น˜ dart-define, ๊ธฐ์กด ์„ค์น˜๋Š” ์ €์žฅ๊ฐ’ยทdotenv ์œ ์ง€)\n --flutter-store CSV Flutter ์Šคํ† ์–ด ๋ฐฐํฌ ๋Œ€์ƒ: android,ios | android | ios | none (๊ธฐ๋ณธ: ๋‘˜ ๋‹ค ์„ค์น˜)\n --android-deploy-mode MODE Play Store ๋ฐฐํฌ ๋ชจ๋“œ: store_only | store_prepare | store_submit (๊ธฐ๋ณธ: store_only)\n --ios-deploy-mode MODE iOS ๋ฐฐํฌ ๋ชจ๋“œ: store_only | store_prepare | store_submit (๊ธฐ๋ณธ: store_only)\n --semver-auto / --no-semver-auto ์ปค๋ฐ‹ ํƒ€์ž… ๊ธฐ๋ฐ˜ ์ž๋™ major/minor/patch ์Šน๊ฒฉ (๊ธฐ๋ณธ: ์‚ฌ์šฉํ•จ)\n --copilot / --no-copilot Copilot์œผ๋กœ AI ์š”์•ฝ ์ƒ์„ฑ (๊ธฐ๋ณธ: ์‚ฌ์šฉ ์•ˆ ํ•จ, GitHub Copilot AI Credits ์†Œ๋น„)\n --lang LANG {langHelp}\n --force full ์‹คํ–‰์— ํ•„์ˆ˜, uninstall์€ ๋น„๋Œ€ํ™”ํ˜• ์‚ญ์ œ (๋ชจ๋“  ํ™•์ธ ์ƒ๋žต, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ)\n --dry-run ์‹ค์ œ ํŒŒ์ผ ๋ณ€๊ฒฝ ์—†์ด ๋ฌด์—‡์ด ๋ฐ”๋€”์ง€๋งŒ ๋ฏธ๋ฆฌ ๋ณด์—ฌ์คŒ (full/uninstall ์ง€์›)\n --purge-readme --mode uninstall --force ์‹œ README.md ๋ฒ„์ „ ์„น์…˜๋„ ์ œ๊ฑฐ\n --purge-gitignore --mode uninstall --force ์‹œ .gitignore ์ž๋™ ์ถ”๊ฐ€ ํ•ญ๋ชฉ๋„ ์ œ๊ฑฐ\n --purge-version --mode uninstall --force ์‹œ version.yml๋„ ์ œ๊ฑฐ\n -v, --version project-auto-wizard ๋ฒ„์ „ ์ถœ๋ ฅ\n -h, --help ์ด ๋„์›€๋ง ํ‘œ์‹œ\n\n์˜ˆ์‹œ:\n npx project-auto-wizard --mode full --force --type spring,react\n npx project-auto-wizard --mode full --force --type flutter --paths \"flutter=app\"\n npx project-auto-wizard --mode status\n npx project-auto-wizard --mode doctor\n npx project-auto-wizard --mode full --force --type node --dry-run\n npx project-auto-wizard --mode uninstall --force --purge-readme --purge-gitignore --purge-version\n",
8
+ "cli.help.text": "project-auto-wizard โ€” One command DevOps: GitHub-native ๋ฆด๋ฆฌ์Šค ์ž๋™ํ™” ์„ค์น˜ ๋งˆ๋ฒ•์‚ฌ\n\n์‚ฌ์šฉ๋ฒ•:\n npx project-auto-wizard [์˜ต์…˜]\n\n์˜ต์…˜:\n -m, --mode MODE ํ†ตํ•ฉ ๋ชจ๋“œ (full | uninstall | status | doctor)\n ๊ธฐ๋ณธ: interactive (๋Œ€ํ™”ํ˜•). full = ์„ค์น˜ ๋ฐ ์—…๋ฐ์ดํŠธ\n uninstall = ์™„์ „ ์‚ญ์ œ(๋Œ€ํ™”ํ˜• ์ฒดํฌ๋ฆฌ์ŠคํŠธ, --force ์‹œ --purge-*๋กœ opt-in)\n status = ์„ค์น˜ ์ƒํƒœยท๋“œ๋ฆฌํ”„ํŠธ ํ™•์ธ(์ฝ๊ธฐ ์ „์šฉ). doctor = ํ™˜๊ฒฝ ์ง„๋‹จ(์ฝ๊ธฐ ์ „์šฉ)\n -t, --type CSV ํ”„๋กœ์ ํŠธ ํƒ€์ž… csv (์˜ˆ: spring,react,python)\n ์ง€์›: {types}\n --project-version V ํ†ตํ•ฉ ๋Œ€์ƒ์˜ ์ดˆ๊ธฐ ๋ฒ„์ „ (์˜ˆ: 1.0.0). ๋ฏธ์ง€์ • ์‹œ ์ž๋™ ๊ฐ์ง€\n --paths \"t=p,...\" ํƒ€์ž…๋ณ„ ํ”„๋กœ์ ํŠธ ๊ฒฝ๋กœ (๋ชจ๋…ธ๋ ˆํฌ). ์˜ˆ: flutter=app,react=client\n --main-branch B ๋ฆด๋ฆฌ์Šค ๋ธŒ๋žœ์น˜ (๊ธฐ๋ณธ: ๊ฐ์ง€๋œ default branch)\n --develop-branch B ๊ฐœ๋ฐœ ๋ธŒ๋žœ์น˜ (๊ธฐ๋ณธ: develop). ๋ฆด๋ฆฌ์Šค ๋ธŒ๋žœ์น˜์™€ ๊ฐ™์œผ๋ฉด trunk-based ๋ชจ๋“œ\n --deploy-style STYLE ์„œ๋ฒ„ ๋ฐฐํฌ ๋ฐฉ์‹: simple | nginx | traefik | none (๊ธฐ๋ณธ: simple)\n --flutter-env-mode MODE Flutter ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋ฐฉ์‹: dart-define | dotenv (๊ธฐ๋ณธ: ์‹ ๊ทœ ์„ค์น˜ dart-define, ๊ธฐ์กด ์„ค์น˜๋Š” ์ €์žฅ๊ฐ’ยทdotenv ์œ ์ง€)\n --flutter-store CSV Flutter ์Šคํ† ์–ด ๋ฐฐํฌ ๋Œ€์ƒ: android,ios | android | ios | none (๊ธฐ๋ณธ: ๋‘˜ ๋‹ค ์„ค์น˜)\n --android-deploy-mode MODE Play Store ๋ฐฐํฌ ๋ชจ๋“œ: store_only | store_prepare | store_submit (๊ธฐ๋ณธ: store_only)\n --ios-deploy-mode MODE iOS ๋ฐฐํฌ ๋ชจ๋“œ: store_only | store_prepare | store_submit (๊ธฐ๋ณธ: store_only)\n --semver-auto / --no-semver-auto ์ปค๋ฐ‹ ํƒ€์ž… ๊ธฐ๋ฐ˜ ์ž๋™ major/minor/patch ์Šน๊ฒฉ (๊ธฐ๋ณธ: ์‚ฌ์šฉํ•จ)\n --copilot / --no-copilot Copilot์œผ๋กœ AI ์š”์•ฝ ์ƒ์„ฑ (๊ธฐ๋ณธ: ์‚ฌ์šฉ ์•ˆ ํ•จ, GitHub Copilot AI Credits ์†Œ๋น„)\n --release-automerge / --no-release-automerge ๋ฆด๋ฆฌ์Šค PR ์ž๋™ ๋จธ์ง€ (๊ธฐ๋ณธ: ์‚ฌ์šฉํ•จ, ๋„๋ฉด ์ง์ ‘ ๋จธ์ง€)\n --lang LANG {langHelp}\n --force full ์‹คํ–‰์— ํ•„์ˆ˜, uninstall์€ ๋น„๋Œ€ํ™”ํ˜• ์‚ญ์ œ (๋ชจ๋“  ํ™•์ธ ์ƒ๋žต, ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ)\n --dry-run ์‹ค์ œ ํŒŒ์ผ ๋ณ€๊ฒฝ ์—†์ด ๋ฌด์—‡์ด ๋ฐ”๋€”์ง€๋งŒ ๋ฏธ๋ฆฌ ๋ณด์—ฌ์คŒ (full/uninstall ์ง€์›)\n --purge-readme --mode uninstall --force ์‹œ README.md ๋ฒ„์ „ ์„น์…˜๋„ ์ œ๊ฑฐ\n --purge-gitignore --mode uninstall --force ์‹œ .gitignore ์ž๋™ ์ถ”๊ฐ€ ํ•ญ๋ชฉ๋„ ์ œ๊ฑฐ\n --purge-version --mode uninstall --force ์‹œ version.yml๋„ ์ œ๊ฑฐ\n -v, --version project-auto-wizard ๋ฒ„์ „ ์ถœ๋ ฅ\n -h, --help ์ด ๋„์›€๋ง ํ‘œ์‹œ\n\n์˜ˆ์‹œ:\n npx project-auto-wizard --mode full --force --type spring,react\n npx project-auto-wizard --mode full --force --type flutter --paths \"flutter=app\"\n npx project-auto-wizard --mode status\n npx project-auto-wizard --mode doctor\n npx project-auto-wizard --mode full --force --type node --dry-run\n npx project-auto-wizard --mode uninstall --force --purge-readme --purge-gitignore --purge-version\n",
9
9
  "cli.args.projectVersionInvalid": "--project-version ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค: '{raw}' (x.y.z ํ˜•์‹, ์˜ˆ: 1.0.0)",
10
10
  "cli.args.typeUnsupported": "์ง€์›ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž…: '{type}'\n์ง€์› ํƒ€์ž…: {valid}",
11
11
  "cli.args.typeEmpty": "--type ์ธ์ž๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค",
@@ -13,6 +13,7 @@ export default {
13
13
  "cli.args.deployStyleInvalid": "--deploy-style ๊ฐ’์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค: {value} ({choices})",
14
14
  "cli.args.semverConflict": "--semver-auto์™€ --no-semver-auto๋Š” ๋™์‹œ์— ์ง€์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
15
15
  "cli.args.copilotConflict": "--copilot๊ณผ --no-copilot์€ ๋™์‹œ์— ์ง€์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
16
+ "cli.args.releaseAutomergeConflict": "--release-automerge์™€ --no-release-automerge๋Š” ๋™์‹œ์— ์ง€์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
16
17
  "cli.args.pathsEmpty": "--paths ์ธ์ž๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค (์˜ˆ: --paths flutter=app,react=client)",
17
18
  "cli.args.branchEmpty": "{flag}์— ๋นˆ ๊ฐ’์„ ์ง€์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค",
18
19
  "cli.args.branchInvalid": "{flag} ๊ฐ’์ด ๋ธŒ๋žœ์น˜ ์ด๋ฆ„์œผ๋กœ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค: '{value}'",
@@ -20,6 +21,7 @@ export default {
20
21
  "cli.args.unknownOption": "์•Œ ์ˆ˜ ์—†๋Š” ์˜ต์…˜: {option}",
21
22
  "cli.args.modeUnsupported": "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ชจ๋“œ: '{mode}'\n์ง€์› ๋ชจ๋“œ: interactive full uninstall status doctor",
22
23
  "cli.args.pathsTypeUnsupported": "--paths์— ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž…: '{type}'",
24
+ "cli.args.pathsDuplicateType": "--paths์— '{type}'์ด ์„œ๋กœ ๋‹ค๋ฅธ ํด๋”๋กœ ๋‘ ๋ฒˆ ์ง€์ •๋จ (next๋Š” react์˜ ์ด์ „ ์ด๋ฆ„์ž…๋‹ˆ๋‹ค): ํ•œ ํ•ญ๋ชฉ๋งŒ ๋‚จ๊ธฐ์„ธ์š”",
23
25
  "cli.args.pathsRelativeOnly": "--paths๋Š” ๋ ˆํฌ ์•ˆ์˜ ์ƒ๋Œ€๊ฒฝ๋กœ๋งŒ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: '{pair}'",
24
26
  "cli.index.purgePrompt": "purge๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด ์ •ํ™•ํžˆ ์ด ๋ ˆํฌ๋ช…์„ ์ž…๋ ฅํ•˜์„ธ์š”: {repoName}\n> ",
25
27
  "cli.index.dryRunNeedsMode": "--dry-run์€ --mode <full|uninstall>์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์„ธ์š” (๋Œ€ํ™”ํ˜• ๋ชจ๋“œ์—์„œ๋Š” ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค).",
@@ -8,10 +8,13 @@ export default {
8
8
  "cmd.status.none": "(์—†์Œ)",
9
9
  "cmd.status.branchMode": "๋ธŒ๋žœ์น˜ ๋ชจ๋“œ : {mode} ({main} / {develop})",
10
10
  "cmd.status.unsetDefaultFalse": "๋ฏธ์„ค์ •(๊ธฐ๋ณธ false)",
11
+ "cmd.status.unsetDefaultTrue": "๋ฏธ์„ค์ •(๊ธฐ๋ณธ true)",
11
12
  "cmd.status.options": "์˜ต์…˜ : {value}",
12
13
  "cmd.status.modified": "์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ •ํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ ({n}๊ฐœ):",
13
14
  "cmd.status.unmodified": "๋ชจ๋“  ์›Œํฌํ”Œ๋กœ์šฐ ํŒŒ์ผ์ด ์„ค์น˜ ์‹œ์  ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค (์ˆ˜์ • ์—†์Œ).",
14
15
  "cmd.status.stale": "ํ˜„์žฌ ๋ฒ„์ „์— ์—†๋Š” ์ด์ „ ์›Œํฌํ”Œ๋กœ์šฐ ({n}๊ฐœ โ€” ๋‹ค์Œ ์—…๋ฐ์ดํŠธ์—์„œ ์ •๋ฆฌ๋จ):",
16
+ "cmd.status.missingScripts": "๊ฒฝ๊ณ : ์„ค์น˜๋œ ์Šคํฌ๋ฆฝํŠธ {n}๊ฐœ๊ฐ€ {dir}/ ์— ์—†์Šต๋‹ˆ๋‹ค (์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ ์‹คํ–‰ ์‹œ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค):",
17
+ "cmd.status.missingScriptsFix": " ๋ณต๊ตฌ: npx project-auto-wizard --mode full --force",
15
18
  "cmd.status.bucketsTitle": "์ง€๊ธˆ ์—…๋ฐ์ดํŠธํ•˜๋ฉด:",
16
19
  "cmd.status.bucketAuto": " ์—…์ŠคํŠธ๋ฆผ ๋ณ€๊ฒฝ โ€” ์ž๋™ ์ ์šฉ ๊ฐ€๋Šฅ {n}",
17
20
  "cmd.status.bucketLocalKept": " ๋‚ด๊ฐ€ ์ˆ˜์ • ยท ์—…์ŠคํŠธ๋ฆผ ๊ทธ๋Œ€๋กœ {n} (์œ ์ง€)",
@@ -24,6 +27,12 @@ export default {
24
27
  "cmd.doctor.install.purpose": "์ด ํด๋”์˜ ๋งˆ๋ฒ•์‚ฌ ์„ค์น˜ ์—ฌ๋ถ€",
25
28
  "cmd.doctor.install.value": "version.yml ์žˆ์Œ",
26
29
  "cmd.doctor.install.note": "์ด ํด๋”์—” ์•„์ง ์„ค์น˜๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค โ€” ์ง€๊ธˆ ์„ค์น˜ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.",
30
+ "cmd.doctor.scripts.name": "์„ค์น˜๋œ ์Šคํฌ๋ฆฝํŠธ",
31
+ "cmd.doctor.scripts.label": "์„ค์น˜๋œ ์Šคํฌ๋ฆฝํŠธ",
32
+ "cmd.doctor.scripts.purpose": "์„ค์น˜๋œ ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ํ˜ธ์ถœํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ",
33
+ "cmd.doctor.scripts.missing": ".github/scripts/ ์— ์—†์Œ: {files}",
34
+ "cmd.doctor.scripts.impact": "์ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์Šคํ…์€ ๋ˆ„๋ฝ์„ ์•Œ๋ฆฌ๋Š” ์˜ค๋ฅ˜(messages.py is missing)๋กœ ๋ช…ํ™•ํžˆ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.",
35
+ "cmd.doctor.scripts.action": "๋ณต๊ตฌ: npx project-auto-wizard --mode full --force",
27
36
  "cmd.doctor.gh.purpose": "๋ ˆํฌ ์„ค์ • ์กฐํšŒ์šฉ",
28
37
  "cmd.doctor.gh.notFound": "gh CLI๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",
29
38
  "cmd.doctor.gh.notFoundImpact": "๋ ˆํฌ ๊ถŒํ•œยทsecret ์„ค์ •์„ ์กฐํšŒํ•  ์ˆ˜ ์—†์–ด ์•„๋ž˜ ์›๊ฒฉ ์ ๊ฒ€์„ ๋ชจ๋‘ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.",
@@ -70,6 +79,12 @@ export default {
70
79
  "cmd.doctor.copilot.purpose": "AI ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ ์ƒ์„ฑ(์„ ํƒ)",
71
80
  "cmd.doctor.copilot.creditsOn": "GitHub Copilot AI Credits๊ฐ€ ์†Œ๋น„๋ฉ๋‹ˆ๋‹ค โ€” ์กฐ์ง์€ 'Allow use of Copilot CLI billed to the organization' ์ •์ฑ…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.",
72
81
  "cmd.doctor.copilot.creditsOff": "์ผœ๋ฉด GitHub Copilot AI Credits๊ฐ€ ์†Œ๋น„๋ฉ๋‹ˆ๋‹ค โ€” ์กฐ์ง์€ 'Allow use of Copilot CLI billed to the organization' ์ •์ฑ…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.",
82
+ "cmd.doctor.automerge.name": "๋ฆด๋ฆฌ์Šค PR ์ž๋™ ๋จธ์ง€",
83
+ "cmd.doctor.automerge.purpose": "๋ฆด๋ฆฌ์Šค PR์ด ์Šค์Šค๋กœ ๋จธ์ง€๋˜๋Š”์ง€",
84
+ "cmd.doctor.automerge.on": "์ผœ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค (version.yml์˜ release_automerge: true, ๋˜๋Š” ํ‚ค๊ฐ€ ์—†์Œ).",
85
+ "cmd.doctor.automerge.onHow": "๋ฆด๋ฆฌ์Šค PR์„ ์ง์ ‘ ๋จธ์ง€ํ•˜๋ ค๋ฉด --no-release-automerge๋กœ ๋‹ค์‹œ ์„ค์น˜ํ•˜๊ฑฐ๋‚˜ version.yml์˜ release_automerge๋ฅผ false๋กœ ๋ฐ”๊พธ์„ธ์š”.",
86
+ "cmd.doctor.automerge.off": "๊บผ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค (version.yml์˜ release_automerge: false) โ€” ์›Œํฌํ”Œ๋กœ์šฐ๊ฐ€ ๋ฆด๋ฆฌ์Šค PR์„ ์ค€๋น„๋งŒ ํ•˜๊ณ  ๋จธ์ง€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
87
+ "cmd.doctor.automerge.offHow": "๋ฆด๋ฆฌ์Šค๊ฐ€ ์ •์ƒ ๋ฐฐํฌ๋˜๋„๋ก ๋จธ์ง€ ์ปค๋ฐ‹์œผ๋กœ ์ง์ ‘ ๋จธ์ง€ํ•˜์„ธ์š”(squashยทrebase ๊ธˆ์ง€). ๋‹ค์‹œ ์ผœ๋ ค๋ฉด --release-automerge๋ฅผ ์“ฐ์„ธ์š”.",
73
88
  "cmd.doctor.copilot.fallback": "๊บผ์ ธ ์žˆ๊ฑฐ๋‚˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์œผ๋ฉด ๊ทœ์น™ ๊ธฐ๋ฐ˜ ์š”์•ฝ์œผ๋กœ ์ž๋™ ์ „ํ™˜๋˜๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ๋‘์…”๋„ ๋ฉ๋‹ˆ๋‹ค.",
74
89
  "cmd.doctor.heading": "ํ™˜๊ฒฝ ์ง„๋‹จ",
75
90
  "cmd.doctor.details": "์ž์„ธํžˆ: ",
@@ -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}",
@@ -20,6 +20,7 @@ export default {
20
20
  "ui.status-cards.card.on": "์ผœ์ง",
21
21
  "ui.status-cards.card.off": "๊บผ์ง",
22
22
  "ui.status-cards.card.autoBump": "์ž๋™์Šน๊ฒฉ",
23
+ "ui.status-cards.card.copilot": "Copilot",
23
24
  "ui.status-cards.card.paths": "๊ฒฝ๋กœ",
24
25
  "ui.status-cards.kind.update": "{title} โ€” ํ…œํ”Œ๋ฆฟ {from} โ†’ {to}",
25
26
  "ui.status-cards.kind.updateTitle": "์—…๋ฐ์ดํŠธ",
package/src/index.js CHANGED
@@ -14,8 +14,9 @@ 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
+ import { explicitFromContext, optionContextFields } from "./core/options.js";
19
20
  import { runBreakingCheck } from "./core/breaking-check.js";
20
21
  import { resolveProjectPaths } from "./core/paths-resolve.js";
21
22
  import {
@@ -149,7 +150,7 @@ async function runInner(argv, {
149
150
  // The log file is created on the first write, so viewing only status/doctor from the menu leaves nothing behind.
150
151
  startLog("install");
151
152
  return await runInteractive(
152
- { includeSemverAuto: opts.includeSemverAuto, includeCopilotAi: opts.includeCopilotAi, language },
153
+ { ...optionContextFields(opts), language },
153
154
  { cwd, payloadRoot: payload, clock },
154
155
  );
155
156
  }
@@ -316,6 +317,9 @@ async function runInner(argv, {
316
317
  throw e;
317
318
  }
318
319
 
320
+ // Warn against the final folders (an explicit --paths may keep the other one). The dry-run preview prints its own notice.
321
+ if (!opts.dryRun) for (const line of droppedPathLines(existing?.droppedPaths, paths)) console.error(`โš ๏ธ ${line}`);
322
+
319
323
  // version: existing version.yml first (SSoT; prevents overwriting on re-run) -> CLI value -> file detection.
320
324
  // Non-interactive, so the fallback notice uses the CLI wording (--project-version) as is.
321
325
  // Detect after paths are confirmed so a monorepo subfolder's version and build number are read.
@@ -363,7 +367,7 @@ async function runInner(argv, {
363
367
  payload, existing, templateVersion: readTemplateVersion(), types, deployStyle: chosenDeployStyle,
364
368
  typeOptions,
365
369
  // Options: CLI flag first -> saved version.yml option -> default (same rule as interactive)
366
- releaseOptions: resolveReleaseOptions({ semverAuto: opts.includeSemverAuto, copilotAi: opts.includeCopilotAi }, existing),
370
+ releaseOptions: resolveReleaseOptions(explicitFromContext(opts), existing),
367
371
  mode: opts.mode, force: opts.force, version, versionCode, branch,
368
372
  branches,
369
373
  paths,
@@ -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/python and react/next keep server deploy workflows directly in the type root; they 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)]);