@huhuhang/multipull 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 huhuhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # multipull
2
+
3
+ Pull many Git repositories safely from one CLI.
4
+
5
+ `multipull` scans a directory tree for real Git repositories, skips noisy generated folders, and runs a conservative pull operation for each repository.
6
+
7
+ ## Features
8
+
9
+ - Discovers repository roots before running Git commands.
10
+ - Skips common heavy directories such as `node_modules`, `dist`, `build`, `target`, `vendor`, `lab-*`, and `challenge-*`.
11
+ - Uses a safe pull strategy: `git pull --ff-only --prune`.
12
+ - Skips dirty worktrees by default.
13
+ - Skips detached HEAD and branches without an upstream.
14
+ - Shows one clear final table with repository, branch, upstream, and result.
15
+ - Optionally parks dirty work on a `multipull-backup/*` branch before switching to the default branch.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install -g @huhuhang/multipull
21
+ ```
22
+
23
+ Development checkout:
24
+
25
+ ```bash
26
+ git clone https://github.com/huhuhang/multipull.git
27
+ cd multipull
28
+ npm install
29
+ npm run build
30
+ npm link
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ multipull [paths...]
37
+ ```
38
+
39
+ Examples:
40
+
41
+ ```bash
42
+ multipull
43
+ multipull ~/GitHub
44
+ multipull ~/GitHub ~/Work
45
+ multipull --dry-run
46
+ multipull --verbose
47
+ multipull --park-to-default-branch
48
+ ```
49
+
50
+ ## Options
51
+
52
+ ```text
53
+ --dry-run Show planned actions without pulling.
54
+ --verbose Show Git output details after the summary table.
55
+ --park-to-default-branch Preserve dirty changes before switching to the default branch.
56
+ ```
57
+
58
+ The public CLI is intentionally small. Concurrency, scan depth, timeout, and ignore rules are internal defaults so the command stays easy to use.
59
+
60
+ ## Default Behavior
61
+
62
+ For each discovered repository, `multipull`:
63
+
64
+ 1. Reads the current branch, upstream, and worktree status.
65
+ 2. Skips dirty worktrees unless `--park-to-default-branch` is enabled and a default branch switch is needed.
66
+ 3. Skips detached HEAD.
67
+ 4. Skips branches without an upstream.
68
+ 5. Runs `git pull --ff-only --prune`.
69
+ 6. Prints a final summary table.
70
+
71
+ ## Parking Local Work
72
+
73
+ `--park-to-default-branch` is explicit because it creates Git history and switches branches.
74
+
75
+ When enabled:
76
+
77
+ - If the repository is already on its default branch, `multipull` pulls normally.
78
+ - If the repository is on another clean branch, `multipull` switches to the default branch and pulls.
79
+ - If the repository is on another dirty branch, `multipull` creates a `multipull-backup/*` branch, commits the current worktree there, switches to the default branch, and pulls.
80
+
81
+ Ignored files are not force-added or deleted.
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ npm install
87
+ npm run typecheck
88
+ npm test
89
+ npm run build
90
+ npm run check
91
+ ```
92
+
93
+ Run from source:
94
+
95
+ ```bash
96
+ npm run dev -- ~/GitHub --dry-run
97
+ ```
98
+
99
+ ## License
100
+
101
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv?: string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { DEFAULT_MAX_DEPTH } from "./defaults.js";
4
+ import { discoverRepos } from "./discover.js";
5
+ import { ok, runGit } from "./git.js";
6
+ import { renderTable } from "./table.js";
7
+ import { runRepos } from "./runner.js";
8
+ function printDetails(results, verbose) {
9
+ const detailItems = results.filter((item) => {
10
+ if (verbose) {
11
+ return Boolean(item.output || item.detail);
12
+ }
13
+ return item.category === "fail" && Boolean(item.detail || item.output);
14
+ });
15
+ if (detailItems.length === 0) {
16
+ return;
17
+ }
18
+ console.log();
19
+ console.log("Details");
20
+ console.log("-------");
21
+ for (const item of detailItems) {
22
+ console.log(`${item.repo.label}: ${item.detail || item.result}`);
23
+ if (verbose && item.output) {
24
+ for (const line of item.output.split(/\r?\n/)) {
25
+ console.log(` ${line}`);
26
+ }
27
+ }
28
+ }
29
+ }
30
+ export async function main(argv = process.argv) {
31
+ const program = new Command()
32
+ .name("multipull")
33
+ .description("Pull many Git repositories safely.")
34
+ .argument("[paths...]", "root directories to scan", ["."])
35
+ .option("--dry-run", "show planned actions without pulling")
36
+ .option("--verbose", "show git output details after the summary table")
37
+ .option("--park-to-default-branch", "preserve dirty changes on a multipull-backup/* branch before switching to the default branch")
38
+ .showHelpAfterError();
39
+ program.parse(argv);
40
+ const paths = program.args.length > 0 ? program.args : ["."];
41
+ const options = program.opts();
42
+ if (!ok(await runGit(process.cwd(), ["--version"], 10_000))) {
43
+ console.error("multipull: git command not found");
44
+ return 127;
45
+ }
46
+ const { repos, missing } = await discoverRepos(paths, { maxDepth: DEFAULT_MAX_DEPTH });
47
+ for (const path of missing) {
48
+ console.error(`multipull: skipped missing directory: ${path}`);
49
+ }
50
+ if (repos.length === 0) {
51
+ console.log("No Git repositories found.");
52
+ return missing.length > 0 ? 1 : 0;
53
+ }
54
+ const results = await runRepos(repos, {
55
+ dryRun: options.dryRun ?? false,
56
+ verbose: options.verbose ?? false,
57
+ parkToDefaultBranch: options.parkToDefaultBranch ?? false
58
+ });
59
+ console.log(renderTable(results));
60
+ const counts = {
61
+ ok: results.filter((item) => item.category === "ok").length,
62
+ skipped: results.filter((item) => item.category === "skip").length,
63
+ failed: results.filter((item) => item.category === "fail").length,
64
+ ready: results.filter((item) => item.category === "dry").length
65
+ };
66
+ console.log();
67
+ if (options.dryRun) {
68
+ console.log(`Summary: ready ${counts.ready}, skipped ${counts.skipped}, failed ${counts.failed}.`);
69
+ }
70
+ else {
71
+ console.log(`Summary: ok ${counts.ok}, skipped ${counts.skipped}, failed ${counts.failed}.`);
72
+ }
73
+ printDetails(results, options.verbose ?? false);
74
+ return counts.failed > 0 ? 1 : 0;
75
+ }
76
+ if (import.meta.url === `file://${process.argv[1]}`) {
77
+ main().then((code) => {
78
+ process.exitCode = code;
79
+ }, (error) => {
80
+ console.error(error instanceof Error ? error.message : String(error));
81
+ process.exitCode = 1;
82
+ });
83
+ }
84
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare const DEFAULT_JOBS = 4;
2
+ export declare const DEFAULT_MAX_DEPTH = 4;
3
+ export declare const DEFAULT_TIMEOUT_MS = 300000;
4
+ export declare const SCAN_IGNORE_NAMES: Set<string>;
5
+ export declare const SCAN_IGNORE_PREFIXES: string[];
@@ -0,0 +1,32 @@
1
+ export const DEFAULT_JOBS = 4;
2
+ export const DEFAULT_MAX_DEPTH = 4;
3
+ export const DEFAULT_TIMEOUT_MS = 300_000;
4
+ export const SCAN_IGNORE_NAMES = new Set([
5
+ ".build",
6
+ ".cache",
7
+ ".gradle",
8
+ ".idea",
9
+ ".mypy_cache",
10
+ ".next",
11
+ ".nuxt",
12
+ ".parcel-cache",
13
+ ".pytest_cache",
14
+ ".ruff_cache",
15
+ ".tox",
16
+ ".turbo",
17
+ ".venv",
18
+ "__pycache__",
19
+ "build",
20
+ "coverage",
21
+ "DerivedData",
22
+ "dist",
23
+ "env",
24
+ "node_modules",
25
+ "out",
26
+ "Pods",
27
+ "target",
28
+ "vendor",
29
+ "venv"
30
+ ]);
31
+ export const SCAN_IGNORE_PREFIXES = ["lab-", "challenge-"];
32
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAC9B,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAE1C,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IACvC,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,OAAO;IACP,aAAa;IACb,OAAO;IACP,OAAO;IACP,eAAe;IACf,eAAe;IACf,aAAa;IACb,MAAM;IACN,QAAQ;IACR,OAAO;IACP,aAAa;IACb,OAAO;IACP,UAAU;IACV,aAAa;IACb,MAAM;IACN,KAAK;IACL,cAAc;IACd,KAAK;IACL,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { DiscoverOptions, Repo } from "./types.js";
2
+ export declare function discoverRepos(rawRoots: string[], options?: DiscoverOptions): Promise<{
3
+ repos: Repo[];
4
+ missing: string[];
5
+ }>;
@@ -0,0 +1,89 @@
1
+ import { opendir, realpath, stat } from "node:fs/promises";
2
+ import { relative, resolve, sep } from "node:path";
3
+ import { DEFAULT_MAX_DEPTH, SCAN_IGNORE_NAMES, SCAN_IGNORE_PREFIXES } from "./defaults.js";
4
+ import { repoName, verifyRepo } from "./git.js";
5
+ function shouldIgnoreDir(name) {
6
+ return SCAN_IGNORE_NAMES.has(name) || SCAN_IGNORE_PREFIXES.some((prefix) => name.startsWith(prefix));
7
+ }
8
+ function scanDepth(root, dirPath) {
9
+ const rel = relative(root, dirPath);
10
+ return rel === "" ? 0 : rel.split(sep).length;
11
+ }
12
+ function displayLabel(repoPath, roots) {
13
+ const containingRoots = roots.filter((root) => repoPath === root || repoPath.startsWith(`${root}${sep}`));
14
+ if (containingRoots.length > 0) {
15
+ const root = containingRoots.sort((a, b) => b.length - a.length)[0];
16
+ if (root) {
17
+ const rel = relative(root, repoPath);
18
+ if (rel !== "") {
19
+ return rel;
20
+ }
21
+ }
22
+ }
23
+ return repoName(repoPath);
24
+ }
25
+ async function isDirectory(path) {
26
+ try {
27
+ return (await stat(path)).isDirectory();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ async function walkForRepos(root, dirPath, options, seen, repoPaths) {
34
+ let dir;
35
+ try {
36
+ dir = await opendir(dirPath);
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ const childDirs = [];
42
+ let hasGitEntry = false;
43
+ for await (const entry of dir) {
44
+ if (entry.name === ".git" && (entry.isDirectory() || entry.isFile())) {
45
+ hasGitEntry = true;
46
+ continue;
47
+ }
48
+ if (!entry.isDirectory() || entry.isSymbolicLink() || shouldIgnoreDir(entry.name)) {
49
+ continue;
50
+ }
51
+ childDirs.push(resolve(dirPath, entry.name));
52
+ }
53
+ if (hasGitEntry) {
54
+ const repoRoot = await verifyRepo(dirPath);
55
+ if (repoRoot && !seen.has(repoRoot)) {
56
+ seen.add(repoRoot);
57
+ repoPaths.push(repoRoot);
58
+ }
59
+ }
60
+ if (options.maxDepth > 0 && scanDepth(root, dirPath) >= options.maxDepth) {
61
+ return;
62
+ }
63
+ await Promise.all(childDirs.map((child) => walkForRepos(root, child, options, seen, repoPaths)));
64
+ }
65
+ export async function discoverRepos(rawRoots, options = { maxDepth: DEFAULT_MAX_DEPTH }) {
66
+ const roots = [];
67
+ const missing = [];
68
+ for (const rawRoot of rawRoots) {
69
+ const resolved = resolve(rawRoot);
70
+ if (!(await isDirectory(resolved))) {
71
+ missing.push(rawRoot);
72
+ continue;
73
+ }
74
+ roots.push(await realpath(resolved));
75
+ }
76
+ const seen = new Set();
77
+ const repoPaths = [];
78
+ await Promise.all(roots.map((root) => walkForRepos(root, root, options, seen, repoPaths)));
79
+ return {
80
+ repos: repoPaths
81
+ .sort((a, b) => a.localeCompare(b))
82
+ .map((path) => ({
83
+ path,
84
+ label: displayLabel(path, roots)
85
+ })),
86
+ missing
87
+ };
88
+ }
89
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"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"}
package/dist/git.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { GitCommandResult, Repo, WorktreeStatus } from "./types.js";
2
+ export declare function combinedOutput(result: GitCommandResult): string;
3
+ export declare function firstLine(text: string): string;
4
+ export declare function runGit(repoPath: string, args: string[], timeoutMs?: number): Promise<GitCommandResult>;
5
+ export declare function ok(result: GitCommandResult): boolean;
6
+ export declare function verifyRepo(candidatePath: string): Promise<string | undefined>;
7
+ export declare function parseStatusHeader(header: string): Pick<WorktreeStatus, "branch" | "upstream" | "detached">;
8
+ export declare function inspectWorktree(repo: Repo, timeoutMs: number): Promise<WorktreeStatus>;
9
+ export declare function localBranchExists(repo: Repo, branch: string, timeoutMs: number): Promise<boolean>;
10
+ export declare function remoteBranchExists(repo: Repo, remote: string, branch: string, timeoutMs: number): Promise<boolean>;
11
+ export declare function resolveDefaultBranch(repo: Repo, timeoutMs: number): Promise<string | undefined>;
12
+ export declare function checkoutDefaultBranch(repo: Repo, targetBranch: string, timeoutMs: number): Promise<GitCommandResult>;
13
+ export declare function uniqueBackupBranch(repo: Repo, sourceBranch: string, timeoutMs: number): Promise<string>;
14
+ export declare function commitWorktreeToBackupBranch(repo: Repo, status: WorktreeStatus, timeoutMs: number): Promise<{
15
+ branch: string;
16
+ result: GitCommandResult;
17
+ }>;
18
+ export declare function parkAndCheckoutDefaultBranch(repo: Repo, status: WorktreeStatus, targetBranch: string, timeoutMs: number): Promise<{
19
+ backupBranch?: string;
20
+ result: GitCommandResult;
21
+ }>;
22
+ export declare function classifyPullSuccess(repo: Repo, beforeHead: string, timeoutMs: number): Promise<string>;
23
+ export declare function repoName(repoPath: string): string;
package/dist/git.js ADDED
@@ -0,0 +1,236 @@
1
+ import { execFile } from "node:child_process";
2
+ import { basename } from "node:path";
3
+ import { DEFAULT_TIMEOUT_MS } from "./defaults.js";
4
+ export function combinedOutput(result) {
5
+ return [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
6
+ }
7
+ export function firstLine(text) {
8
+ return text
9
+ .split(/\r?\n/)
10
+ .map((line) => line.trim())
11
+ .find(Boolean) ?? "";
12
+ }
13
+ export function runGit(repoPath, args, timeoutMs = DEFAULT_TIMEOUT_MS) {
14
+ return new Promise((resolve) => {
15
+ execFile("git", ["-C", repoPath, ...args], {
16
+ encoding: "utf8",
17
+ env: {
18
+ ...process.env,
19
+ GIT_TERMINAL_PROMPT: process.env.GIT_TERMINAL_PROMPT ?? "0"
20
+ },
21
+ maxBuffer: 10 * 1024 * 1024,
22
+ timeout: timeoutMs
23
+ }, (error, stdout, stderr) => {
24
+ if (!error) {
25
+ resolve({ code: 0, stdout, stderr, timedOut: false });
26
+ return;
27
+ }
28
+ const nodeError = error;
29
+ const code = typeof nodeError.code === "number" ? nodeError.code : nodeError.killed ? 124 : 1;
30
+ resolve({
31
+ code,
32
+ stdout,
33
+ stderr,
34
+ timedOut: nodeError.killed === true || nodeError.signal === "SIGTERM"
35
+ });
36
+ });
37
+ });
38
+ }
39
+ export function ok(result) {
40
+ return result.code === 0 && !result.timedOut;
41
+ }
42
+ export async function verifyRepo(candidatePath) {
43
+ const result = await runGit(candidatePath, ["rev-parse", "--show-toplevel"], 30_000);
44
+ if (!ok(result)) {
45
+ return undefined;
46
+ }
47
+ const root = result.stdout.trim();
48
+ return root.length > 0 ? root : undefined;
49
+ }
50
+ export function parseStatusHeader(header) {
51
+ const branchText = header.replace(/^##\s*/, "").trim();
52
+ if (branchText.startsWith("HEAD ")) {
53
+ return { branch: "detached", detached: true };
54
+ }
55
+ if (branchText.startsWith("No commits yet on ")) {
56
+ const branch = branchText.replace(/^No commits yet on\s*/, "").trim();
57
+ return { branch: branch || "-", detached: false };
58
+ }
59
+ if (branchText.includes("...")) {
60
+ const [branch = "-", upstreamText = ""] = branchText.split("...", 2);
61
+ const upstream = upstreamText.split(" [", 1)[0]?.trim();
62
+ const parsed = {
63
+ branch: branch.trim() || "-",
64
+ detached: false
65
+ };
66
+ if (upstream) {
67
+ parsed.upstream = upstream;
68
+ }
69
+ return parsed;
70
+ }
71
+ return {
72
+ branch: branchText.split(" [", 1)[0]?.trim() || "-",
73
+ detached: false
74
+ };
75
+ }
76
+ export async function inspectWorktree(repo, timeoutMs) {
77
+ const result = await runGit(repo.path, ["status", "--porcelain=v1", "--branch"], timeoutMs);
78
+ if (!ok(result)) {
79
+ return {
80
+ branch: "-",
81
+ detached: false,
82
+ dirty: false,
83
+ dirtyText: "",
84
+ error: firstLine(combinedOutput(result)) || "status failed"
85
+ };
86
+ }
87
+ const lines = result.stdout.split(/\r?\n/).filter((line) => line.trim().length > 0);
88
+ const header = lines[0];
89
+ if (!header?.startsWith("## ")) {
90
+ return {
91
+ branch: "-",
92
+ detached: false,
93
+ dirty: false,
94
+ dirtyText: "",
95
+ error: "unable to read status"
96
+ };
97
+ }
98
+ const parsed = parseStatusHeader(header);
99
+ const dirtyCount = lines.length - 1;
100
+ return {
101
+ ...parsed,
102
+ dirty: dirtyCount > 0,
103
+ dirtyText: dirtyCount > 0 ? `${dirtyCount} change${dirtyCount === 1 ? "" : "s"}` : ""
104
+ };
105
+ }
106
+ export async function localBranchExists(repo, branch, timeoutMs) {
107
+ const result = await runGit(repo.path, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], timeoutMs);
108
+ return ok(result);
109
+ }
110
+ export async function remoteBranchExists(repo, remote, branch, timeoutMs) {
111
+ const result = await runGit(repo.path, ["show-ref", "--verify", "--quiet", `refs/remotes/${remote}/${branch}`], timeoutMs);
112
+ return ok(result);
113
+ }
114
+ export async function resolveDefaultBranch(repo, timeoutMs) {
115
+ const originHead = await runGit(repo.path, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], timeoutMs);
116
+ if (ok(originHead)) {
117
+ const ref = originHead.stdout.trim();
118
+ if (ref.startsWith("origin/")) {
119
+ return ref.slice("origin/".length);
120
+ }
121
+ }
122
+ for (const branch of ["main", "master"]) {
123
+ if (await localBranchExists(repo, branch, timeoutMs)) {
124
+ return branch;
125
+ }
126
+ if (await remoteBranchExists(repo, "origin", branch, timeoutMs)) {
127
+ return branch;
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+ export async function checkoutDefaultBranch(repo, targetBranch, timeoutMs) {
133
+ if (await localBranchExists(repo, targetBranch, timeoutMs)) {
134
+ return runGit(repo.path, ["checkout", targetBranch], timeoutMs);
135
+ }
136
+ if (await remoteBranchExists(repo, "origin", targetBranch, timeoutMs)) {
137
+ return runGit(repo.path, ["checkout", "-B", targetBranch, `origin/${targetBranch}`], timeoutMs);
138
+ }
139
+ return {
140
+ code: 1,
141
+ stdout: "",
142
+ stderr: `default branch not found: ${targetBranch}`,
143
+ timedOut: false
144
+ };
145
+ }
146
+ function branchNameFragment(value) {
147
+ const fragment = value.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^[-_.]+|[-_.]+$/g, "");
148
+ return (fragment || "worktree").slice(0, 48);
149
+ }
150
+ export async function uniqueBackupBranch(repo, sourceBranch, timeoutMs) {
151
+ const timestamp = new Date()
152
+ .toISOString()
153
+ .replace(/[-:]/g, "")
154
+ .replace(/\..+$/, "")
155
+ .replace("T", "-");
156
+ const base = `multipull-backup/${timestamp}-${branchNameFragment(sourceBranch)}`;
157
+ let candidate = base;
158
+ let suffix = 2;
159
+ while (await localBranchExists(repo, candidate, timeoutMs)) {
160
+ candidate = `${base}-${suffix}`;
161
+ suffix += 1;
162
+ }
163
+ return candidate;
164
+ }
165
+ export async function commitWorktreeToBackupBranch(repo, status, timeoutMs) {
166
+ const source = status.detached ? "detached" : status.branch;
167
+ const branch = await uniqueBackupBranch(repo, source, timeoutMs);
168
+ const outputs = [];
169
+ for (const args of [
170
+ ["checkout", "-b", branch],
171
+ ["add", "-A"],
172
+ ["commit", "--no-verify", "-m", `multipull backup before switching from ${source}`]
173
+ ]) {
174
+ const result = await runGit(repo.path, args, timeoutMs);
175
+ outputs.push(combinedOutput(result));
176
+ if (!ok(result)) {
177
+ return {
178
+ branch,
179
+ result: {
180
+ ...result,
181
+ stdout: outputs.filter(Boolean).join("\n")
182
+ }
183
+ };
184
+ }
185
+ }
186
+ return {
187
+ branch,
188
+ result: {
189
+ code: 0,
190
+ stdout: outputs.filter(Boolean).join("\n"),
191
+ stderr: "",
192
+ timedOut: false
193
+ }
194
+ };
195
+ }
196
+ export async function parkAndCheckoutDefaultBranch(repo, status, targetBranch, timeoutMs) {
197
+ const outputs = [];
198
+ let backupBranch;
199
+ if (status.dirty) {
200
+ const backup = await commitWorktreeToBackupBranch(repo, status, timeoutMs);
201
+ backupBranch = backup.branch;
202
+ outputs.push(combinedOutput(backup.result));
203
+ if (!ok(backup.result)) {
204
+ return {
205
+ backupBranch,
206
+ result: {
207
+ ...backup.result,
208
+ stdout: outputs.filter(Boolean).join("\n")
209
+ }
210
+ };
211
+ }
212
+ }
213
+ const checkout = await checkoutDefaultBranch(repo, targetBranch, timeoutMs);
214
+ outputs.push(combinedOutput(checkout));
215
+ const parkedResult = {
216
+ result: {
217
+ ...checkout,
218
+ stdout: outputs.filter(Boolean).join("\n")
219
+ }
220
+ };
221
+ if (backupBranch) {
222
+ parkedResult.backupBranch = backupBranch;
223
+ }
224
+ return parkedResult;
225
+ }
226
+ export async function classifyPullSuccess(repo, beforeHead, timeoutMs) {
227
+ const after = await runGit(repo.path, ["rev-parse", "HEAD"], timeoutMs);
228
+ if (ok(after) && after.stdout.trim() !== beforeHead) {
229
+ return "OK updated";
230
+ }
231
+ return "OK current";
232
+ }
233
+ export function repoName(repoPath) {
234
+ return basename(repoPath) || repoPath;
235
+ }
236
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Repo, RepoResult, RunOptions } from "./types.js";
2
+ export declare function processRepo(repo: Repo, options: RunOptions): Promise<RepoResult>;
3
+ export declare function runRepos(repos: Repo[], options?: Partial<RunOptions>): Promise<RepoResult[]>;
package/dist/runner.js ADDED
@@ -0,0 +1,171 @@
1
+ import { DEFAULT_JOBS, DEFAULT_TIMEOUT_MS } from "./defaults.js";
2
+ import { classifyPullSuccess, combinedOutput, firstLine, inspectWorktree, ok, parkAndCheckoutDefaultBranch, resolveDefaultBranch, runGit } from "./git.js";
3
+ function pendingResult(repo) {
4
+ return {
5
+ repo,
6
+ branch: "-",
7
+ upstream: "-",
8
+ result: "PENDING",
9
+ category: "pending"
10
+ };
11
+ }
12
+ function failedResult(repo, error) {
13
+ return {
14
+ repo,
15
+ branch: "-",
16
+ upstream: "-",
17
+ result: "FAIL internal error",
18
+ category: "fail",
19
+ detail: error instanceof Error ? error.message : String(error)
20
+ };
21
+ }
22
+ export async function processRepo(repo, options) {
23
+ const result = pendingResult(repo);
24
+ let switchTarget = "";
25
+ let switchAction = "";
26
+ let switchOutput = "";
27
+ let status = await inspectWorktree(repo, options.timeoutMs);
28
+ result.branch = status.branch;
29
+ if (status.error) {
30
+ return {
31
+ ...result,
32
+ result: "FAIL status",
33
+ category: "fail",
34
+ detail: status.error
35
+ };
36
+ }
37
+ if (options.parkToDefaultBranch) {
38
+ const targetBranch = await resolveDefaultBranch(repo, options.timeoutMs);
39
+ if (!targetBranch) {
40
+ return {
41
+ ...result,
42
+ result: "FAIL default branch",
43
+ category: "fail",
44
+ detail: "no default branch found"
45
+ };
46
+ }
47
+ const shouldSwitch = status.detached || status.branch !== targetBranch;
48
+ if (shouldSwitch) {
49
+ switchTarget = targetBranch;
50
+ switchAction = status.dirty ? "parked" : "switched";
51
+ if (options.dryRun) {
52
+ return {
53
+ ...result,
54
+ result: `READY ${switchAction} -> ${switchTarget}`,
55
+ category: "dry"
56
+ };
57
+ }
58
+ const switchResult = await parkAndCheckoutDefaultBranch(repo, status, targetBranch, options.timeoutMs);
59
+ switchOutput = combinedOutput(switchResult.result);
60
+ if (!ok(switchResult.result)) {
61
+ return {
62
+ ...result,
63
+ result: `FAIL ${switchAction} -> ${switchTarget}`,
64
+ category: "fail",
65
+ detail: firstLine(switchOutput) || "switch failed",
66
+ output: switchOutput
67
+ };
68
+ }
69
+ status = await inspectWorktree(repo, options.timeoutMs);
70
+ result.branch = status.branch;
71
+ if (switchResult.backupBranch) {
72
+ result.detail = `backup branch: ${switchResult.backupBranch}`;
73
+ }
74
+ if (status.error) {
75
+ return {
76
+ ...result,
77
+ result: "FAIL status",
78
+ category: "fail",
79
+ detail: status.error,
80
+ output: switchOutput
81
+ };
82
+ }
83
+ }
84
+ }
85
+ if (status.detached) {
86
+ return {
87
+ ...result,
88
+ result: "SKIP detached HEAD",
89
+ category: "skip"
90
+ };
91
+ }
92
+ if (status.dirty) {
93
+ return {
94
+ ...result,
95
+ result: `SKIP dirty (${status.dirtyText})`,
96
+ category: "skip"
97
+ };
98
+ }
99
+ if (!status.upstream) {
100
+ return {
101
+ ...result,
102
+ result: "SKIP no upstream",
103
+ category: "skip"
104
+ };
105
+ }
106
+ result.upstream = status.upstream;
107
+ if (options.dryRun) {
108
+ return {
109
+ ...result,
110
+ result: "READY pull",
111
+ category: "dry"
112
+ };
113
+ }
114
+ const before = await runGit(repo.path, ["rev-parse", "HEAD"], options.timeoutMs);
115
+ if (!ok(before)) {
116
+ const output = combinedOutput(before);
117
+ return {
118
+ ...result,
119
+ result: "FAIL read HEAD",
120
+ category: "fail",
121
+ detail: firstLine(output),
122
+ output
123
+ };
124
+ }
125
+ const pull = await runGit(repo.path, ["pull", "--ff-only", "--prune"], options.timeoutMs);
126
+ const pullOutput = [switchOutput, combinedOutput(pull)].filter(Boolean).join("\n");
127
+ if (!ok(pull)) {
128
+ return {
129
+ ...result,
130
+ result: "FAIL pull",
131
+ category: "fail",
132
+ detail: pull.timedOut ? `timed out after ${Math.round(options.timeoutMs / 1000)}s` : firstLine(combinedOutput(pull)),
133
+ output: pullOutput
134
+ };
135
+ }
136
+ const pullResult = await classifyPullSuccess(repo, before.stdout.trim(), options.timeoutMs);
137
+ return {
138
+ ...result,
139
+ result: switchTarget ? `${pullResult} (${switchAction} -> ${switchTarget})` : pullResult,
140
+ category: "ok",
141
+ output: pullOutput
142
+ };
143
+ }
144
+ export async function runRepos(repos, options = {}) {
145
+ const runOptions = {
146
+ dryRun: options.dryRun ?? false,
147
+ verbose: options.verbose ?? false,
148
+ parkToDefaultBranch: options.parkToDefaultBranch ?? false,
149
+ jobs: options.jobs ?? DEFAULT_JOBS,
150
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
151
+ };
152
+ const results = new Map();
153
+ const queue = [...repos];
154
+ async function worker() {
155
+ while (queue.length > 0) {
156
+ const repo = queue.shift();
157
+ if (!repo) {
158
+ continue;
159
+ }
160
+ try {
161
+ results.set(repo.path, await processRepo(repo, runOptions));
162
+ }
163
+ catch (error) {
164
+ results.set(repo.path, failedResult(repo, error));
165
+ }
166
+ }
167
+ }
168
+ await Promise.all(Array.from({ length: Math.min(runOptions.jobs, repos.length) }, () => worker()));
169
+ return repos.map((repo) => results.get(repo.path) ?? failedResult(repo, "missing result"));
170
+ }
171
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { RepoResult } from "./types.js";
2
+ export declare function renderTable(results: RepoResult[]): string;
package/dist/table.js ADDED
@@ -0,0 +1,36 @@
1
+ function fit(text, width) {
2
+ if (text.length <= width) {
3
+ return text.padEnd(width, " ");
4
+ }
5
+ if (width <= 3) {
6
+ return text.slice(0, width);
7
+ }
8
+ return `${text.slice(0, width - 3)}...`;
9
+ }
10
+ function widths(results) {
11
+ const rows = [
12
+ ["Repo", "Branch", "Upstream", "Result"],
13
+ ...results.map((item) => [item.repo.label, item.branch, item.upstream, item.result])
14
+ ];
15
+ const maxes = [0, 1, 2, 3].map((index) => Math.max(...rows.map((row) => row[index]?.length ?? 0)));
16
+ const branch = Math.min(Math.max(maxes[1] ?? 8, 8), 20);
17
+ const upstream = Math.min(Math.max(maxes[2] ?? 12, 12), 28);
18
+ const result = Math.min(Math.max(maxes[3] ?? 16, 16), 40);
19
+ const terminal = process.stdout.columns ?? 120;
20
+ const repo = Math.max(16, Math.min(maxes[0] ?? 16, terminal - branch - upstream - result - 6));
21
+ return [repo, branch, upstream, result];
22
+ }
23
+ export function renderTable(results) {
24
+ const headers = ["Repo", "Branch", "Upstream", "Result"];
25
+ const columnWidths = widths(results);
26
+ const lines = [
27
+ headers.map((header, index) => fit(header, columnWidths[index] ?? header.length)).join(" "),
28
+ columnWidths.map((width) => "-".repeat(width)).join(" ")
29
+ ];
30
+ for (const item of results) {
31
+ const row = [item.repo.label, item.branch, item.upstream, item.result];
32
+ lines.push(row.map((cell, index) => fit(cell, columnWidths[index] ?? cell.length)).join(" "));
33
+ }
34
+ return lines.join("\n");
35
+ }
36
+ //# sourceMappingURL=table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.js","sourceRoot":"","sources":["../src/table.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ export type ResultCategory = "ok" | "skip" | "fail" | "dry" | "pending" | "running";
2
+ export interface Repo {
3
+ path: string;
4
+ label: string;
5
+ }
6
+ export interface GitCommandResult {
7
+ code: number;
8
+ stdout: string;
9
+ stderr: string;
10
+ timedOut: boolean;
11
+ }
12
+ export interface WorktreeStatus {
13
+ branch: string;
14
+ upstream?: string;
15
+ detached: boolean;
16
+ dirty: boolean;
17
+ dirtyText: string;
18
+ error?: string;
19
+ }
20
+ export interface RepoResult {
21
+ repo: Repo;
22
+ branch: string;
23
+ upstream: string;
24
+ result: string;
25
+ category: ResultCategory;
26
+ detail?: string;
27
+ output?: string;
28
+ }
29
+ export interface RunOptions {
30
+ dryRun: boolean;
31
+ verbose: boolean;
32
+ parkToDefaultBranch: boolean;
33
+ jobs: number;
34
+ timeoutMs: number;
35
+ }
36
+ export interface DiscoverOptions {
37
+ maxDepth: number;
38
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@huhuhang/multipull",
3
+ "version": "0.1.0",
4
+ "description": "Pull many Git repositories safely from one CLI.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/huhuhang/multipull.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/huhuhang/multipull/issues"
12
+ },
13
+ "homepage": "https://github.com/huhuhang/multipull#readme",
14
+ "bin": {
15
+ "multipull": "./dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "dev": "tsx src/cli.ts",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc -p tsconfig.json --noEmit",
27
+ "check": "npm run typecheck && npm run test && npm run build",
28
+ "prepack": "npm run build"
29
+ },
30
+ "keywords": [
31
+ "git",
32
+ "pull",
33
+ "cli",
34
+ "monorepo",
35
+ "repositories"
36
+ ],
37
+ "author": "huhuhang",
38
+ "license": "MIT",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "dependencies": {
46
+ "commander": "^14.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^24.0.0",
50
+ "tsx": "^4.20.0",
51
+ "typescript": "^5.8.0",
52
+ "vitest": "^4.1.9"
53
+ }
54
+ }