@solaqua/gji 0.10.0 → 0.12.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/README.md +83 -426
- package/dist/back.d.ts +3 -0
- package/dist/back.js +19 -7
- package/dist/bootstrap-output.d.ts +3 -0
- package/dist/bootstrap-output.js +27 -0
- package/dist/bootstrap-preview.d.ts +10 -0
- package/dist/bootstrap-preview.js +21 -0
- package/dist/clean.js +27 -3
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +140 -22
- package/dist/clone-failure-store.d.ts +18 -0
- package/dist/clone-failure-store.js +230 -0
- package/dist/command-runner.d.ts +6 -0
- package/dist/command-runner.js +38 -0
- package/dist/completion.js +7 -2
- package/dist/config-command.d.ts +1 -0
- package/dist/config-command.js +7 -1
- package/dist/config.d.ts +26 -1
- package/dist/config.js +110 -6
- package/dist/context-card.d.ts +1 -0
- package/dist/context-card.js +25 -0
- package/dist/dependency-bootstrap-prompt.d.ts +24 -0
- package/dist/dependency-bootstrap-prompt.js +103 -0
- package/dist/dependency-bootstrap.d.ts +129 -0
- package/dist/dependency-bootstrap.js +566 -0
- package/dist/dir-clone.d.ts +33 -0
- package/dist/dir-clone.js +505 -0
- package/dist/doctor.js +21 -0
- package/dist/done.d.ts +10 -0
- package/dist/done.js +210 -0
- package/dist/file-sync.js +71 -22
- package/dist/format-bytes.d.ts +1 -0
- package/dist/format-bytes.js +14 -0
- package/dist/fs-utils.d.ts +4 -0
- package/dist/fs-utils.js +26 -0
- package/dist/git.d.ts +5 -0
- package/dist/git.js +50 -0
- package/dist/gji-bundle.mjs +6189 -1313
- package/dist/go-resolver.d.ts +35 -0
- package/dist/go-resolver.js +185 -0
- package/dist/go.d.ts +6 -2
- package/dist/go.js +270 -62
- package/dist/hooks.d.ts +2 -1
- package/dist/hooks.js +20 -8
- package/dist/hub.d.ts +31 -0
- package/dist/hub.js +287 -0
- package/dist/init.js +30 -22
- package/dist/install-prompt.d.ts +1 -0
- package/dist/install-prompt.js +3 -25
- package/dist/ls.js +9 -1
- package/dist/navigation-output.d.ts +11 -0
- package/dist/navigation-output.js +6 -0
- package/dist/new.d.ts +14 -2
- package/dist/new.js +422 -38
- package/dist/pr-open.js +2 -10
- package/dist/pr.d.ts +6 -2
- package/dist/pr.js +135 -30
- package/dist/pull-requests.d.ts +1 -0
- package/dist/pull-requests.js +32 -3
- package/dist/remove.js +22 -0
- package/dist/repo.d.ts +4 -0
- package/dist/repo.js +61 -6
- package/dist/run-hook.js +2 -0
- package/dist/safe-destination.d.ts +15 -0
- package/dist/safe-destination.js +166 -0
- package/dist/shell-completion.js +52 -13
- package/dist/slots.d.ts +8 -0
- package/dist/slots.js +61 -0
- package/dist/status.d.ts +2 -0
- package/dist/status.js +9 -1
- package/dist/sync-directories.d.ts +33 -0
- package/dist/sync-directories.js +97 -0
- package/dist/sync-plan.d.ts +16 -0
- package/dist/sync-plan.js +128 -0
- package/dist/sync.js +7 -13
- package/dist/task-command.d.ts +9 -0
- package/dist/task-command.js +29 -0
- package/dist/task.d.ts +9 -0
- package/dist/task.js +37 -0
- package/dist/terminal-text.d.ts +4 -0
- package/dist/terminal-text.js +72 -0
- package/dist/undo.d.ts +36 -0
- package/dist/undo.js +392 -0
- package/dist/warp.d.ts +3 -1
- package/dist/warp.js +32 -125
- package/dist/worktree-bootstrap.d.ts +35 -0
- package/dist/worktree-bootstrap.js +96 -0
- package/dist/worktree-info.d.ts +6 -1
- package/dist/worktree-info.js +14 -4
- package/dist/worktree-management.d.ts +1 -0
- package/dist/worktree-management.js +23 -4
- package/dist/worktree-picker.d.ts +24 -11
- package/dist/worktree-picker.js +176 -95
- package/dist/worktree-prompts.js +1 -1
- package/dist/worktree-source.d.ts +6 -0
- package/dist/worktree-source.js +1 -0
- package/dist/worktree-sources.d.ts +4 -0
- package/dist/worktree-sources.js +77 -0
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-doctor.1 +1 -1
- package/man/man1/gji-done.1 +19 -0
- package/man/man1/gji-go.1 +12 -3
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +21 -3
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +7 -1
- package/man/man1/gji-remove.1 +3 -3
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-task.1 +16 -0
- package/man/man1/gji-undo.1 +16 -0
- package/man/man1/gji-warp.1 +3 -6
- package/man/man1/gji.1 +17 -5
- package/package.json +3 -1
package/dist/done.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { confirm, isCancel } from "@clack/prompts";
|
|
3
|
+
import { loadEffectiveConfig, resolveConfigString } from "./config.js";
|
|
4
|
+
import { isBranchMergedInto, readWorktreeHealth, resolveRemoteDefaultBranch, runGit, } from "./git.js";
|
|
5
|
+
import { isHeadless } from "./headless.js";
|
|
6
|
+
import { loadHistory } from "./history.js";
|
|
7
|
+
import { extractHooks, runHook } from "./hooks.js";
|
|
8
|
+
import { detectRepository, listWorktrees } from "./repo.js";
|
|
9
|
+
import { writeShellOutput } from "./shell-handoff.js";
|
|
10
|
+
import { releaseWorktreeSlot } from "./slots.js";
|
|
11
|
+
import { finalizeUndoOperation, recordUndoOperation } from "./undo.js";
|
|
12
|
+
import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeForceRemovalError, removeWorktree, } from "./worktree-management.js";
|
|
13
|
+
const DONE_OUTPUT_FILE_ENV = "GJI_DONE_OUTPUT_FILE";
|
|
14
|
+
export async function runDoneCommand(options) {
|
|
15
|
+
const repository = await detectRepository(options.cwd);
|
|
16
|
+
if (!repository.isWorktree && !options.branch)
|
|
17
|
+
return doneError(options, "gji done: not inside a linked worktree");
|
|
18
|
+
const linked = (await listWorktrees(options.cwd)).filter((entry) => entry.path !== repository.repoRoot);
|
|
19
|
+
const target = options.branch
|
|
20
|
+
? linked.find((entry) => entry.branch === options.branch || entry.path === options.branch)
|
|
21
|
+
: linked.find((entry) => entry.path === repository.currentRoot);
|
|
22
|
+
if (!target)
|
|
23
|
+
return doneError(options, options.branch
|
|
24
|
+
? `gji done: no linked worktree found for ${options.branch}`
|
|
25
|
+
: "gji done: not inside a linked worktree");
|
|
26
|
+
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
27
|
+
await refreshUpstream(repository.repoRoot, target.path);
|
|
28
|
+
const health = await readWorktreeHealth(target.path);
|
|
29
|
+
if (target.branch &&
|
|
30
|
+
!options.force &&
|
|
31
|
+
!options.keepBranch &&
|
|
32
|
+
!(await isSafeToComplete(repository.repoRoot, target.branch, health, config))) {
|
|
33
|
+
if (options.json || isHeadless())
|
|
34
|
+
return doneError(options, "branch is not merged and its upstream is not gone; use --force");
|
|
35
|
+
const choice = await confirm({
|
|
36
|
+
message: `branch "${target.branch}" doesn't look merged. Delete anyway?`,
|
|
37
|
+
active: "Yes",
|
|
38
|
+
inactive: "No",
|
|
39
|
+
initialValue: false,
|
|
40
|
+
});
|
|
41
|
+
if (isCancel(choice) || !choice)
|
|
42
|
+
return doneError(options, "Aborted");
|
|
43
|
+
}
|
|
44
|
+
if (health.status === "dirty" && !options.force) {
|
|
45
|
+
if (options.json || isHeadless())
|
|
46
|
+
return doneError(options, "worktree has uncommitted changes; use --force");
|
|
47
|
+
const choice = await confirm({
|
|
48
|
+
message: `worktree ${target.path} has uncommitted changes that cannot be undone. Remove anyway?`,
|
|
49
|
+
active: "Yes",
|
|
50
|
+
inactive: "No",
|
|
51
|
+
initialValue: false,
|
|
52
|
+
});
|
|
53
|
+
if (isCancel(choice) || !choice)
|
|
54
|
+
return doneError(options, "Aborted");
|
|
55
|
+
}
|
|
56
|
+
let journal;
|
|
57
|
+
try {
|
|
58
|
+
journal = await recordUndoOperation("done", repository.repoRoot, [target]);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return doneError(options, `could not write undo journal; no worktree was removed: ${toMessage(error)}`);
|
|
62
|
+
}
|
|
63
|
+
if (!journal)
|
|
64
|
+
return doneError(options, "could not capture undo state; no worktree was removed");
|
|
65
|
+
await runHook(extractHooks(config)["before-remove"], target.path, {
|
|
66
|
+
branch: target.branch ?? undefined,
|
|
67
|
+
path: target.path,
|
|
68
|
+
repo: basename(repository.repoRoot),
|
|
69
|
+
}, options.stderr);
|
|
70
|
+
try {
|
|
71
|
+
await removeWorktree(repository.repoRoot, target.path);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (!isWorktreeForceRemovalError(error)) {
|
|
75
|
+
await finalizeUndoOperation(journal, []);
|
|
76
|
+
return doneError(options, `Failed to remove worktree at ${target.path}: ${toMessage(error)}`);
|
|
77
|
+
}
|
|
78
|
+
if (!options.force) {
|
|
79
|
+
if (options.json || isHeadless()) {
|
|
80
|
+
await finalizeUndoOperation(journal, []);
|
|
81
|
+
return doneError(options, "worktree requires force removal; use --force");
|
|
82
|
+
}
|
|
83
|
+
const choice = await confirm({
|
|
84
|
+
message: `worktree ${target.path} contains files or initialized submodules that cannot be undone. Force remove?`,
|
|
85
|
+
active: "Yes",
|
|
86
|
+
inactive: "No",
|
|
87
|
+
initialValue: false,
|
|
88
|
+
});
|
|
89
|
+
if (isCancel(choice) || !choice) {
|
|
90
|
+
await finalizeUndoOperation(journal, []);
|
|
91
|
+
return doneError(options, "Aborted");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await forceRemoveWorktree(repository.repoRoot, target.path);
|
|
96
|
+
}
|
|
97
|
+
catch (forceError) {
|
|
98
|
+
await finalizeUndoOperation(journal, []);
|
|
99
|
+
return doneError(options, `Failed to remove worktree at ${target.path}: ${toMessage(forceError)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let branchDeleted = false;
|
|
103
|
+
if (target.branch && !options.keepBranch) {
|
|
104
|
+
try {
|
|
105
|
+
await deleteBranch(repository.repoRoot, target.branch);
|
|
106
|
+
branchDeleted = true;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (!isBranchUnmergedError(error))
|
|
110
|
+
return doneError(options, `Failed to delete branch ${target.branch}: ${toMessage(error)}`);
|
|
111
|
+
if (options.force) {
|
|
112
|
+
try {
|
|
113
|
+
await forceDeleteBranch(repository.repoRoot, target.branch);
|
|
114
|
+
branchDeleted = true;
|
|
115
|
+
}
|
|
116
|
+
catch (forceError) {
|
|
117
|
+
return doneError(options, `Failed to delete branch ${target.branch}: ${toMessage(forceError)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (!options.json) {
|
|
121
|
+
options.stderr(`Branch ${target.branch} was not deleted (has unmerged commits)\n`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await finalizeUndoOperation(journal, [target]);
|
|
126
|
+
await releaseWorktreeSlot(target.path);
|
|
127
|
+
await runGit(repository.repoRoot, ["worktree", "prune"]).catch(() => undefined);
|
|
128
|
+
const shouldMove = options.branch === undefined || target.path === repository.currentRoot;
|
|
129
|
+
const movedTo = shouldMove
|
|
130
|
+
? await resolveDoneDestination(repository.repoRoot, target.path)
|
|
131
|
+
: null;
|
|
132
|
+
if (!options.json && movedTo)
|
|
133
|
+
await writeShellOutput(DONE_OUTPUT_FILE_ENV, movedTo, options.stdout);
|
|
134
|
+
else if (!options.json && options.branch && process.env[DONE_OUTPUT_FILE_ENV])
|
|
135
|
+
await writeShellOutput(DONE_OUTPUT_FILE_ENV, options.cwd, options.stdout);
|
|
136
|
+
if (options.json)
|
|
137
|
+
options.stdout(`${JSON.stringify({ branch: target.branch, path: target.path, deleted: true, branchDeleted, movedTo }, null, 2)}\n`);
|
|
138
|
+
else
|
|
139
|
+
options.stderr(`✓ removed ${target.branch ?? "detached worktree"} (worktree${branchDeleted ? " + branch" : ""})${movedTo ? ` → back at ${movedTo}` : ""} · undo: gji undo\n`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
async function isSafeToComplete(repoRoot, branch, health, config) {
|
|
143
|
+
if (health.upstreamGone)
|
|
144
|
+
return true;
|
|
145
|
+
const remote = resolveConfigString(config, "syncRemote") ?? "origin";
|
|
146
|
+
const configuredDefault = resolveConfigString(config, "syncDefaultBranch");
|
|
147
|
+
const remoteDefault = configuredDefault ??
|
|
148
|
+
(await resolveRemoteDefaultBranch(repoRoot, remote).catch(() => null));
|
|
149
|
+
if (remoteDefault) {
|
|
150
|
+
for (const base of [`${remote}/${remoteDefault}`, remoteDefault]) {
|
|
151
|
+
if (await gitRefExists(repoRoot, base))
|
|
152
|
+
return isBranchMergedInto(repoRoot, branch, base).catch(() => false);
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const base = await runGit(repoRoot, [
|
|
157
|
+
"symbolic-ref",
|
|
158
|
+
"--short",
|
|
159
|
+
"HEAD",
|
|
160
|
+
]).catch(() => null);
|
|
161
|
+
return base === null
|
|
162
|
+
? false
|
|
163
|
+
: isBranchMergedInto(repoRoot, branch, base).catch(() => false);
|
|
164
|
+
}
|
|
165
|
+
async function gitRefExists(repoRoot, ref) {
|
|
166
|
+
try {
|
|
167
|
+
await runGit(repoRoot, ["rev-parse", "--verify", ref]);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function refreshUpstream(repoRoot, worktreePath) {
|
|
175
|
+
const upstream = await runGit(worktreePath, [
|
|
176
|
+
"rev-parse",
|
|
177
|
+
"--abbrev-ref",
|
|
178
|
+
"--symbolic-full-name",
|
|
179
|
+
"@{u}",
|
|
180
|
+
]).catch(() => null);
|
|
181
|
+
const remote = upstream?.split("/")[0];
|
|
182
|
+
if (!remote)
|
|
183
|
+
return;
|
|
184
|
+
await runGit(repoRoot, ["fetch", "--prune", "--quiet", remote]).catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
async function resolveDoneDestination(repoRoot, removedPath) {
|
|
187
|
+
const history = await loadHistory();
|
|
188
|
+
for (const entry of history) {
|
|
189
|
+
if (entry.path !== removedPath && entry.path !== repoRoot) {
|
|
190
|
+
try {
|
|
191
|
+
await runGit(entry.path, ["rev-parse", "--show-toplevel"]);
|
|
192
|
+
return entry.path;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* stale history */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return repoRoot;
|
|
200
|
+
}
|
|
201
|
+
function doneError(options, message) {
|
|
202
|
+
if (options.json)
|
|
203
|
+
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
204
|
+
else
|
|
205
|
+
options.stderr(`${message}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
function toMessage(error) {
|
|
209
|
+
return error instanceof Error ? error.message : String(error);
|
|
210
|
+
}
|
package/dist/file-sync.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { copyFile, lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep, } from "node:path";
|
|
4
|
+
import { isAlreadyExistsError, isNotFoundError } from "./fs-utils.js";
|
|
5
|
+
import { inspectDestination, openDestinationDirectory, } from "./safe-destination.js";
|
|
3
6
|
/**
|
|
4
7
|
* Copies files matching each pattern (relative to mainRoot) into the equivalent
|
|
5
8
|
* relative path under targetPath, creating parent directories as needed.
|
|
@@ -11,20 +14,43 @@ import { dirname, isAbsolute, join, normalize } from "node:path";
|
|
|
11
14
|
export async function syncFiles(mainRoot, targetPath, patterns) {
|
|
12
15
|
for (const pattern of patterns) {
|
|
13
16
|
const normalized = validateSyncFilePattern(pattern);
|
|
14
|
-
const sourcePath =
|
|
15
|
-
|
|
16
|
-
// Skip silently if source does not exist
|
|
17
|
-
const sourceExists = await fileExists(sourcePath);
|
|
18
|
-
if (!sourceExists) {
|
|
17
|
+
const sourcePath = await resolveSyncFileSource(mainRoot, normalized);
|
|
18
|
+
if (!sourcePath)
|
|
19
19
|
continue;
|
|
20
|
+
const destPath = join(targetPath, normalized);
|
|
21
|
+
// Skip ordinary existing targets, but fail closed on any symlink.
|
|
22
|
+
const existingDestination = await readDestinationEntry(destPath);
|
|
23
|
+
if (existingDestination?.isSymbolicLink()) {
|
|
24
|
+
throw new Error(`destination is a symbolic link: ${destPath}`);
|
|
20
25
|
}
|
|
21
|
-
|
|
22
|
-
const destExists = await fileExists(destPath);
|
|
23
|
-
if (destExists) {
|
|
26
|
+
if (existingDestination) {
|
|
24
27
|
continue;
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
await
|
|
29
|
+
const destinationParent = dirname(destPath);
|
|
30
|
+
const beforeCreate = await inspectDestination(targetPath, destinationParent);
|
|
31
|
+
if (beforeCreate.kind === "unsafe") {
|
|
32
|
+
throw new Error(beforeCreate.reason);
|
|
33
|
+
}
|
|
34
|
+
const safeParent = await openDestinationDirectory(targetPath, destinationParent);
|
|
35
|
+
try {
|
|
36
|
+
const safeDestination = join(safeParent.path, basename(destPath));
|
|
37
|
+
const safeExistingDestination = await readDestinationEntry(safeDestination);
|
|
38
|
+
if (safeExistingDestination?.isSymbolicLink()) {
|
|
39
|
+
throw new Error(`destination is a symbolic link: ${destPath}`);
|
|
40
|
+
}
|
|
41
|
+
if (safeExistingDestination)
|
|
42
|
+
continue;
|
|
43
|
+
try {
|
|
44
|
+
await copyFile(sourcePath, safeDestination, constants.COPYFILE_EXCL);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!isAlreadyExistsError(error))
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await safeParent.close().catch(() => undefined);
|
|
53
|
+
}
|
|
28
54
|
}
|
|
29
55
|
}
|
|
30
56
|
export function validateSyncFilePattern(pattern) {
|
|
@@ -37,20 +63,43 @@ export function validateSyncFilePattern(pattern) {
|
|
|
37
63
|
}
|
|
38
64
|
return normalized;
|
|
39
65
|
}
|
|
40
|
-
async function
|
|
66
|
+
async function resolveSyncFileSource(mainRoot, pattern) {
|
|
67
|
+
let resolvedRoot;
|
|
68
|
+
let resolvedSource;
|
|
41
69
|
try {
|
|
42
|
-
await
|
|
43
|
-
|
|
70
|
+
[resolvedRoot, resolvedSource] = await Promise.all([
|
|
71
|
+
realpath(mainRoot),
|
|
72
|
+
realpath(join(mainRoot, pattern)),
|
|
73
|
+
]);
|
|
44
74
|
}
|
|
45
75
|
catch (error) {
|
|
46
|
-
if (isNotFoundError(error))
|
|
47
|
-
return
|
|
48
|
-
|
|
76
|
+
if (isNotFoundError(error))
|
|
77
|
+
return undefined;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (!isPathInside(resolvedRoot, resolvedSource)) {
|
|
81
|
+
throw new Error(`syncFiles: source symlink resolves outside the repository: ${resolvedSource}`);
|
|
82
|
+
}
|
|
83
|
+
const sourceStats = await lstat(resolvedSource);
|
|
84
|
+
if (!sourceStats.isFile()) {
|
|
85
|
+
throw new Error(`syncFiles: source is not a file: ${join(mainRoot, pattern)}`);
|
|
86
|
+
}
|
|
87
|
+
return resolvedSource;
|
|
88
|
+
}
|
|
89
|
+
async function readDestinationEntry(path) {
|
|
90
|
+
try {
|
|
91
|
+
return await lstat(path);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (isNotFoundError(error))
|
|
95
|
+
return undefined;
|
|
49
96
|
throw error;
|
|
50
97
|
}
|
|
51
98
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
99
|
+
function isPathInside(root, candidate) {
|
|
100
|
+
const distance = relative(resolve(root), resolve(candidate));
|
|
101
|
+
return (distance === "" ||
|
|
102
|
+
(!isAbsolute(distance) &&
|
|
103
|
+
distance !== ".." &&
|
|
104
|
+
!distance.startsWith(`..${sep}`)));
|
|
56
105
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatBytes(bytes: number | undefined): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function formatBytes(bytes) {
|
|
2
|
+
if (bytes === undefined)
|
|
3
|
+
return "size unavailable";
|
|
4
|
+
if (bytes < 1024)
|
|
5
|
+
return `${bytes} B`;
|
|
6
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
7
|
+
let value = bytes;
|
|
8
|
+
let unit = -1;
|
|
9
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
10
|
+
value /= 1024;
|
|
11
|
+
unit += 1;
|
|
12
|
+
}
|
|
13
|
+
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unit]}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function pathExists(path: string): Promise<boolean>;
|
|
2
|
+
export declare function isAlreadyExistsError(error: unknown): boolean;
|
|
3
|
+
export declare function isNotDirectoryError(error: unknown): boolean;
|
|
4
|
+
export declare function isNotFoundError(error: unknown): boolean;
|
package/dist/fs-utils.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
export async function pathExists(path) {
|
|
3
|
+
try {
|
|
4
|
+
await lstat(path);
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (isNotFoundError(error))
|
|
9
|
+
return false;
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function isAlreadyExistsError(error) {
|
|
14
|
+
return (hasErrorCode(error, "EEXIST") || hasErrorCode(error, "ERR_FS_CP_EEXIST"));
|
|
15
|
+
}
|
|
16
|
+
export function isNotDirectoryError(error) {
|
|
17
|
+
return hasErrorCode(error, "ENOTDIR");
|
|
18
|
+
}
|
|
19
|
+
export function isNotFoundError(error) {
|
|
20
|
+
return hasErrorCode(error, "ENOENT");
|
|
21
|
+
}
|
|
22
|
+
function hasErrorCode(error, code) {
|
|
23
|
+
return (error instanceof Error &&
|
|
24
|
+
"code" in error &&
|
|
25
|
+
error.code === code);
|
|
26
|
+
}
|
package/dist/git.d.ts
CHANGED
|
@@ -5,9 +5,14 @@ export interface WorktreeHealth {
|
|
|
5
5
|
upstreamGone: boolean;
|
|
6
6
|
status: "clean" | "dirty";
|
|
7
7
|
}
|
|
8
|
+
export interface RemoteBase {
|
|
9
|
+
branch: string;
|
|
10
|
+
ref: string;
|
|
11
|
+
}
|
|
8
12
|
export declare function runGit(cwd: string, args: string[]): Promise<string>;
|
|
9
13
|
export declare function readWorktreeHealth(cwd: string): Promise<WorktreeHealth>;
|
|
10
14
|
export declare function isDirtyWorktree(cwd: string): Promise<boolean>;
|
|
11
15
|
export declare function isBranchMergedInto(cwd: string, branch: string, base?: string): Promise<boolean>;
|
|
12
16
|
export declare function resolveRemoteDefaultBranch(cwd: string, remote: string): Promise<string | null>;
|
|
17
|
+
export declare function resolveRemoteBase(cwd: string, remote: string, configuredBranch?: string): Promise<RemoteBase | null>;
|
|
13
18
|
export declare function readBranchLastCommitTimestamp(cwd: string, branch: string): Promise<number | null>;
|
package/dist/git.js
CHANGED
|
@@ -44,6 +44,56 @@ export async function resolveRemoteDefaultBranch(cwd, remote) {
|
|
|
44
44
|
const match = /^ref: refs\/heads\/(.+)\tHEAD$/.exec(refLine);
|
|
45
45
|
return match?.[1] ?? null;
|
|
46
46
|
}
|
|
47
|
+
export async function resolveRemoteBase(cwd, remote, configuredBranch) {
|
|
48
|
+
try {
|
|
49
|
+
await runGit(cwd, ["remote", "get-url", remote]);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
let branch = configuredBranch;
|
|
55
|
+
if (!branch) {
|
|
56
|
+
try {
|
|
57
|
+
const discoveredBranch = await resolveRemoteDefaultBranch(cwd, remote);
|
|
58
|
+
branch =
|
|
59
|
+
discoveredBranch ??
|
|
60
|
+
(await resolveCachedRemoteDefaultBranch(cwd, remote));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
branch = await resolveCachedRemoteDefaultBranch(cwd, remote);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!branch)
|
|
67
|
+
return null;
|
|
68
|
+
return {
|
|
69
|
+
branch,
|
|
70
|
+
ref: `${remote}/${branch}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function resolveCachedRemoteDefaultBranch(cwd, remote) {
|
|
74
|
+
try {
|
|
75
|
+
const ref = await runGit(cwd, [
|
|
76
|
+
"symbolic-ref",
|
|
77
|
+
"--short",
|
|
78
|
+
`refs/remotes/${remote}/HEAD`,
|
|
79
|
+
]);
|
|
80
|
+
const prefix = `${remote}/`;
|
|
81
|
+
return ref.startsWith(prefix) ? ref.slice(prefix.length) : undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
try {
|
|
85
|
+
const refs = await runGit(cwd, [
|
|
86
|
+
"for-each-ref",
|
|
87
|
+
"--format=%(refname:strip=3)",
|
|
88
|
+
`refs/remotes/${remote}`,
|
|
89
|
+
]);
|
|
90
|
+
return refs.split("\n").find((ref) => ref !== "HEAD");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
47
97
|
export async function readBranchLastCommitTimestamp(cwd, branch) {
|
|
48
98
|
try {
|
|
49
99
|
const { stdout } = await execFileAsync("git", ["log", "-1", "--format=%ct", branch], { cwd });
|