worktree-add 1.2.1 → 1.3.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 +57 -22
- package/dist/app/get-unsafe-app-name-reason.d.ts +11 -0
- package/dist/app/get-unsafe-app-name-reason.js +21 -0
- package/dist/app/resolve-apps.d.ts +6 -0
- package/dist/app/resolve-apps.js +31 -0
- package/dist/cli/cleanup-worktree.d.ts +2 -0
- package/dist/cli/cleanup-worktree.js +13 -0
- package/dist/cli/open-worktree-apps.d.ts +5 -0
- package/dist/cli/open-worktree-apps.js +33 -0
- package/dist/cli/register-sigint-handler.d.ts +8 -0
- package/dist/cli/register-sigint-handler.js +19 -0
- package/dist/cli/resolve-worktree-context.d.ts +6 -0
- package/dist/cli/resolve-worktree-context.js +22 -0
- package/dist/cli/run-worktree-add.d.ts +9 -0
- package/dist/cli/run-worktree-add.js +100 -0
- package/dist/cli.d.ts +0 -6
- package/dist/cli.js +38 -76
- package/dist/git/create-worktree.d.ts +9 -0
- package/dist/git/create-worktree.js +39 -0
- package/dist/git/extract-diagnostic-line.d.ts +1 -0
- package/dist/git/extract-diagnostic-line.js +13 -0
- package/dist/git/fetch-remote-branch.d.ts +9 -0
- package/dist/git/fetch-remote-branch.js +102 -0
- package/dist/git/git.d.ts +11 -5
- package/dist/git/git.js +25 -17
- package/dist/git/remove-worktree.d.ts +1 -0
- package/dist/git/remove-worktree.js +4 -0
- package/dist/output/create-status-logger.d.ts +13 -0
- package/dist/output/create-status-logger.js +32 -0
- package/dist/output/get-status-logger.d.ts +2 -0
- package/dist/output/get-status-logger.js +12 -0
- package/dist/project/install-commands.js +2 -2
- package/dist/project/next.d.ts +4 -1
- package/dist/project/next.js +2 -2
- package/dist/project/package-manager.d.ts +7 -1
- package/dist/project/package-manager.js +12 -3
- package/dist/project/setup.d.ts +5 -1
- package/dist/project/setup.js +15 -7
- package/dist/worktree/destination-directory.d.ts +10 -3
- package/dist/worktree/destination-directory.js +94 -16
- package/dist/worktree/untracked-file-copy.d.ts +5 -1
- package/dist/worktree/untracked-file-copy.js +51 -9
- package/package.json +4 -2
- package/dist/git/parse-branch-head-mismatch-choice.d.ts +0 -7
- package/dist/git/parse-branch-head-mismatch-choice.js +0 -26
- package/dist/git/resolve-branch-head-mismatch.d.ts +0 -6
- package/dist/git/resolve-branch-head-mismatch.js +0 -79
- package/dist/git/worktree-creation.d.ts +0 -13
- package/dist/git/worktree-creation.js +0 -35
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function extractDiagnosticLine(error) {
|
|
2
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3
|
+
const trimmed = message.trim();
|
|
4
|
+
const nonEmptyLines = trimmed
|
|
5
|
+
.split("\n")
|
|
6
|
+
.map((line) => line.trim())
|
|
7
|
+
.filter(Boolean);
|
|
8
|
+
return (
|
|
9
|
+
// Best-effort: match English prefixes; localized messages fall back to the last non-empty line.
|
|
10
|
+
nonEmptyLines.find((line) => /(?:^|\s)(?:fatal:|error:)/iu.test(line)) ??
|
|
11
|
+
nonEmptyLines.at(-1) ??
|
|
12
|
+
trimmed);
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StatusLogger } from "../output/create-status-logger.js";
|
|
2
|
+
export type RemoteBranchStatus = "exists" | "missing" | "unknown";
|
|
3
|
+
export declare function fetchRemoteBranch(branch: string, options?: {
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
logger?: StatusLogger;
|
|
6
|
+
}): {
|
|
7
|
+
status: RemoteBranchStatus;
|
|
8
|
+
localExists: boolean;
|
|
9
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getStatusLogger } from "../output/get-status-logger.js";
|
|
2
|
+
import { fetchOriginBranch, getAheadBehindCounts, getLocalBranchHead, getRemoteBranchHead, git, findWorktreeByBranchName, localBranchExists, normalizeBranchName, remoteBranchExists, } from "./git.js";
|
|
3
|
+
import { extractDiagnosticLine } from "./extract-diagnostic-line.js";
|
|
4
|
+
export function fetchRemoteBranch(branch, options) {
|
|
5
|
+
const logger = getStatusLogger(options?.logger);
|
|
6
|
+
const dryRun = options?.dryRun ?? false;
|
|
7
|
+
const normalized = normalizeBranchName(branch);
|
|
8
|
+
const localExists = localBranchExists(normalized);
|
|
9
|
+
if (localExists) {
|
|
10
|
+
let remoteExists;
|
|
11
|
+
try {
|
|
12
|
+
remoteExists = remoteBranchExists(normalized);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
const diagnostic = extractDiagnosticLine(error);
|
|
16
|
+
logger.warn(`Failed to query origin for '${normalized}': ${diagnostic}. Using existing local branch. (If you expected a remote branch, double-check your network connection and branch name.)`);
|
|
17
|
+
return { status: "unknown", localExists };
|
|
18
|
+
}
|
|
19
|
+
if (!remoteExists)
|
|
20
|
+
return { status: "missing", localExists };
|
|
21
|
+
try {
|
|
22
|
+
if (dryRun) {
|
|
23
|
+
logger.step(`Would fetch origin/${normalized}`);
|
|
24
|
+
logger.detail(`Dry run: local '${normalized}' may be fast-forwarded after fetch if it is behind origin/${normalized}.`);
|
|
25
|
+
return { status: "exists", localExists };
|
|
26
|
+
}
|
|
27
|
+
logger.step(`Fetching origin/${normalized} …`);
|
|
28
|
+
fetchOriginBranch(normalized);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const diagnostic = extractDiagnosticLine(error);
|
|
32
|
+
logger.warn(`Failed to fetch origin/${normalized}: ${diagnostic}. Using existing local branch (origin status unknown).`);
|
|
33
|
+
return { status: "unknown", localExists };
|
|
34
|
+
}
|
|
35
|
+
const localHead = getLocalBranchHead(normalized);
|
|
36
|
+
const remoteHead = getRemoteBranchHead(normalized);
|
|
37
|
+
if (!localHead || !remoteHead || localHead === remoteHead) {
|
|
38
|
+
return { status: "exists", localExists };
|
|
39
|
+
}
|
|
40
|
+
let counts;
|
|
41
|
+
try {
|
|
42
|
+
counts = getAheadBehindCounts(localHead, remoteHead);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const diagnostic = extractDiagnosticLine(error);
|
|
46
|
+
logger.warn(`Failed to compare local '${normalized}' with origin/${normalized}: ${diagnostic}. Using existing local branch as-is.`);
|
|
47
|
+
return { status: "exists", localExists };
|
|
48
|
+
}
|
|
49
|
+
const { ahead, behind } = counts;
|
|
50
|
+
if (ahead === 0 && behind > 0) {
|
|
51
|
+
// Note: dryRun returns before fetching, so this path only runs when dryRun is false.
|
|
52
|
+
// resolveWorktreeContext already blocks this, but keep as a defensive check.
|
|
53
|
+
const activeWorktree = findWorktreeByBranchName(normalized);
|
|
54
|
+
if (activeWorktree) {
|
|
55
|
+
logger.warn(`Local branch '${normalized}' is checked out in ${activeWorktree}; skipping fast-forward.`);
|
|
56
|
+
return { status: "exists", localExists };
|
|
57
|
+
}
|
|
58
|
+
logger.detail(`Fast-forwarding '${normalized}' from ${localHead} to ${remoteHead}.`);
|
|
59
|
+
logger.step(`Fast-forwarding local '${normalized}' to origin/${normalized} …`);
|
|
60
|
+
git("branch", "-f", "--", normalized, `origin/${normalized}`);
|
|
61
|
+
return { status: "exists", localExists };
|
|
62
|
+
}
|
|
63
|
+
if (ahead === 0 && behind === 0) {
|
|
64
|
+
// Defensive: rev-list should report differences when heads differ,
|
|
65
|
+
// but shallow/corrupted refs can yield 0/0 counts.
|
|
66
|
+
logger.warn(`Local branch '${normalized}' differs from origin/${normalized}, but Git reports no ahead/behind differences. Using existing local branch; if you encounter issues, try re-cloning the repository.`);
|
|
67
|
+
return { status: "exists", localExists };
|
|
68
|
+
}
|
|
69
|
+
const descriptors = [];
|
|
70
|
+
if (ahead > 0)
|
|
71
|
+
descriptors.push(`ahead by ${ahead}`);
|
|
72
|
+
if (behind > 0)
|
|
73
|
+
descriptors.push(`behind by ${behind}`);
|
|
74
|
+
const relationship = behind > 0 ? "has diverged from" : "is ahead of";
|
|
75
|
+
logger.warn(`Local branch '${normalized}' ${relationship} origin/${normalized} (${descriptors.join(" and ")}); using existing local branch as-is.`);
|
|
76
|
+
return { status: "exists", localExists };
|
|
77
|
+
}
|
|
78
|
+
let remoteExists;
|
|
79
|
+
try {
|
|
80
|
+
remoteExists = remoteBranchExists(normalized);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const diagnostic = extractDiagnosticLine(error);
|
|
84
|
+
logger.warn(`Failed to reach origin to check whether '${normalized}' exists: ${diagnostic}. (If you expected a remote branch, double-check your network connection and branch name.)`);
|
|
85
|
+
return { status: "unknown", localExists };
|
|
86
|
+
}
|
|
87
|
+
if (!remoteExists)
|
|
88
|
+
return { status: "missing", localExists };
|
|
89
|
+
try {
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
logger.step(`Would fetch origin/${normalized}`);
|
|
92
|
+
return { status: "exists", localExists };
|
|
93
|
+
}
|
|
94
|
+
logger.step(`Fetching origin/${normalized} …`);
|
|
95
|
+
fetchOriginBranch(normalized);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const diagnostic = extractDiagnosticLine(error);
|
|
99
|
+
throw new Error(`Failed to fetch origin/${normalized}: ${diagnostic}. Cannot proceed without a local branch.`);
|
|
100
|
+
}
|
|
101
|
+
return { status: "exists", localExists };
|
|
102
|
+
}
|
package/dist/git/git.d.ts
CHANGED
|
@@ -21,10 +21,16 @@ export declare function normalizeBranchName(name: string): string;
|
|
|
21
21
|
export declare function localBranchExists(branch: string): boolean;
|
|
22
22
|
/**
|
|
23
23
|
* Check if a remote branch exists on origin.
|
|
24
|
+
*
|
|
25
|
+
* Note: this function may throw on network/auth failures or when the `origin`
|
|
26
|
+
* remote is missing/misconfigured. Callers that want a degraded-mode fallback
|
|
27
|
+
* should wrap it in try/catch.
|
|
24
28
|
*/
|
|
25
29
|
export declare function remoteBranchExists(branch: string): boolean;
|
|
26
30
|
/**
|
|
27
|
-
* Fetch a branch from origin.
|
|
31
|
+
* Fetch a branch from origin into the remote-tracking ref.
|
|
32
|
+
*
|
|
33
|
+
* This does not update any local branch heads.
|
|
28
34
|
*/
|
|
29
35
|
export declare function fetchOriginBranch(branch: string): void;
|
|
30
36
|
/**
|
|
@@ -35,6 +41,10 @@ export declare function getLocalBranchHead(branch: string): string | undefined;
|
|
|
35
41
|
* Get the current head commit for a remote-tracking branch.
|
|
36
42
|
*/
|
|
37
43
|
export declare function getRemoteBranchHead(branch: string): string | undefined;
|
|
44
|
+
export declare function getAheadBehindCounts(localHead: string, remoteHead: string): {
|
|
45
|
+
ahead: number;
|
|
46
|
+
behind: number;
|
|
47
|
+
};
|
|
38
48
|
/**
|
|
39
49
|
* Convert an arbitrary string to a filesystem-safe single path segment.
|
|
40
50
|
*
|
|
@@ -50,10 +60,6 @@ export declare function fileExists(p: string): Promise<boolean>;
|
|
|
50
60
|
* Print an error message and exit the process with status 1.
|
|
51
61
|
*/
|
|
52
62
|
export declare function exitWithMessage(message: string): never;
|
|
53
|
-
/**
|
|
54
|
-
* Prompt for input and return the user's response.
|
|
55
|
-
*/
|
|
56
|
-
export declare function prompt(message: string): Promise<string>;
|
|
57
63
|
/**
|
|
58
64
|
* Prompts user for yes/no confirmation
|
|
59
65
|
* @param message The message to display to the user
|
package/dist/git/git.js
CHANGED
|
@@ -24,6 +24,8 @@ export function git(...arguments_) {
|
|
|
24
24
|
const result = spawnSync("git", gitArguments, {
|
|
25
25
|
encoding: "utf8",
|
|
26
26
|
maxBuffer: 100 * 1024 * 1024, // 100 MB to accommodate large outputs
|
|
27
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
27
29
|
...(cwd && { cwd }),
|
|
28
30
|
});
|
|
29
31
|
if (result.error)
|
|
@@ -63,30 +65,26 @@ export function localBranchExists(branch) {
|
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
67
|
* Check if a remote branch exists on origin.
|
|
68
|
+
*
|
|
69
|
+
* Note: this function may throw on network/auth failures or when the `origin`
|
|
70
|
+
* remote is missing/misconfigured. Callers that want a degraded-mode fallback
|
|
71
|
+
* should wrap it in try/catch.
|
|
66
72
|
*/
|
|
67
73
|
export function remoteBranchExists(branch) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
74
|
+
const normalized = normalizeBranchName(branch);
|
|
75
|
+
// Use a fully-qualified ref to avoid option-parsing ambiguity for branch
|
|
76
|
+
// names starting with '-'.
|
|
77
|
+
return !!git("ls-remote", "--heads", "origin", `refs/heads/${normalized}`);
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
|
-
* Fetch a branch from origin.
|
|
80
|
+
* Fetch a branch from origin into the remote-tracking ref.
|
|
81
|
+
*
|
|
82
|
+
* This does not update any local branch heads.
|
|
78
83
|
*/
|
|
79
84
|
export function fetchOriginBranch(branch) {
|
|
80
85
|
const normalized = normalizeBranchName(branch);
|
|
81
86
|
const refspec = `+refs/heads/${normalized}:refs/remotes/origin/${normalized}`;
|
|
82
|
-
|
|
83
|
-
stdio: "inherit",
|
|
84
|
-
});
|
|
85
|
-
if (result.error)
|
|
86
|
-
throw result.error;
|
|
87
|
-
if (result.status !== 0) {
|
|
88
|
-
throw new Error(`git fetch origin ${refspec} failed`);
|
|
89
|
-
}
|
|
87
|
+
git("fetch", "origin", "--", refspec);
|
|
90
88
|
}
|
|
91
89
|
/**
|
|
92
90
|
* Get the current head commit for a local branch.
|
|
@@ -112,6 +110,16 @@ export function getRemoteBranchHead(branch) {
|
|
|
112
110
|
return undefined;
|
|
113
111
|
}
|
|
114
112
|
}
|
|
113
|
+
export function getAheadBehindCounts(localHead, remoteHead) {
|
|
114
|
+
const output = git("rev-list", "--left-right", "--count", `${localHead}...${remoteHead}`);
|
|
115
|
+
const [aheadRaw, behindRaw] = output.split(/\s+/u);
|
|
116
|
+
const ahead = Number.parseInt(aheadRaw ?? "0");
|
|
117
|
+
const behind = Number.parseInt(behindRaw ?? "0");
|
|
118
|
+
return {
|
|
119
|
+
ahead: Number.isNaN(ahead) ? 0 : ahead,
|
|
120
|
+
behind: Number.isNaN(behind) ? 0 : behind,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
115
123
|
/**
|
|
116
124
|
* Convert an arbitrary string to a filesystem-safe single path segment.
|
|
117
125
|
*
|
|
@@ -149,7 +157,7 @@ export function exitWithMessage(message) {
|
|
|
149
157
|
/**
|
|
150
158
|
* Prompt for input and return the user's response.
|
|
151
159
|
*/
|
|
152
|
-
|
|
160
|
+
async function prompt(message) {
|
|
153
161
|
const rl = readline.createInterface({
|
|
154
162
|
input: process.stdin,
|
|
155
163
|
output: process.stderr,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function removeWorktree(destinationDirectory: string): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type StatusLogger = {
|
|
2
|
+
readonly step: (message: string) => void;
|
|
3
|
+
readonly success: (message: string) => void;
|
|
4
|
+
readonly detail: (message: string) => void;
|
|
5
|
+
readonly warn: (message: string) => void;
|
|
6
|
+
};
|
|
7
|
+
type StatusLoggerOptions = {
|
|
8
|
+
readonly verbose: boolean;
|
|
9
|
+
readonly dryRun: boolean;
|
|
10
|
+
readonly decorate: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function createStatusLogger(options: StatusLoggerOptions): StatusLogger;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const emit = (message) => {
|
|
2
|
+
console.error(message);
|
|
3
|
+
};
|
|
4
|
+
export function createStatusLogger(options) {
|
|
5
|
+
const shouldLogSteps = options.verbose || options.dryRun;
|
|
6
|
+
const shouldLogDetails = options.verbose || options.dryRun;
|
|
7
|
+
const stepPrefix = options.decorate ? "➤ " : "";
|
|
8
|
+
const successPrefix = options.decorate ? "✓ " : "";
|
|
9
|
+
const warningPrefix = options.decorate ? "⚠ " : "Warning: ";
|
|
10
|
+
const detailPrefix = options.decorate ? " • " : " - ";
|
|
11
|
+
const dryRunPrefix = options.dryRun ? "DRY RUN: " : "";
|
|
12
|
+
return {
|
|
13
|
+
step(message) {
|
|
14
|
+
if (!shouldLogSteps)
|
|
15
|
+
return;
|
|
16
|
+
emit(`${dryRunPrefix}${stepPrefix}${message}`);
|
|
17
|
+
},
|
|
18
|
+
success(message) {
|
|
19
|
+
if (!options.verbose)
|
|
20
|
+
return;
|
|
21
|
+
emit(`${dryRunPrefix}${successPrefix}${message}`);
|
|
22
|
+
},
|
|
23
|
+
detail(message) {
|
|
24
|
+
if (!shouldLogDetails)
|
|
25
|
+
return;
|
|
26
|
+
emit(`${dryRunPrefix}${detailPrefix}${message}`);
|
|
27
|
+
},
|
|
28
|
+
warn(message) {
|
|
29
|
+
emit(`${warningPrefix}${message}`);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Fallback logger only emits warnings so important signals aren't dropped.
|
|
2
|
+
const fallbackLogger = {
|
|
3
|
+
step() { },
|
|
4
|
+
success() { },
|
|
5
|
+
detail() { },
|
|
6
|
+
warn(message) {
|
|
7
|
+
console.error(message);
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
export function getStatusLogger(logger) {
|
|
11
|
+
return logger ?? fallbackLogger;
|
|
12
|
+
}
|
|
@@ -40,7 +40,7 @@ export async function getInstallCommand(pm, cwd) {
|
|
|
40
40
|
*/
|
|
41
41
|
export function getInstallMessage(cmd) {
|
|
42
42
|
if (cmd.command === "npm" && cmd.args[0] === "ci") {
|
|
43
|
-
return "
|
|
43
|
+
return "Running npm ci (using package-lock.json) …";
|
|
44
44
|
}
|
|
45
|
-
return
|
|
45
|
+
return `Running ${formatCommand(cmd.command, cmd.args)} …`;
|
|
46
46
|
}
|
package/dist/project/next.d.ts
CHANGED
|
@@ -3,5 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for working with Next.js projects
|
|
5
5
|
*/
|
|
6
|
+
import type { StatusLogger } from "../output/create-status-logger.js";
|
|
6
7
|
export declare const isNextProject: (directory: string) => Promise<boolean>;
|
|
7
|
-
export declare const isNextTypegenSupported: (directory: string
|
|
8
|
+
export declare const isNextTypegenSupported: (directory: string, options?: {
|
|
9
|
+
logger?: StatusLogger;
|
|
10
|
+
}) => Promise<boolean>;
|
package/dist/project/next.js
CHANGED
|
@@ -26,9 +26,9 @@ export const isNextProject = async (directory) => {
|
|
|
26
26
|
}
|
|
27
27
|
return Boolean(packageJson.dependencies?.next ?? packageJson.devDependencies?.next);
|
|
28
28
|
};
|
|
29
|
-
export const isNextTypegenSupported = async (directory) => {
|
|
29
|
+
export const isNextTypegenSupported = async (directory, options = {}) => {
|
|
30
30
|
try {
|
|
31
|
-
const result = await runPackageManagerBinary(directory, "next", ["--help"], { stdio: "pipe" });
|
|
31
|
+
const result = await runPackageManagerBinary(directory, "next", ["--help"], { stdio: "pipe", logger: options.logger });
|
|
32
32
|
if (!result) {
|
|
33
33
|
return false;
|
|
34
34
|
}
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Helpers for detecting and running package managers
|
|
5
5
|
*/
|
|
6
|
+
import type { StatusLogger } from "../output/create-status-logger.js";
|
|
6
7
|
interface RunOptions {
|
|
7
8
|
readonly stdio?: "inherit" | "pipe";
|
|
9
|
+
readonly dryRun?: boolean;
|
|
10
|
+
readonly logger?: StatusLogger;
|
|
8
11
|
}
|
|
9
12
|
interface RunBinaryResult {
|
|
10
13
|
readonly stdout: string;
|
|
@@ -13,6 +16,9 @@ interface RunBinaryResult {
|
|
|
13
16
|
/**
|
|
14
17
|
* Install dependencies for a project using the detected package manager.
|
|
15
18
|
*/
|
|
16
|
-
export declare function installDependencies(cwd: string
|
|
19
|
+
export declare function installDependencies(cwd: string, options?: {
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
logger?: StatusLogger;
|
|
22
|
+
}): Promise<void>;
|
|
17
23
|
export declare function runPackageManagerBinary(cwd: string, binary: string, arguments_?: string[], options?: RunOptions): Promise<RunBinaryResult | undefined>;
|
|
18
24
|
export {};
|
|
@@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process";
|
|
|
7
7
|
import { detect } from "package-manager-detector/detect";
|
|
8
8
|
import { formatCommand, getBinaryRunCommand, } from "./package-manager-commands.js";
|
|
9
9
|
import { getInstallCommand, getInstallMessage } from "./install-commands.js";
|
|
10
|
+
import { getStatusLogger } from "../output/get-status-logger.js";
|
|
10
11
|
const runOrThrow = (command, arguments_, cwd, options = {}) => {
|
|
11
12
|
const result = spawnSync(command, arguments_, {
|
|
12
13
|
cwd,
|
|
@@ -42,16 +43,24 @@ async function detectPackageManager(cwd) {
|
|
|
42
43
|
/**
|
|
43
44
|
* Install dependencies for a project using the detected package manager.
|
|
44
45
|
*/
|
|
45
|
-
export async function installDependencies(cwd) {
|
|
46
|
+
export async function installDependencies(cwd, options = {}) {
|
|
47
|
+
const logger = getStatusLogger(options.logger);
|
|
48
|
+
const dryRun = options.dryRun ?? false;
|
|
46
49
|
const pm = await detectPackageManager(cwd);
|
|
47
50
|
const cmd = await getInstallCommand(pm, cwd);
|
|
48
|
-
|
|
51
|
+
logger.step(getInstallMessage(cmd));
|
|
52
|
+
if (dryRun)
|
|
53
|
+
return;
|
|
49
54
|
runOrThrow(cmd.command, cmd.args, cwd);
|
|
50
55
|
}
|
|
51
56
|
export async function runPackageManagerBinary(cwd, binary, arguments_ = [], options = {}) {
|
|
57
|
+
const logger = getStatusLogger(options.logger);
|
|
58
|
+
const dryRun = options.dryRun ?? false;
|
|
52
59
|
const pm = await detectPackageManager(cwd);
|
|
53
60
|
const { command, args: commandArguments } = getBinaryRunCommand(pm, binary, arguments_);
|
|
54
|
-
|
|
61
|
+
logger.step(`Running ${formatCommand(command, commandArguments)} …`);
|
|
62
|
+
if (dryRun)
|
|
63
|
+
return undefined;
|
|
55
64
|
const result = runOrThrow(command, commandArguments, cwd, options);
|
|
56
65
|
if (options.stdio === "pipe") {
|
|
57
66
|
return {
|
package/dist/project/setup.d.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for setting up project dependencies and build artifacts
|
|
5
5
|
*/
|
|
6
|
+
import type { StatusLogger } from "../output/create-status-logger.js";
|
|
6
7
|
/**
|
|
7
8
|
* Install dependencies and run Next.js typegen if applicable
|
|
8
9
|
*/
|
|
9
|
-
export declare function setupProject(destinationDirectory: string
|
|
10
|
+
export declare function setupProject(destinationDirectory: string, options?: {
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
logger?: StatusLogger;
|
|
13
|
+
}): Promise<void>;
|
package/dist/project/setup.js
CHANGED
|
@@ -5,27 +5,35 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileExists } from "../git/git.js";
|
|
8
|
+
import { getStatusLogger } from "../output/get-status-logger.js";
|
|
8
9
|
import { installDependencies, runPackageManagerBinary, } from "./package-manager.js";
|
|
9
10
|
import { isNextProject, isNextTypegenSupported } from "./next.js";
|
|
10
11
|
/**
|
|
11
12
|
* Install dependencies and run Next.js typegen if applicable
|
|
12
13
|
*/
|
|
13
|
-
export async function setupProject(destinationDirectory) {
|
|
14
|
+
export async function setupProject(destinationDirectory, options = {}) {
|
|
15
|
+
const logger = getStatusLogger(options.logger);
|
|
16
|
+
const dryRun = options.dryRun ?? false;
|
|
14
17
|
const packageJsonPath = path.join(destinationDirectory, "package.json");
|
|
15
18
|
if (!(await fileExists(packageJsonPath))) {
|
|
16
19
|
return;
|
|
17
20
|
}
|
|
18
|
-
await installDependencies(destinationDirectory);
|
|
21
|
+
await installDependencies(destinationDirectory, { dryRun, logger });
|
|
19
22
|
// Run Next.js type generation if applicable
|
|
20
23
|
if (await isNextProject(destinationDirectory)) {
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
if (dryRun) {
|
|
25
|
+
logger.step("Would run next typegen if supported");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
logger.step("Checking Next.js CLI typegen support …");
|
|
29
|
+
const hasTypegenSupport = await isNextTypegenSupported(destinationDirectory, { logger });
|
|
23
30
|
if (hasTypegenSupport) {
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
await runPackageManagerBinary(destinationDirectory, "next", ["typegen"], {
|
|
32
|
+
logger,
|
|
33
|
+
});
|
|
26
34
|
}
|
|
27
35
|
else {
|
|
28
|
-
|
|
36
|
+
logger.warn("Skipping next typegen: installed Next.js CLI does not support this command.");
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
}
|
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for managing worktree directories
|
|
5
5
|
*/
|
|
6
|
+
import type { StatusLogger } from "../output/create-status-logger.js";
|
|
6
7
|
/**
|
|
7
|
-
* Check if directory exists and
|
|
8
|
-
* Returns true if the directory should be created
|
|
8
|
+
* Check if directory exists and handle removal if needed.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
type HandleExistingDirectoryOptions = {
|
|
11
|
+
readonly dryRun?: boolean;
|
|
12
|
+
readonly assumeYes?: boolean;
|
|
13
|
+
readonly interactive?: boolean;
|
|
14
|
+
readonly logger?: StatusLogger;
|
|
15
|
+
};
|
|
16
|
+
export declare function handleExistingDirectory(destinationDirectory: string, options?: HandleExistingDirectoryOptions): Promise<boolean>;
|
|
17
|
+
export {};
|
|
@@ -15,31 +15,109 @@
|
|
|
15
15
|
* may not be visible in a GUI. Files can be recovered by navigating to
|
|
16
16
|
* the platform-specific trash location.
|
|
17
17
|
*/
|
|
18
|
-
import trash from "trash";
|
|
19
18
|
import path from "node:path";
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*/
|
|
25
|
-
export async function handleExistingDirectory(destinationDirectory) {
|
|
19
|
+
import trash from "trash";
|
|
20
|
+
import { fileExists, confirm, exitWithMessage, git } from "../git/git.js";
|
|
21
|
+
import { getStatusLogger } from "../output/get-status-logger.js";
|
|
22
|
+
export async function handleExistingDirectory(destinationDirectory, options = {}) {
|
|
26
23
|
if (!(await fileExists(destinationDirectory))) {
|
|
27
|
-
return;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
const logger = getStatusLogger(options.logger);
|
|
27
|
+
const dryRun = options.dryRun ?? false;
|
|
28
|
+
const assumeYes = options.assumeYes ?? false;
|
|
29
|
+
const interactive = options.interactive ?? false;
|
|
30
|
+
const isTty = process.stdin.isTTY;
|
|
31
|
+
const ciValue = process.env.CI?.toLowerCase();
|
|
32
|
+
const isCi = ciValue === "true" || ciValue === "1";
|
|
33
|
+
const directoryName = path.basename(destinationDirectory);
|
|
34
|
+
if (dryRun) {
|
|
35
|
+
if (!assumeYes) {
|
|
36
|
+
// Explicit --interactive in CI without a TTY can't prompt; call it out.
|
|
37
|
+
if (isCi && interactive && !isTty) {
|
|
38
|
+
logger.warn(`Dry run: directory '${directoryName}' already exists, and CI mode is enabled. Interactive prompts are disabled in CI. Re-run with --yes to move the directory to trash, or remove it manually.`);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (!interactive) {
|
|
42
|
+
const ciHint = isCi ? " (CI is enabled)" : "";
|
|
43
|
+
logger.warn(`Dry run: directory '${directoryName}' already exists${ciHint}. Refusing to prompt in non-interactive mode. Re-run with --interactive to confirm, or --yes to move it to trash.`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (!isTty) {
|
|
47
|
+
logger.warn(`Dry run: directory '${directoryName}' already exists, but stdin is not a TTY. Re-run with --yes to move it to trash, or remove it manually.`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
logger.step(`Would prompt to move existing directory '${directoryName}' to trash`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
logger.step(`Would move existing directory '${directoryName}' to trash`);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (!assumeYes) {
|
|
57
|
+
// Explicit --interactive in CI without a TTY can't prompt; call it out.
|
|
58
|
+
if (isCi && interactive && !isTty) {
|
|
59
|
+
exitWithMessage(`Directory '${directoryName}' already exists, and CI mode is enabled.\n` +
|
|
60
|
+
"Interactive prompts are disabled in CI.\n" +
|
|
61
|
+
"Re-run with --yes to move the directory to trash, or remove it manually.");
|
|
62
|
+
}
|
|
63
|
+
if (!interactive) {
|
|
64
|
+
const ciHint = isCi ? " (CI is enabled)" : "";
|
|
65
|
+
exitWithMessage(`Directory '${directoryName}' already exists${ciHint}.\n` +
|
|
66
|
+
"Refusing to prompt in non-interactive mode.\n" +
|
|
67
|
+
"Re-run with --interactive to confirm, or --yes to move it to trash.");
|
|
68
|
+
}
|
|
69
|
+
if (!isTty) {
|
|
70
|
+
exitWithMessage(`Directory '${directoryName}' already exists, but stdin is not a TTY.\n` +
|
|
71
|
+
"Re-run with --yes to move it to trash, or remove it manually.");
|
|
72
|
+
}
|
|
73
|
+
const proceed = await confirm(`Directory '${directoryName}' already exists. Move to trash and recreate? (You can restore it from your system trash if needed)`);
|
|
74
|
+
if (!proceed) {
|
|
75
|
+
console.error("Operation cancelled.");
|
|
76
|
+
// eslint-disable-next-line unicorn/no-process-exit -- User requested cancellation
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const resolvedDestination = path.resolve(destinationDirectory);
|
|
81
|
+
let shouldPruneWorktree = false;
|
|
82
|
+
try {
|
|
83
|
+
const worktreeList = git("worktree", "list", "--porcelain");
|
|
84
|
+
const lines = worktreeList.split(/\n/u);
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (!line.startsWith("worktree "))
|
|
87
|
+
continue;
|
|
88
|
+
const worktreePath = line.replace(/^worktree\s+/u, "").trim();
|
|
89
|
+
if (path.resolve(worktreePath) === resolvedDestination) {
|
|
90
|
+
shouldPruneWorktree = true;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
28
94
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// eslint-disable-next-line unicorn/no-process-exit -- User requested cancellation
|
|
33
|
-
process.exit(0);
|
|
95
|
+
catch (error) {
|
|
96
|
+
const details = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
97
|
+
logger.detail(`Error checking for existing worktree registration: ${details}`);
|
|
34
98
|
}
|
|
35
99
|
// Move the existing directory to trash
|
|
36
|
-
|
|
100
|
+
logger.step(`Moving existing directory '${directoryName}' to trash...`);
|
|
37
101
|
try {
|
|
38
102
|
await trash(destinationDirectory);
|
|
39
|
-
|
|
103
|
+
logger.success("Directory moved to trash successfully");
|
|
40
104
|
}
|
|
41
105
|
catch (error) {
|
|
42
|
-
|
|
106
|
+
const details = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
107
|
+
logger.detail(`Error details: ${details}`);
|
|
43
108
|
exitWithMessage(`Failed to move existing directory to trash: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
109
|
}
|
|
110
|
+
if (!shouldPruneWorktree) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
logger.detail(`Pruning stale worktree registration for '${directoryName}'.`);
|
|
114
|
+
try {
|
|
115
|
+
git("worktree", "prune");
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const details = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
119
|
+
logger.detail(`Error details: ${details}`);
|
|
120
|
+
exitWithMessage(`Failed to prune stale worktree registration for '${directoryName}'. Run 'git worktree prune' and retry.`);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
45
123
|
}
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for copying untracked files to worktrees
|
|
5
5
|
*/
|
|
6
|
+
import type { StatusLogger } from "../output/create-status-logger.js";
|
|
6
7
|
/**
|
|
7
8
|
* Copy untracked files from source to destination, excluding ignored patterns
|
|
8
9
|
*/
|
|
9
|
-
export declare function copyUntrackedFiles(repoRoot: string, destinationDirectory: string
|
|
10
|
+
export declare function copyUntrackedFiles(repoRoot: string, destinationDirectory: string, options?: {
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
logger?: StatusLogger;
|
|
13
|
+
}): Promise<void>;
|