project-auto-wizard 0.13.2 → 0.14.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.md +1 -1
- 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 +296 -257
- package/payload/scripts/issue_helper.py +54 -50
- package/payload/scripts/messages.py +2279 -0
- package/payload/scripts/truncate_release_notes.py +7 -6
- package/payload/scripts/version_manager.py +64 -61
- package/payload/version.yml.template +2 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
- 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 +85 -89
- package/src/commands/dry-run.js +63 -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 +45 -35
- 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 +187 -0
- package/src/i18n/catalog/en/copy.js +19 -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 +187 -0
- package/src/i18n/catalog/ko/copy.js +19 -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 +128 -94
- 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 +89 -88
package/src/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
// project-auto-wizard CLI
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// project-auto-wizard CLI entry pipeline.
|
|
2
|
+
// detect -> resolve payload -> route by mode -> run the install. Non-interactive (--force) first.
|
|
3
|
+
// All installed assets come from the payload/ bundled in the npm package (single source of truth). The tool
|
|
4
|
+
// makes no network requests of its own; only the git command that detects the default branch may contact the repo's origin.
|
|
5
5
|
import { join, dirname } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { readFileSync, existsSync } from "node:fs";
|
|
8
8
|
import { createInterface } from "node:readline/promises";
|
|
9
9
|
import { parseArgs, parsePathsCsv, CliError, TYPE_CLI_FLAGS } from "./cli/args.js";
|
|
10
|
-
import {
|
|
10
|
+
import { helpText } from "./cli/help.js";
|
|
11
|
+
import { resolveLanguage, setLanguage, t, DEFAULT_LANGUAGE, LANG_ENV_VAR, normalizeLanguage } from "./i18n/index.js";
|
|
11
12
|
import { fallbackStyleTypes } from "./core/deploy-style.js";
|
|
12
13
|
import { hooksFor, mergeHookResults } from "./core/types.js";
|
|
13
14
|
import { PATHS } from "./core/paths.js";
|
|
@@ -34,7 +35,7 @@ import { runDoctor, printDoctorReport } from "./commands/doctor.js";
|
|
|
34
35
|
import { planDryRun, printDryRun } from "./commands/dry-run.js";
|
|
35
36
|
import { planPurge, executePurge, printPurgePlan, printPurgeResult } from "./commands/purge.js";
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
+
// Read the package version (for -v/--version output) from src/../package.json.
|
|
38
39
|
function readPkgVersion() {
|
|
39
40
|
try {
|
|
40
41
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -45,34 +46,47 @@ function readPkgVersion() {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
//
|
|
49
|
+
// Deterministic UTC timestamp (injectable for tests/goldens)
|
|
49
50
|
function utcNow(date = new Date()) {
|
|
50
51
|
const p = (n) => String(n).padStart(2, "0");
|
|
51
52
|
const d = `${date.getUTCFullYear()}-${p(date.getUTCMonth() + 1)}-${p(date.getUTCDate())}`;
|
|
52
53
|
const t = `${p(date.getUTCHours())}:${p(date.getUTCMinutes())}:${p(date.getUTCSeconds())}`;
|
|
53
|
-
// ms:
|
|
54
|
+
// ms: keeps log file names from colliding between runs in the same second (taken from the same instant as now).
|
|
54
55
|
return { now: `${d} ${t}`, today: d, ms: date.getUTCMilliseconds() };
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
// purge TTY
|
|
58
|
+
// purge TTY confirmation: reads one line from the real stdin (tests replace it by injecting promptRepoName).
|
|
58
59
|
async function defaultPromptRepoName(repoName) {
|
|
59
60
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
60
61
|
try {
|
|
61
|
-
return await rl.question(
|
|
62
|
+
return await rl.question(t("cli.index.purgePrompt", { repoName }));
|
|
62
63
|
} finally {
|
|
63
64
|
rl.close();
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
68
|
+
// Provisional language from argv/env so argument errors are already printed in the requested language.
|
|
69
|
+
// Mirrors how parseArgs reads `--lang X` (last valid value wins); invalid values are ignored silently here
|
|
70
|
+
// because parseArgs / resolveLanguage report them. The saved version.yml language only applies later.
|
|
71
|
+
function presetLanguage(argv) {
|
|
72
|
+
setLanguage(DEFAULT_LANGUAGE);
|
|
73
|
+
setLanguage(normalizeLanguage(process.env[LANG_ENV_VAR]));
|
|
74
|
+
argv.forEach((a, i) => {
|
|
75
|
+
if (a === "--lang") setLanguage(normalizeLanguage(argv[i + 1]));
|
|
76
|
+
else if (a.startsWith("--lang=")) setLanguage(normalizeLanguage(a.slice("--lang=".length)));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// run(argv, opts) -> exitCode. opts: { cwd, payloadRoot?, clock?, exec?, promptRepoName? }
|
|
81
|
+
// payloadRoot: test fixture injection point (default: payload/ bundled in the package)
|
|
82
|
+
// clock: inject {now, today} (default: current UTC).
|
|
83
|
+
// exec/promptRepoName: injection points for the purge safety gates (real implementations by default, mocks in tests).
|
|
71
84
|
async function runInner(argv, {
|
|
72
85
|
cwd = process.cwd(), payloadRoot, clock,
|
|
73
86
|
exec = defaultExec, promptRepoName = defaultPromptRepoName,
|
|
74
87
|
} = {}) {
|
|
75
88
|
let opts;
|
|
89
|
+
presetLanguage(argv);
|
|
76
90
|
try {
|
|
77
91
|
opts = parseArgs(argv);
|
|
78
92
|
} catch (e) {
|
|
@@ -80,98 +94,118 @@ async function runInner(argv, {
|
|
|
80
94
|
throw e;
|
|
81
95
|
}
|
|
82
96
|
if (opts.showVersion) { console.log(readPkgVersion()); return 0; }
|
|
83
|
-
if (opts.help) { console.log(
|
|
97
|
+
if (opts.help) { console.log(helpText()); return 0; }
|
|
98
|
+
|
|
99
|
+
// Language: --lang -> env var -> saved version.yml value -> en. Resolved after --help/--version
|
|
100
|
+
// so an invalid env var cannot block the help output.
|
|
101
|
+
let language;
|
|
102
|
+
try {
|
|
103
|
+
const savedVy = join(cwd, "version.yml");
|
|
104
|
+
const saved = existsSync(savedVy) ? parseExisting(readFileSync(savedVy, "utf8")).language : null;
|
|
105
|
+
language = resolveLanguage({ flag: opts.lang, env: process.env[LANG_ENV_VAR], saved });
|
|
106
|
+
// An existing install without a saved language now falls back to English: say so once, since
|
|
107
|
+
// its workflow messages switch from Korean on the next update.
|
|
108
|
+
const existingWithoutLanguage = existsSync(savedVy) && !saved;
|
|
109
|
+
const unspecified = !normalizeLanguage(opts.lang) && !normalizeLanguage(process.env[LANG_ENV_VAR]);
|
|
110
|
+
if (existingWithoutLanguage && unspecified && ["full", "interactive"].includes(opts.mode)) {
|
|
111
|
+
console.error(t("cli.lang.defaultNotice", {}, DEFAULT_LANGUAGE));
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
if (e instanceof CliError) { console.error(e.message); return 1; }
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
setLanguage(language);
|
|
84
118
|
|
|
85
119
|
const payload = assertPayload(payloadRoot ?? resolvePayloadRoot());
|
|
86
120
|
|
|
87
|
-
//
|
|
121
|
+
// Compute the time once here so the log file name and the install record use the same value.
|
|
88
122
|
const { now, today, ms } = clock || utcNow();
|
|
89
123
|
|
|
90
|
-
//
|
|
91
|
-
// status/doctor(
|
|
124
|
+
// Only runs that pass argument validation and every mode gate and actually change files leave a log.
|
|
125
|
+
// status/doctor (read-only), dry-run and rejected runs must not create any file in the target repo.
|
|
92
126
|
const startLog = (action) => initLogger(cwd, { action, now, ms, argv, templateVersion: readTemplateVersion() });
|
|
93
127
|
|
|
94
|
-
//
|
|
128
|
+
// Interactive mode: run without arguments or with --mode interactive
|
|
95
129
|
if (opts.mode === "interactive") {
|
|
96
|
-
// --dry-run
|
|
130
|
+
// --dry-run must not be silently ignored in interactive mode (a real install would proceed), so fail explicitly.
|
|
97
131
|
if (opts.dryRun) {
|
|
98
|
-
console.error("
|
|
132
|
+
console.error(t("cli.index.dryRunNeedsMode"));
|
|
99
133
|
return 1;
|
|
100
134
|
}
|
|
101
135
|
if (!process.stdout.isTTY) {
|
|
102
|
-
console.error("
|
|
136
|
+
console.error(t("cli.index.noTty"));
|
|
103
137
|
return 1;
|
|
104
138
|
}
|
|
105
|
-
//
|
|
106
|
-
//
|
|
139
|
+
// On/off options also apply in interactive mode (their question is skipped). The remaining install flags
|
|
140
|
+
// are decided by the interactive questions, so report them instead of ignoring them silently.
|
|
107
141
|
const ignored = [
|
|
108
142
|
[opts.types.length, "--type"], [opts.version, "--project-version"], [opts.pathsCsv, "--paths"],
|
|
109
143
|
[opts.mainBranch, "--main-branch"], [opts.developBranch, "--develop-branch"], [opts.deployStyle, "--deploy-style"],
|
|
110
144
|
...TYPE_CLI_FLAGS.map((f) => [opts[f.field], f.flag]),
|
|
111
145
|
].filter(([v]) => v).map(([, flag]) => flag);
|
|
112
146
|
if (ignored.length) {
|
|
113
|
-
console.error(`⚠️
|
|
147
|
+
console.error(`⚠️ ${t("cli.index.ignoredFlags", { flags: ignored.join(", ") })}`);
|
|
114
148
|
}
|
|
115
|
-
//
|
|
149
|
+
// The log file is created on the first write, so viewing only status/doctor from the menu leaves nothing behind.
|
|
116
150
|
startLog("install");
|
|
117
151
|
return await runInteractive(
|
|
118
|
-
{ includeSemverAuto: opts.includeSemverAuto, includeCopilotAi: opts.includeCopilotAi },
|
|
152
|
+
{ includeSemverAuto: opts.includeSemverAuto, includeCopilotAi: opts.includeCopilotAi, language },
|
|
119
153
|
{ cwd, payloadRoot: payload, clock },
|
|
120
154
|
);
|
|
121
155
|
}
|
|
122
156
|
|
|
123
|
-
// purge
|
|
124
|
-
//
|
|
157
|
+
// purge mode: removes everything the wizard created and restores the pre-install state.
|
|
158
|
+
// Hidden development/testing mode; not exposed in --help or the interactive menu.
|
|
125
159
|
if (opts.mode === "purge") {
|
|
126
160
|
if (!existsSync(join(cwd, ".git"))) {
|
|
127
|
-
console.error("
|
|
161
|
+
console.error(t("cli.index.purgeNotGit"));
|
|
128
162
|
return 1;
|
|
129
163
|
}
|
|
130
164
|
const keepFlags = {
|
|
131
165
|
versionYml: opts.keepVersionYml, readme: opts.keepReadme, changelog: opts.keepChangelog,
|
|
132
166
|
workflows: opts.keepWorkflows, scripts: opts.keepScripts,
|
|
133
167
|
};
|
|
134
|
-
// version.yml
|
|
135
|
-
//
|
|
136
|
-
//
|
|
168
|
+
// Read version.yml up front: (a) executePurge() may delete version.yml itself, so it cannot be read
|
|
169
|
+
// afterwards, and (b) the dry-run notice below must know whether the repo is trunk-based (develop === main)
|
|
170
|
+
// to tell in advance whether the real run will skip the deletion. Both need it before this point.
|
|
137
171
|
const vyPath = join(cwd, "version.yml");
|
|
138
172
|
const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
|
|
139
173
|
if (opts.dryRun) {
|
|
140
174
|
printPurgePlan(planPurge(payload, cwd, keepFlags), { dryRun: true });
|
|
141
|
-
// develop
|
|
142
|
-
//
|
|
175
|
+
// Deleting the develop branch is not part of the plan (git state can only be judged at run time), so
|
|
176
|
+
// without a separate notice the dry-run preview would hide the only destructive action from the user.
|
|
143
177
|
if (opts.deleteDevelopBranch) {
|
|
144
178
|
const developBranch = existing?.branches?.develop || "develop";
|
|
145
179
|
if (existing?.branches?.main && developBranch === existing.branches.main) {
|
|
146
|
-
console.log("
|
|
180
|
+
console.log(t("cli.index.deleteDevelopSkipDry"));
|
|
147
181
|
} else {
|
|
148
|
-
console.log(
|
|
182
|
+
console.log(t("cli.index.deleteDevelopTryDry"));
|
|
149
183
|
}
|
|
150
184
|
}
|
|
151
185
|
return 0;
|
|
152
186
|
}
|
|
153
187
|
if (!opts.yes) {
|
|
154
|
-
console.error("
|
|
188
|
+
console.error(t("cli.index.purgeNeedsYes"));
|
|
155
189
|
return 1;
|
|
156
190
|
}
|
|
157
191
|
const st = await exec("git", ["status", "--porcelain"], { cwd });
|
|
158
192
|
if (st.code !== 0) {
|
|
159
|
-
console.error("
|
|
193
|
+
console.error(t("cli.index.purgeGitStatusFailed"));
|
|
160
194
|
return 1;
|
|
161
195
|
}
|
|
162
196
|
if (!opts.allowDirty && st.stdout.trim() !== "") {
|
|
163
|
-
console.error("
|
|
197
|
+
console.error(t("cli.index.purgeDirty"));
|
|
164
198
|
return 1;
|
|
165
199
|
}
|
|
166
200
|
if (!opts.force) {
|
|
167
201
|
if (!process.stdout.isTTY) {
|
|
168
|
-
console.error("
|
|
202
|
+
console.error(t("cli.index.forceRequiredNonTty"));
|
|
169
203
|
return 1;
|
|
170
204
|
}
|
|
171
205
|
const repoName = detectRepoName(cwd);
|
|
172
206
|
const typed = await promptRepoName(repoName);
|
|
173
207
|
if (typed !== repoName) {
|
|
174
|
-
console.error("
|
|
208
|
+
console.error(t("cli.index.purgeRepoMismatch"));
|
|
175
209
|
return 1;
|
|
176
210
|
}
|
|
177
211
|
}
|
|
@@ -183,20 +217,20 @@ async function runInner(argv, {
|
|
|
183
217
|
if (opts.deleteDevelopBranch) {
|
|
184
218
|
const developBranch = existing?.branches?.develop || "develop";
|
|
185
219
|
if (existing?.branches?.main && developBranch === existing.branches.main) {
|
|
186
|
-
console.error(
|
|
220
|
+
console.error(t("cli.index.deleteDevelopSkip"));
|
|
187
221
|
} else {
|
|
188
222
|
const br = await exec("git", ["branch", "-d", developBranch], { cwd });
|
|
189
223
|
if (br.code !== 0) {
|
|
190
|
-
console.error(`⚠️
|
|
224
|
+
console.error(`⚠️ ${t("cli.index.deleteDevelopFailed", { branch: developBranch, reason: (br.stderr || "").trim() || t("cli.index.reasonUnknown") })}`);
|
|
191
225
|
} else {
|
|
192
|
-
console.error(
|
|
226
|
+
console.error(t("cli.index.deleteDevelopDone", { branch: developBranch }));
|
|
193
227
|
}
|
|
194
228
|
}
|
|
195
229
|
}
|
|
196
230
|
return 0;
|
|
197
231
|
}
|
|
198
232
|
|
|
199
|
-
// uninstall
|
|
233
|
+
// uninstall mode: removes the installed files and, optionally, the README section, gitignore entries and version.yml.
|
|
200
234
|
if (opts.mode === "uninstall") {
|
|
201
235
|
const safeSelection = {
|
|
202
236
|
workflows: true, scripts: true,
|
|
@@ -210,50 +244,50 @@ async function runInner(argv, {
|
|
|
210
244
|
startLog("uninstall");
|
|
211
245
|
const r = runUninstall({}, payload, cwd, safeSelection);
|
|
212
246
|
const removed = [
|
|
213
|
-
|
|
214
|
-
r.appFiles.length > 0 &&
|
|
215
|
-
r.readme && "
|
|
216
|
-
r.gitignore && ".
|
|
247
|
+
t("cli.index.removedWorkflows", { n: r.workflows.length }), t("cli.index.removedScripts", { n: r.scripts.length }),
|
|
248
|
+
r.appFiles.length > 0 && t("cli.index.removedAppFiles", { n: r.appFiles.length }),
|
|
249
|
+
r.readme && t("cli.index.removedReadme"),
|
|
250
|
+
r.gitignore && t("cli.index.removedGitignore"), r.versionYml && "version.yml",
|
|
217
251
|
].filter(Boolean).join(", ");
|
|
218
|
-
console.error(
|
|
252
|
+
console.error(t("cli.index.removed", { removed }));
|
|
219
253
|
return 0;
|
|
220
254
|
}
|
|
221
255
|
if (!process.stdout.isTTY) {
|
|
222
|
-
console.error("
|
|
256
|
+
console.error(t("cli.index.forceRequiredNonTty"));
|
|
223
257
|
return 1;
|
|
224
258
|
}
|
|
225
259
|
startLog("uninstall");
|
|
226
|
-
// --purge-*
|
|
260
|
+
// --purge-* flags become the initial checklist selection (never silently ignored).
|
|
227
261
|
await runUninstallFlow(payload, cwd, prompts, {
|
|
228
262
|
readme: opts.purgeReadme, gitignore: opts.purgeGitignore, versionYml: opts.purgeVersion,
|
|
229
263
|
});
|
|
230
264
|
return 0;
|
|
231
265
|
}
|
|
232
266
|
|
|
233
|
-
// status
|
|
267
|
+
// status mode: read-only, always works regardless of TTY/--force
|
|
234
268
|
if (opts.mode === "status") {
|
|
235
269
|
printStatus(runStatus(payload, cwd));
|
|
236
270
|
return 0;
|
|
237
271
|
}
|
|
238
|
-
// doctor
|
|
272
|
+
// doctor mode: read-only, always works regardless of TTY/--force
|
|
239
273
|
if (opts.mode === "doctor") {
|
|
240
274
|
printDoctorReport(runDoctor(cwd));
|
|
241
275
|
return 0;
|
|
242
276
|
}
|
|
243
|
-
//
|
|
244
|
-
// (TTY
|
|
245
|
-
// --dry-run
|
|
277
|
+
// An explicit mode (full) without --force is rejected immediately, TTY or not
|
|
278
|
+
// (fixes a defect where a TTY run installed at once without confirmation).
|
|
279
|
+
// --dry-run writes no files, so it bypasses the --force gate (same safety as status/doctor).
|
|
246
280
|
if (!opts.force && !opts.dryRun) {
|
|
247
|
-
console.error("
|
|
281
|
+
console.error(t("cli.index.forceRequired"));
|
|
248
282
|
return 1;
|
|
249
283
|
}
|
|
250
284
|
|
|
251
|
-
//
|
|
285
|
+
// Load the existing version.yml: the single source of truth for preserving version/version_code/project_paths
|
|
252
286
|
const vyPath = join(cwd, "version.yml");
|
|
253
287
|
const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
|
|
254
288
|
|
|
255
|
-
//
|
|
256
|
-
// --paths
|
|
289
|
+
// Detection (CLI args first, otherwise auto-detect; the version.yml-first rule lives inside detectTypes/detectVersion).
|
|
290
|
+
// Pass --paths to detection so a monorepo that gives only --paths and omits --type is installed with those types.
|
|
257
291
|
let cliPaths;
|
|
258
292
|
try {
|
|
259
293
|
cliPaths = parsePathsCsv(opts.pathsCsv);
|
|
@@ -266,10 +300,10 @@ async function runInner(argv, {
|
|
|
266
300
|
const detectWarnings = [];
|
|
267
301
|
const branch = detectDefaultBranch(cwd, {
|
|
268
302
|
warn: (m) => { detectWarnings.push(m); console.error(m); },
|
|
269
|
-
hint: "
|
|
303
|
+
hint: t("cli.index.mainBranchHint"),
|
|
270
304
|
});
|
|
271
305
|
const repoName = detectRepoName(cwd);
|
|
272
|
-
//
|
|
306
|
+
// Confirm paths (non-interactive: --paths first -> saved value -> single candidate auto-picked -> error)
|
|
273
307
|
let paths;
|
|
274
308
|
try {
|
|
275
309
|
paths = await resolveProjectPaths({
|
|
@@ -281,25 +315,25 @@ async function runInner(argv, {
|
|
|
281
315
|
throw e;
|
|
282
316
|
}
|
|
283
317
|
|
|
284
|
-
// version:
|
|
285
|
-
//
|
|
286
|
-
//
|
|
318
|
+
// version: existing version.yml first (SSoT; prevents overwriting on re-run) -> CLI value -> file detection.
|
|
319
|
+
// Non-interactive, so the fallback notice uses the CLI wording (--project-version) as is.
|
|
320
|
+
// Detect after paths are confirmed so a monorepo subfolder's version and build number are read.
|
|
287
321
|
const version = resolveVersion({
|
|
288
322
|
cwd, existing, explicit: opts.version,
|
|
289
323
|
types, paths, warn: (m) => { detectWarnings.push(m); console.error(m); },
|
|
290
324
|
});
|
|
291
325
|
const versionCode = resolveVersionCode({ cwd, existing, types, paths });
|
|
292
326
|
|
|
293
|
-
//
|
|
294
|
-
//
|
|
327
|
+
// Branch config (--main-branch/--develop-branch -> saved version.yml value -> detected default -> main/develop).
|
|
328
|
+
// Failed-detection values saved by an earlier version ("(unknown)" etc.) are not accepted as saved values, otherwise re-running would never recover.
|
|
295
329
|
const savedBranch = (b) => (isValidBranchName(b) ? b : "");
|
|
296
330
|
const branches = resolveBranchConfig({
|
|
297
331
|
mainBranch: opts.mainBranch || savedBranch(existing?.branches?.main),
|
|
298
332
|
developBranch: opts.developBranch || savedBranch(existing?.branches?.develop),
|
|
299
333
|
defaultBranch: branch,
|
|
300
334
|
});
|
|
301
|
-
// pr-flow
|
|
302
|
-
//
|
|
335
|
+
// In pr-flow, if develop is missing on the remote, create and push it automatically (--force non-interactive, no question).
|
|
336
|
+
// If the remote has no branches at all (empty remote / no origin) there is nothing to base the push on, so notify instead of creating.
|
|
303
337
|
let developMissing = false;
|
|
304
338
|
if (branches.mode === "pr-flow" && !opts.dryRun) {
|
|
305
339
|
const remoteBranches = await detectRemoteBranches(cwd);
|
|
@@ -315,41 +349,41 @@ async function runInner(argv, {
|
|
|
315
349
|
}
|
|
316
350
|
}
|
|
317
351
|
|
|
318
|
-
//
|
|
319
|
-
// (Flutter
|
|
352
|
+
// Type-specific options: the type hook decides CLI flag -> saved version.yml value -> default. Empty if the type is absent.
|
|
353
|
+
// (For an existing Flutter install without a saved value, the stores are inferred from the installed workflows, same as interactive.)
|
|
320
354
|
const typeOptions = mergeHookResults(types, "resolveOptions", {
|
|
321
355
|
opts, existing, workflowsDir: join(cwd, PATHS.workflowsDir),
|
|
322
356
|
});
|
|
323
357
|
|
|
324
|
-
//
|
|
358
|
+
// Chosen deploy style: flag -> saved value -> default. Normalizing to the style actually installed happens when the context is assembled.
|
|
325
359
|
const chosenDeployStyle = resolveDeployStyle({ payload, types, explicit: opts.deployStyle, existing });
|
|
326
360
|
|
|
327
361
|
const context = buildInstallContext({
|
|
328
362
|
payload, existing, templateVersion: readTemplateVersion(), types, deployStyle: chosenDeployStyle,
|
|
329
363
|
typeOptions,
|
|
330
|
-
//
|
|
364
|
+
// Options: CLI flag first -> saved version.yml option -> default (same rule as interactive)
|
|
331
365
|
releaseOptions: resolveReleaseOptions({ semverAuto: opts.includeSemverAuto, copilotAi: opts.includeCopilotAi }, existing),
|
|
332
366
|
mode: opts.mode, force: opts.force, version, versionCode, branch,
|
|
333
367
|
branches,
|
|
334
368
|
paths,
|
|
335
369
|
repoName,
|
|
336
|
-
// @wizard ask/auto
|
|
370
|
+
// resolvers that compute @wizard ask/auto token values
|
|
337
371
|
resolvers: makeResolvers(cwd, repoName, paths, typeOptions),
|
|
338
|
-
now, today,
|
|
339
|
-
//
|
|
372
|
+
now, today, language,
|
|
373
|
+
// Extra context for the install log; does not change the install itself.
|
|
340
374
|
markers: detectMarkers(cwd, types), detectWarnings,
|
|
341
375
|
});
|
|
342
376
|
|
|
343
|
-
//
|
|
377
|
+
// Types without a zero-downtime (nginx/traefik) workflow are installed with the single-server deploy; say so instead of passing silently.
|
|
344
378
|
const fallbackTypes = fallbackStyleTypes(payload, types, chosenDeployStyle);
|
|
345
379
|
if (fallbackTypes.length) {
|
|
346
|
-
console.error(`⚠️ ${fallbackTypes.join(", ")
|
|
380
|
+
console.error(`⚠️ ${t("cli.index.deployFallback", { types: fallbackTypes.join(", "), style: chosenDeployStyle })}`);
|
|
347
381
|
}
|
|
348
382
|
|
|
349
|
-
//
|
|
383
|
+
// Compact non-interactive banner (one line, minimal log noise)
|
|
350
384
|
printBannerCompact({ version: context.templateVersion, mode: opts.mode });
|
|
351
385
|
|
|
352
|
-
// Breaking Changes
|
|
386
|
+
// Breaking Changes gate (non-interactive warns, then proceeds)
|
|
353
387
|
const proceed = await runBreakingCheck({ cwd, payloadRoot: payload, templateVersion: context.templateVersion });
|
|
354
388
|
if (!proceed) return 0;
|
|
355
389
|
|
|
@@ -359,12 +393,12 @@ async function runInner(argv, {
|
|
|
359
393
|
}
|
|
360
394
|
|
|
361
395
|
startLog("install");
|
|
362
|
-
// opts.mode
|
|
363
|
-
//
|
|
364
|
-
//
|
|
396
|
+
// opts.mode passed the whitelist check in parseArgs(), and interactive/purge/uninstall/status/doctor all
|
|
397
|
+
// returned early above, so it is guaranteed to be full here (the default branch is gone along with the
|
|
398
|
+
// removed partial-install modes).
|
|
365
399
|
const result = runFull(context, payload, cwd);
|
|
366
400
|
|
|
367
|
-
//
|
|
401
|
+
// Completion summary (also printed in CLI mode)
|
|
368
402
|
printSummary({
|
|
369
403
|
mode: opts.mode, types, version, versionCode, branches, developMissing,
|
|
370
404
|
copiedFiles: result?.workflows?.copiedFiles ?? [],
|
|
@@ -382,8 +416,8 @@ async function runInner(argv, {
|
|
|
382
416
|
scripts: result?.scripts ?? null,
|
|
383
417
|
});
|
|
384
418
|
for (const n of postInstallNotices(result)) console.error(n.startsWith(" ") ? n : `⚠️ ${n}`);
|
|
385
|
-
// store_submit
|
|
386
|
-
// (
|
|
419
|
+
// The store_submit deploy mode auto-submits for review on every main push, so show the same warning non-interactively
|
|
420
|
+
// (the interactive path shows ui/prompts.js#deployModeWarning as a note at selection time). Only the matching type hook emits it.
|
|
387
421
|
for (const { hook } of hooksFor(types, "installNotices")) {
|
|
388
422
|
for (const w of hook(typeOptions)) {
|
|
389
423
|
if (w) console.error(`⚠️ ${w}`);
|
|
@@ -392,20 +426,20 @@ async function runInner(argv, {
|
|
|
392
426
|
return 0;
|
|
393
427
|
}
|
|
394
428
|
|
|
395
|
-
//
|
|
396
|
-
//
|
|
429
|
+
// Public entry point: closes the logger however the run ends (normal return, CliError, exception).
|
|
430
|
+
// A thin wrapper, since wrapping the whole body in try would re-indent all of it.
|
|
397
431
|
export async function run(argv, opts = {}) {
|
|
398
432
|
try {
|
|
399
433
|
return await runInner(argv, opts);
|
|
400
434
|
} catch (e) {
|
|
401
|
-
// Ctrl+C/EOF
|
|
435
|
+
// Ctrl+C/EOF aborts immediately at any question: exit before writing install files and return 130 by shell convention.
|
|
402
436
|
if (isPromptAbort(e)) {
|
|
403
|
-
prompts.cancelMessage("
|
|
437
|
+
prompts.cancelMessage(t("cli.index.aborted"));
|
|
404
438
|
return e.signal === "SIGTERM" ? 143 : 130;
|
|
405
439
|
}
|
|
406
|
-
//
|
|
440
|
+
// If a run that already started logging dies midway, a header-only log hides the cause, so record the reason.
|
|
407
441
|
if (currentLogPath()) log.fail("run", "error", e?.message || String(e));
|
|
408
|
-
//
|
|
442
|
+
// Failures the user can fix (permissions, etc.) end with a readable message instead of a stack trace.
|
|
409
443
|
if (e instanceof CliError) { console.error(e.message); return 1; }
|
|
410
444
|
throw e;
|
|
411
445
|
} finally {
|
package/src/ui/ansi.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Shared ANSI helpers used by banner/status-cards/summary (independent of readline-engine's own helpers, zero dependencies)
|
|
2
2
|
const E = "\x1b[";
|
|
3
3
|
export const A = {
|
|
4
4
|
reset: `${E}0m`,
|
|
@@ -12,10 +12,10 @@ export const A = {
|
|
|
12
12
|
gray: `${E}90m`,
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
// NO_COLOR(https://no-color.org)
|
|
16
|
-
// no-color.org
|
|
17
|
-
//
|
|
18
|
-
// TERM=dumb(Emacs shell
|
|
15
|
+
// Disable color when NO_COLOR (https://no-color.org) is set or the target stream is not a TTY.
|
|
16
|
+
// Per the no-color.org spec only the presence of NO_COLOR matters, not its value: NO_COLOR="" (empty)
|
|
17
|
+
// counts as set, so we check presence instead of truthiness (`!process.env.NO_COLOR`).
|
|
18
|
+
// TERM=dumb (Emacs shell, some CI pseudo-TTYs) cannot interpret ANSI sequences and prints them raw, so color is off too.
|
|
19
19
|
export const isDumbTerminal = () => process.env.TERM === "dumb";
|
|
20
20
|
export function colorEnabled(stream = process.stdout) {
|
|
21
21
|
return process.env.NO_COLOR === undefined && !!stream.isTTY && !isDumbTerminal();
|
|
@@ -23,13 +23,13 @@ export function colorEnabled(stream = process.stdout) {
|
|
|
23
23
|
|
|
24
24
|
export const paint = (s, color, enabled = colorEnabled()) => (enabled ? `${color}${s}${A.reset}` : String(s));
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// Approximate display width (CJK 2 columns, ANSI sequences 0) - used to align the right edge of boxes
|
|
27
27
|
export function visualWidth(s) {
|
|
28
28
|
const plain = String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
29
29
|
let w = 0;
|
|
30
30
|
for (const ch of plain) {
|
|
31
31
|
const cp = ch.codePointAt(0);
|
|
32
|
-
//
|
|
32
|
+
// Hangul/CJK/emoji take roughly 2 columns (terminal convention)
|
|
33
33
|
w += (cp >= 0x1100 && (cp <= 0x115f || (cp >= 0x2e80 && cp <= 0xa4cf) || (cp >= 0xac00 && cp <= 0xd7a3)
|
|
34
34
|
|| (cp >= 0xf900 && cp <= 0xfaff) || (cp >= 0xff00 && cp <= 0xff60) || cp >= 0x1f300)) ? 2 : 1;
|
|
35
35
|
}
|
package/src/ui/banner.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
//
|
|
1
|
+
// First-screen banner - classic boxed style
|
|
2
2
|
import { A, paint, visualWidth } from "./ansi.js";
|
|
3
|
+
import { t } from "../i18n/index.js";
|
|
3
4
|
|
|
4
|
-
const INNER = 56; //
|
|
5
|
+
const INNER = 56; // inner box width
|
|
5
6
|
|
|
6
7
|
function boxLine(out, content = "") {
|
|
7
8
|
const pad = Math.max(0, INNER - visualWidth(content));
|
|
8
9
|
out(paint("║", A.cyan) + content + " ".repeat(pad) + paint("║", A.cyan) + "\n");
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
+
// Interactive first-screen banner - boxed title + meta lines
|
|
12
13
|
export function printBanner({ version, modeLabel }, out = (s) => process.stderr.write(s), columns = process.stderr.columns) {
|
|
13
14
|
out("\n");
|
|
14
|
-
//
|
|
15
|
+
// On terminals narrower than the box the border wraps and breaks the shape, so print just the title.
|
|
15
16
|
if (columns && columns < INNER + 2) {
|
|
16
17
|
out(`${paint("✦", A.yellow)} ${paint("PROJECT AUTO WIZARD", A.bold)}\n`);
|
|
17
18
|
} else {
|
|
@@ -27,7 +28,7 @@ export function printBanner({ version, modeLabel }, out = (s) => process.stderr.
|
|
|
27
28
|
out("\n");
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
//
|
|
31
|
+
// Compact banner for non-interactive (--force/CI) runs - one line (minimal log noise, keeps the version traceable)
|
|
31
32
|
export function printBannerCompact({ version, mode }, out = (s) => process.stderr.write(s)) {
|
|
32
|
-
out(`${paint("✦", A.yellow)} ${paint("project-auto-wizard", A.bold)} v${version} — ${mode}
|
|
33
|
+
out(`${paint("✦", A.yellow)} ${paint("project-auto-wizard", A.bold)} v${version} — ${t("ui.banner.compact", { mode })}\n`);
|
|
33
34
|
}
|