@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/new.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type PathConflictChoice } from "./conflict.js";
|
|
2
|
-
import { type
|
|
3
|
-
import { type CloneDirectory } from "./dir-clone.js";
|
|
4
|
-
import type { InstallPromptDependencies } from "./install-prompt.js";
|
|
2
|
+
import { type BootstrapCommandRunner } from "./dependency-bootstrap.js";
|
|
5
3
|
export type { PathConflictChoice };
|
|
6
4
|
export type NewWorktreeMode = "create" | "checkout" | "track";
|
|
7
5
|
export interface NewCommandOptions {
|
|
@@ -16,6 +14,7 @@ export interface NewCommandOptions {
|
|
|
16
14
|
noFetch?: boolean;
|
|
17
15
|
json?: boolean;
|
|
18
16
|
mode?: NewWorktreeMode;
|
|
17
|
+
noInstall?: boolean;
|
|
19
18
|
open?: boolean;
|
|
20
19
|
outputEnv?: string;
|
|
21
20
|
remote?: string;
|
|
@@ -24,12 +23,12 @@ export interface NewCommandOptions {
|
|
|
24
23
|
stdout: (chunk: string) => void;
|
|
25
24
|
task?: string;
|
|
26
25
|
}
|
|
27
|
-
export interface NewCommandDependencies
|
|
28
|
-
cloneDir: CloneDirectory;
|
|
26
|
+
export interface NewCommandDependencies {
|
|
29
27
|
createBranchPlaceholder: () => string;
|
|
30
28
|
promptForBranch: (placeholder: string) => Promise<string | null>;
|
|
31
29
|
promptForFetchFailure: (message: string) => Promise<boolean>;
|
|
32
30
|
promptForPathConflict: (path: string) => Promise<PathConflictChoice>;
|
|
31
|
+
runCommand: BootstrapCommandRunner;
|
|
33
32
|
spawnEditor: (cli: string, args: string[]) => Promise<void>;
|
|
34
33
|
}
|
|
35
34
|
export declare function createNewCommand(dependencies?: Partial<NewCommandDependencies>): (options: NewCommandOptions) => Promise<number>;
|
package/dist/new.js
CHANGED
|
@@ -5,26 +5,22 @@ import { promisify } from "node:util";
|
|
|
5
5
|
import { confirm, isCancel, text } from "@clack/prompts";
|
|
6
6
|
import { createBootstrapReporter } from "./bootstrap-output.js";
|
|
7
7
|
import { createDependencyBootstrapPreview, formatDependencyBootstrapPreview, } from "./bootstrap-preview.js";
|
|
8
|
-
import {
|
|
8
|
+
import { loadEffectiveConfig, resolveConfigString, } from "./config.js";
|
|
9
9
|
import { pathExists, promptForPathConflict, } from "./conflict.js";
|
|
10
|
-
import {
|
|
11
|
-
import { cloneDir } from "./dir-clone.js";
|
|
10
|
+
import { resolveDependencyBootstrapMode, } from "./dependency-bootstrap.js";
|
|
12
11
|
import { defaultSpawnEditor, EDITORS } from "./editor.js";
|
|
13
|
-
import { formatBytes } from "./format-bytes.js";
|
|
14
12
|
import { resolveRemoteBase, runGit } from "./git.js";
|
|
15
13
|
import { isHeadless } from "./headless.js";
|
|
16
14
|
import { recordWorktreeUsage } from "./history.js";
|
|
17
15
|
import { createNavigationRepository, createNavigationTarget, } from "./navigation-output.js";
|
|
18
16
|
import { detectRepository, resolveWorktreePath, validateBranchName, } from "./repo.js";
|
|
19
17
|
import { writeShellOutput } from "./shell-handoff.js";
|
|
20
|
-
import { estimateSyncDirectories } from "./sync-plan.js";
|
|
21
18
|
import { writeTask } from "./task.js";
|
|
22
19
|
import { bootstrapWorktree } from "./worktree-bootstrap.js";
|
|
23
20
|
const execFileAsync = promisify(execFile);
|
|
24
21
|
const NEW_OUTPUT_FILE_ENV = "GJI_NEW_OUTPUT_FILE";
|
|
25
22
|
export function createNewCommand(dependencies = {}) {
|
|
26
23
|
const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
|
|
27
|
-
const cloneDirectory = dependencies.cloneDir ?? cloneDir;
|
|
28
24
|
const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
|
|
29
25
|
const promptForFetchFailure = dependencies.promptForFetchFailure ?? defaultPromptForFetchFailure;
|
|
30
26
|
const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
|
|
@@ -44,11 +40,8 @@ export function createNewCommand(dependencies = {}) {
|
|
|
44
40
|
}
|
|
45
41
|
const repository = await detectRepository(options.cwd);
|
|
46
42
|
let config;
|
|
47
|
-
let dependencyBootstrapExplicit;
|
|
48
43
|
try {
|
|
49
|
-
|
|
50
|
-
config = loaded.config;
|
|
51
|
-
dependencyBootstrapExplicit = loaded.dependencyBootstrapExplicit;
|
|
44
|
+
config = await loadEffectiveConfig(repository.repoRoot, undefined, options.json ? undefined : options.stderr);
|
|
52
45
|
}
|
|
53
46
|
catch (error) {
|
|
54
47
|
return emitNewError(options, error instanceof Error ? error.message : String(error));
|
|
@@ -157,21 +150,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
157
150
|
return 1;
|
|
158
151
|
}
|
|
159
152
|
}
|
|
160
|
-
const
|
|
161
|
-
currentRoot: repository.currentRoot,
|
|
162
|
-
repoRoot: repository.repoRoot,
|
|
163
|
-
worktreePath,
|
|
164
|
-
}, config, dependencyBootstrapExplicit, {
|
|
165
|
-
dependencies,
|
|
166
|
-
dryRun: options.dryRun,
|
|
167
|
-
legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== undefined,
|
|
168
|
-
nonInteractive: !!options.json,
|
|
169
|
-
stderr: options.stderr,
|
|
170
|
-
});
|
|
171
|
-
config = {
|
|
172
|
-
...config,
|
|
173
|
-
dependencyBootstrap: dependencyPolicy.mode,
|
|
174
|
-
};
|
|
153
|
+
const dependencyMode = resolveDependencyBootstrapMode(config.dependencyBootstrap, options.noInstall);
|
|
175
154
|
if (options.dryRun) {
|
|
176
155
|
if (options.take) {
|
|
177
156
|
const changedFiles = await listTakeFiles(options.cwd);
|
|
@@ -191,8 +170,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
191
170
|
}
|
|
192
171
|
return 0;
|
|
193
172
|
}
|
|
194
|
-
const
|
|
195
|
-
const dryRunDependencyBootstrap = await createDependencyBootstrapPreview(config.dependencyBootstrap ?? "off", {
|
|
173
|
+
const dryRunDependencyBootstrap = await createDependencyBootstrapPreview(dependencyMode, {
|
|
196
174
|
currentRoot: repository.currentRoot,
|
|
197
175
|
repoRoot: repository.repoRoot,
|
|
198
176
|
cargoBuildCommand: config.dependencyBuildCommand,
|
|
@@ -203,8 +181,6 @@ export function createNewCommand(dependencies = {}) {
|
|
|
203
181
|
...createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), worktreePath, worktreeName),
|
|
204
182
|
dryRun: true,
|
|
205
183
|
};
|
|
206
|
-
if (dryRunSyncDirs.length > 0)
|
|
207
|
-
output.syncDirs = dryRunSyncDirs;
|
|
208
184
|
if (dryRunDependencyBootstrap.targets.length > 0)
|
|
209
185
|
output.dependencyBootstrap = dryRunDependencyBootstrap;
|
|
210
186
|
options.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
@@ -216,7 +192,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
216
192
|
const openNote = resolvedEditor
|
|
217
193
|
? `, then open in ${resolvedEditor}`
|
|
218
194
|
: "";
|
|
219
|
-
options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})\n${
|
|
195
|
+
options.stdout(`Would create worktree at ${worktreePath} (branch: ${worktreeName}${openNote})\n${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`);
|
|
220
196
|
}
|
|
221
197
|
return 0;
|
|
222
198
|
}
|
|
@@ -330,25 +306,22 @@ export function createNewCommand(dependencies = {}) {
|
|
|
330
306
|
}
|
|
331
307
|
const bootstrap = await bootstrapWorktree({
|
|
332
308
|
branch: worktreeName,
|
|
333
|
-
cloneDirectory,
|
|
334
309
|
config,
|
|
335
310
|
currentRoot: repository.currentRoot,
|
|
336
|
-
|
|
311
|
+
dependencyDetectionRoot: worktreePath,
|
|
312
|
+
dependencyMode,
|
|
337
313
|
repoRoot: repository.repoRoot,
|
|
338
314
|
reporter: createBootstrapReporter(options.stderr, !!options.json),
|
|
339
|
-
runCommand: dependencies.
|
|
315
|
+
runCommand: dependencies.runCommand,
|
|
340
316
|
commandStdout: options.json ? () => undefined : options.stdout,
|
|
341
317
|
commandStderr: options.json ? () => undefined : options.stderr,
|
|
342
318
|
json: options.json,
|
|
343
319
|
worktreePath,
|
|
344
|
-
installDependencies: dependencies,
|
|
345
|
-
dependencyBootstrapPolicy: dependencyPolicy,
|
|
346
320
|
});
|
|
347
321
|
if (!bootstrap.ready) {
|
|
348
322
|
return emitNewError(options, "worktree bootstrap failed", {
|
|
349
323
|
dependencyBootstrap: bootstrap.dependencyBootstrap,
|
|
350
324
|
path: worktreePath,
|
|
351
|
-
skipped: bootstrap.skippedDirs,
|
|
352
325
|
syncFiles: bootstrap.syncFileFailures,
|
|
353
326
|
});
|
|
354
327
|
}
|
|
@@ -364,15 +337,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
364
337
|
},
|
|
365
338
|
}
|
|
366
339
|
: { ...navigation };
|
|
367
|
-
if (bootstrap.
|
|
368
|
-
output.cloned = bootstrap.clonedDirs.map(({ dir, ms }) => ({
|
|
369
|
-
dir,
|
|
370
|
-
ms,
|
|
371
|
-
}));
|
|
372
|
-
}
|
|
373
|
-
if (bootstrap.skippedDirs.length > 0)
|
|
374
|
-
output.skipped = bootstrap.skippedDirs;
|
|
375
|
-
if (bootstrap.dependencyBootstrap.mode !== "off")
|
|
340
|
+
if (bootstrap.dependencyBootstrap.events.length > 0)
|
|
376
341
|
output.dependencyBootstrap = bootstrap.dependencyBootstrap;
|
|
377
342
|
options.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
378
343
|
}
|
package/dist/pr.d.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { type PathConflictChoice } from "./conflict.js";
|
|
2
|
-
import { type
|
|
3
|
-
import type { CloneDirectory } from "./dir-clone.js";
|
|
4
|
-
import type { InstallPromptDependencies } from "./install-prompt.js";
|
|
2
|
+
import { type BootstrapCommandRunner } from "./dependency-bootstrap.js";
|
|
5
3
|
export type { PathConflictChoice };
|
|
6
4
|
export interface PrCommandOptions {
|
|
7
5
|
cwd: string;
|
|
8
6
|
dryRun?: boolean;
|
|
9
7
|
json?: boolean;
|
|
8
|
+
noInstall?: boolean;
|
|
10
9
|
number: string;
|
|
11
10
|
outputEnv?: string;
|
|
12
11
|
stderr: (chunk: string) => void;
|
|
13
12
|
stdout: (chunk: string) => void;
|
|
14
13
|
}
|
|
15
|
-
export interface PrCommandDependencies
|
|
16
|
-
cloneDir?: CloneDirectory;
|
|
14
|
+
export interface PrCommandDependencies {
|
|
17
15
|
promptForPathConflict: (path: string) => Promise<PathConflictChoice>;
|
|
16
|
+
runCommand: BootstrapCommandRunner;
|
|
18
17
|
}
|
|
19
18
|
export declare function parsePrInput(input: string): string | null;
|
|
20
19
|
export declare function createPrCommand(dependencies?: Partial<PrCommandDependencies>): (options: PrCommandOptions) => Promise<number>;
|
package/dist/pr.js
CHANGED
|
@@ -4,16 +4,14 @@ import { dirname } from "node:path";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { createBootstrapReporter } from "./bootstrap-output.js";
|
|
6
6
|
import { createDependencyBootstrapPreview, formatDependencyBootstrapPreview, } from "./bootstrap-preview.js";
|
|
7
|
-
import {
|
|
7
|
+
import { loadEffectiveConfig, resolveConfigString, } from "./config.js";
|
|
8
8
|
import { pathExists, promptForPathConflict, } from "./conflict.js";
|
|
9
|
-
import {
|
|
10
|
-
import { formatBytes } from "./format-bytes.js";
|
|
9
|
+
import { resolveDependencyBootstrapMode, } from "./dependency-bootstrap.js";
|
|
11
10
|
import { isHeadless } from "./headless.js";
|
|
12
11
|
import { recordWorktreeUsage } from "./history.js";
|
|
13
12
|
import { createNavigationRepository, createNavigationTarget, } from "./navigation-output.js";
|
|
14
13
|
import { detectRepository, resolveWorktreePath } from "./repo.js";
|
|
15
14
|
import { writeShellOutput } from "./shell-handoff.js";
|
|
16
|
-
import { estimateSyncDirectories } from "./sync-plan.js";
|
|
17
15
|
import { bootstrapWorktree } from "./worktree-bootstrap.js";
|
|
18
16
|
const execFileAsync = promisify(execFile);
|
|
19
17
|
const PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
|
|
@@ -44,11 +42,8 @@ export function createPrCommand(dependencies = {}) {
|
|
|
44
42
|
}
|
|
45
43
|
const repository = await detectRepository(options.cwd);
|
|
46
44
|
let config;
|
|
47
|
-
let dependencyBootstrapExplicit;
|
|
48
45
|
try {
|
|
49
|
-
|
|
50
|
-
config = loaded.config;
|
|
51
|
-
dependencyBootstrapExplicit = loaded.dependencyBootstrapExplicit;
|
|
46
|
+
config = await loadEffectiveConfig(repository.repoRoot, undefined, options.json ? undefined : options.stderr);
|
|
52
47
|
}
|
|
53
48
|
catch (error) {
|
|
54
49
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -88,33 +83,9 @@ export function createPrCommand(dependencies = {}) {
|
|
|
88
83
|
options.stderr(`Aborted because target worktree path already exists: ${worktreePath}\n`);
|
|
89
84
|
return 1;
|
|
90
85
|
}
|
|
91
|
-
|
|
92
|
-
mode: config.dependencyBootstrap ?? "off",
|
|
93
|
-
prompted: false,
|
|
94
|
-
source: dependencyBootstrapExplicit ? "explicit" : "default",
|
|
95
|
-
};
|
|
96
|
-
if (options.dryRun) {
|
|
97
|
-
dependencyPolicy = await resolveDependencyBootstrapPolicy({
|
|
98
|
-
currentRoot: repository.currentRoot,
|
|
99
|
-
repoRoot: repository.repoRoot,
|
|
100
|
-
worktreePath,
|
|
101
|
-
}, config, dependencyBootstrapExplicit, {
|
|
102
|
-
dependencies,
|
|
103
|
-
dryRun: true,
|
|
104
|
-
legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== undefined,
|
|
105
|
-
nonInteractive: !!options.json,
|
|
106
|
-
stderr: options.stderr,
|
|
107
|
-
});
|
|
108
|
-
config = {
|
|
109
|
-
...config,
|
|
110
|
-
dependencyBootstrap: dependencyPolicy.mode,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
const dryRunSyncDirs = options.dryRun
|
|
114
|
-
? await estimateSyncDirectories(repository.repoRoot, worktreePath, config.syncDirs ?? [])
|
|
115
|
-
: [];
|
|
86
|
+
const dependencyMode = resolveDependencyBootstrapMode(config.dependencyBootstrap, options.noInstall);
|
|
116
87
|
const dryRunDependencyBootstrap = options.dryRun
|
|
117
|
-
? await createDependencyBootstrapPreview(
|
|
88
|
+
? await createDependencyBootstrapPreview(dependencyMode, {
|
|
118
89
|
currentRoot: repository.currentRoot,
|
|
119
90
|
repoRoot: repository.repoRoot,
|
|
120
91
|
cargoBuildCommand: config.dependencyBuildCommand,
|
|
@@ -127,14 +98,12 @@ export function createPrCommand(dependencies = {}) {
|
|
|
127
98
|
...createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), worktreePath, branchName),
|
|
128
99
|
dryRun: true,
|
|
129
100
|
};
|
|
130
|
-
if (dryRunSyncDirs.length > 0)
|
|
131
|
-
output.syncDirs = dryRunSyncDirs;
|
|
132
101
|
if (dryRunDependencyBootstrap?.targets.length)
|
|
133
102
|
output.dependencyBootstrap = dryRunDependencyBootstrap;
|
|
134
103
|
options.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
135
104
|
}
|
|
136
105
|
else {
|
|
137
|
-
options.stdout(`Would create worktree at ${worktreePath} (branch: ${branchName})\n${
|
|
106
|
+
options.stdout(`Would create worktree at ${worktreePath} (branch: ${branchName})\n${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`);
|
|
138
107
|
}
|
|
139
108
|
return 0;
|
|
140
109
|
}
|
|
@@ -158,45 +127,24 @@ export function createPrCommand(dependencies = {}) {
|
|
|
158
127
|
? ["worktree", "add", worktreePath, branchName]
|
|
159
128
|
: ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
|
|
160
129
|
await execFileAsync("git", worktreeArgs, { cwd: repository.repoRoot });
|
|
161
|
-
if (!dependencyBootstrapExplicit) {
|
|
162
|
-
dependencyPolicy = await resolveDependencyBootstrapPolicy({
|
|
163
|
-
currentRoot: repository.currentRoot,
|
|
164
|
-
detectionRoot: worktreePath,
|
|
165
|
-
repoRoot: repository.repoRoot,
|
|
166
|
-
worktreePath,
|
|
167
|
-
}, config, false, {
|
|
168
|
-
dependencies,
|
|
169
|
-
legacyInstallPromptConfigured: dependencies.promptForInstallChoice !== undefined,
|
|
170
|
-
nonInteractive: !!options.json,
|
|
171
|
-
stderr: options.stderr,
|
|
172
|
-
});
|
|
173
|
-
config = {
|
|
174
|
-
...config,
|
|
175
|
-
dependencyBootstrap: dependencyPolicy.mode,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
130
|
const bootstrap = await bootstrapWorktree({
|
|
179
131
|
branch: branchName,
|
|
180
|
-
cloneDirectory: dependencies.cloneDir,
|
|
181
132
|
config,
|
|
182
133
|
currentRoot: repository.currentRoot,
|
|
183
134
|
dependencyDetectionRoot: worktreePath,
|
|
184
|
-
|
|
135
|
+
dependencyMode,
|
|
185
136
|
repoRoot: repository.repoRoot,
|
|
186
137
|
reporter: createBootstrapReporter(options.stderr, !!options.json),
|
|
187
|
-
runCommand: dependencies.
|
|
138
|
+
runCommand: dependencies.runCommand,
|
|
188
139
|
commandStdout: options.json ? () => undefined : options.stdout,
|
|
189
140
|
commandStderr: options.json ? () => undefined : options.stderr,
|
|
190
141
|
json: options.json,
|
|
191
142
|
worktreePath,
|
|
192
|
-
installDependencies: dependencies,
|
|
193
|
-
dependencyBootstrapPolicy: dependencyPolicy,
|
|
194
143
|
});
|
|
195
144
|
if (!bootstrap.ready) {
|
|
196
145
|
const details = {
|
|
197
146
|
dependencyBootstrap: bootstrap.dependencyBootstrap,
|
|
198
147
|
path: worktreePath,
|
|
199
|
-
skipped: bootstrap.skippedDirs,
|
|
200
148
|
syncFiles: bootstrap.syncFileFailures,
|
|
201
149
|
};
|
|
202
150
|
if (options.json) {
|
|
@@ -212,15 +160,7 @@ export function createPrCommand(dependencies = {}) {
|
|
|
212
160
|
const output = {
|
|
213
161
|
...createNavigationTarget(createNavigationRepository(repository.repoName, repository.repoRoot), worktreePath, branchName),
|
|
214
162
|
};
|
|
215
|
-
if (bootstrap.
|
|
216
|
-
output.cloned = bootstrap.clonedDirs.map(({ dir, ms }) => ({
|
|
217
|
-
dir,
|
|
218
|
-
ms,
|
|
219
|
-
}));
|
|
220
|
-
}
|
|
221
|
-
if (bootstrap.skippedDirs.length > 0)
|
|
222
|
-
output.skipped = bootstrap.skippedDirs;
|
|
223
|
-
if (bootstrap.dependencyBootstrap.mode !== "off")
|
|
163
|
+
if (bootstrap.dependencyBootstrap.events.length > 0)
|
|
224
164
|
output.dependencyBootstrap = bootstrap.dependencyBootstrap;
|
|
225
165
|
options.stdout(`${JSON.stringify(output, null, 2)}\n`);
|
|
226
166
|
}
|
package/dist/shell-completion.js
CHANGED
|
@@ -2,7 +2,7 @@ import { KNOWN_GLOBAL_CONFIG_KEYS } from "./config.js";
|
|
|
2
2
|
const TOP_LEVEL_COMMANDS = [
|
|
3
3
|
{
|
|
4
4
|
name: "new",
|
|
5
|
-
description: "create a new branch or detached linked worktree
|
|
5
|
+
description: "create a new branch or detached linked worktree",
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
8
|
name: "done",
|
|
@@ -149,7 +149,7 @@ _gji_completion() {
|
|
|
149
149
|
|
|
150
150
|
case "$command_name" in
|
|
151
151
|
new)
|
|
152
|
-
COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --task --force --open --editor --dry-run --json --help" -- "$cur") )
|
|
152
|
+
COMPREPLY=( $(compgen -W "--detached --from-current --no-fetch --take --copy --task --force --open --editor --dry-run --no-install --json --help" -- "$cur") )
|
|
153
153
|
;;
|
|
154
154
|
done)
|
|
155
155
|
COMPREPLY=( $(compgen -W "--force --keep-branch --json --help" -- "$cur") )
|
|
@@ -187,7 +187,7 @@ _gji_completion() {
|
|
|
187
187
|
COMPREPLY=( $(compgen -W "$(__gji_pr_targets) --select --help" -- "$cur") )
|
|
188
188
|
fi
|
|
189
189
|
else
|
|
190
|
-
COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
|
|
190
|
+
COMPREPLY=( $(compgen -W "--dry-run --no-install --json --help" -- "$cur") )
|
|
191
191
|
fi
|
|
192
192
|
;;
|
|
193
193
|
back)
|
|
@@ -395,6 +395,7 @@ complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and rec
|
|
|
395
395
|
complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
|
|
396
396
|
complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, …)'
|
|
397
397
|
complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what would be created without executing any git commands or writing files'
|
|
398
|
+
complete -c gji -n '__fish_seen_subcommand_from new' -l no-install -d 'skip automatic dependency setup in the new worktree'
|
|
398
399
|
complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
399
400
|
|
|
400
401
|
complete -c gji -n '__fish_seen_subcommand_from init' -l write -d 'write the integration to the shell config file'
|
|
@@ -410,6 +411,7 @@ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'fish' -d 'shell'
|
|
|
410
411
|
complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
|
|
411
412
|
|
|
412
413
|
complete -c gji -n '__fish_seen_subcommand_from pr; and test (commandline -opc)[3] != open' -l dry-run -d 'show what would be created without executing any git commands or writing files'
|
|
414
|
+
complete -c gji -n '__fish_seen_subcommand_from pr; and test (commandline -opc)[3] != open' -l no-install -d 'skip automatic dependency setup in the PR worktree'
|
|
413
415
|
complete -c gji -n '__fish_seen_subcommand_from pr; and test (commandline -opc)[3] != open' -l json -d 'emit JSON on success or error instead of human-readable output'
|
|
414
416
|
complete -c gji -n '__fish_seen_subcommand_from pr' -a 'open' -d 'open a pull request in the default browser'
|
|
415
417
|
complete -c gji -n '__gji_should_complete_pr_select' -l select -d 'choose a pull request from any linked worktree'
|
|
@@ -551,7 +553,7 @@ fi
|
|
|
551
553
|
|
|
552
554
|
case "\${words[2]}" in
|
|
553
555
|
new)
|
|
554
|
-
_arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--task[record the purpose of the new worktree]:description:' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
556
|
+
_arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--no-fetch[skip refreshing the remote default branch before creating the new branch]' '--take[move current uncommitted changes into the new worktree]' '--copy[copy current uncommitted changes instead of moving them (requires --take)]' '--task[record the purpose of the new worktree]:description:' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--no-install[skip automatic dependency setup in the new worktree]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
555
557
|
;;
|
|
556
558
|
done)
|
|
557
559
|
_arguments '--force[remove dirty or unmerged worktrees without prompting]' '--keep-branch[remove the worktree but preserve its branch]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
|
|
@@ -578,7 +580,7 @@ case "\${words[2]}" in
|
|
|
578
580
|
_arguments '--select[choose a pull request from any linked worktree]' '4:branch or PR number:->pr_targets'
|
|
579
581
|
fi
|
|
580
582
|
else
|
|
581
|
-
_arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref:(open)'
|
|
583
|
+
_arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--no-install[skip automatic dependency setup in the PR worktree]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref:(open)'
|
|
582
584
|
fi
|
|
583
585
|
;;
|
|
584
586
|
back)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BootstrapExecutionContext, BootstrapTarget } from "./dependency-bootstrap.js";
|
|
2
|
+
export declare function validateUvRelocation(target: BootstrapTarget, context: BootstrapExecutionContext): Promise<void>;
|
|
3
|
+
export declare function validateUvInstallation(target: BootstrapTarget, context: BootstrapExecutionContext): Promise<void>;
|
|
4
|
+
export declare function validateUvStructure(target: BootstrapTarget, context: BootstrapExecutionContext): Promise<void>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, readdir, realpath } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
+
import { promisify, TextDecoder } from "node:util";
|
|
6
|
+
import { isNotFoundError } from "./fs-utils.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const UV_VALIDATION_MAX_ENTRIES = 10_000;
|
|
9
|
+
const UV_VALIDATION_MAX_FILE_BYTES = 1024 * 1024;
|
|
10
|
+
const UV_VALIDATION_MAX_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const UV_VALIDATION_TEXT_PROBE_BYTES = 4 * 1024;
|
|
12
|
+
function virtualEnvironmentPrefixes(text) {
|
|
13
|
+
const prefixes = [];
|
|
14
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
15
|
+
if (!/\bVIRTUAL_ENV\b/u.test(line))
|
|
16
|
+
continue;
|
|
17
|
+
const quotedAssignment = line.match(/\bVIRTUAL_ENV(?:\s*=|\s+)\s*(?:"([^"]+)"|'([^']+)')/u);
|
|
18
|
+
const quotedPrefix = quotedAssignment?.[1] ?? quotedAssignment?.[2];
|
|
19
|
+
if (quotedPrefix && isAbsolute(quotedPrefix))
|
|
20
|
+
prefixes.push(quotedPrefix);
|
|
21
|
+
const unquoted = line.match(/\bVIRTUAL_ENV(?:\s*=|\s+)\s*([^\s"']+)/u)?.[1];
|
|
22
|
+
if (unquoted && isAbsolute(unquoted))
|
|
23
|
+
prefixes.push(unquoted);
|
|
24
|
+
}
|
|
25
|
+
return uniquePaths(prefixes);
|
|
26
|
+
}
|
|
27
|
+
function pythonPrefixFromShebang(text) {
|
|
28
|
+
const command = text
|
|
29
|
+
.match(/^#!\s*([^\r\n]+)/u)?.[1]
|
|
30
|
+
?.trim()
|
|
31
|
+
.split(/\s+/u)[0];
|
|
32
|
+
if (!command || !isAbsolute(command))
|
|
33
|
+
return undefined;
|
|
34
|
+
if (!/^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(command.split(sep).at(-1) ?? "")) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return dirname(dirname(command));
|
|
38
|
+
}
|
|
39
|
+
function pythonPrefixesFromText(text) {
|
|
40
|
+
const interpreters = [];
|
|
41
|
+
for (const match of text.matchAll(/["'](\/[^"']+)["']/gu)) {
|
|
42
|
+
if (match[1])
|
|
43
|
+
interpreters.push(match[1]);
|
|
44
|
+
}
|
|
45
|
+
for (const match of text.matchAll(/(?:^|\s)(\/[^\s"']+)/gmu)) {
|
|
46
|
+
if (match[1])
|
|
47
|
+
interpreters.push(match[1]);
|
|
48
|
+
}
|
|
49
|
+
return uniquePaths(interpreters.flatMap((interpreter) => {
|
|
50
|
+
const name = interpreter.split(sep).at(-1) ?? "";
|
|
51
|
+
return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name)
|
|
52
|
+
? [dirname(dirname(interpreter))]
|
|
53
|
+
: [];
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
export async function validateUvRelocation(target, context) {
|
|
57
|
+
if (context.input === "preserve")
|
|
58
|
+
return;
|
|
59
|
+
if (!target.targetPath || !target.sourcePath) {
|
|
60
|
+
throw new Error("uv environment target is not project-local");
|
|
61
|
+
}
|
|
62
|
+
const scriptsDirectory = join(target.targetPath, process.platform === "win32" ? "Scripts" : "bin");
|
|
63
|
+
const interpreter = join(scriptsDirectory, process.platform === "win32" ? "python.exe" : "python");
|
|
64
|
+
const { stdout } = await execFileAsync(interpreter, [
|
|
65
|
+
"-c",
|
|
66
|
+
"import os, sys; print(os.path.realpath(sys.prefix))",
|
|
67
|
+
]);
|
|
68
|
+
const [actualPrefix, expectedPrefix] = await Promise.all([
|
|
69
|
+
realpath(stdout.trim()),
|
|
70
|
+
realpath(target.targetPath),
|
|
71
|
+
]);
|
|
72
|
+
if (actualPrefix !== expectedPrefix) {
|
|
73
|
+
throw new Error(`uv environment still points to its source prefix: ${actualPrefix}`);
|
|
74
|
+
}
|
|
75
|
+
const sourcePaths = uniquePaths([
|
|
76
|
+
target.sourcePath,
|
|
77
|
+
(await realpath(target.sourcePath).catch(() => undefined)) ?? "",
|
|
78
|
+
]).filter(Boolean);
|
|
79
|
+
const entries = await readdir(scriptsDirectory, { withFileTypes: true });
|
|
80
|
+
assertSafeUvScriptEntries(entries);
|
|
81
|
+
const environmentName = basename(target.targetPath);
|
|
82
|
+
const acceptedPrefixes = new Set([
|
|
83
|
+
resolve(target.targetPath),
|
|
84
|
+
await realpath(target.targetPath),
|
|
85
|
+
]);
|
|
86
|
+
let validatedBytes = 0;
|
|
87
|
+
for (const path of [
|
|
88
|
+
join(target.targetPath, "pyvenv.cfg"),
|
|
89
|
+
...entries
|
|
90
|
+
.filter((entry) => entry.isFile())
|
|
91
|
+
.map((entry) => join(scriptsDirectory, entry.name)),
|
|
92
|
+
]) {
|
|
93
|
+
const validated = await readBoundedUvTextFile(path, UV_VALIDATION_MAX_TOTAL_BYTES - validatedBytes);
|
|
94
|
+
if (!validated)
|
|
95
|
+
continue;
|
|
96
|
+
validatedBytes += validated.bytes;
|
|
97
|
+
if (validated.text === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
const { text } = validated;
|
|
100
|
+
if (sourcePaths.some((sourcePath) => text.includes(sourcePath))) {
|
|
101
|
+
throw new Error(`uv environment contains a stale source path: ${path}`);
|
|
102
|
+
}
|
|
103
|
+
const shebangPrefix = pythonPrefixFromShebang(text);
|
|
104
|
+
if (shebangPrefix &&
|
|
105
|
+
basename(shebangPrefix) === environmentName &&
|
|
106
|
+
!acceptedPrefixes.has(resolve(shebangPrefix))) {
|
|
107
|
+
throw new Error(`uv launcher points outside its environment: ${path}`);
|
|
108
|
+
}
|
|
109
|
+
if (pythonPrefixesFromText(text)
|
|
110
|
+
.filter((prefix) => basename(prefix) === environmentName)
|
|
111
|
+
.some((prefix) => !acceptedPrefixes.has(resolve(prefix)))) {
|
|
112
|
+
throw new Error(`uv launcher points outside its environment: ${path}`);
|
|
113
|
+
}
|
|
114
|
+
if (virtualEnvironmentPrefixes(text).some((prefix) => !acceptedPrefixes.has(resolve(prefix)))) {
|
|
115
|
+
throw new Error(`uv activation script points outside its environment: ${path}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export async function validateUvInstallation(target, context) {
|
|
120
|
+
await validateUvStructure(target, context);
|
|
121
|
+
await validateUvRelocation(target, context);
|
|
122
|
+
}
|
|
123
|
+
export async function validateUvStructure(target, context) {
|
|
124
|
+
if (context.input === "preserve")
|
|
125
|
+
return;
|
|
126
|
+
if (!target.targetPath)
|
|
127
|
+
throw new Error("uv environment target is not project-local");
|
|
128
|
+
const targetStats = await lstat(target.targetPath).catch(() => undefined);
|
|
129
|
+
if (!targetStats)
|
|
130
|
+
return;
|
|
131
|
+
if (!targetStats.isDirectory() || targetStats.isSymbolicLink()) {
|
|
132
|
+
throw new Error("uv environment must be a real directory");
|
|
133
|
+
}
|
|
134
|
+
const configStats = await lstat(join(target.targetPath, "pyvenv.cfg")).catch(() => undefined);
|
|
135
|
+
if (configStats && (!configStats.isFile() || configStats.isSymbolicLink())) {
|
|
136
|
+
throw new Error("uv pyvenv.cfg must be a regular file");
|
|
137
|
+
}
|
|
138
|
+
const scriptsDirectory = join(target.targetPath, process.platform === "win32" ? "Scripts" : "bin");
|
|
139
|
+
const scriptsStats = await lstat(scriptsDirectory).catch(() => undefined);
|
|
140
|
+
if (!scriptsStats)
|
|
141
|
+
return;
|
|
142
|
+
if (!scriptsStats.isDirectory() || scriptsStats.isSymbolicLink()) {
|
|
143
|
+
throw new Error("uv scripts path must be a real directory");
|
|
144
|
+
}
|
|
145
|
+
assertSafeUvScriptEntries(await readdir(scriptsDirectory, { withFileTypes: true }));
|
|
146
|
+
}
|
|
147
|
+
function assertSafeUvScriptEntries(entries) {
|
|
148
|
+
if (entries.length > UV_VALIDATION_MAX_ENTRIES) {
|
|
149
|
+
throw new Error("uv scripts directory exceeds the validation entry limit");
|
|
150
|
+
}
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (entry.isSymbolicLink() && !isUvInterpreterName(entry.name)) {
|
|
153
|
+
throw new Error(`uv script must not be a symbolic link: ${entry.name}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function readBoundedUvTextFile(path, remainingBytes) {
|
|
158
|
+
let handle;
|
|
159
|
+
try {
|
|
160
|
+
handle = await open(path, constants.O_RDONLY |
|
|
161
|
+
(process.platform === "win32" ? 0 : constants.O_NOFOLLOW));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (isNotFoundError(error))
|
|
165
|
+
return undefined;
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const stats = await handle.stat();
|
|
170
|
+
if (!stats.isFile())
|
|
171
|
+
return undefined;
|
|
172
|
+
const probeBytes = Math.min(UV_VALIDATION_TEXT_PROBE_BYTES, stats.size);
|
|
173
|
+
if (probeBytes > remainingBytes) {
|
|
174
|
+
throw new Error("uv launchers exceed the total validation size limit");
|
|
175
|
+
}
|
|
176
|
+
const probe = await readFilePrefix(handle, probeBytes);
|
|
177
|
+
if (!looksLikeUtf8Text(probe, probe.byteLength === stats.size)) {
|
|
178
|
+
return { bytes: probe.byteLength };
|
|
179
|
+
}
|
|
180
|
+
if (stats.size > UV_VALIDATION_MAX_FILE_BYTES) {
|
|
181
|
+
throw new Error(`uv launcher exceeds the validation size limit: ${path}`);
|
|
182
|
+
}
|
|
183
|
+
if (stats.size > remainingBytes) {
|
|
184
|
+
throw new Error("uv launchers exceed the total validation size limit");
|
|
185
|
+
}
|
|
186
|
+
const readLimit = Math.min(UV_VALIDATION_MAX_FILE_BYTES, remainingBytes);
|
|
187
|
+
const contents = await readFilePrefix(handle, readLimit + 1);
|
|
188
|
+
if (contents.byteLength > UV_VALIDATION_MAX_FILE_BYTES) {
|
|
189
|
+
throw new Error(`uv launcher exceeds the validation size limit: ${path}`);
|
|
190
|
+
}
|
|
191
|
+
if (contents.byteLength > remainingBytes) {
|
|
192
|
+
throw new Error("uv launchers exceed the total validation size limit");
|
|
193
|
+
}
|
|
194
|
+
const text = contents.toString("utf8");
|
|
195
|
+
return looksLikeUtf8Text(contents, true)
|
|
196
|
+
? { bytes: contents.byteLength, text }
|
|
197
|
+
: { bytes: contents.byteLength };
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await handle.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function looksLikeUtf8Text(contents, complete) {
|
|
204
|
+
if (contents.includes(0))
|
|
205
|
+
return false;
|
|
206
|
+
try {
|
|
207
|
+
new TextDecoder("utf-8", { fatal: true }).decode(contents, {
|
|
208
|
+
stream: !complete,
|
|
209
|
+
});
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function readFilePrefix(handle, maxBytes) {
|
|
217
|
+
const contents = Buffer.allocUnsafe(maxBytes);
|
|
218
|
+
let offset = 0;
|
|
219
|
+
while (offset < contents.byteLength) {
|
|
220
|
+
const { bytesRead } = await handle.read(contents, offset, contents.byteLength - offset, offset);
|
|
221
|
+
if (bytesRead === 0)
|
|
222
|
+
break;
|
|
223
|
+
offset += bytesRead;
|
|
224
|
+
}
|
|
225
|
+
return contents.subarray(0, offset);
|
|
226
|
+
}
|
|
227
|
+
function isUvInterpreterName(name) {
|
|
228
|
+
return /^python(?:\d+(?:\.\d+)?)?(?:\.exe)?$/iu.test(name);
|
|
229
|
+
}
|
|
230
|
+
function uniquePaths(paths) {
|
|
231
|
+
return [...new Set(paths.filter((path) => Boolean(path)))];
|
|
232
|
+
}
|