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/commands/purge.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// purge
|
|
2
|
-
// AUTO-VERSION-SECTION
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// develop
|
|
6
|
-
//
|
|
1
|
+
// purge mode: on top of everything planRemoval identifies (workflows, scripts), also removes version.yml, the README
|
|
2
|
+
// AUTO-VERSION-SECTION block, auto-added .gitignore entries and the CHANGELOG created by the release
|
|
3
|
+
// workflow, restoring the pre-install state completely. Files the wizard did not create are never deleted.
|
|
4
|
+
// Hidden development/testing mode.
|
|
5
|
+
// Deleting the develop branch differs in nature from deleting files (it needs a run-time git state
|
|
6
|
+
// judgment), so it is not part of this plan and is handled directly in the purge branch of index.js.
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { PATHS } from "../core/paths.js";
|
|
@@ -11,12 +11,17 @@ import { planRemoval, backupArtifacts } from "../core/removal-plan.js";
|
|
|
11
11
|
import { executeRemoval } from "../core/removal-exec.js";
|
|
12
12
|
import { hasVersionSection } from "../core/copy/readme.js";
|
|
13
13
|
import { hasAutoAddedEntries } from "../core/copy/gitignore.js";
|
|
14
|
+
import { t } from "../i18n/index.js";
|
|
14
15
|
|
|
15
16
|
const CHANGELOG_FILES = ["CHANGELOG.json", "CHANGELOG.md"];
|
|
16
17
|
|
|
17
|
-
// changelog_manager.py
|
|
18
|
-
// CHANGELOG.json
|
|
19
|
-
//
|
|
18
|
+
// Whether the CHANGELOG was created by changelog_manager.py: judged by content so a CHANGELOG.md the user
|
|
19
|
+
// wrote themselves is not deleted. CHANGELOG.json is recognized by its metadata/releases structure, and
|
|
20
|
+
// CHANGELOG.md by the header generate-md writes. If the JSON is ours, the MD is regenerated wholesale from
|
|
21
|
+
// the JSON on every release, so it is ours too.
|
|
22
|
+
// The header's label may be localized by the script, so both the Korean and English wording are accepted
|
|
23
|
+
// (the Korean label is written as escapes to keep this source free of Hangul).
|
|
24
|
+
const GENERATED_MD_HEADER = /^# Changelog\n\n\*\*(?:\uD604\uC7AC \uBC84\uC804|Current version):\*\*/;
|
|
20
25
|
function readText(targetRoot, f) {
|
|
21
26
|
try { return readFileSync(join(targetRoot, f), "utf8").replace(/\r\n/g, "\n"); } catch { return null; }
|
|
22
27
|
}
|
|
@@ -31,20 +36,20 @@ function isGeneratedChangelogJson(targetRoot) {
|
|
|
31
36
|
function generatedChangelogFiles(targetRoot) {
|
|
32
37
|
const jsonOurs = isGeneratedChangelogJson(targetRoot);
|
|
33
38
|
const md = readText(targetRoot, "CHANGELOG.md");
|
|
34
|
-
const mdOurs = md !== null && (jsonOurs ||
|
|
39
|
+
const mdOurs = md !== null && (jsonOurs || GENERATED_MD_HEADER.test(md));
|
|
35
40
|
return CHANGELOG_FILES.filter((f) => (f === "CHANGELOG.json" ? jsonOurs : mdOurs));
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
// keepFlags: { versionYml, readme, changelog, workflows, scripts }
|
|
39
|
-
//
|
|
43
|
+
// keepFlags: { versionYml, readme, changelog, workflows, scripts }: categories set to true are excluded from the candidates.
|
|
44
|
+
// Returns { workflows, scripts, versionYml, readmeSection, gitignore, changelog }: a pure function that deletes nothing.
|
|
40
45
|
export function planPurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
41
46
|
const removalPlan = planRemoval(payloadRoot, targetRoot);
|
|
42
|
-
//
|
|
47
|
+
// If the workflows stay, so do their .bak/.template.yaml; removing the .gitignore entries too would surface the backup files in git status.
|
|
43
48
|
const gitignoreKept = hasAutoAddedEntries(targetRoot) && keepFlags.workflows === true
|
|
44
49
|
&& backupArtifacts(removalPlan.workflows).length > 0;
|
|
45
50
|
return {
|
|
46
51
|
workflows: keepFlags.workflows ? [] : removalPlan.workflows,
|
|
47
|
-
// Flutter
|
|
52
|
+
// The creation record of Flutter app files lives in the baseline, so they follow the same flag as the baseline.
|
|
48
53
|
appFiles: keepFlags.workflows ? [] : removalPlan.appFiles,
|
|
49
54
|
baseline: keepFlags.workflows ? [] : removalPlan.baseline,
|
|
50
55
|
scripts: keepFlags.scripts ? [] : removalPlan.scripts,
|
|
@@ -56,55 +61,55 @@ export function planPurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
|
56
61
|
};
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
//
|
|
64
|
+
// Summary printed before deletion; reused by both the dry-run preview and the summary before a real run.
|
|
60
65
|
export function printPurgePlan(plan, { dryRun = false } = {}) {
|
|
61
66
|
const lines = ["",
|
|
62
67
|
dryRun
|
|
63
|
-
? "
|
|
64
|
-
: "
|
|
68
|
+
? t("cmd.purge.headerDry")
|
|
69
|
+
: t("cmd.purge.header"),
|
|
65
70
|
""];
|
|
66
|
-
lines.push(
|
|
71
|
+
lines.push(t("cmd.purge.workflows", { n: plan.workflows.length }));
|
|
67
72
|
for (const f of plan.workflows) lines.push(` - ${f}`);
|
|
68
|
-
lines.push(
|
|
73
|
+
lines.push(t("cmd.purge.scripts", { n: plan.scripts.length }));
|
|
69
74
|
for (const f of plan.scripts) lines.push(` - ${f}`);
|
|
70
|
-
for (const f of plan.appFiles || []) lines.push(
|
|
71
|
-
if (plan.versionYml) lines.push("
|
|
72
|
-
if (plan.readmeSection) lines.push("
|
|
73
|
-
if (plan.gitignore) lines.push(".gitignore
|
|
74
|
-
if (plan.gitignoreKept) lines.push(".
|
|
75
|
-
for (const f of plan.changelog) lines.push(
|
|
75
|
+
for (const f of plan.appFiles || []) lines.push(t("cmd.purge.appFile", { file: f }));
|
|
76
|
+
if (plan.versionYml) lines.push(t("cmd.purge.versionYml"));
|
|
77
|
+
if (plan.readmeSection) lines.push(t("cmd.purge.readmeSection"));
|
|
78
|
+
if (plan.gitignore) lines.push(t("cmd.purge.gitignore"));
|
|
79
|
+
if (plan.gitignoreKept) lines.push(t("cmd.purge.gitignoreKept"));
|
|
80
|
+
for (const f of plan.changelog) lines.push(t("cmd.purge.file", { file: f }));
|
|
76
81
|
lines.push("");
|
|
77
82
|
console.log(lines.join("\n"));
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
//
|
|
81
|
-
// planRemoval
|
|
82
|
-
//
|
|
83
|
-
// readmeSection
|
|
84
|
-
// removeVersionSectionFromReadme()
|
|
85
|
-
//
|
|
85
|
+
// Performs the actual deletion: returns the same shape as planPurge() but reflecting what was really removed.
|
|
86
|
+
// Why the planRemoval result is not deleted as is: that list is always the full set, which does not fit
|
|
87
|
+
// the requirement to preserve selected categories via --keep-*.
|
|
88
|
+
// readmeSection does not echo the plan's decision but reflects the actual return value of
|
|
89
|
+
// removeVersionSectionFromReadme() (whether it was "removed"), so even if the plan and the real removal
|
|
90
|
+
// condition theoretically diverge, printPurgeResult never falsely reports "removed".
|
|
86
91
|
export function executePurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
87
92
|
const plan = planPurge(payloadRoot, targetRoot, keepFlags);
|
|
88
93
|
const { readme, gitignore } = executeRemoval(targetRoot, { ...plan, readme: plan.readmeSection }, {
|
|
89
94
|
logOrder: ["version", "gitignore", "changelog", "readme"],
|
|
90
|
-
gitignoreDetail: (status) => (status === "removed" || status === "file-deleted" ? ".
|
|
95
|
+
gitignoreDetail: (status) => (status === "removed" || status === "file-deleted" ? t("cmd.purge.gitignoreLog") : null),
|
|
91
96
|
});
|
|
92
97
|
return { ...plan, readmeSection: readme, gitignore };
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
100
|
+
// Prints the list actually removed after deletion, in exactly the same form as printPurgePlan (file names
|
|
101
|
+
// listed); printing only counts would not show what was deleted.
|
|
97
102
|
export function printPurgeResult(result) {
|
|
98
|
-
const lines = ["", "
|
|
99
|
-
lines.push(
|
|
103
|
+
const lines = ["", t("cmd.purge.removedHeading"), ""];
|
|
104
|
+
lines.push(t("cmd.purge.workflows", { n: result.workflows.length }));
|
|
100
105
|
for (const f of result.workflows) lines.push(` - ${f}`);
|
|
101
|
-
lines.push(
|
|
106
|
+
lines.push(t("cmd.purge.scripts", { n: result.scripts.length }));
|
|
102
107
|
for (const f of result.scripts) lines.push(` - ${f}`);
|
|
103
|
-
for (const f of result.appFiles || []) lines.push(
|
|
104
|
-
if (result.versionYml) lines.push("
|
|
105
|
-
if (result.readmeSection) lines.push("
|
|
106
|
-
if (result.gitignore) lines.push(".gitignore
|
|
107
|
-
for (const f of result.changelog) lines.push(
|
|
108
|
+
for (const f of result.appFiles || []) lines.push(t("cmd.purge.appFile", { file: f }));
|
|
109
|
+
if (result.versionYml) lines.push(t("cmd.purge.versionYml"));
|
|
110
|
+
if (result.readmeSection) lines.push(t("cmd.purge.readmeSection"));
|
|
111
|
+
if (result.gitignore) lines.push(t("cmd.purge.gitignore"));
|
|
112
|
+
for (const f of result.changelog) lines.push(t("cmd.purge.file", { file: f }));
|
|
108
113
|
lines.push("");
|
|
109
114
|
console.log(lines.join("\n"));
|
|
110
115
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// status
|
|
1
|
+
// status command: read-only install state check. No network access (local file comparison only).
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parseExisting } from "../core/version-yml.js";
|
|
@@ -9,32 +9,33 @@ import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
|
9
9
|
import { findStaleWorkflows } from "../core/removal-plan.js";
|
|
10
10
|
import { readBaseline } from "../core/baseline.js";
|
|
11
11
|
import { hooksFor, mergeHookResults } from "../core/types.js";
|
|
12
|
+
import { t } from "../i18n/index.js";
|
|
12
13
|
|
|
13
|
-
// payloadRoot:
|
|
14
|
+
// payloadRoot: the package's payload/ root. targetRoot: the repo whose state is checked.
|
|
14
15
|
export function runStatus(payloadRoot, targetRoot = ".") {
|
|
15
16
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
16
17
|
if (!existsSync(vyPath)) return { installed: false };
|
|
17
18
|
|
|
18
19
|
const existing = parseExisting(readFileSync(vyPath, "utf8"));
|
|
19
20
|
const repoName = detectRepoName(targetRoot);
|
|
20
|
-
//
|
|
21
|
-
//
|
|
21
|
+
// Apply the env mode, deploy mode and store selection substituted into the workflows at install time to the
|
|
22
|
+
// comparison as well; otherwise unmodified files show up as drift and deselected store workflows as "deleted".
|
|
22
23
|
const typeOptions = mergeHookResults(existing.types, "resolveOptions", { existing });
|
|
23
24
|
const resolvers = makeResolvers(targetRoot, repoName, existing.paths, typeOptions);
|
|
24
|
-
// version.yml
|
|
25
|
-
// {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}}
|
|
26
|
-
//
|
|
25
|
+
// Without a branches block in version.yml (install predating the feature, or hand-edited), makeSrcText(null)
|
|
26
|
+
// cannot substitute {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} and every workflow is falsely reported as drift, so
|
|
27
|
+
// fall back to defaults for the comparison (not the real saved values, but enough for drift detection).
|
|
27
28
|
const branchesForCompare = existing.branches || { main: detectDefaultBranch(targetRoot) || "main", develop: "develop", mode: "pr-flow" };
|
|
28
29
|
const context = {
|
|
29
30
|
types: existing.types, paths: existing.paths,
|
|
30
31
|
repoName, resolvers, branches: branchesForCompare,
|
|
31
32
|
...mergeHookResults(existing.types, "contextFields", typeOptions),
|
|
32
|
-
//
|
|
33
|
-
//
|
|
33
|
+
// Compare using the deploy style chosen at install so nginx/traefik CD edits are caught as drift too.
|
|
34
|
+
// If omitted, the default (simple) filters out the installed zero-downtime CD from the comparison.
|
|
34
35
|
deployStyle: isDeployStyle(existing.options.deployStyle) ? existing.options.deployStyle : DEFAULT_DEPLOY_STYLE,
|
|
35
36
|
};
|
|
36
37
|
const plan = planWorkflows(context, payloadRoot, targetRoot);
|
|
37
|
-
//
|
|
38
|
+
// Old workflows absent from the current version are cleaned up on the next update; until then they keep running on the old triggers.
|
|
38
39
|
const stale = findStaleWorkflows(payloadRoot, targetRoot, readBaseline(targetRoot));
|
|
39
40
|
|
|
40
41
|
return {
|
|
@@ -44,60 +45,59 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
44
45
|
types: existing.types,
|
|
45
46
|
branches: existing.branches,
|
|
46
47
|
options: existing.options,
|
|
47
|
-
//
|
|
48
|
-
// baseline
|
|
48
|
+
// Files the user touched = real conflicts (changed) + upstream unchanged but locally edited (localOnly).
|
|
49
|
+
// Without a baseline localOnly is always empty, so behavior is the same as before.
|
|
49
50
|
modifiedFiles: [...plan.changed, ...plan.localOnly].map((f) => f.filename),
|
|
50
|
-
//
|
|
51
|
-
// 직접 git diff를 떠야 했던 문제를 없앤다.
|
|
51
|
+
// Buckets that preview what an update would do, so users no longer have to run git diff themselves for lack of information.
|
|
52
52
|
buckets: {
|
|
53
|
-
autoUpdatable: plan.upstreamOnly.map((f) => f.filename), //
|
|
54
|
-
localKept: plan.localOnly.map((f) => f.filename), //
|
|
55
|
-
conflicts: plan.changed.map((f) => f.filename), //
|
|
56
|
-
removed: plan.removed.map((f) => f.filename), //
|
|
53
|
+
autoUpdatable: plan.upstreamOnly.map((f) => f.filename), // replaced with the latest without asking
|
|
54
|
+
localKept: plan.localOnly.map((f) => f.filename), // local edit kept without asking
|
|
55
|
+
conflicts: plan.changed.map((f) => f.filename), // changed on both sides; needs review
|
|
56
|
+
removed: plan.removed.map((f) => f.filename), // deleted by the user and not restored
|
|
57
57
|
},
|
|
58
58
|
staleFiles: stale,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function printStatus(status) {
|
|
63
|
-
const lines = ["", "
|
|
63
|
+
const lines = ["", t("cmd.status.title"), ""];
|
|
64
64
|
if (!status.installed) {
|
|
65
|
-
lines.push("
|
|
65
|
+
lines.push(t("cmd.status.notInstalled"), "");
|
|
66
66
|
console.log(lines.join("\n"));
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
lines.push(
|
|
70
|
-
lines.push(
|
|
71
|
-
lines.push(
|
|
69
|
+
lines.push(t("cmd.status.version", { value: status.version }));
|
|
70
|
+
lines.push(t("cmd.status.templateVersion", { value: status.templateVersion }));
|
|
71
|
+
lines.push(t("cmd.status.types", { value: status.types.join(", ") || t("cmd.status.none") }));
|
|
72
72
|
if (status.branches) {
|
|
73
|
-
lines.push(
|
|
73
|
+
lines.push(t("cmd.status.branchMode", { mode: status.branches.mode, main: status.branches.main, develop: status.branches.develop }));
|
|
74
74
|
}
|
|
75
|
-
const boolLabel = (v) => (v === null ? "
|
|
76
|
-
const semverAutoLabel = status.options.semverAuto === null ? "
|
|
75
|
+
const boolLabel = (v) => (v === null ? t("cmd.status.unsetDefaultFalse") : v);
|
|
76
|
+
const semverAutoLabel = status.options.semverAuto === null ? t("cmd.status.unsetDefaultFalse") : status.options.semverAuto;
|
|
77
77
|
const copilotAiLabel = boolLabel(status.options.copilotAi ?? null);
|
|
78
78
|
const typeLabels = hooksFor(status.types, "statusLabels").map(({ hook }) => hook(status.options)).join("");
|
|
79
79
|
const deployLabel = status.options.deployStyle ? ` deploy_style=${status.options.deployStyle}` : "";
|
|
80
|
-
lines.push(
|
|
80
|
+
lines.push(t("cmd.status.options", { value: `semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${deployLabel}${typeLabels}` }));
|
|
81
81
|
if (status.modifiedFiles.length) {
|
|
82
|
-
lines.push("",
|
|
82
|
+
lines.push("", t("cmd.status.modified", { n: status.modifiedFiles.length }));
|
|
83
83
|
for (const f of status.modifiedFiles) lines.push(` - ${f}`);
|
|
84
84
|
} else {
|
|
85
|
-
lines.push("", "
|
|
85
|
+
lines.push("", t("cmd.status.unmodified"));
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (status.staleFiles?.length) {
|
|
89
|
-
lines.push("",
|
|
89
|
+
lines.push("", t("cmd.status.stale", { n: status.staleFiles.length }));
|
|
90
90
|
for (const f of status.staleFiles) lines.push(` - ${f}`);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
//
|
|
93
|
+
// What an update would do. Installs without a baseline are all zeros, so nothing is printed.
|
|
94
94
|
const b = status.buckets || { autoUpdatable: [], localKept: [], conflicts: [], removed: [] };
|
|
95
95
|
if (b.autoUpdatable.length || b.localKept.length || b.conflicts.length || b.removed.length) {
|
|
96
|
-
lines.push("", "
|
|
97
|
-
lines.push(
|
|
98
|
-
lines.push(
|
|
99
|
-
lines.push(
|
|
100
|
-
lines.push(
|
|
96
|
+
lines.push("", t("cmd.status.bucketsTitle"));
|
|
97
|
+
lines.push(t("cmd.status.bucketAuto", { n: String(b.autoUpdatable.length).padStart(3) }));
|
|
98
|
+
lines.push(t("cmd.status.bucketLocalKept", { n: String(b.localKept.length).padStart(3) }));
|
|
99
|
+
lines.push(t("cmd.status.bucketConflicts", { n: String(b.conflicts.length).padStart(3) }));
|
|
100
|
+
lines.push(t("cmd.status.bucketRemoved", { n: String(b.removed.length).padStart(3) }));
|
|
101
101
|
}
|
|
102
102
|
lines.push("");
|
|
103
103
|
console.log(lines.join("\n"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// uninstall
|
|
2
|
-
//
|
|
3
|
-
// core/removal-plan.js
|
|
1
|
+
// uninstall mode: removes the files the wizard installed (as identified by planRemoval) and, optionally,
|
|
2
|
+
// README/.gitignore/version.yml too. Per-item opt-in via the interactive checklist or --force + --purge-*.
|
|
3
|
+
// core/removal-plan.js is only reused read-only (a pure function that deletes nothing).
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { PATHS } from "../core/paths.js";
|
|
@@ -9,20 +9,21 @@ import { executeRemoval } from "../core/removal-exec.js";
|
|
|
9
9
|
import { hasVersionSection } from "../core/copy/readme.js";
|
|
10
10
|
import { hasAutoAddedEntries } from "../core/copy/gitignore.js";
|
|
11
11
|
import { CANCEL } from "../ui/prompts.js";
|
|
12
|
+
import { t } from "../i18n/index.js";
|
|
12
13
|
|
|
13
|
-
// selection: { workflows, scripts, readme, gitignore, versionYml } (
|
|
14
|
-
//
|
|
14
|
+
// selection: { workflows, scripts, readme, gitignore, versionYml } (all booleans).
|
|
15
|
+
// Returns: boolean/array values under the same keys, telling what is actually a removal "target" (pure function, deletes nothing).
|
|
15
16
|
export function planUninstall(payloadRoot, targetRoot, selection) {
|
|
16
17
|
const removalPlan = planRemoval(payloadRoot, targetRoot);
|
|
17
18
|
return {
|
|
18
19
|
workflows: selection.workflows ? removalPlan.workflows : [],
|
|
19
|
-
// baseline
|
|
20
|
-
// Flutter
|
|
20
|
+
// baseline records the workflow hashes, so it must disappear together with the workflows.
|
|
21
|
+
// The creation record of Flutter app files also lives only in the baseline, so it goes under the same item.
|
|
21
22
|
appFiles: selection.workflows ? removalPlan.appFiles : [],
|
|
22
23
|
baseline: selection.workflows ? removalPlan.baseline : [],
|
|
23
24
|
scripts: selection.scripts ? removalPlan.scripts : [],
|
|
24
25
|
readme: selection.readme ? hasVersionSection(targetRoot) : false,
|
|
25
|
-
//
|
|
26
|
+
// If the workflows stay, so do their backup files (.bak etc.); removing the .gitignore entries would surface them in git status, so keep them.
|
|
26
27
|
gitignore: selection.gitignore
|
|
27
28
|
? hasAutoAddedEntries(targetRoot) && (selection.workflows || backupArtifacts(removalPlan.workflows).length === 0)
|
|
28
29
|
: false,
|
|
@@ -30,29 +31,30 @@ export function planUninstall(payloadRoot, targetRoot, selection) {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
//
|
|
34
|
+
// Returns: the same shape as planUninstall, the items actually removed.
|
|
34
35
|
export function runUninstall(context, payloadRoot, targetRoot, selection) {
|
|
35
36
|
const plan = planUninstall(payloadRoot, targetRoot, selection);
|
|
36
37
|
const { readme, gitignore } = executeRemoval(targetRoot, plan, {
|
|
37
38
|
logOrder: ["readme", "gitignore", "version"],
|
|
38
|
-
gitignoreDetail: (status) =>
|
|
39
|
+
gitignoreDetail: (status) => t("cmd.uninstall.gitignoreDetail", { status }),
|
|
39
40
|
});
|
|
40
41
|
return { ...plan, readme, gitignore };
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
// ──
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
{ key: "
|
|
50
|
-
{ key: "
|
|
51
|
-
{ key: "
|
|
52
|
-
{ key: "
|
|
44
|
+
// ── Interactive checklist flow ────────────────────────────────────────
|
|
45
|
+
// Built lazily: labels are translated at use time, after the language is resolved.
|
|
46
|
+
const itemDefs = () => [
|
|
47
|
+
// .github/.wizard holds baseline.json and the install logs (.wizard/logs); they disappear together
|
|
48
|
+
// with the workflows, so the label says so.
|
|
49
|
+
// Flutter app files are removed too, but only those the wizard created and the user did not modify.
|
|
50
|
+
{ key: "workflows", label: t("cmd.uninstall.item.workflows") },
|
|
51
|
+
{ key: "scripts", label: t("cmd.uninstall.item.scripts") },
|
|
52
|
+
{ key: "readme", label: t("cmd.uninstall.item.readme") },
|
|
53
|
+
{ key: "gitignore", label: t("cmd.uninstall.item.gitignore") },
|
|
54
|
+
{ key: "versionYml", label: t("cmd.uninstall.item.versionYml") },
|
|
53
55
|
];
|
|
54
56
|
|
|
55
|
-
//
|
|
57
|
+
// Default checked state: only the two "safe delete" items are checked; the rest (readme/gitignore/versionYml) are opt-in.
|
|
56
58
|
export const SAFE_ITEMS = ["workflows", "scripts"];
|
|
57
59
|
|
|
58
60
|
function detectAvailableItems(payloadRoot, targetRoot) {
|
|
@@ -64,7 +66,7 @@ function detectAvailableItems(payloadRoot, targetRoot) {
|
|
|
64
66
|
gitignore: hasAutoAddedEntries(targetRoot),
|
|
65
67
|
versionYml: existsSync(join(targetRoot, PATHS.versionFile)),
|
|
66
68
|
};
|
|
67
|
-
return
|
|
69
|
+
return itemDefs().filter((d) => presence[d.key]).map((d) => ({ value: d.key, label: d.label }));
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
function toSelection(checkedKeys) {
|
|
@@ -76,51 +78,51 @@ function toSelection(checkedKeys) {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
function summarizeSelection(selection) {
|
|
79
|
-
const labelOf = Object.fromEntries(
|
|
81
|
+
const labelOf = Object.fromEntries(itemDefs().map((d) => [d.key, d.label]));
|
|
80
82
|
const chosen = Object.keys(selection).filter((k) => selection[k]).map((k) => `- ${labelOf[k]}`);
|
|
81
|
-
return chosen.length ? chosen.join("\n") : "
|
|
83
|
+
return chosen.length ? chosen.join("\n") : t("cmd.uninstall.noneSelected");
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
function summarizeResult(result) {
|
|
85
87
|
const lines = [];
|
|
86
|
-
if (result.workflows.length) lines.push(
|
|
87
|
-
if (result.scripts.length) lines.push(
|
|
88
|
-
if (result.appFiles?.length) lines.push(
|
|
89
|
-
if (result.readme) lines.push("
|
|
90
|
-
if (result.gitignore) lines.push(".gitignore
|
|
91
|
-
if (result.versionYml) lines.push("
|
|
92
|
-
return lines.length ? lines.join("\n") : "
|
|
88
|
+
if (result.workflows.length) lines.push(t("cmd.uninstall.result.workflows", { n: result.workflows.length }));
|
|
89
|
+
if (result.scripts.length) lines.push(t("cmd.uninstall.result.scripts", { n: result.scripts.length }));
|
|
90
|
+
if (result.appFiles?.length) lines.push(t("cmd.uninstall.result.appFiles", { n: result.appFiles.length }));
|
|
91
|
+
if (result.readme) lines.push(t("cmd.uninstall.result.readme"));
|
|
92
|
+
if (result.gitignore) lines.push(t("cmd.uninstall.result.gitignore"));
|
|
93
|
+
if (result.versionYml) lines.push(t("cmd.uninstall.result.versionYml"));
|
|
94
|
+
return lines.length ? lines.join("\n") : t("cmd.uninstall.result.none");
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
// io
|
|
96
|
-
// note(text,title)?, cancelMessage(text)?
|
|
97
|
-
// preset: CLI
|
|
97
|
+
// io contract: engineIo.multiselect({message,options,initialValues}), askYesNo(msg,def),
|
|
98
|
+
// note(text,title)?, cancelMessage(text)? - src/ui/prompts.js is the real one, tests inject stubs.
|
|
99
|
+
// preset: the CLI --purge-* flags { readme, gitignore, versionYml }, reflected in the checklist's initial selection.
|
|
98
100
|
export async function runUninstallFlow(payloadRoot, targetRoot, io, preset = {}) {
|
|
99
101
|
const available = detectAvailableItems(payloadRoot, targetRoot);
|
|
100
102
|
if (available.length === 0) {
|
|
101
|
-
io.note?.("
|
|
103
|
+
io.note?.(t("cmd.uninstall.flow.nothing"), t("cmd.uninstall.flow.title"));
|
|
102
104
|
return null;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
const checked = await io.engineIo.multiselect({
|
|
106
|
-
message: "
|
|
108
|
+
message: t("cmd.uninstall.flow.select"),
|
|
107
109
|
options: available,
|
|
108
110
|
initialValues: available.map((o) => o.value).filter((v) => SAFE_ITEMS.includes(v) || preset[v] === true),
|
|
109
111
|
});
|
|
110
112
|
if (checked === CANCEL || !Array.isArray(checked) || checked.length === 0) {
|
|
111
|
-
io.cancelMessage?.("
|
|
113
|
+
io.cancelMessage?.(t("cmd.uninstall.flow.cancelled"));
|
|
112
114
|
return null;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
const selection = toSelection(checked);
|
|
116
|
-
io.note?.(summarizeSelection(selection), "
|
|
117
|
-
const ok = await io.askYesNo("
|
|
118
|
+
io.note?.(summarizeSelection(selection), t("cmd.uninstall.flow.plannedTitle"));
|
|
119
|
+
const ok = await io.askYesNo(t("cmd.uninstall.flow.confirm"), false);
|
|
118
120
|
if (ok !== true) {
|
|
119
|
-
io.cancelMessage?.("
|
|
121
|
+
io.cancelMessage?.(t("cmd.uninstall.flow.cancelled"));
|
|
120
122
|
return null;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
const result = runUninstall({}, payloadRoot, targetRoot, selection);
|
|
124
|
-
io.note?.(summarizeResult(result), "
|
|
126
|
+
io.note?.(summarizeResult(result), t("cmd.uninstall.flow.doneTitle"));
|
|
125
127
|
return result;
|
|
126
128
|
}
|
package/src/context.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Wizard-wide state made explicit as a single object
|
|
2
2
|
import { TYPE_IDS, allHookValues } from "./core/types.js";
|
|
3
3
|
|
|
4
|
-
// --type
|
|
4
|
+
// --type whitelist: the display order of the type registry (core/types.js).
|
|
5
5
|
export const VALID_TYPES = TYPE_IDS;
|
|
6
6
|
|
|
7
|
-
// --mode
|
|
8
|
-
// purge
|
|
9
|
-
// version/workflows(
|
|
10
|
-
//
|
|
7
|
+
// --mode whitelist: unknown values must be rejected before any side effect (branch lookup, etc.).
|
|
8
|
+
// purge is a hidden mode (not in --help or the interactive menu) but is still validated.
|
|
9
|
+
// version/workflows (partial install) and revert were removed: a partial install refreshes the
|
|
10
|
+
// install-time baseline only halfway and blurs update detection, and revert was a subset of uninstall.
|
|
11
11
|
export const VALID_MODES = [
|
|
12
12
|
"interactive", "full",
|
|
13
13
|
"uninstall", "status", "doctor", "purge",
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
-
export const DEFAULT_VERSION = "0.0.0"; //
|
|
16
|
+
export const DEFAULT_VERSION = "0.0.0"; // fallback when reading the package version fails (banner only; not used for breaking comparison)
|
|
17
17
|
|
|
18
18
|
export function createContext(overrides = {}) {
|
|
19
19
|
return {
|
|
@@ -22,12 +22,13 @@ export function createContext(overrides = {}) {
|
|
|
22
22
|
types: [],
|
|
23
23
|
version: "",
|
|
24
24
|
branch: "",
|
|
25
|
-
branches: null, // { main, develop, mode: "pr-flow"|"trunk-based" }
|
|
25
|
+
branches: null, // { main, develop, mode: "pr-flow"|"trunk-based" } - result of resolveBranchConfig
|
|
26
26
|
paths: new Map(), // type -> path
|
|
27
|
-
includeSemverAuto: null, // null
|
|
28
|
-
includeCopilotAi: null, // null
|
|
29
|
-
//
|
|
27
|
+
includeSemverAuto: null, // null = unset (resolved to true downstream), true/false = explicit
|
|
28
|
+
includeCopilotAi: null, // null = unset (resolved to false downstream), true/false = explicit; Copilot AI summary opt-in
|
|
29
|
+
// Type-specific option fields; all ignored in projects without that type. Fields and defaults come from the type hook (contextDefaults).
|
|
30
30
|
...allHookValues("contextDefaults"),
|
|
31
|
+
language: "en", // message language (--lang / env / version.yml), default en
|
|
31
32
|
templateVersion: "",
|
|
32
33
|
deployValues: new Map(), // "type.KEY" -> value
|
|
33
34
|
counters: {},
|
package/src/core/assets.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// npx
|
|
1
|
+
// Payload asset resolution: zero network access (replaces the old acquireTemplate/git clone).
|
|
2
|
+
// The payload/ bundled in the npm package is the single source of truth for every asset the wizard installs.
|
|
3
|
+
// Even when run from the npx global cache, import.meta.url points precisely at the payload inside the package.
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
import { exists, readText, listYamlFiles } from "./fsutil.js";
|
|
8
8
|
import { DEFAULT_VERSION } from "../context.js";
|
|
9
|
+
import { t } from "../i18n/index.js";
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
+
// Absolute path of payload/ inside the package. Tests and fixtures can substitute it by argument injection.
|
|
11
12
|
export function resolvePayloadRoot() {
|
|
12
13
|
return fileURLToPath(new URL("../../payload/", import.meta.url));
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
16
|
+
// Wizard (= template) version: the version in the package's package.json.
|
|
17
|
+
// Serves the same consumers as the former readTemplateVersion(tempDir/version.yml): banner, breaking-change comparison, version.yml record.
|
|
17
18
|
export function readTemplateVersion() {
|
|
18
19
|
try {
|
|
19
20
|
const pkgPath = fileURLToPath(new URL("../../package.json", import.meta.url));
|
|
@@ -24,23 +25,23 @@ export function readTemplateVersion() {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
// payload/workflows/common
|
|
28
|
+
// List of yaml files directly under payload/workflows/common.
|
|
28
29
|
export function listCommonWorkflows(payloadRoot = resolvePayloadRoot()) {
|
|
29
30
|
return listYamlFiles(join(payloadRoot, "workflows", "common"));
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
+
// Payload structure self-check: fails clearly when a required folder is missing (catches packaging errors early).
|
|
33
34
|
export function assertPayload(payloadRoot = resolvePayloadRoot()) {
|
|
34
35
|
if (!exists(join(payloadRoot, "workflows"))) {
|
|
35
|
-
throw new Error("
|
|
36
|
+
throw new Error(t("core.assets.missingWorkflows"));
|
|
36
37
|
}
|
|
37
38
|
if (!exists(join(payloadRoot, "scripts"))) {
|
|
38
|
-
throw new Error("
|
|
39
|
+
throw new Error(t("core.assets.missingScripts"));
|
|
39
40
|
}
|
|
40
41
|
return payloadRoot;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
// payload/version.yml.template
|
|
44
|
+
// Raw text of payload/version.yml.template.
|
|
44
45
|
export function readVersionYmlTemplate(payloadRoot = resolvePayloadRoot()) {
|
|
45
46
|
const p = join(payloadRoot, "version.yml.template");
|
|
46
47
|
return exists(p) ? readText(p) : null;
|