pan-stack-cli 0.1.4 → 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 (64) hide show
  1. package/README.md +572 -23
  2. package/bin/pan.js +206 -20
  3. package/lib/ai-skills.js +829 -0
  4. package/lib/create.js +22 -4
  5. package/lib/discover.js +69 -2
  6. package/lib/git/git-checkout.js +395 -0
  7. package/lib/git/git-clone.js +163 -28
  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 +334 -0
  15. package/lib/git/helpers/bulk-report.js +202 -0
  16. package/lib/git/helpers/cli.js +41 -5
  17. package/lib/git/helpers/colors.js +19 -0
  18. package/lib/git/helpers/git.js +74 -0
  19. package/lib/git/helpers/manifest.js +93 -0
  20. package/lib/git/helpers/repo-select.js +416 -0
  21. package/lib/git/helpers/repo-summary.js +161 -0
  22. package/lib/i18n.js +61 -0
  23. package/lib/inquirer-theme.js +20 -0
  24. package/lib/language.js +121 -0
  25. package/lib/languages.js +14 -0
  26. package/lib/manifest.js +115 -14
  27. package/lib/menu-prompt.js +201 -0
  28. package/lib/menu.js +134 -0
  29. package/lib/messages.js +647 -0
  30. package/lib/run-command.js +173 -52
  31. package/lib/select.js +0 -0
  32. package/lib/serve.js +75 -0
  33. package/lib/skill-registry-client.js +173 -0
  34. package/lib/stats/aggregate.js +51 -0
  35. package/lib/stats/identity.js +20 -0
  36. package/lib/stats/index.js +189 -0
  37. package/lib/stats/parse.js +46 -0
  38. package/lib/stats/range.js +38 -0
  39. package/lib/stats/report.js +264 -0
  40. package/lib/stats/review.js +157 -0
  41. package/lib/stats/scan.js +92 -0
  42. package/lib/stats/serve.js +25 -0
  43. package/lib/ui/api.js +202 -0
  44. package/lib/ui/clone.js +115 -0
  45. package/lib/ui/config.js +182 -0
  46. package/lib/ui/diff.js +128 -0
  47. package/lib/ui/export-gate.js +79 -0
  48. package/lib/ui/gen-react-mobile-code.js +6 -0
  49. package/lib/ui/gen-react-web-code.js +75 -0
  50. package/lib/ui/index.js +89 -0
  51. package/lib/ui/init.js +252 -0
  52. package/lib/ui/login.js +128 -0
  53. package/lib/ui/logout.js +22 -0
  54. package/lib/ui/palettes.js +103 -0
  55. package/lib/ui/published-html.js +24 -0
  56. package/lib/ui/sync.js +137 -0
  57. package/lib/ui/workspace.js +213 -0
  58. package/package.json +10 -3
  59. package/templates/workspace/.claude/skills/writing-guidelines/SKILL.md +0 -48
  60. package/templates/workspace/.codex/skills/writing-guidelines/SKILL.md +0 -48
  61. package/templates/workspace/.cursor/skills/writing-guidelines/SKILL.md +0 -48
  62. package/templates/workspace/.pan.yaml +0 -25
  63. package/templates/workspace/.project.yaml +0 -15
  64. 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); }