pan-stack-cli 0.2.1 → 0.2.3

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 (59) hide show
  1. package/README.md +37 -731
  2. package/bin/pan.js +4978 -269
  3. package/package.json +17 -5
  4. package/lib/ai-skills.js +0 -829
  5. package/lib/create.js +0 -105
  6. package/lib/discover.js +0 -294
  7. package/lib/git/git-checkout.js +0 -395
  8. package/lib/git/git-clone.js +0 -206
  9. package/lib/git/git-fetch-all.js +0 -129
  10. package/lib/git/git-grv.js +0 -125
  11. package/lib/git/git-gst.js +0 -165
  12. package/lib/git/git-mr.js +0 -546
  13. package/lib/git/git-pull.js +0 -137
  14. package/lib/git/git-push.js +0 -185
  15. package/lib/git/git-sync-main.js +0 -334
  16. package/lib/git/helpers/bulk-report.js +0 -202
  17. package/lib/git/helpers/cli.js +0 -188
  18. package/lib/git/helpers/colors.js +0 -19
  19. package/lib/git/helpers/git.js +0 -136
  20. package/lib/git/helpers/manifest.js +0 -223
  21. package/lib/git/helpers/repo-select.js +0 -416
  22. package/lib/git/helpers/repo-summary.js +0 -161
  23. package/lib/i18n.js +0 -61
  24. package/lib/inquirer-theme.js +0 -20
  25. package/lib/language.js +0 -121
  26. package/lib/languages.js +0 -14
  27. package/lib/manifest.js +0 -467
  28. package/lib/menu-prompt.js +0 -201
  29. package/lib/menu.js +0 -134
  30. package/lib/messages.js +0 -647
  31. package/lib/platform.js +0 -140
  32. package/lib/run-command.js +0 -334
  33. package/lib/select.js +0 -0
  34. package/lib/serve.js +0 -75
  35. package/lib/skill-registry-client.js +0 -173
  36. package/lib/stats/aggregate.js +0 -51
  37. package/lib/stats/identity.js +0 -20
  38. package/lib/stats/index.js +0 -189
  39. package/lib/stats/parse.js +0 -46
  40. package/lib/stats/range.js +0 -38
  41. package/lib/stats/report.js +0 -264
  42. package/lib/stats/review.js +0 -157
  43. package/lib/stats/scan.js +0 -92
  44. package/lib/stats/serve.js +0 -25
  45. package/lib/ui/api.js +0 -202
  46. package/lib/ui/clone.js +0 -115
  47. package/lib/ui/config.js +0 -182
  48. package/lib/ui/diff.js +0 -128
  49. package/lib/ui/export-gate.js +0 -79
  50. package/lib/ui/gen-react-mobile-code.js +0 -6
  51. package/lib/ui/gen-react-web-code.js +0 -75
  52. package/lib/ui/index.js +0 -89
  53. package/lib/ui/init.js +0 -252
  54. package/lib/ui/login.js +0 -128
  55. package/lib/ui/logout.js +0 -22
  56. package/lib/ui/palettes.js +0 -103
  57. package/lib/ui/published-html.js +0 -24
  58. package/lib/ui/sync.js +0 -137
  59. package/lib/ui/workspace.js +0 -213
