pan-stack-cli 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +572 -24
  2. package/bin/pan.js +204 -21
  3. package/lib/ai-skills.js +829 -0
  4. package/lib/create.js +22 -4
  5. package/lib/discover.js +59 -1
  6. package/lib/git/git-checkout.js +395 -0
  7. package/lib/git/git-clone.js +162 -27
  8. package/lib/git/git-fetch-all.js +100 -33
  9. package/lib/git/git-grv.js +68 -37
  10. package/lib/git/git-gst.js +97 -26
  11. package/lib/git/git-mr.js +546 -0
  12. package/lib/git/git-pull.js +104 -43
  13. package/lib/git/git-push.js +150 -41
  14. package/lib/git/git-sync-main.js +42 -28
  15. package/lib/git/helpers/bulk-report.js +202 -0
  16. package/lib/git/helpers/cli.js +40 -5
  17. package/lib/git/helpers/manifest.js +93 -0
  18. package/lib/git/helpers/repo-select.js +328 -9
  19. package/lib/git/helpers/repo-summary.js +161 -0
  20. package/lib/i18n.js +61 -0
  21. package/lib/inquirer-theme.js +20 -0
  22. package/lib/language.js +121 -0
  23. package/lib/languages.js +14 -0
  24. package/lib/manifest.js +115 -14
  25. package/lib/menu-prompt.js +201 -0
  26. package/lib/menu.js +134 -0
  27. package/lib/messages.js +647 -0
  28. package/lib/run-command.js +173 -52
  29. package/lib/select.js +0 -0
  30. package/lib/serve.js +75 -0
  31. package/lib/skill-registry-client.js +173 -0
  32. package/lib/stats/aggregate.js +51 -0
  33. package/lib/stats/identity.js +20 -0
  34. package/lib/stats/index.js +189 -0
  35. package/lib/stats/parse.js +46 -0
  36. package/lib/stats/range.js +38 -0
  37. package/lib/stats/report.js +264 -0
  38. package/lib/stats/review.js +157 -0
  39. package/lib/stats/scan.js +92 -0
  40. package/lib/stats/serve.js +25 -0
  41. package/lib/ui/api.js +202 -0
  42. package/lib/ui/clone.js +115 -0
  43. package/lib/ui/config.js +182 -0
  44. package/lib/ui/diff.js +128 -0
  45. package/lib/ui/export-gate.js +79 -0
  46. package/lib/ui/gen-react-mobile-code.js +6 -0
  47. package/lib/ui/gen-react-web-code.js +75 -0
  48. package/lib/ui/index.js +89 -0
  49. package/lib/ui/init.js +252 -0
  50. package/lib/ui/login.js +128 -0
  51. package/lib/ui/logout.js +22 -0
  52. package/lib/ui/palettes.js +103 -0
  53. package/lib/ui/published-html.js +24 -0
  54. package/lib/ui/sync.js +137 -0
  55. package/lib/ui/workspace.js +213 -0
  56. package/package.json +10 -3
  57. package/templates/workspace/.claude/skills/writing-guidelines/SKILL.md +0 -48
  58. package/templates/workspace/.codex/skills/writing-guidelines/SKILL.md +0 -48
  59. package/templates/workspace/.cursor/skills/writing-guidelines/SKILL.md +0 -48
  60. package/templates/workspace/.pan.yaml +0 -25
  61. package/templates/workspace/.project.yaml +0 -15
  62. package/templates/workspace/README.md +0 -35
@@ -1,39 +1,80 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * Pull latest changes for PAN Stack project repositories from a clone manifest.
4
- *
5
- * Skips repositories that do not exist or are not git checkouts. Failures warn
6
- * and continue so one broken repo does not block the rest.
3
+ * Pull latest changes for PAN Stack project repositories.
4
+ * In compact summary table: repo · branch · result · changes · conflict.
7
5
  */
8
6
 
9
7
  const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
10
- const { ensureRepoExists, runGit } = require("./helpers/git");
8
+ const { ensureRepoExists } = require("./helpers/git");
9
+ const {
10
+ runGitSilent, parsePullOutput, printSummary, statusBadge, bulkColumns, pullModeLabel,
11
+ getCurrentBranch, listConflictedFiles,
12
+ } = require("./helpers/bulk-report");
13
+ const colors = require("./helpers/colors");
14
+ const { t } = require("../i18n");
11
15
 
