@solaqua/gji 0.12.2 → 0.12.4
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/dist/bootstrap-output.d.ts +3 -2
- package/dist/bootstrap-output.js +1 -16
- package/dist/bootstrap-preview.d.ts +2 -3
- package/dist/bootstrap-preview.js +1 -11
- package/dist/cli.js +5 -1
- package/dist/config.d.ts +3 -13
- package/dist/config.js +12 -59
- package/dist/dependency-bootstrap.d.ts +11 -45
- package/dist/dependency-bootstrap.js +142 -608
- package/dist/gji-bundle.mjs +580 -2360
- package/dist/new.d.ts +4 -5
- package/dist/new.js +10 -45
- package/dist/pr.d.ts +4 -5
- package/dist/pr.js +9 -69
- package/dist/shell-completion.js +7 -5
- package/dist/uv-validation.d.ts +4 -0
- package/dist/uv-validation.js +232 -0
- package/dist/worktree-bootstrap.d.ts +5 -16
- package/dist/worktree-bootstrap.js +1 -24
- 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 +1 -1
- package/man/man1/gji-go.1 +1 -1
- 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 +8 -5
- package/man/man1/gji-open.1 +1 -1
- package/man/man1/gji-pr.1 +6 -3
- package/man/man1/gji-remove.1 +1 -1
- 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 +1 -1
- package/man/man1/gji-undo.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +2 -2
- package/package.json +1 -1
- package/dist/dependency-bootstrap-prompt.d.ts +0 -24
- package/dist/dependency-bootstrap-prompt.js +0 -103
- package/dist/dir-clone.d.ts +0 -32
- package/dist/dir-clone.js +0 -641
- package/dist/install-prompt.d.ts +0 -12
- package/dist/install-prompt.js +0 -127
- package/dist/package-manager.d.ts +0 -5
- package/dist/package-manager.js +0 -159
- package/dist/sync-directories.d.ts +0 -29
- package/dist/sync-directories.js +0 -77
- package/dist/sync-plan.d.ts +0 -16
- package/dist/sync-plan.js +0 -128
package/dist/dir-clone.js
DELETED
|
@@ -1,641 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { constants } from "node:fs";
|
|
4
|
-
import { chmod, cp, lstat, mkdir, mkdtemp, opendir, readdir, readFile, readlink, realpath, rename, rm, rmdir, symlink, unlink, utimes, writeFile, } from "node:fs/promises";
|
|
5
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
import { isAlreadyExistsError, isNotFoundError } from "./fs-utils.js";
|
|
8
|
-
import { inspectDestination, openDestinationDirectory, } from "./safe-destination.js";
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
const CLONE_LOCK_TTL_MS = 5 * 60 * 1000;
|
|
11
|
-
const CLONE_GUARD_TTL_MS = 30 * 1000;
|
|
12
|
-
const CLONE_LOCK_SUFFIX = ".gji-clone-lock";
|
|
13
|
-
const SIZE_ESTIMATE_MAX_ENTRIES = 1_000_000;
|
|
14
|
-
const SIZE_ESTIMATE_MAX_MS = 5_000;
|
|
15
|
-
export async function cloneDir(source, destination, options = {}) {
|
|
16
|
-
const platform = options.platform ?? process.platform;
|
|
17
|
-
if (!isClonePlatformSupported(platform)) {
|
|
18
|
-
throw new CloneUnsupportedError(`platform ${platform} has no CoW strategy`);
|
|
19
|
-
}
|
|
20
|
-
const sourcePath = await realpath(source);
|
|
21
|
-
const sourceStats = await lstat(sourcePath);
|
|
22
|
-
if (!sourceStats.isDirectory()) {
|
|
23
|
-
throw new Error("source is not a directory");
|
|
24
|
-
}
|
|
25
|
-
const destinationPath = await resolveProspectivePath(destination);
|
|
26
|
-
const destinationDistance = relative(sourcePath, destinationPath);
|
|
27
|
-
if (destinationDistance === "" ||
|
|
28
|
-
(destinationDistance !== ".." &&
|
|
29
|
-
!destinationDistance.startsWith(`..${sep}`))) {
|
|
30
|
-
throw new Error("clone destination must not be inside its source");
|
|
31
|
-
}
|
|
32
|
-
const startedAt = Date.now();
|
|
33
|
-
const parent = dirname(destination);
|
|
34
|
-
let safeParent;
|
|
35
|
-
try {
|
|
36
|
-
if (options.destinationRoot) {
|
|
37
|
-
const parentInspection = await inspectDestination(options.destinationRoot, parent);
|
|
38
|
-
if (parentInspection.kind === "unsafe") {
|
|
39
|
-
throw new Error(parentInspection.reason);
|
|
40
|
-
}
|
|
41
|
-
safeParent = await openDestinationDirectory(options.destinationRoot, parent);
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
await mkdir(parent, { recursive: true });
|
|
45
|
-
}
|
|
46
|
-
const operationParent = safeParent?.path ?? parent;
|
|
47
|
-
const operationDestination = join(operationParent, basename(destination));
|
|
48
|
-
const cloneLock = await acquireCloneLock(`${operationDestination}${CLONE_LOCK_SUFFIX}`, destination);
|
|
49
|
-
const stopLockHeartbeat = startLockHeartbeat(cloneLock);
|
|
50
|
-
let temporaryRoot;
|
|
51
|
-
let reservation;
|
|
52
|
-
const publishedEntries = [];
|
|
53
|
-
try {
|
|
54
|
-
if (await destinationExists(operationDestination)) {
|
|
55
|
-
throw new CloneDestinationExistsError(destination);
|
|
56
|
-
}
|
|
57
|
-
reservation = await reserveDestination(operationDestination);
|
|
58
|
-
temporaryRoot = await mkdtemp(join(operationParent, `.${basename(destination)}.gji-clone-`));
|
|
59
|
-
const temporaryDestination = join(temporaryRoot, basename(destination));
|
|
60
|
-
try {
|
|
61
|
-
await (options.copyDirectory ??
|
|
62
|
-
((sourcePath, targetPath) => copyDirectoryWithCow(sourcePath, targetPath, platform, options.runLinuxCommand)))(sourcePath, temporaryDestination);
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
if (error instanceof CloneUnsupportedError)
|
|
66
|
-
throw error;
|
|
67
|
-
if (isUnsupportedCloneError(error)) {
|
|
68
|
-
throw new CloneUnsupportedError(toErrorMessage(error));
|
|
69
|
-
}
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
await publishCloneContents(temporaryDestination, operationDestination, reservation.path, (entry) => publishedEntries.push(entry), options.copyFile ?? runForcedCloneFileCopy);
|
|
73
|
-
reservation = undefined;
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
stopLockHeartbeat();
|
|
77
|
-
if (reservation) {
|
|
78
|
-
await cleanupReservedDestination(operationDestination, reservation, publishedEntries);
|
|
79
|
-
}
|
|
80
|
-
if (temporaryRoot) {
|
|
81
|
-
await rm(temporaryRoot, { force: true, recursive: true }).catch(() => undefined);
|
|
82
|
-
}
|
|
83
|
-
await releaseCloneLock(cloneLock).catch(() => undefined);
|
|
84
|
-
}
|
|
85
|
-
return cloneResult(sourcePath, startedAt, options.measureBytes);
|
|
86
|
-
}
|
|
87
|
-
finally {
|
|
88
|
-
await safeParent?.close().catch(() => undefined);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async function copyDirectoryWithCow(source, destination, platform, runLinuxCommand) {
|
|
92
|
-
if (platform === "darwin") {
|
|
93
|
-
await cp(source, destination, {
|
|
94
|
-
errorOnExist: true,
|
|
95
|
-
force: false,
|
|
96
|
-
mode: constants.COPYFILE_FICLONE_FORCE,
|
|
97
|
-
preserveTimestamps: true,
|
|
98
|
-
recursive: true,
|
|
99
|
-
});
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const strategy = cloneStrategy(platform);
|
|
103
|
-
if (!strategy) {
|
|
104
|
-
throw new CloneUnsupportedError(`platform ${platform} has no CoW strategy`);
|
|
105
|
-
}
|
|
106
|
-
await (runLinuxCommand ?? runCloneCommand)("cp", strategy(source, destination));
|
|
107
|
-
}
|
|
108
|
-
async function resolveProspectivePath(path) {
|
|
109
|
-
let current = resolve(path);
|
|
110
|
-
const missing = [];
|
|
111
|
-
while (true) {
|
|
112
|
-
try {
|
|
113
|
-
return join(await realpath(current), ...missing);
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
if (!isNotFoundError(error))
|
|
117
|
-
throw error;
|
|
118
|
-
const parent = dirname(current);
|
|
119
|
-
if (parent === current)
|
|
120
|
-
throw error;
|
|
121
|
-
missing.unshift(basename(current));
|
|
122
|
-
current = parent;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
async function cloneResult(source, startedAt, measureBytes) {
|
|
127
|
-
const bytes = measureBytes === false ? undefined : await estimateCloneBytes(source);
|
|
128
|
-
return { bytes, ms: Date.now() - startedAt };
|
|
129
|
-
}
|
|
130
|
-
export class CloneDestinationExistsError extends Error {
|
|
131
|
-
code = "GJI_CLONE_DESTINATION_EXISTS";
|
|
132
|
-
constructor(destination) {
|
|
133
|
-
super(`destination already exists: ${destination}`);
|
|
134
|
-
this.name = "CloneDestinationExistsError";
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
export class CloneUnsupportedError extends Error {
|
|
138
|
-
code = "GJI_CLONE_UNSUPPORTED";
|
|
139
|
-
constructor(reason) {
|
|
140
|
-
super(`copy-on-write cloning is not supported: ${reason}`);
|
|
141
|
-
this.name = "CloneUnsupportedError";
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
export class CloneInProgressError extends Error {
|
|
145
|
-
code = "GJI_CLONE_IN_PROGRESS";
|
|
146
|
-
constructor(destination) {
|
|
147
|
-
super(`copy-on-write clone already in progress: ${destination}`);
|
|
148
|
-
this.name = "CloneInProgressError";
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
export function isCloneDestinationExistsError(error) {
|
|
152
|
-
return error instanceof CloneDestinationExistsError;
|
|
153
|
-
}
|
|
154
|
-
export function isCloneUnsupportedError(error) {
|
|
155
|
-
return error instanceof CloneUnsupportedError;
|
|
156
|
-
}
|
|
157
|
-
export function isCloneInProgressError(error) {
|
|
158
|
-
return error instanceof CloneInProgressError;
|
|
159
|
-
}
|
|
160
|
-
export async function directorySize(path) {
|
|
161
|
-
const stats = await lstat(path);
|
|
162
|
-
if (!stats.isDirectory())
|
|
163
|
-
return stats.size;
|
|
164
|
-
const pending = [path];
|
|
165
|
-
let total = 0;
|
|
166
|
-
let entryCount = 0;
|
|
167
|
-
const startedAt = Date.now();
|
|
168
|
-
while (pending.length > 0) {
|
|
169
|
-
const current = pending.pop();
|
|
170
|
-
if (!current)
|
|
171
|
-
continue;
|
|
172
|
-
const directory = await opendir(current);
|
|
173
|
-
try {
|
|
174
|
-
for await (const entry of directory) {
|
|
175
|
-
entryCount += 1;
|
|
176
|
-
if (entryCount > SIZE_ESTIMATE_MAX_ENTRIES ||
|
|
177
|
-
Date.now() - startedAt > SIZE_ESTIMATE_MAX_MS) {
|
|
178
|
-
throw new Error("directory size estimate exceeded its safety limit");
|
|
179
|
-
}
|
|
180
|
-
const entryPath = join(current, entry.name);
|
|
181
|
-
if (entry.isDirectory())
|
|
182
|
-
pending.push(entryPath);
|
|
183
|
-
else
|
|
184
|
-
total += (await lstat(entryPath)).size;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
await directory.close().catch(() => undefined);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return total;
|
|
192
|
-
}
|
|
193
|
-
function cloneStrategy(platform) {
|
|
194
|
-
if (platform === "linux") {
|
|
195
|
-
return (source, destination) => [
|
|
196
|
-
"-a",
|
|
197
|
-
"--reflink=always",
|
|
198
|
-
source,
|
|
199
|
-
destination,
|
|
200
|
-
];
|
|
201
|
-
}
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
function isClonePlatformSupported(platform) {
|
|
205
|
-
return platform === "darwin" || platform === "linux";
|
|
206
|
-
}
|
|
207
|
-
async function estimateCloneBytes(path) {
|
|
208
|
-
try {
|
|
209
|
-
return await directorySize(path);
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
async function runCloneCommand(command, args) {
|
|
216
|
-
try {
|
|
217
|
-
await execFileAsync(command, args);
|
|
218
|
-
}
|
|
219
|
-
catch (error) {
|
|
220
|
-
if (isUnsupportedCloneError(error)) {
|
|
221
|
-
throw new CloneUnsupportedError(toErrorMessage(error));
|
|
222
|
-
}
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
async function acquireCloneLock(lockPath, destination) {
|
|
227
|
-
const releaseGuard = await acquireCloneLockGuard(lockPath, destination);
|
|
228
|
-
try {
|
|
229
|
-
return await acquireCloneLockWithGuard(lockPath, destination);
|
|
230
|
-
}
|
|
231
|
-
finally {
|
|
232
|
-
await releaseGuard();
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async function acquireCloneLockWithGuard(lockPath, destination) {
|
|
236
|
-
const lockToken = randomUUID();
|
|
237
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
238
|
-
const published = await publishCloneLock(lockPath, lockToken);
|
|
239
|
-
if (published)
|
|
240
|
-
return published;
|
|
241
|
-
let staleLock;
|
|
242
|
-
try {
|
|
243
|
-
staleLock = await inspectCloneLock(lockPath);
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
if (isNotFoundError(error))
|
|
247
|
-
continue;
|
|
248
|
-
throw error;
|
|
249
|
-
}
|
|
250
|
-
if (staleLock.kind === "invalid" ||
|
|
251
|
-
Date.now() - staleLock.mtimeMs < CLONE_LOCK_TTL_MS) {
|
|
252
|
-
throw new CloneInProgressError(destination);
|
|
253
|
-
}
|
|
254
|
-
const stalePath = `${lockPath}.stale-${randomUUID()}`;
|
|
255
|
-
try {
|
|
256
|
-
await rename(lockPath, stalePath);
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
if (isNotFoundError(error))
|
|
260
|
-
continue;
|
|
261
|
-
throw error;
|
|
262
|
-
}
|
|
263
|
-
try {
|
|
264
|
-
const replacement = await publishCloneLock(lockPath, lockToken);
|
|
265
|
-
if (!replacement) {
|
|
266
|
-
await cleanupStaleCloneLock(stalePath, staleLock);
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
try {
|
|
270
|
-
await cleanupStaleCloneLock(stalePath, staleLock);
|
|
271
|
-
}
|
|
272
|
-
catch (cleanupError) {
|
|
273
|
-
await releaseCloneLockWithGuard(replacement).catch(() => undefined);
|
|
274
|
-
throw cleanupError;
|
|
275
|
-
}
|
|
276
|
-
return replacement;
|
|
277
|
-
}
|
|
278
|
-
catch (error) {
|
|
279
|
-
await cleanupStaleCloneLock(stalePath, staleLock).catch(() => undefined);
|
|
280
|
-
if (isAlreadyExistsError(error))
|
|
281
|
-
continue;
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
throw new CloneInProgressError(destination);
|
|
286
|
-
}
|
|
287
|
-
async function acquireCloneLockGuard(lockPath, destination) {
|
|
288
|
-
const guardPath = `${lockPath}.guard`;
|
|
289
|
-
const deadline = Date.now() + 5_000;
|
|
290
|
-
while (true) {
|
|
291
|
-
try {
|
|
292
|
-
await mkdir(guardPath, { mode: 0o700 });
|
|
293
|
-
const markerName = `${process.pid}-${randomUUID()}`;
|
|
294
|
-
const markerPath = join(guardPath, markerName);
|
|
295
|
-
try {
|
|
296
|
-
await writeFile(markerPath, `${markerName}\n`, {
|
|
297
|
-
flag: "wx",
|
|
298
|
-
mode: 0o600,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
catch (error) {
|
|
302
|
-
await rmdir(guardPath).catch(() => undefined);
|
|
303
|
-
throw error;
|
|
304
|
-
}
|
|
305
|
-
return async () => {
|
|
306
|
-
await unlink(markerPath).catch((error) => {
|
|
307
|
-
if (!isNotFoundError(error))
|
|
308
|
-
throw error;
|
|
309
|
-
});
|
|
310
|
-
await rmdir(guardPath).catch((error) => {
|
|
311
|
-
if (!isNotFoundError(error))
|
|
312
|
-
throw error;
|
|
313
|
-
});
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
if (!isAlreadyExistsError(error))
|
|
318
|
-
throw error;
|
|
319
|
-
if (await reclaimAbandonedCloneGuard(guardPath))
|
|
320
|
-
continue;
|
|
321
|
-
if (Date.now() >= deadline) {
|
|
322
|
-
throw new CloneInProgressError(destination);
|
|
323
|
-
}
|
|
324
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
async function reclaimAbandonedCloneGuard(guardPath) {
|
|
329
|
-
let expectedMarker;
|
|
330
|
-
try {
|
|
331
|
-
const entries = await readdir(guardPath);
|
|
332
|
-
if (entries.length > 1)
|
|
333
|
-
return false;
|
|
334
|
-
expectedMarker = entries[0];
|
|
335
|
-
const freshnessPath = expectedMarker
|
|
336
|
-
? join(guardPath, expectedMarker)
|
|
337
|
-
: guardPath;
|
|
338
|
-
const stats = await lstat(freshnessPath);
|
|
339
|
-
if (Date.now() - stats.mtimeMs < CLONE_GUARD_TTL_MS)
|
|
340
|
-
return false;
|
|
341
|
-
if (expectedMarker) {
|
|
342
|
-
const ownerPid = Number(expectedMarker.split("-", 1)[0]);
|
|
343
|
-
if (!Number.isSafeInteger(ownerPid) || ownerPid <= 0)
|
|
344
|
-
return false;
|
|
345
|
-
if (processIsAlive(ownerPid))
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
return isNotFoundError(error);
|
|
351
|
-
}
|
|
352
|
-
const stalePath = `${guardPath}.stale-${randomUUID()}`;
|
|
353
|
-
try {
|
|
354
|
-
await rename(guardPath, stalePath);
|
|
355
|
-
}
|
|
356
|
-
catch (error) {
|
|
357
|
-
if (isNotFoundError(error))
|
|
358
|
-
return true;
|
|
359
|
-
throw error;
|
|
360
|
-
}
|
|
361
|
-
const movedEntries = await readdir(stalePath).catch(() => []);
|
|
362
|
-
if (movedEntries.length !== (expectedMarker ? 1 : 0) ||
|
|
363
|
-
(expectedMarker && movedEntries[0] !== expectedMarker)) {
|
|
364
|
-
await rename(stalePath, guardPath).catch(() => undefined);
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
if (expectedMarker)
|
|
368
|
-
await unlink(join(stalePath, expectedMarker));
|
|
369
|
-
await rmdir(stalePath);
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
function processIsAlive(pid) {
|
|
373
|
-
try {
|
|
374
|
-
process.kill(pid, 0);
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
catch (error) {
|
|
378
|
-
return error.code === "EPERM";
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
async function publishCloneLock(lockPath, lockToken) {
|
|
382
|
-
try {
|
|
383
|
-
await mkdir(lockPath, { mode: 0o700 });
|
|
384
|
-
}
|
|
385
|
-
catch (error) {
|
|
386
|
-
if (isAlreadyExistsError(error))
|
|
387
|
-
return undefined;
|
|
388
|
-
throw error;
|
|
389
|
-
}
|
|
390
|
-
const markerPath = join(lockPath, lockToken);
|
|
391
|
-
try {
|
|
392
|
-
await writeFile(markerPath, `${lockToken}\n`, { flag: "wx", mode: 0o600 });
|
|
393
|
-
return {
|
|
394
|
-
lockPath,
|
|
395
|
-
markerName: lockToken,
|
|
396
|
-
markerPath,
|
|
397
|
-
token: lockToken,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
catch (error) {
|
|
401
|
-
await rmdir(lockPath).catch(() => undefined);
|
|
402
|
-
throw error;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
async function inspectCloneLock(lockPath) {
|
|
406
|
-
try {
|
|
407
|
-
const stats = await lstat(lockPath);
|
|
408
|
-
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
409
|
-
return { kind: "invalid", mtimeMs: stats.mtimeMs };
|
|
410
|
-
}
|
|
411
|
-
const entries = await readdir(lockPath);
|
|
412
|
-
if (entries.length === 0)
|
|
413
|
-
return { kind: "empty", mtimeMs: stats.mtimeMs };
|
|
414
|
-
if (entries.length !== 1) {
|
|
415
|
-
return { kind: "invalid", mtimeMs: stats.mtimeMs };
|
|
416
|
-
}
|
|
417
|
-
const markerName = entries[0];
|
|
418
|
-
const token = markerName?.startsWith("owner-")
|
|
419
|
-
? markerName.slice("owner-".length)
|
|
420
|
-
: markerName;
|
|
421
|
-
if (!token || !/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(token)) {
|
|
422
|
-
return { kind: "invalid", mtimeMs: stats.mtimeMs };
|
|
423
|
-
}
|
|
424
|
-
const markerPath = join(lockPath, markerName);
|
|
425
|
-
const markerStats = await lstat(markerPath);
|
|
426
|
-
if (!markerStats.isFile() || markerStats.isSymbolicLink()) {
|
|
427
|
-
return { kind: "invalid", mtimeMs: markerStats.mtimeMs };
|
|
428
|
-
}
|
|
429
|
-
if ((await readFile(markerPath, "utf8")).trim() !== token) {
|
|
430
|
-
return { kind: "invalid", mtimeMs: markerStats.mtimeMs };
|
|
431
|
-
}
|
|
432
|
-
return {
|
|
433
|
-
kind: "owned",
|
|
434
|
-
lockPath,
|
|
435
|
-
markerName: markerName,
|
|
436
|
-
markerPath,
|
|
437
|
-
mtimeMs: markerStats.mtimeMs,
|
|
438
|
-
token,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
catch (error) {
|
|
442
|
-
if (isNotFoundError(error))
|
|
443
|
-
throw error;
|
|
444
|
-
return { kind: "invalid", mtimeMs: Date.now() };
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
async function cleanupStaleCloneLock(lockPath, inspection) {
|
|
448
|
-
if (inspection.kind === "owned") {
|
|
449
|
-
await unlink(join(lockPath, inspection.markerName));
|
|
450
|
-
}
|
|
451
|
-
await rmdir(lockPath);
|
|
452
|
-
}
|
|
453
|
-
async function publishCloneContents(temporaryDestination, destination, reservationPath, onEntryPublished, copyFileEntry) {
|
|
454
|
-
const reservationName = basename(reservationPath);
|
|
455
|
-
const destinationEntries = await readdir(destination);
|
|
456
|
-
if (destinationEntries.length !== 1 ||
|
|
457
|
-
destinationEntries[0] !== reservationName) {
|
|
458
|
-
throw new CloneDestinationExistsError(destination);
|
|
459
|
-
}
|
|
460
|
-
const temporaryEntries = await readdir(temporaryDestination);
|
|
461
|
-
for (const entry of temporaryEntries) {
|
|
462
|
-
await publishCloneEntry(join(temporaryDestination, entry), join(destination, entry), (entry) => onEntryPublished(entry), copyFileEntry);
|
|
463
|
-
}
|
|
464
|
-
await unlink(reservationPath);
|
|
465
|
-
}
|
|
466
|
-
async function publishCloneEntry(source, destination, onCreated, copyFileEntry) {
|
|
467
|
-
const sourceStats = await lstat(source);
|
|
468
|
-
if (sourceStats.isDirectory()) {
|
|
469
|
-
try {
|
|
470
|
-
await mkdir(destination);
|
|
471
|
-
}
|
|
472
|
-
catch (error) {
|
|
473
|
-
if (isAlreadyExistsError(error)) {
|
|
474
|
-
throw new CloneDestinationExistsError(destination);
|
|
475
|
-
}
|
|
476
|
-
throw error;
|
|
477
|
-
}
|
|
478
|
-
onCreated(await inspectOwnedCloneEntry(destination, "directory"));
|
|
479
|
-
for (const entry of await readdir(source)) {
|
|
480
|
-
await publishCloneEntry(join(source, entry), join(destination, entry), () => undefined, copyFileEntry);
|
|
481
|
-
}
|
|
482
|
-
await copyCloneMetadata(sourceStats, destination);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
if (sourceStats.isSymbolicLink()) {
|
|
486
|
-
try {
|
|
487
|
-
await symlink(await readlink(source), destination);
|
|
488
|
-
}
|
|
489
|
-
catch (error) {
|
|
490
|
-
if (isAlreadyExistsError(error)) {
|
|
491
|
-
throw new CloneDestinationExistsError(destination);
|
|
492
|
-
}
|
|
493
|
-
throw error;
|
|
494
|
-
}
|
|
495
|
-
onCreated(await inspectOwnedCloneEntry(destination, "symlink"));
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
if (!sourceStats.isFile()) {
|
|
499
|
-
throw new Error(`unsupported clone entry type: ${source}`);
|
|
500
|
-
}
|
|
501
|
-
try {
|
|
502
|
-
await copyFileEntry(source, destination);
|
|
503
|
-
}
|
|
504
|
-
catch (error) {
|
|
505
|
-
if (isAlreadyExistsError(error)) {
|
|
506
|
-
throw new CloneDestinationExistsError(destination);
|
|
507
|
-
}
|
|
508
|
-
if (isUnsupportedCloneError(error)) {
|
|
509
|
-
throw new CloneUnsupportedError(toErrorMessage(error));
|
|
510
|
-
}
|
|
511
|
-
throw error;
|
|
512
|
-
}
|
|
513
|
-
onCreated(await inspectOwnedCloneEntry(destination, "file"));
|
|
514
|
-
await copyCloneMetadata(sourceStats, destination);
|
|
515
|
-
}
|
|
516
|
-
async function runForcedCloneFileCopy(source, destination) {
|
|
517
|
-
await cp(source, destination, {
|
|
518
|
-
errorOnExist: true,
|
|
519
|
-
force: false,
|
|
520
|
-
mode: constants.COPYFILE_FICLONE_FORCE,
|
|
521
|
-
preserveTimestamps: true,
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
async function copyCloneMetadata(sourceStats, destination) {
|
|
525
|
-
await chmod(destination, Number(sourceStats.mode) & 0o7777);
|
|
526
|
-
await utimes(destination, sourceStats.atime, sourceStats.mtime);
|
|
527
|
-
}
|
|
528
|
-
async function reserveDestination(destination) {
|
|
529
|
-
try {
|
|
530
|
-
await mkdir(destination);
|
|
531
|
-
}
|
|
532
|
-
catch (error) {
|
|
533
|
-
if (isAlreadyExistsError(error)) {
|
|
534
|
-
throw new CloneDestinationExistsError(destination);
|
|
535
|
-
}
|
|
536
|
-
throw error;
|
|
537
|
-
}
|
|
538
|
-
const reservationPath = join(destination, `.gji-clone-reservation-${randomUUID()}`);
|
|
539
|
-
try {
|
|
540
|
-
await writeFile(reservationPath, "gji clone reservation\n", {
|
|
541
|
-
flag: "wx",
|
|
542
|
-
});
|
|
543
|
-
return await inspectOwnedCloneEntry(reservationPath, "file");
|
|
544
|
-
}
|
|
545
|
-
catch (error) {
|
|
546
|
-
await rmdir(destination).catch(() => undefined);
|
|
547
|
-
throw error;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
async function cleanupReservedDestination(destination, reservation, publishedEntries) {
|
|
551
|
-
for (const entry of [...publishedEntries, reservation].reverse()) {
|
|
552
|
-
await removeOwnedCloneEntry(entry);
|
|
553
|
-
}
|
|
554
|
-
await rmdir(destination).catch(() => undefined);
|
|
555
|
-
}
|
|
556
|
-
async function inspectOwnedCloneEntry(path, kind) {
|
|
557
|
-
const stats = await lstat(path, { bigint: true });
|
|
558
|
-
return { dev: stats.dev, ino: stats.ino, kind, path };
|
|
559
|
-
}
|
|
560
|
-
async function removeOwnedCloneEntry(entry) {
|
|
561
|
-
try {
|
|
562
|
-
const current = await lstat(entry.path, { bigint: true });
|
|
563
|
-
if (current.dev !== entry.dev || current.ino !== entry.ino)
|
|
564
|
-
return;
|
|
565
|
-
if (entry.kind === "directory")
|
|
566
|
-
await rmdir(entry.path);
|
|
567
|
-
else
|
|
568
|
-
await unlink(entry.path);
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
// Missing, replaced, or non-empty entries are no longer exclusively ours.
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
async function destinationExists(path) {
|
|
575
|
-
try {
|
|
576
|
-
await lstat(path);
|
|
577
|
-
return true;
|
|
578
|
-
}
|
|
579
|
-
catch (error) {
|
|
580
|
-
if (isNotFoundError(error))
|
|
581
|
-
return false;
|
|
582
|
-
throw error;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
function startLockHeartbeat(lock) {
|
|
586
|
-
const timer = setInterval(() => {
|
|
587
|
-
void refreshCloneLock(lock);
|
|
588
|
-
}, CLONE_LOCK_TTL_MS / 3);
|
|
589
|
-
timer.unref?.();
|
|
590
|
-
return () => clearInterval(timer);
|
|
591
|
-
}
|
|
592
|
-
async function refreshCloneLock(lock) {
|
|
593
|
-
try {
|
|
594
|
-
const now = new Date();
|
|
595
|
-
await utimes(lock.markerPath, now, now);
|
|
596
|
-
}
|
|
597
|
-
catch {
|
|
598
|
-
// A missing marker means this owner was already fenced out as stale.
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
async function releaseCloneLock(lock) {
|
|
602
|
-
const releaseGuard = await acquireCloneLockGuard(lock.lockPath, lock.lockPath);
|
|
603
|
-
try {
|
|
604
|
-
await releaseCloneLockWithGuard(lock);
|
|
605
|
-
}
|
|
606
|
-
finally {
|
|
607
|
-
await releaseGuard();
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
async function releaseCloneLockWithGuard(lock) {
|
|
611
|
-
try {
|
|
612
|
-
await unlink(join(lock.lockPath, lock.token));
|
|
613
|
-
}
|
|
614
|
-
catch (error) {
|
|
615
|
-
if (!isNotFoundError(error))
|
|
616
|
-
throw error;
|
|
617
|
-
}
|
|
618
|
-
try {
|
|
619
|
-
await rmdir(lock.lockPath);
|
|
620
|
-
}
|
|
621
|
-
catch (error) {
|
|
622
|
-
const code = "code" in error
|
|
623
|
-
? error.code
|
|
624
|
-
: undefined;
|
|
625
|
-
if (!isNotFoundError(error) && code !== "ENOTEMPTY" && code !== "EEXIST") {
|
|
626
|
-
throw error;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
function isUnsupportedCloneError(error) {
|
|
631
|
-
if (!(error instanceof Error))
|
|
632
|
-
return false;
|
|
633
|
-
const code = "code" in error ? error.code : "";
|
|
634
|
-
if (["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EXDEV"].includes(code ?? "")) {
|
|
635
|
-
return true;
|
|
636
|
-
}
|
|
637
|
-
return /reflink|unsupported|operation not supported|not supported|invalid cross-device|cross-device/iu.test(error.message);
|
|
638
|
-
}
|
|
639
|
-
function toErrorMessage(error) {
|
|
640
|
-
return error instanceof Error ? error.message : String(error);
|
|
641
|
-
}
|
package/dist/install-prompt.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type GjiConfig } from "./config.js";
|
|
2
|
-
import { type PackageManager } from "./package-manager.js";
|
|
3
|
-
export type InstallChoice = "yes" | "no" | "always" | "never";
|
|
4
|
-
export interface InstallPromptDependencies {
|
|
5
|
-
detectInstallPackageManager?: (root: string) => Promise<PackageManager | null>;
|
|
6
|
-
promptForInstallChoice?: (pm: PackageManager) => Promise<InstallChoice | null>;
|
|
7
|
-
runInstallCommand?: (command: string, cwd: string, stderr: (chunk: string) => void) => Promise<void>;
|
|
8
|
-
writeConfigKey?: (root: string, key: string, value: unknown) => Promise<void>;
|
|
9
|
-
writeGlobalRepoConfigKey?: (repoRoot: string, key: string, value: unknown) => Promise<void>;
|
|
10
|
-
}
|
|
11
|
-
export declare function maybeRunInstallPrompt(worktreePath: string, repoRoot: string, config: GjiConfig, stderr: (chunk: string) => void, dependencies?: InstallPromptDependencies, nonInteractive?: boolean): Promise<void>;
|
|
12
|
-
export declare const runInstallCommand: import("./command-runner.js").CommandRunner;
|