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
@@ -1,202 +0,0 @@
1
- "use strict";
2
-
3
- const { spawnSync } = require("child_process");
4
- const { spawnOptions } = require("../../platform");
5
- const colors = require("./colors");
6
- const { runGitCapture, getCurrentBranch, listConflictedFiles } = require("./git");
7
- const { t } = require("../../i18n");
8
-
9
- const ICON = { ok: "✅", fail: "❌", warn: "⚠️", skip: "⏭️", info: "ℹ️", up: "↑", down: "↓" };
10
-
11
- // ───────── shared helpers ─────────
12
-
13
- /** Chạy git im lặng, capture stdout/stderr; trả { status, stdout, stderr }. */
14
- function runGitSilent(gitArgs, cwd) {
15
- const result = spawnSync("git", gitArgs, spawnOptions({ cwd, encoding: "utf8" }));
16
- return {
17
- status: result.status ?? 1,
18
- stdout: (result.stdout || "").trim(),
19
- stderr: (result.stderr || "").trim(),
20
- };
21
- }
22
-
23
- /** ahead/behind so với upstream. Trả null nếu không có upstream. */
24
- function aheadBehind(cwd) {
25
- const upstream = runGitCapture(["rev-parse", "--abbrev-ref", "@{u}"], { cwd });
26
- if (upstream.status !== 0 || !upstream.stdout) return null;
27
- const rl = runGitCapture(["rev-list", "--left-right", "--count", "HEAD...@{u}"], { cwd });
28
- if (rl.status !== 0) return null;
29
- const [ahead, behind] = rl.stdout.split(/\s+/).map((n) => parseInt(n, 10) || 0);
30
- return { ahead, behind, upstream: upstream.stdout };
31
- }
32
-
33
- /**
34
- * Tổng line change của các commit sắp push (@{u}..HEAD).
35
- * Cộng theo từng commit (`git log --numstat`), không dùng net-diff.
36
- * Trả { insertions, deletions, filesChanged } — filesChanged = số path unique.
37
- */
38
- function aheadLineStats(cwd, { range } = {}) {
39
- const empty = { insertions: 0, deletions: 0, filesChanged: 0 };
40
- let revRange = range;
41
- if (!revRange) {
42
- const ab = aheadBehind(cwd);
43
- if (!ab || ab.ahead === 0) return empty;
44
- revRange = "@{u}..HEAD";
45
- }
46
- const res = runGitCapture(
47
- ["log", "--numstat", "--pretty=tformat:", "--no-merges", revRange],
48
- { cwd }
49
- );
50
- if (res.status !== 0) return empty;
51
- let insertions = 0;
52
- let deletions = 0;
53
- const files = new Set();
54
- for (const line of (res.stdout || "").split(/\r?\n/)) {
55
- if (!line) continue;
56
- // numstat: "<add>\t<del>\t<path>" — binary: "-\t-\t<path>"
57
- const parts = line.split("\t");
58
- if (parts.length < 3) continue;
59
- const add = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
60
- const del = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
61
- insertions += add;
62
- deletions += del;
63
- files.add(parts.slice(2).join("\t"));
64
- }
65
- return { insertions, deletions, filesChanged: files.size };
66
- }
67
-
68
- // ───────── output parsers ─────────
69
-
70
- /** Parse `git pull` output. */
71
- function parsePullOutput(stdout, stderr) {
72
- const text = `${stdout}\n${stderr}`;
73
- const out = { mode: "unknown", filesChanged: 0, insertions: 0, deletions: 0, conflict: false, upToDate: false };
74
- if (/Already up to date|Already up-to-date/i.test(text)) out.upToDate = true;
75
- if (/Fast-forward/i.test(text)) out.mode = "fast-forward";
76
- else if (/Merge made by/i.test(text)) out.mode = "merge";
77
- else if (/Successfully rebased/i.test(text)) out.mode = "rebase";
78
- else if (/CONFLICT/i.test(text)) { out.mode = "conflict"; out.conflict = true; }
79
- // shortstat line: " 12 files changed, 234 insertions(+), 56 deletions(-)"
80
- const stat = text.match(/(\d+)\s+files?\s+changed(?:,\s*(\d+)\s+insertions?\(\+\))?(?:,\s*(\d+)\s+deletions?\(-\))?/);
81
- if (stat) {
82
- out.filesChanged = parseInt(stat[1], 10);
83
- out.insertions = parseInt(stat[2] || "0", 10);
84
- out.deletions = parseInt(stat[3] || "0", 10);
85
- }
86
- return out;
87
- }
88
-
89
- /** Parse `git push` output. GitLab/GitHub in gợi ý URL MR/PR ở stderr. */
90
- function parsePushOutput(stdout, stderr) {
91
- const text = `${stdout}\n${stderr}`;
92
- const out = { pushed: false, upToDate: false, rejected: false, mrLink: "", refs: [] };
93
- if (/Everything up-to-date|up to date/i.test(text)) out.upToDate = true;
94
- if (/\[rejected\]|! \[remote rejected\]|failed to push some refs/i.test(text)) out.rejected = true;
95
- // URL merge_requests hoặc pull/new
96
- const url =
97
- text.match(/https?:\/\/\S*merge_requests\/new\S*/) ||
98
- text.match(/https?:\/\/\S*\/merge_requests\/\d+/) ||
99
- text.match(/https?:\/\/\S*\/-\/merge_requests\S*/) ||
100
- text.match(/https?:\/\/\S*\/pull\/new\/\S+/) ||
101
- text.match(/https?:\/\/\S*\/pull\/\d+/);
102
- if (url) out.mrLink = url[0].replace(/[.,)]+$/, "");
103
- // refs pushed: "abcdef..123456 main -> main"
104
- const refRe = /([0-9a-f]{7,}\.\.[0-9a-f]{7,})\s+(\S+)\s+->\s+(\S+)/g;
105
- let m;
106
- while ((m = refRe.exec(text)) !== null) {
107
- out.refs.push({ range: m[1], local: m[2], remote: m[3] });
108
- out.pushed = true;
109
- }
110
- // "* [new branch]"
111
- if (/\*\s+\[new branch\]/.test(text)) out.pushed = true;
112
- return out;
113
- }
114
-
115
- /** Parse `git fetch --all` output — số dòng update ref. */
116
- function parseFetchOutput(stdout, stderr) {
117
- const text = `${stdout}\n${stderr}`;
118
- const out = { newBranches: 0, updatedBranches: 0, newTags: 0, prunedRefs: 0 };
119
- out.newBranches = (text.match(/\*\s+\[new branch\]/g) || []).length;
120
- out.updatedBranches = (text.match(/^\s*[0-9a-f]{6,}\.\.[0-9a-f]{6,}\s+/gm) || []).length;
121
- out.newTags = (text.match(/\*\s+\[new tag\]/g) || []).length;
122
- out.prunedRefs = (text.match(/\[deleted\]/g) || []).length;
123
- return out;
124
- }
125
-
126
- // ───────── table renderer ─────────
127
-
128
- /**
129
- * @param {Array<{key:string,label:string,align?:'l'|'r'}>} columns
130
- * @param {Array<Object>} rows
131
- */
132
- function renderTable(columns, rows) {
133
- // strip ANSI để đo width thật
134
- const strip = (s) => String(s ?? "").replace(/\x1b\[[0-9;]*m/g, "");
135
- const visLen = (s) => strip(s).length;
136
- const widths = columns.map((c) => Math.max(c.label.length, ...rows.map((r) => visLen(r[c.key]))));
137
-
138
- const pad = (s, w, align) => {
139
- const len = visLen(s);
140
- const spaces = " ".repeat(Math.max(0, w - len));
141
- return align === "r" ? spaces + s : s + spaces;
142
- };
143
-
144
- const header = columns.map((c, i) => colors.bold(pad(c.label, widths[i], c.align))).join(" ");
145
- const sep = columns.map((_, i) => colors.dim("─".repeat(widths[i]))).join(" ");
146
- const body = rows.map((r) =>
147
- columns.map((c, i) => pad(String(r[c.key] ?? ""), widths[i], c.align)).join(" ")
148
- );
149
- return [header, sep, ...body].join("\n");
150
- }
151
-
152
- function statusBadge(kind, text) {
153
- if (kind === "ok") return colors.green(`${ICON.ok} ${text}`);
154
- if (kind === "warn") return colors.yellow(`${ICON.warn} ${text}`);
155
- if (kind === "fail") return colors.red(`${ICON.fail} ${text}`);
156
- if (kind === "skip") return colors.dim(`${ICON.skip} ${text}`);
157
- return text;
158
- }
159
-
160
- function printSummary({ title, rows, columns, footerLines = [] }) {
161
- console.log("");
162
- console.log(colors.bold(`─── ${title} ───`));
163
- if (rows.length === 0) {
164
- console.log(colors.dim("(no data)"));
165
- return;
166
- }
167
- console.log(renderTable(columns, rows));
168
- for (const line of footerLines) console.log(line);
169
- }
170
-
171
- /** Helper: cột bảng có label theo i18n. */
172
- function bulkColumns(specs) {
173
- return specs.map((s) => ({ key: s.key, label: t(s.labelKey), align: s.align }));
174
- }
175
-
176
- /** Dịch mode pull (fast-forward/merge/rebase/conflict) qua i18n. */
177
- function pullModeLabel(mode) {
178
- switch (mode) {
179
- case "fast-forward": return t("BULK_MODE_FAST_FORWARD");
180
- case "merge": return t("BULK_MODE_MERGE");
181
- case "rebase": return t("BULK_MODE_REBASE");
182
- case "conflict": return t("BULK_STATUS_CONFLICT");
183
- default: return mode;
184
- }
185
- }
186
-
187
- module.exports = {
188
- ICON,
189
- runGitSilent,
190
- aheadBehind,
191
- aheadLineStats,
192
- parsePullOutput,
193
- parsePushOutput,
194
- parseFetchOutput,
195
- renderTable,
196
- statusBadge,
197
- printSummary,
198
- bulkColumns,
199
- pullModeLabel,
200
- listConflictedFiles,
201
- getCurrentBranch,
202
- };
@@ -1,188 +0,0 @@
1
- const path = require("path");
2
- const fs = require("fs");
3
- const { findNearestProjectManifest, discoverGitRepos } = require("../../discover");
4
- const { parseManifestWithLocal, manifestRepos, validateRepo } = require("./manifest");
5
- const { t } = require("../../i18n");
6
-
7
- function parseCommonArgs(argv, extraFlags = {}) {
8
- const args = {
9
- manifest: "",
10
- baseDir: ".",
11
- dryRun: false,
12
- ...extraFlags.defaults,
13
- };
14
-
15
- for (let index = 0; index < argv.length; index += 1) {
16
- const arg = argv[index];
17
- if (arg === "-h" || arg === "--help") {
18
- args.help = true;
19
- } else if (arg === "--base-dir") {
20
- args.baseDir = argv[++index] || ".";
21
- } else if (arg === "--dry-run") {
22
- args.dryRun = true;
23
- } else if (extraFlags.handlers && extraFlags.handlers[arg]) {
24
- extraFlags.handlers[arg](args, argv, () => {
25
- index += 1;
26
- return argv[index];
27
- });
28
- } else if (extraFlags.passthrough) {
29
- if (!args.manifest && looksLikeManifest(arg)) {
30
- args.manifest = arg;
31
- } else {
32
- args.gitArgs = args.gitArgs || [];
33
- args.gitArgs.push(arg);
34
- }
35
- } else if (!args.manifest) {
36
- args.manifest = arg;
37
- } else {
38
- throw new Error(`Unexpected argument: ${arg}`);
39
- }
40
- }
41
-
42
- return args;
43
- }
44
-
45
- function looksLikeManifest(arg) {
46
- if (arg.endsWith(".yaml") || arg.endsWith(".yml")) {
47
- return true;
48
- }
49
- try {
50
- return fs.existsSync(arg) && fs.statSync(arg).isFile();
51
- } catch {
52
- return false;
53
- }
54
- }
55
-
56
- function resolveProjectWorkspace({
57
- manifestPath = "",
58
- baseDir = ".",
59
- startDir = process.cwd(),
60
- }) {
61
- const manifestName = manifestPath || ".project.yaml";
62
- const cwdManifest = path.isAbsolute(manifestName)
63
- ? manifestName
64
- : path.resolve(startDir, manifestName);
65
-
66
- let resolvedManifest = "";
67
- let usedWalkUp = false;
68
-
69
- if (fs.existsSync(cwdManifest)) {
70
- resolvedManifest = cwdManifest;
71
- } else {
72
- const found = findNearestProjectManifest(startDir);
73
- if (!found) {
74
- const label = manifestPath || ".project.yaml";
75
- throw new Error(
76
- `Manifest not found: ${label} (walked up from ${startDir})`
77
- );
78
- }
79
- resolvedManifest = found;
80
- usedWalkUp = true;
81
- }
82
-
83
- const workspaceRoot = path.dirname(resolvedManifest);
84
- let resolvedBaseDir;
85
- if (baseDir === ".") {
86
- resolvedBaseDir = usedWalkUp ? workspaceRoot : path.resolve(startDir, ".");
87
- } else if (path.isAbsolute(baseDir)) {
88
- resolvedBaseDir = baseDir;
89
- } else {
90
- resolvedBaseDir = path.resolve(startDir, baseDir);
91
- }
92
-
93
- return {
94
- manifestPath: resolvedManifest,
95
- baseDir: resolvedBaseDir,
96
- workspaceRoot,
97
- };
98
- }
99
-
100
- /**
101
- * Fallback khi không có `.project.yaml`: coi `startDir` (nơi gõ lệnh) là gốc dự
102
- * án, quét git repo con bên trong (giống `pan stats`) — dùng cho các lệnh git
103
- * thuần (pull/push/fetch/checkout/sync-main/mr/grv/gst), KHÔNG áp dụng cho
104
- * `clone` (cần url khai báo trong manifest, không tự suy ra được).
105
- */
106
- function fallbackReposFromScan(startDir) {
107
- const found = discoverGitRepos(startDir);
108
- if (!found.length) {
109
- throw new Error(
110
- t("MANIFEST_NOT_FOUND_NO_GIT_FALLBACK__DIR", { dir: startDir })
111
- );
112
- }
113
- console.log(t("MANIFEST_NOT_FOUND_FALLBACK_SCAN__DIR", { dir: startDir }));
114
- return found.map((repo) => ({
115
- name: repo.name,
116
- url: null,
117
- repoPath: path.relative(startDir, repo.dir) || ".",
118
- destination: repo.dir,
119
- }));
120
- }
121
-
122
- function loadManifestRepos({ manifestPath, baseDir, startDir = process.cwd(), allowFallback = true }) {
123
- let workspace;
124
- try {
125
- workspace = resolveProjectWorkspace({ manifestPath, baseDir, startDir });
126
- } catch (error) {
127
- if (!allowFallback) throw error;
128
- return {
129
- repos: fallbackReposFromScan(startDir),
130
- manifestPath: "",
131
- baseDir: startDir,
132
- workspaceRoot: startDir,
133
- };
134
- }
135
-
136
- const { manifestPath: resolvedManifest, baseDir: resolvedBaseDir, workspaceRoot } =
137
- workspace;
138
-
139
- const data = parseManifestWithLocal(resolvedManifest);
140
- const repos = manifestRepos(data);
141
- if (repos.length === 0) {
142
- throw new Error("No repositories found in manifest.");
143
- }
144
-
145
- const results = [];
146
- for (const repo of repos) {
147
- try {
148
- const { name, url, repoPath } = validateRepo(repo);
149
- results.push({
150
- name,
151
- url,
152
- repoPath,
153
- destination: path.resolve(resolvedBaseDir, repoPath),
154
- });
155
- } catch (error) {
156
- console.log(`[warn] ${error.message}`);
157
- }
158
- }
159
-
160
- if (results.length === 0) {
161
- throw new Error("No valid repositories found in manifest.");
162
- }
163
-
164
- return {
165
- repos: results,
166
- manifestPath: resolvedManifest,
167
- baseDir: resolvedBaseDir,
168
- workspaceRoot,
169
- };
170
- }
171
-
172
- function printCommonOptions() {
173
- return `positional arguments:
174
- manifest Path to clone manifest YAML. Default: .project.yaml
175
-
176
- options:
177
- -h, --help show this help message and exit
178
- --base-dir BASE_DIR Base directory for relative repo paths. Default: directory containing .project.yaml (walk-up from cwd when needed).
179
- --dry-run Print commands only`;
180
- }
181
-
182
- module.exports = {
183
- parseCommonArgs,
184
- looksLikeManifest,
185
- resolveProjectWorkspace,
186
- loadManifestRepos,
187
- printCommonOptions,
188
- };
@@ -1,19 +0,0 @@
1
- const supportsColor =
2
- Boolean(process.stdout.isTTY) && !process.env.NO_COLOR && process.env.FORCE_COLOR !== "0";
3
-
4
- function wrap(code, text) {
5
- if (!supportsColor || text == null) {
6
- return String(text ?? "");
7
- }
8
- return `\u001b[${code}m${text}\u001b[0m`;
9
- }
10
-
11
- module.exports = {
12
- supportsColor,
13
- bold: (text) => wrap("1", text),
14
- red: (text) => wrap("31", text),
15
- green: (text) => wrap("32", text),
16
- yellow: (text) => wrap("33", text),
17
- cyan: (text) => wrap("36", text),
18
- dim: (text) => wrap("2", text),
19
- };
@@ -1,136 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const { spawnSync } = require("child_process");
4
- const { spawnOptions } = require("../../platform");
5
-
6
- function isGitRepo(directory) {
7
- return fs.existsSync(path.join(directory, ".git"));
8
- }
9
-
10
- function runGit(gitArgs, { cwd, dryRun, action, name }) {
11
- const command = ["git", ...gitArgs];
12
- if (dryRun) {
13
- console.log(`[dry-run] (in ${cwd}) ${command.join(" ")}`);
14
- return 0;
15
- }
16
-
17
- console.log(`[${action}] ${name}: ${cwd}`);
18
- const result = spawnSync(command[0], command.slice(1), spawnOptions({ stdio: "inherit", cwd }));
19
- return result.status ?? 1;
20
- }
21
-
22
- function ensureRepoExists({ name, destination }) {
23
- if (!fs.existsSync(destination)) {
24
- console.log(`[skip] ${name}: ${destination} does not exist`);
25
- return false;
26
- }
27
- if (!isGitRepo(destination)) {
28
- console.log(`[skip] ${name}: ${destination} is not a git repository`);
29
- return false;
30
- }
31
- return true;
32
- }
33
-
34
- function listRemotesVerbose(directory) {
35
- const result = spawnSync("git", ["remote", "-v"], spawnOptions({
36
- cwd: directory,
37
- encoding: "utf8",
38
- }));
39
- if (result.status !== 0) {
40
- return [];
41
- }
42
- return result.stdout
43
- .split(/\r?\n/)
44
- .map((line) => line.trimEnd())
45
- .filter(Boolean);
46
- }
47
-
48
- function runGitStatus(directory, gitArgs = []) {
49
- const result = spawnSync("git", ["status", ...gitArgs], spawnOptions({
50
- cwd: directory,
51
- stdio: "inherit",
52
- }));
53
- return result.status ?? 1;
54
- }
55
-
56
- function runGitCapture(gitArgs, { cwd } = {}) {
57
- const result = spawnSync("git", gitArgs, spawnOptions({
58
- cwd,
59
- encoding: "utf8",
60
- }));
61
- return {
62
- status: result.status ?? 1,
63
- stdout: (result.stdout || "").trim(),
64
- stderr: (result.stderr || "").trim(),
65
- };
66
- }
67
-
68
- function getCurrentBranch(directory) {
69
- const branch = runGitCapture(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: directory });
70
- if (branch.status !== 0) {
71
- return "";
72
- }
73
- if (branch.stdout === "HEAD") {
74
- return "";
75
- }
76
- return branch.stdout;
77
- }
78
-
79
- function refExists(directory, ref) {
80
- const result = runGitCapture(["rev-parse", "--verify", ref], { cwd: directory });
81
- return result.status === 0;
82
- }
83
-
84
- function listConflictedFiles(directory) {
85
- const result = runGitCapture(
86
- ["diff", "--name-only", "--diff-filter=U"],
87
- { cwd: directory }
88
- );
89
- if (result.status !== 0) {
90
- return [];
91
- }
92
- return result.stdout
93
- .split(/\r?\n/)
94
- .map((line) => line.trim())
95
- .filter(Boolean);
96
- }
97
-
98
- function isMergeOrRebaseInProgress(directory) {
99
- const merge = runGitCapture(["rev-parse", "-q", "--verify", "MERGE_HEAD"], {
100
- cwd: directory,
101
- });
102
- if (merge.status === 0) {
103
- return "merge";
104
- }
105
- const rebaseMerge = fs.existsSync(path.join(directory, ".git", "rebase-merge"));
106
- const rebaseApply = fs.existsSync(path.join(directory, ".git", "rebase-apply"));
107
- if (rebaseMerge || rebaseApply) {
108
- return "rebase";
109
- }
110
- return "";
111
- }
112
-
113
- function countDirtyFiles(directory) {
114
- const result = runGitCapture(["status", "--porcelain"], { cwd: directory });
115
- if (result.status !== 0) {
116
- return 0;
117
- }
118
- return result.stdout
119
- .split(/\r?\n/)
120
- .map((line) => line.trim())
121
- .filter(Boolean).length;
122
- }
123
-
124
- module.exports = {
125
- isGitRepo,
126
- runGit,
127
- runGitCapture,
128
- getCurrentBranch,
129
- refExists,
130
- listConflictedFiles,
131
- isMergeOrRebaseInProgress,
132
- countDirtyFiles,
133
- ensureRepoExists,
134
- listRemotesVerbose,
135
- runGitStatus,
136
- };