12
16
  function pullRepo({ name, destination, dryRun, rebase }) {
13
17
  if (!ensureRepoExists({ name, destination })) {
14
- return;
18
+ return { name, skipped: true };
15
19
  }
20
+ const branch = getCurrentBranch(destination) || t("GST_DETACHED");
21
+ const gitArgs = ["pull", "--stat"];
22
+ if (rebase) gitArgs.push("--rebase");
16
23
 
17
- const gitArgs = ["pull"];
18
- if (rebase) {
19
- gitArgs.push("--rebase");
24
+ if (dryRun) {
25
+ console.log(colors.dim(`[dry-run] (in ${destination}) git ${gitArgs.join(" ")}`));
26
+ return { name, branch, dryRun: true };
20
27
  }
21
28
 
22
- const status = runGit(gitArgs, {
23
- cwd: destination,
24
- dryRun,
25
- action: "pull",
26
- name,
27
- });
28
- if (status !== 0) {
29
- console.log(`[warn] ${name}: pull failed, check branch state or remote access`);
30
- }
29
+ process.stdout.write(colors.dim(` ↻ ${name} (${branch}) ... `));
30
+ const res = runGitSilent(gitArgs, destination);
31
+ const parsed = parsePullOutput(res.stdout, res.stderr);
32
+ const conflictFiles = parsed.conflict ? listConflictedFiles(destination) : [];
33
+ const ok = res.status === 0 && !parsed.conflict;
34
+
35
+ const shortMsg = parsed.upToDate ? t("BULK_STATUS_UP_TO_DATE")
36
+ : parsed.conflict ? t("BULK_STATUS_CONFLICT")
37
+ : parsed.mode !== "unknown" ? pullModeLabel(parsed.mode)
38
+ : ok ? t("BULK_STATUS_PULLED") : t("BULK_STATUS_FAILED");
39
+ console.log((ok ? colors.green : parsed.conflict ? colors.yellow : colors.red)(shortMsg));
40
+
41
+ return {
42
+ name, branch,
43
+ status: res.status,
44
+ upToDate: parsed.upToDate,
45
+ conflict: parsed.conflict,
46
+ conflictFiles,
47
+ mode: parsed.mode,
48
+ filesChanged: parsed.filesChanged,
49
+ insertions: parsed.insertions,
50
+ deletions: parsed.deletions,
51
+ error: res.status !== 0 && !parsed.conflict ? (res.stderr.split("\n")[0] || "").slice(0, 80) : "",
52
+ };
53
+ }
54
+
55
+ function row(r) {
56
+ if (r.skipped) return { repo: colors.dim(r.name), branch: "", result: colors.dim(t("BULK_STATUS_SKIPPED")), changes: "", conflict: "" };
57
+ if (r.dryRun) return { repo: r.name, branch: r.branch, result: colors.dim(t("BULK_STATUS_DRY_RUN")), changes: "", conflict: "" };
58
+ const result =
59
+ r.upToDate ? statusBadge("ok", t("BULK_STATUS_UP_TO_DATE"))
60
+ : r.conflict ? statusBadge("warn", t("BULK_STATUS_CONFLICT"))
61
+ : r.error ? statusBadge("fail", t("BULK_STATUS_FAILED"))
62
+ : statusBadge("ok", r.mode && r.mode !== "unknown" ? pullModeLabel(r.mode) : t("BULK_STATUS_PULLED"));
63
+ const filesLabel = r.filesChanged === 1 ? t("BULK_FILES_ONE") : t("BULK_FILES_MANY");
64
+ const changes = r.filesChanged
65
+ ? `${r.filesChanged} ${filesLabel} ${colors.green("+" + r.insertions)} ${colors.red("−" + r.deletions)}`
66
+ : colors.dim("—");
67
+ const conflict = r.conflict
68
+ ? colors.yellow(`${r.conflictFiles.length} ${r.conflictFiles.length === 1 ? t("BULK_FILES_ONE") : t("BULK_FILES_MANY")}`)
69
+ : colors.dim("—");
70
+ return { repo: r.name, branch: r.branch, result, changes, conflict };
31
71
  }
32
72
 
33
73
  function printHelp() {
34
74
  console.log(`usage: git-pull.js [--base-dir BASE_DIR] [--dry-run] [--rebase] [manifest]
35
75
 
36
76
  Pull latest changes for repositories listed in a PAN Stack clone manifest.
77
+ Prints a compact summary table (repo · branch · result · changes · conflict).
37
78
 
38
79
  ${printCommonOptions()}
39
80
  --rebase Use git pull --rebase`);
@@ -42,35 +83,55 @@ ${printCommonOptions()}
42
83
  function main() {
43
84
  const args = parseCommonArgs(process.argv.slice(2), {
44
85
  defaults: { rebase: false },
45
- handlers: {
46
- "--rebase": (parsed) => {
47
- parsed.rebase = true;
48
- },
49
- },
86
+ handlers: { "--rebase": (p) => { p.rebase = true; } },
50
87
  });
51
- if (args.help) {
52
- printHelp();
53
- return;
54
- }
88
+ if (args.help) { printHelp(); return; }
55
89
 
56
- const { repos } = loadManifestRepos({
57
- manifestPath: args.manifest,
58
- baseDir: args.baseDir,
59
- });
90
+ const { repos } = loadManifestRepos({ manifestPath: args.manifest, baseDir: args.baseDir });
91
+ const results = repos.map((r) => pullRepo({ name: r.name, destination: r.destination, dryRun: args.dryRun, rebase: args.rebase }));
92
+
93
+ const conflicts = results.filter((r) => r.conflict);
94
+ const pulled = results.filter((r) => !r.skipped && !r.dryRun && !r.upToDate && !r.conflict && !r.error);
95
+ const failed = results.filter((r) => r.error);
96
+ const upToDate = results.filter((r) => r.upToDate);
97
+ // Chỉ đưa vào bảng những repo có thay đổi / conflict / lỗi / dry-run (bỏ up-to-date).
98
+ const shown = results.filter((r) => r.dryRun || (!r.skipped && !r.upToDate));
99
+ const rows = shown.map(row);
100
+
101
+ const footer = [];
102
+ footer.push(colors.dim(t("PULL_FOOTER_COUNTS__TOTAL_CHANGED_UPTODATE_CONFLICT_FAILED", {
103
+ total: results.length, changed: pulled.length, upToDate: upToDate.length,
104
+ conflict: conflicts.length, failed: failed.length,
105
+ })));
106
+ if (conflicts.length) {
107
+ footer.push("");
108
+ footer.push(colors.bold(colors.yellow(t("PULL_CONFLICT_HEADER"))));
109
+ for (const c of conflicts) {
110
+ footer.push(` ${colors.cyan(c.name)} (${c.branch}): ${c.conflictFiles.join(", ") || "(git status)"}`);
111
+ }
112
+ }
60
113
 
61
- for (const repo of repos) {
62
- pullRepo({
63
- name: repo.name,
64
- destination: repo.destination,
65
- dryRun: args.dryRun,
66
- rebase: args.rebase,
114
+ if (rows.length === 0) {
115
+ console.log("");
116
+ console.log(colors.bold(`─── ${t("PULL_TITLE")} ───`));
117
+ console.log(colors.green(t("PULL_ALL_UP_TO_DATE__N", { n: results.length })));
118
+ for (const line of footer) console.log(line);
119
+ } else {
120
+ printSummary({
121
+ title: t("PULL_TITLE"),
122
+ rows,
123
+ columns: bulkColumns([
124
+ { key: "repo", labelKey: "BULK_COL_REPO" },
125
+ { key: "branch", labelKey: "BULK_COL_BRANCH" },
126
+ { key: "result", labelKey: "BULK_COL_RESULT" },
127
+ { key: "changes", labelKey: "BULK_COL_CHANGES" },
128
+ { key: "conflict", labelKey: "BULK_COL_CONFLICT" },
129
+ ]),
130
+ footerLines: footer,
67
131
  });
68
132
  }
69
- }
70
133
 
71
- try {
72
- main();
73
- } catch (error) {
74
- console.error(error.message);
75
- process.exit(1);
134
+ if (conflicts.length || failed.length) process.exitCode = 1;
76
135
  }
136
+
137
+ try { main(); } catch (error) { console.error(error.message); process.exit(1); }
@@ -1,39 +1,104 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * Push local commits for PAN Stack project repositories from a clone manifest.
4
- *
5
- * Skips repositories that do not exist or are not git checkouts. Failures warn
6
- * and continue so one broken repo does not block the rest.
3
+ * Push local commits for PAN Stack project repositories.
4
+ * Compact summary table: repo · branch · result · commits · MR/PR link.
7
5
  */
8
6
 
9
7
  const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
10
- const { ensureRepoExists, runGit } = require("./helpers/git");
8
+ const { ensureRepoExists } = require("./helpers/git");
9
+ const {
10
+ runGitSilent, parsePushOutput, printSummary, statusBadge, bulkColumns,
11
+ aheadBehind, aheadLineStats, getCurrentBranch,
12
+ } = require("./helpers/bulk-report");
13
+ const colors = require("./helpers/colors");
14
+ const { t } = require("../i18n");
15
+
16
+ function formatChanges(r) {
17
+ if (!r.filesChanged && !r.insertions && !r.deletions) return colors.dim("—");
18
+ const filesLabel = r.filesChanged === 1 ? t("BULK_FILES_ONE") : t("BULK_FILES_MANY");
19
+ const filesPart = r.filesChanged
20
+ ? `${r.filesChanged} ${filesLabel} `
21
+ : "";
22
+ return `${filesPart}${colors.green("+" + (r.insertions || 0))} ${colors.red("−" + (r.deletions || 0))}`;
23
+ }
11
24
 
12
25
  function pushRepo({ name, destination, dryRun, setUpstream }) {
13
26
  if (!ensureRepoExists({ name, destination })) {
14
- return;
27
+ return { name, skipped: true };
15
28
  }
29
+ const branch = getCurrentBranch(destination) || t("GST_DETACHED");
30
+ // Đếm commit + line change chờ push TRƯỚC push (sau push @{u} đã bắt kịp HEAD).
31
+ const ab = aheadBehind(destination);
32
+ const commitsAhead = ab ? ab.ahead : 0;
33
+ const lines = commitsAhead > 0
34
+ ? aheadLineStats(destination)
35
+ : { insertions: 0, deletions: 0, filesChanged: 0 };
16
36
 
17
37
  const gitArgs = ["push"];
18
- if (setUpstream) {
19
- gitArgs.push("-u", "origin", "HEAD");
38
+ if (setUpstream) gitArgs.push("-u", "origin", "HEAD");
39
+
40
+ if (dryRun) {
41
+ console.log(colors.dim(`[dry-run] (in ${destination}) git ${gitArgs.join(" ")}`));
42
+ return { name, branch, dryRun: true, commitsAhead, ...lines };
20
43
  }
21
44
 
22
- const status = runGit(gitArgs, {
23
- cwd: destination,
24
- dryRun,
25
- action: "push",
26
- name,
27
- });
28
- if (status !== 0) {
29
- console.log(`[warn] ${name}: push failed, check branch state or remote access`);
45
+ process.stdout.write(colors.dim(` ↑ ${name} (${branch}) ... `));
46
+ const res = runGitSilent(gitArgs, destination);
47
+ const parsed = parsePushOutput(res.stdout, res.stderr);
48
+ const ok = res.status === 0 && !parsed.rejected;
49
+ const shortMsg = parsed.rejected ? t("BULK_STATUS_REJECTED")
50
+ : parsed.upToDate ? t("BULK_STATUS_UP_TO_DATE")
51
+ : parsed.pushed ? t("BULK_STATUS_PUSHED")
52
+ : ok ? t("BULK_STATUS_OK")
53
+ : t("BULK_STATUS_FAILED");
54
+ console.log((ok ? (parsed.upToDate ? colors.dim : colors.green) : colors.red)(shortMsg));
55
+
56
+ return {
57
+ name, branch, commitsAhead,
58
+ insertions: lines.insertions,
59
+ deletions: lines.deletions,
60
+ filesChanged: lines.filesChanged,
61
+ status: res.status,
62
+ pushed: parsed.pushed,
63
+ upToDate: parsed.upToDate,
64
+ rejected: parsed.rejected,
65
+ mrLink: parsed.mrLink,
66
+ refs: parsed.refs,
67
+ error: res.status !== 0 && !parsed.rejected ? (res.stderr.split("\n")[0] || "").slice(0, 80) : "",
68
+ };
69
+ }
70
+
71
+ function row(r) {
72
+ if (r.skipped) {
73
+ return {
74
+ repo: colors.dim(r.name), branch: "", result: colors.dim(t("BULK_STATUS_SKIPPED")),
75
+ commits: "", changes: "", link: "",
76
+ };
30
77
  }
78
+ if (r.dryRun) {
79
+ return {
80
+ repo: r.name, branch: r.branch, result: colors.dim(t("BULK_STATUS_DRY_RUN")),
81
+ commits: String(r.commitsAhead || 0), changes: formatChanges(r), link: "",
82
+ };
83
+ }
84
+ const result =
85
+ r.rejected ? statusBadge("fail", t("BULK_STATUS_REJECTED"))
86
+ : r.upToDate ? statusBadge("skip", t("BULK_STATUS_UP_TO_DATE"))
87
+ : r.error ? statusBadge("fail", t("BULK_STATUS_FAILED"))
88
+ : r.pushed ? statusBadge("ok", t("BULK_STATUS_PUSHED"))
89
+ : statusBadge("ok", t("BULK_STATUS_OK"));
90
+ const commits = r.commitsAhead
91
+ ? colors.cyan(String(r.commitsAhead))
92
+ : colors.dim("—");
93
+ const link = r.mrLink ? colors.cyan(r.mrLink) : colors.dim("—");
94
+ return { repo: r.name, branch: r.branch, result, commits, changes: formatChanges(r), link };
31
95
  }
32
96
 
33
97
  function printHelp() {
34
98
  console.log(`usage: git-push.js [--base-dir BASE_DIR] [--dry-run] [--set-upstream] [manifest]
35
99
 
36
100
  Push local commits for repositories listed in a PAN Stack clone manifest.
101
+ Prints a compact summary table (repo · branch · result · commits · changes · MR/PR link).
37
102
 
38
103
  ${printCommonOptions()}
39
104
  --set-upstream Use git push -u origin HEAD for new branches`);
@@ -42,35 +107,79 @@ ${printCommonOptions()}
42
107
  function main() {
43
108
  const args = parseCommonArgs(process.argv.slice(2), {
44
109
  defaults: { setUpstream: false },
45
- handlers: {
46
- "--set-upstream": (parsed) => {
47
- parsed.setUpstream = true;
48
- },
49
- },
110
+ handlers: { "--set-upstream": (p) => { p.setUpstream = true; } },
50
111
  });
51
- if (args.help) {
52
- printHelp();
53
- return;
54
- }
112
+ if (args.help) { printHelp(); return; }
55
113
 
56
- const { repos } = loadManifestRepos({
57
- manifestPath: args.manifest,
58
- baseDir: args.baseDir,
59
- });
114
+ const { repos } = loadManifestRepos({ manifestPath: args.manifest, baseDir: args.baseDir });
115
+ const results = repos.map((r) => pushRepo({ name: r.name, destination: r.destination, dryRun: args.dryRun, setUpstream: args.setUpstream }));
116
+
117
+ const pushed = results.filter((r) => r.pushed);
118
+ const rejected = results.filter((r) => r.rejected);
119
+ const failed = results.filter((r) => r.error);
120
+ const upToDate = results.filter((r) => r.upToDate);
121
+ const withLink = results.filter((r) => r.mrLink);
122
+ // Chỉ đưa vào bảng: pushed / rejected / có MR link / lỗi / dry-run (bỏ up-to-date).
123
+ const shown = results.filter((r) => r.dryRun || r.pushed || r.rejected || r.error || r.mrLink);
124
+ const rows = shown.map(row);
125
+
126
+ const linesOf = (list) => list.reduce(
127
+ (acc, r) => ({
128
+ commits: acc.commits + (r.commitsAhead || 0),
129
+ insertions: acc.insertions + (r.insertions || 0),
130
+ deletions: acc.deletions + (r.deletions || 0),
131
+ }),
132
+ { commits: 0, insertions: 0, deletions: 0 }
133
+ );
134
+ const pushedLines = linesOf(pushed.length ? pushed : shown.filter((r) => r.dryRun && r.commitsAhead));
60
135
 
61
- for (const repo of repos) {
62
- pushRepo({
63
- name: repo.name,
64
- destination: repo.destination,
65
- dryRun: args.dryRun,
66
- setUpstream: args.setUpstream,
136
+ const footer = [];
137
+ footer.push(colors.dim(t("PUSH_FOOTER_COUNTS__TOTAL_PUSHED_UPTODATE_REJECTED_FAILED", {
138
+ total: results.length, pushed: pushed.length, upToDate: upToDate.length,
139
+ rejected: rejected.length, failed: failed.length,
140
+ })));
141
+ if (pushedLines.commits > 0 || pushedLines.insertions > 0 || pushedLines.deletions > 0) {
142
+ footer.push(colors.dim(t("PUSH_FOOTER_LINES__COMMITS_ADD_DEL", {
143
+ commits: pushedLines.commits,
144
+ add: pushedLines.insertions,
145
+ del: pushedLines.deletions,
146
+ })));
147
+ }
148
+ if (withLink.length) {
149
+ footer.push("");
150
+ footer.push(colors.bold(t("PUSH_LINKS_HEADER")));
151
+ for (const r of withLink) {
152
+ footer.push(` ${colors.cyan(r.name)} (${r.branch}): ${r.mrLink}`);
153
+ }
154
+ }
155
+ if (rejected.length) {
156
+ footer.push("");
157
+ footer.push(colors.yellow(t("PUSH_REJECTED_HEADER")));
158
+ for (const r of rejected) footer.push(` ${r.name}`);
159
+ }
160
+
161
+ if (rows.length === 0) {
162
+ console.log("");
163
+ console.log(colors.bold(`─── ${t("PUSH_TITLE")} ───`));
164
+ console.log(colors.green(t("PUSH_ALL_UP_TO_DATE__N", { n: results.length })));
165
+ for (const line of footer) console.log(line);
166
+ } else {
167
+ printSummary({
168
+ title: t("PUSH_TITLE"),
169
+ rows,
170
+ columns: bulkColumns([
171
+ { key: "repo", labelKey: "BULK_COL_REPO" },
172
+ { key: "branch", labelKey: "BULK_COL_BRANCH" },
173
+ { key: "result", labelKey: "BULK_COL_RESULT" },
174
+ { key: "commits", labelKey: "BULK_COL_COMMITS", align: "r" },
175
+ { key: "changes", labelKey: "BULK_COL_CHANGES" },
176
+ { key: "link", labelKey: "BULK_COL_LINK" },
177
+ ]),
178
+ footerLines: footer,
67
179
  });
68
180
  }
69
- }
70
181
 
71
- try {
72
- main();
73
- } catch (error) {
74
- console.error(error.message);
75
- process.exit(1);
182
+ if (rejected.length || failed.length) process.exitCode = 1;
76
183
  }
184
+
185
+ try { main(); } catch (error) { console.error(error.message); process.exit(1); }
@@ -13,6 +13,7 @@ const {
13
13
  } = require("./helpers/cli");
14
14
  const colors = require("./helpers/colors");
15
15
  const { resolveTargets } = require("./helpers/repo-select");
16
+ const { t } = require("../i18n");
16
17
  const {
17
18
  ensureRepoExists,
18
19
  runGit,
@@ -93,14 +94,16 @@ function printEarlyConflict({ name, branch, ref, files, mode }) {
93
94
  colors.red(`${ICON.conflict} [conflict] ${name} (${branch})`)
94
95
  );
95
96
  const detail = colors.yellow(
96
- `${mode} ${ref} thất bại — ${files.length} file conflict`
97
+ t("SYNCMAIN_CONFLICT_SHORT__MODE_REF_N", { mode, ref, n: files.length })
97
98
  );
98
99
  console.log(`${headline} — ${detail}`);
99
100
  for (const file of files.slice(0, 8)) {
100
101
  console.log(colors.dim(` • ${file}`));
101
102
  }
102
103
  if (files.length > 8) {
103
- console.log(colors.dim(` … và ${files.length - 8} file khác`));
104
+ console.log(
105
+ colors.dim(` ${t("SYNCMAIN_MORE_FILES__N", { n: files.length - 8 })}`)
106
+ );
104
107
  }
105
108
  }
106
109
 
@@ -122,14 +125,14 @@ function syncMainRepo({
122
125
  };
123
126
 
124
127
  if (!ensureRepoExists({ name, destination })) {
125
- result.message = "repo không tồn tại hoặc chưa clone";
128
+ result.message = t("SYNCMAIN_MSG_NOT_CLONED");
126
129
  return result;
127
130
  }
128
131
 
129
132
  const branch = getCurrentBranch(destination);
130
- result.branch = branch || "(detached HEAD)";
133
+ result.branch = branch || t("SYNCMAIN_DETACHED_LABEL");
131
134
  if (!branch) {
132
- result.message = "đang detached HEAD — bỏ qua";
135
+ result.message = t("SYNCMAIN_MSG_DETACHED");
133
136
  console.log(`${ICON.skip} [skip] ${name}: ${result.message}`);
134
137
  return result;
135
138
  }
@@ -139,7 +142,7 @@ function syncMainRepo({
139
142
  const files = listConflictedFiles(destination);
140
143
  result.status = "conflict";
141
144
  result.conflictFiles = files;
142
- result.message = `đang dở ${inProgress} — cần resolve trước`;
145
+ result.message = t("SYNCMAIN_MSG_IN_PROGRESS__MODE", { mode: inProgress });
143
146
  printEarlyConflict({ name, branch, ref, files, mode: inProgress });
144
147
  return result;
145
148
  }
@@ -147,7 +150,7 @@ function syncMainRepo({
147
150
  const dirtyCount = countDirtyFiles(destination);
148
151
  if (dirtyCount > 0) {
149
152
  console.log(
150
- `${ICON.warn} ${colors.yellow(`[warn] ${name}: working tree có ${dirtyCount} thay đổi chưa commit`)}`
153
+ `${ICON.warn} ${colors.yellow(t("SYNCMAIN_MSG_DIRTY__NAME_N", { name, n: dirtyCount }))}`
151
154
  );
152
155
  }
153
156
 
@@ -164,14 +167,14 @@ function syncMainRepo({
164
167
  const fetch = runGitCapture(["fetch", "origin"], { cwd: destination });
165
168
  if (fetch.status !== 0) {
166
169
  result.status = "error";
167
- result.message = fetch.stderr || "git fetch origin thất bại";
170
+ result.message = fetch.stderr || t("SYNCMAIN_MSG_FETCH_FAILED");
168
171
  console.log(`${ICON.warn} ${colors.yellow(`[warn] ${name}: ${result.message}`)}`);
169
172
  return result;
170
173
  }
171
174
 
172
175
  if (!refExists(destination, ref)) {
173
176
  result.status = "error";
174
- result.message = `không tìm thấy ref ${ref} (chạy fetch trước?)`;
177
+ result.message = t("SYNCMAIN_MSG_REF_MISSING__REF", { ref });
175
178
  console.log(`${ICON.warn} ${colors.yellow(`[warn] ${name}: ${result.message}`)}`);
176
179
  return result;
177
180
  }
@@ -188,8 +191,9 @@ function syncMainRepo({
188
191
  const conflictFiles = listConflictedFiles(destination);
189
192
  if (conflictFiles.length > 0 || action !== 0) {
190
193
  result.status = "conflict";
191
- result.conflictFiles = conflictFiles.length > 0 ? conflictFiles : ["(xem git status)"];
192
- result.message = `${result.mode} conflict`;
194
+ result.conflictFiles =
195
+ conflictFiles.length > 0 ? conflictFiles : [t("SYNCMAIN_MSG_SEE_STATUS")];
196
+ result.message = t("SYNCMAIN_MSG_CONFLICT__MODE", { mode: result.mode });
193
197
  printEarlyConflict({
194
198
  name,
195
199
  branch,
@@ -201,8 +205,10 @@ function syncMainRepo({
201
205
  }
202
206
 
203
207
  result.status = "ok";
204
- result.message = "đã cập nhật";
205
- console.log(`${ICON.ok} ${colors.green(`[ok] ${name}: ${branch} đã sync ${ref}`)}`);
208
+ result.message = t("SYNCMAIN_MSG_UPDATED");
209
+ console.log(
210
+ `${ICON.ok} ${colors.green(t("SYNCMAIN_MSG_SYNCED__NAME_BRANCH_REF", { name, branch, ref }))}`
211
+ );
206
212
  return result;
207
213
  }
208
214
 
@@ -215,17 +221,23 @@ function printSummary(results, { ref, rebase }) {
215
221
  );
216
222
 
217
223
  console.log("");
218
- console.log(colors.bold("─── sync-main summary ───"));
224
+ console.log(colors.bold(t("SYNCMAIN_HEADER")));
219
225
  console.log(
220
- `${ICON.info} ref: ${ref} · mode: ${rebase ? "rebase" : "merge"} · repos: ${results.length}`
226
+ `${ICON.info} ${t("SYNCMAIN_INFO__REF_MODE_REPOS", {
227
+ ref, mode: rebase ? "rebase" : "merge", repos: results.length,
228
+ })}`
221
229
  );
222
230
  console.log(
223
- ` ${ICON.ok} ok: ${ok.length} · ${ICON.conflict} conflict: ${conflicts.length} · ${ICON.warn} error: ${errors.length} · ${ICON.skip} skip: ${skipped.length}`
231
+ t("SYNCMAIN_COUNTS__OK_CONFLICT_ERR_SKIP", {
232
+ ok: ok.length, conflict: conflicts.length, err: errors.length, skip: skipped.length,
233
+ })
224
234
  );
225
235
 
226
236
  if (conflicts.length > 0) {
227
237
  console.log("");
228
- console.log(colors.bold(colors.red(`${ICON.conflict} CONFLICT — cần xử lý thủ công`)));
238
+ console.log(
239
+ colors.bold(colors.red(`${ICON.conflict} ${t("SYNCMAIN_CONFLICT_HEADER")}`))
240
+ );
229
241
  for (const item of conflicts) {
230
242
  console.log(colors.yellow(`\n ${item.name} (${item.branch})`));
231
243
  console.log(colors.dim(` ${item.destination}`));
@@ -233,15 +245,15 @@ function printSummary(results, { ref, rebase }) {
233
245
  console.log(` • ${file}`);
234
246
  }
235
247
  const hint = item.mode === "rebase"
236
- ? "git add <file>… && git rebase --continue (hoặc git rebase --abort)"
237
- : "git add <file>… && git commit";
248
+ ? t("SYNCMAIN_HINT_REBASE")
249
+ : t("SYNCMAIN_HINT_MERGE");
238
250
  console.log(colors.dim(` → ${hint}`));
239
251
  }
240
252
  }
241
253
 
242
254
  if (errors.length > 0) {
243
255
  console.log("");
244
- console.log(colors.bold(colors.yellow(`${ICON.warn} LỖI`)));
256
+ console.log(colors.bold(colors.yellow(`${ICON.warn} ${t("SYNCMAIN_ERRORS_HEADER")}`)));
245
257
  for (const item of errors) {
246
258
  console.log(` • ${item.name}: ${item.message}`);
247
259
  }
@@ -250,20 +262,18 @@ function printSummary(results, { ref, rebase }) {
250
262
  if (conflicts.length > 0) {
251
263
  console.log("");
252
264
  console.log(
253
- colors.bold(
254
- colors.red(
255
- `${ICON.conflict} Kết thúc với conflict — resolve từng repo ở trên rồi commit/rebase --continue`
256
- )
257
- )
265
+ colors.bold(colors.red(`${ICON.conflict} ${t("SYNCMAIN_END_CONFLICT")}`))
258
266
  );
259
267
  } else if (errors.length > 0) {
260
268
  console.log("");
261
269
  console.log(
262
- colors.bold(colors.yellow(`${ICON.warn} Kết thúc với lỗi — xem chi tiết ở trên`))
270
+ colors.bold(colors.yellow(`${ICON.warn} ${t("SYNCMAIN_END_ERROR")}`))
263
271
  );
264
272
  } else if (ok.length > 0) {
265
273
  console.log("");
266
- console.log(colors.green(`${ICON.ok} Tất cả repo đã chọn đã sync ${ref}`));
274
+ console.log(
275
+ colors.green(`${ICON.ok} ${t("SYNCMAIN_END_ALL_SYNCED__REF", { ref })}`)
276
+ );
267
277
  }
268
278
  }
269
279
 
@@ -288,7 +298,11 @@ async function main() {
288
298
  });
289
299
 
290
300
  console.log(
291
- `${ICON.info} sync-main: ${targets.length} repo · ${args.rebase ? "rebase" : "merge"} ← ${args.ref}`
301
+ `${ICON.info} ${t("SYNCMAIN_INTRO__COUNT_MODE_REF", {
302
+ count: targets.length,
303
+ mode: args.rebase ? "rebase" : "merge",
304
+ ref: args.ref,
305
+ })}`
292
306
  );
293
307
 
294
308
  const results = [];