package/lib/git/git-mr.js DELETED
@@ -1,546 +0,0 @@
1
- #!/usr/bin/env node
2
- /*
3
- * Create GitLab Merge Requests for PAN Stack workspace + child repos.
4
- *
5
- * Reads repos from .project.yaml (same source as other pan git commands).
6
- * Interactive multi-select picker when running in a TTY without an explicit
7
- * selection. Non-interactive selection: pass "all", a single repo name, or a
8
- * comma/space-separated list of repo names.
9
- *
10
- * Does NOT auto-push — run `pan push` first if HEAD is not on the remote yet.
11
- * Skips repos with no commits ahead of origin/<target>.
12
- *
13
- * Token resolution order:
14
- * 1. --token flag
15
- * 2. GITLAB_TOKEN env var
16
- * 3. GITLAB_TOKEN in <workspace>/.pan-env-build.yaml
17
- */
18
-
19
- const fs = require("fs");
20
- const path = require("path");
21
- const https = require("https");
22
- const {
23
- looksLikeManifest,
24
- loadManifestRepos,
25
- printCommonOptions,
26
- } = require("./helpers/cli");
27
- const colors = require("./helpers/colors");
28
- const { resolveMultiTargets } = require("./helpers/repo-select");
29
- const { runGitCapture, getCurrentBranch, refExists } = require("./helpers/git");
30
- const { t } = require("../i18n");
31
-
32
- const ICON = {
33
- ok: "✓",
34
- exists: "=",
35
- warn: "!",
36
- skip: "-",
37
- err: "✗",
38
- };
39
-
40
- const DEFAULT_TARGET_BRANCH = "main";
41
-
42
- function printHelp() {
43
- console.log(`usage: pan mr [target] [repo|all|repo1,repo2] [options]
44
-
45
- Tạo GitLab Merge Request cho workspace + các repo trong .project.yaml.
46
- Interactive multi-select khi chạy trong TTY và không truyền lựa chọn.
47
-
48
- positional:
49
- target Branch đích (default: ${DEFAULT_TARGET_BRANCH}). Vd: pan mr develop
50
- repo|all|<list> Repo tạo MR: tên repo trong .project.yaml, "all",
51
- hoặc danh sách phân tách bởi phẩy/space.
52
-
53
- ${printCommonOptions()}
54
- --no-workspace Bỏ qua workspace root (chỉ repo con)
55
- --gitlab-host HOST Override GitLab host (default: derive từ origin URL)
56
- --token TOKEN Override GitLab token (default: env → .pan-env-build.yaml)
57
-
58
- examples:
59
- pan mr # target=main, chọn tương tác
60
- pan mr develop # target=develop, chọn tương tác
61
- pan mr main all # target=main, tất cả repo
62
- pan mr main csdl-cc-backend # target=main, một repo
63
- pan mr main csdl-cc-backend,csdl-cc-b-frontend
64
-
65
- env:
66
- GITLAB_TOKEN Personal Access Token (scope: api).
67
- Fallback: đọc từ <workspace>/.pan-env-build.yaml.`);
68
- }
69
-
70
- function parseMrArgs(argv) {
71
- const args = {
72
- help: false,
73
- manifest: "",
74
- baseDir: ".",
75
- dryRun: false,
76
- noWorkspace: false,
77
- gitlabHost: "",
78
- token: "",
79
- target: "",
80
- selection: "",
81
- };
82
-
83
- const positional = [];
84
- for (let index = 0; index < argv.length; index += 1) {
85
- const arg = argv[index];
86
- if (arg === "-h" || arg === "--help") {
87
- args.help = true;
88
- continue;
89
- }
90
- if (arg === "--base-dir") {
91
- args.baseDir = argv[++index] || ".";
92
- continue;
93
- }
94
- if (arg === "--dry-run") {
95
- args.dryRun = true;
96
- continue;
97
- }
98
- if (arg === "--no-workspace") {
99
- args.noWorkspace = true;
100
- continue;
101
- }
102
- if (arg === "--gitlab-host") {
103
- args.gitlabHost = argv[++index] || "";
104
- continue;
105
- }
106
- if (arg === "--token") {
107
- args.token = argv[++index] || "";
108
- continue;
109
- }
110
- if (arg.startsWith("-")) {
111
- throw new Error(`Unexpected argument: ${arg}`);
112
- }
113
- if (looksLikeManifest(arg) && !args.manifest) {
114
- args.manifest = arg;
115
- continue;
116
- }
117
- positional.push(arg);
118
- }
119
-
120
- if (positional[0]) {
121
- args.target = positional[0];
122
- }
123
- if (positional.length > 1) {
124
- args.selection = positional.slice(1).join(" ");
125
- }
126
-
127
- return args;
128
- }
129
-
130
- // ── Env & URL helpers ───────────────────────────────────────────────────────
131
-
132
- function loadGitlabToken(workspaceRoot, override) {
133
- if (override) {
134
- return override;
135
- }
136
- if (process.env.GITLAB_TOKEN) {
137
- return process.env.GITLAB_TOKEN;
138
- }
139
- const envFile = path.join(workspaceRoot, ".pan-env-build.yaml");
140
- if (!fs.existsSync(envFile)) {
141
- return "";
142
- }
143
- const content = fs.readFileSync(envFile, "utf8");
144
- for (const rawLine of content.split(/\r?\n/)) {
145
- let line = rawLine;
146
- const commentIdx = line.indexOf("#");
147
- if (commentIdx >= 0) {
148
- line = line.slice(0, commentIdx);
149
- }
150
- line = line.trim();
151
- if (!line) {
152
- continue;
153
- }
154
- const idx = line.indexOf(":");
155
- if (idx < 0) {
156
- continue;
157
- }
158
- const key = line.slice(0, idx).trim();
159
- let val = line.slice(idx + 1).trim();
160
- if (
161
- (val.startsWith('"') && val.endsWith('"')) ||
162
- (val.startsWith("'") && val.endsWith("'"))
163
- ) {
164
- val = val.slice(1, -1);
165
- }
166
- if (key === "GITLAB_TOKEN" && val) {
167
- return val;
168
- }
169
- }
170
- return "";
171
- }
172
-
173
- function extractProjectPath(originUrl) {
174
- return String(originUrl || "")
175
- .replace(/^https?:\/\/[^/]+\//, "")
176
- .replace(/^git@[^:]+:/, "")
177
- .replace(/\.git$/, "");
178
- }
179
-
180
- function extractGitlabHost(originUrl) {
181
- const httpsMatch = String(originUrl || "").match(/^https?:\/\/([^/]+)/);
182
- if (httpsMatch) {
183
- return httpsMatch[1];
184
- }
185
- const sshMatch = String(originUrl || "").match(/^git@([^:]+):/);
186
- if (sshMatch) {
187
- return sshMatch[1];
188
- }
189
- return "";
190
- }
191
-
192
- function getOriginUrl(directory) {
193
- const result = runGitCapture(["remote", "get-url", "origin"], {
194
- cwd: directory,
195
- });
196
- if (result.status !== 0) {
197
- return "";
198
- }
199
- return result.stdout;
200
- }
201
-
202
- function apiRequest(host, method, urlPath, token, body) {
203
- return new Promise((resolve, reject) => {
204
- const payload = body != null ? Buffer.from(body, "utf8") : null;
205
- const options = {
206
- hostname: host,
207
- port: 443,
208
- path: urlPath,
209
- method,
210
- headers: {
211
- "PRIVATE-TOKEN": token,
212
- "User-Agent": "pan-stack-cli",
213
- Accept: "application/json",
214
- },
215
- };
216
- if (payload) {
217
- options.headers["Content-Type"] = "application/json";
218
- options.headers["Content-Length"] = payload.length;
219
- }
220
-
221
- const req = https.request(options, (res) => {
222
- const chunks = [];
223
- res.on("data", (chunk) => chunks.push(chunk));
224
- res.on("end", () => {
225
- const text = Buffer.concat(chunks).toString("utf8");
226
- resolve({ status: res.statusCode || 0, body: text });
227
- });
228
- });
229
- req.on("error", (err) => reject(err));
230
- if (payload) {
231
- req.write(payload);
232
- }
233
- req.end();
234
- });
235
- }
236
-
237
- // ── Per-repo processing ────────────────────────────────────────────────────
238
-
239
- function safeJsonParse(text) {
240
- try {
241
- return JSON.parse(text);
242
- } catch {
243
- return null;
244
- }
245
- }
246
-
247
- function truncate(text, max) {
248
- const clean = String(text || "").replace(/\s+/g, " ").trim();
249
- return clean.length > max ? `${clean.slice(0, max)}…` : clean;
250
- }
251
-
252
- async function processRepo({
253
- name,
254
- destination,
255
- target,
256
- token,
257
- gitlabHostOverride,
258
- dryRun,
259
- }) {
260
- const record = { name, status: "", message: "", url: "" };
261
-
262
- if (!fs.existsSync(path.join(destination, ".git"))) {
263
- return { ...record, status: "skip", message: t("MR_MSG_NOT_CLONED") };
264
- }
265
-
266
- const originUrl = getOriginUrl(destination);
267
- if (!originUrl) {
268
- return { ...record, status: "skip", message: t("MR_MSG_NO_ORIGIN") };
269
- }
270
-
271
- const projPath = extractProjectPath(originUrl);
272
- const host = gitlabHostOverride || extractGitlabHost(originUrl);
273
- if (!host) {
274
- return {
275
- ...record,
276
- status: "err",
277
- message: t("MR_MSG_HOST_PARSE__URL", { url: originUrl }),
278
- };
279
- }
280
-
281
- const branch = getCurrentBranch(destination);
282
- if (!branch) {
283
- return { ...record, status: "skip", message: t("MR_MSG_HEAD_DETACHED") };
284
- }
285
- if (branch === target) {
286
- return {
287
- ...record,
288
- status: "skip",
289
- message: t("MR_MSG_ON_TARGET__TARGET", { target }),
290
- };
291
- }
292
-
293
- console.log(colors.dim(`→ [${name}] fetch origin...`));
294
- runGitCapture(["fetch", "--quiet", "origin"], { cwd: destination });
295
-
296
- if (!refExists(destination, `refs/remotes/origin/${target}`)) {
297
- return {
298
- ...record,
299
- status: "err",
300
- message: t("MR_MSG_ORIGIN_MISSING__TARGET", { target }),
301
- };
302
- }
303
-
304
- const aheadTargetResult = runGitCapture(
305
- ["rev-list", "--count", `origin/${target}..HEAD`],
306
- { cwd: destination }
307
- );
308
- const aheadTarget = Number.parseInt(aheadTargetResult.stdout || "0", 10) || 0;
309
- if (aheadTarget === 0) {
310
- return {
311
- ...record,
312
- status: "skip",
313
- message: t("MR_MSG_NO_AHEAD__TARGET", { target }),
314
- };
315
- }
316
-
317
- if (!refExists(destination, `refs/remotes/origin/${branch}`)) {
318
- return {
319
- ...record,
320
- status: "warn",
321
- message: t("MR_MSG_BRANCH_MISSING_REMOTE__BRANCH", { branch }),
322
- };
323
- }
324
- const aheadRemoteResult = runGitCapture(
325
- ["rev-list", "--count", `origin/${branch}..HEAD`],
326
- { cwd: destination }
327
- );
328
- const aheadRemote = Number.parseInt(aheadRemoteResult.stdout || "0", 10) || 0;
329
- if (aheadRemote > 0) {
330
- return {
331
- ...record,
332
- status: "warn",
333
- message: t("MR_MSG_HEAD_AHEAD__BRANCH_N", { branch, n: aheadRemote }),
334
- };
335
- }
336
-
337
- const apiBase = `/api/v4/projects/${encodeURIComponent(projPath)}`;
338
- const listQuery =
339
- `${apiBase}/merge_requests?state=opened` +
340
- `&source_branch=${encodeURIComponent(branch)}` +
341
- `&target_branch=${encodeURIComponent(target)}`;
342
-
343
- if (dryRun) {
344
- return {
345
- ...record,
346
- status: "skip",
347
- message: t("MR_MSG_DRY_RUN_GET__URL", { url: `https://${host}${listQuery}` }),
348
- };
349
- }
350
-
351
- let listRes;
352
- try {
353
- listRes = await apiRequest(host, "GET", listQuery, token);
354
- } catch (err) {
355
- return { ...record, status: "err", message: t("MR_MSG_GET_ERROR__MSG", { msg: err.message }) };
356
- }
357
- if (listRes.status !== 200) {
358
- return {
359
- ...record,
360
- status: "err",
361
- message: t("MR_MSG_GET_HTTP__STATUS_BODY", {
362
- status: listRes.status, body: truncate(listRes.body, 200),
363
- }),
364
- };
365
- }
366
- const list = safeJsonParse(listRes.body);
367
- if (Array.isArray(list) && list.length > 0 && list[0].web_url) {
368
- return {
369
- ...record,
370
- status: "exists",
371
- url: list[0].web_url,
372
- message: list[0].web_url,
373
- };
374
- }
375
-
376
- const titleResult = runGitCapture(
377
- ["log", "--reverse", "--pretty=%s", `origin/${target}..HEAD`],
378
- { cwd: destination }
379
- );
380
- const firstTitle = (titleResult.stdout || "").split(/\r?\n/)[0] || "";
381
- const title = firstTitle || `Merge ${branch} → ${target}`;
382
- const descResult = runGitCapture(
383
- ["log", "--pretty=- %s (%h)", `origin/${target}..HEAD`],
384
- { cwd: destination }
385
- );
386
- const description = descResult.stdout || "";
387
-
388
- const body = JSON.stringify({
389
- source_branch: branch,
390
- target_branch: target,
391
- title,
392
- description,
393
- remove_source_branch: false,
394
- squash: false,
395
- });
396
-
397
- let createRes;
398
- try {
399
- createRes = await apiRequest(
400
- host,
401
- "POST",
402
- `${apiBase}/merge_requests`,
403
- token,
404
- body
405
- );
406
- } catch (err) {
407
- return { ...record, status: "err", message: t("MR_MSG_POST_ERROR__MSG", { msg: err.message }) };
408
- }
409
- if (createRes.status !== 201) {
410
- return {
411
- ...record,
412
- status: "err",
413
- message: t("MR_MSG_POST_HTTP__STATUS_BODY", {
414
- status: createRes.status, body: truncate(createRes.body, 300),
415
- }),
416
- };
417
- }
418
- const created = safeJsonParse(createRes.body);
419
- if (created && created.web_url) {
420
- return {
421
- ...record,
422
- status: "ok",
423
- url: created.web_url,
424
- message: created.web_url,
425
- };
426
- }
427
- return { ...record, status: "err", message: t("MR_MSG_NO_WEB_URL") };
428
- }
429
-
430
- // ── Reporting ──────────────────────────────────────────────────────────────
431
-
432
- function styleForStatus(status) {
433
- switch (status) {
434
- case "ok":
435
- return { icon: ICON.ok, suffix: t("MR_LABEL_NEW"), color: colors.green };
436
- case "exists":
437
- return { icon: ICON.exists, suffix: t("MR_LABEL_EXISTS"), color: colors.cyan };
438
- case "warn":
439
- return { icon: ICON.warn, suffix: "", color: colors.yellow };
440
- case "skip":
441
- return { icon: ICON.skip, suffix: "", color: colors.dim };
442
- case "err":
443
- return { icon: ICON.err, suffix: "", color: colors.red };
444
- default:
445
- return { icon: "?", suffix: "", color: (text) => text };
446
- }
447
- }
448
-
449
- function printSummary(results, { target }) {
450
- const nameWidth = results.reduce(
451
- (max, item) => Math.max(max, item.name.length),
452
- 0
453
- );
454
-
455
- console.log("");
456
- console.log(colors.bold(t("MR_RESULT_HEADER__TARGET", { target })));
457
- for (const result of results) {
458
- const { icon, suffix, color } = styleForStatus(result.status);
459
- const line = `${icon} ${result.name.padEnd(nameWidth)} ${result.message}${suffix}`;
460
- console.log(color(line));
461
- }
462
-
463
- const created = results.filter((r) => r.status === "ok").length;
464
- const existing = results.filter((r) => r.status === "exists").length;
465
- const warned = results.filter((r) => r.status === "warn").length;
466
- const skipped = results.filter((r) => r.status === "skip").length;
467
- const errored = results.filter((r) => r.status === "err").length;
468
-
469
- console.log("");
470
- console.log(colors.dim(t("MR_LEGEND")));
471
- console.log(
472
- colors.bold(
473
- t("MR_SUMMARY__OK_EXISTS_WARN_SKIP_ERR", {
474
- ok: created, exists: existing, warn: warned, skip: skipped, err: errored,
475
- })
476
- )
477
- );
478
- }
479
-
480
- // ── Main ───────────────────────────────────────────────────────────────────
481
-
482
- async function main() {
483
- const args = parseMrArgs(process.argv.slice(2));
484
- if (args.help) {
485
- printHelp();
486
- return;
487
- }
488
-
489
- const target = (args.target || DEFAULT_TARGET_BRANCH).trim();
490
-
491
- const { repos, workspaceRoot } = loadManifestRepos({
492
- manifestPath: args.manifest,
493
- baseDir: args.baseDir,
494
- });
495
-
496
- const token = loadGitlabToken(workspaceRoot, args.token);
497
- if (!token) {
498
- console.error(t("MR_MISSING_TOKEN"));
499
- console.error(t("MR_MISSING_TOKEN_1"));
500
- console.error(t("MR_MISSING_TOKEN_2__PATH", { path: workspaceRoot }));
501
- console.error(t("MR_MISSING_TOKEN_URL"));
502
- process.exit(1);
503
- }
504
-
505
- const targets = await resolveMultiTargets({
506
- repos,
507
- workspaceRoot,
508
- includeWorkspace: !args.noWorkspace,
509
- selection: args.selection,
510
- action: t("MR_SELECT_ACTION__TARGET", { target }),
511
- });
512
-
513
- console.log("");
514
- console.log(
515
- colors.bold(t("MR_HEADER__TARGET_N", { target, n: targets.length }))
516
- );
517
- for (const target1 of targets) {
518
- console.log(colors.dim(` • ${target1.name} (${target1.destination})`));
519
- }
520
-
521
- const results = [];
522
- for (const target1 of targets) {
523
- console.log("");
524
- const result = await processRepo({
525
- name: target1.name,
526
- destination: target1.destination,
527
- target,
528
- token,
529
- gitlabHostOverride: args.gitlabHost,
530
- dryRun: args.dryRun,
531
- });
532
- results.push(result);
533
- }
534
-
535
- printSummary(results, { target });
536
-
537
- const hasError = results.some(
538
- (item) => item.status === "err" || item.status === "warn"
539
- );
540
- process.exit(hasError ? 1 : 0);
541
- }
542
-
543
- main().catch((error) => {
544
- console.error(error.message);
545
- process.exit(1);
546
- });
@@ -1,137 +0,0 @@
1
- #!/usr/bin/env node
2
- /*
3
- * Pull latest changes for PAN Stack project repositories.
4
- * In compact summary table: repo · branch · result · changes · conflict.
5
- */
6
-
7
- const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
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");
15
-
16
- function pullRepo({ name, destination, dryRun, rebase }) {
17
- if (!ensureRepoExists({ name, destination })) {
18
- return { name, skipped: true };
19
- }
20
- const branch = getCurrentBranch(destination) || t("GST_DETACHED");
21
- const gitArgs = ["pull", "--stat"];
22
- if (rebase) gitArgs.push("--rebase");
23
-
24
- if (dryRun) {
25
- console.log(colors.dim(`[dry-run] (in ${destination}) git ${gitArgs.join(" ")}`));
26
- return { name, branch, dryRun: true };
27
- }
28
-
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 };
71
- }
72
-
73
- function printHelp() {
74
- console.log(`usage: git-pull.js [--base-dir BASE_DIR] [--dry-run] [--rebase] [manifest]
75
-
76
- Pull latest changes for repositories listed in a PAN Stack clone manifest.
77
- Prints a compact summary table (repo · branch · result · changes · conflict).
78
-
79
- ${printCommonOptions()}
80
- --rebase Use git pull --rebase`);
81
- }
82
-
83
- function main() {
84
- const args = parseCommonArgs(process.argv.slice(2), {
85
- defaults: { rebase: false },
86
- handlers: { "--rebase": (p) => { p.rebase = true; } },
87
- });
88
- if (args.help) { printHelp(); return; }
89
-
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
- }
113
-
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,
131
- });
132
- }
133
-
134
- if (conflicts.length || failed.length) process.exitCode = 1;
135
- }
136
-
137
- try { main(); } catch (error) { console.error(error.message); process.exit(1); }