worktree-add 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/worktree-add +2 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +60 -0
- package/dist/git/git.d.ts +47 -0
- package/dist/git/git.js +129 -0
- package/dist/git/worktree-creation.d.ts +13 -0
- package/dist/git/worktree-creation.js +38 -0
- package/dist/git/worktree-discovery.d.ts +15 -0
- package/dist/git/worktree-discovery.js +48 -0
- package/dist/project/install-commands.d.ts +19 -0
- package/dist/project/install-commands.js +46 -0
- package/dist/project/next.d.ts +7 -0
- package/dist/project/next.js +42 -0
- package/dist/project/package-manager-commands.d.ts +11 -0
- package/dist/project/package-manager-commands.js +49 -0
- package/dist/project/package-manager.d.ts +18 -0
- package/dist/project/package-manager.js +63 -0
- package/dist/project/setup.d.ts +9 -0
- package/dist/project/setup.js +31 -0
- package/dist/worktree/destination-directory.d.ts +10 -0
- package/dist/worktree/destination-directory.js +33 -0
- package/dist/worktree/file-patterns.d.ts +8 -0
- package/dist/worktree/file-patterns.js +53 -0
- package/dist/worktree/untracked-file-copy.d.ts +9 -0
- package/dist/worktree/untracked-file-copy.js +35 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Łukasz Jerciński
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/worktree-add
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
/**
|
|
3
|
+
* worktree-add.ts
|
|
4
|
+
*
|
|
5
|
+
* Create or reuse a Git worktree for <branch> as a sibling of the current worktree.
|
|
6
|
+
* Note: the destination is placed next to whichever worktree you run this from,
|
|
7
|
+
* not necessarily the original "main" checkout.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
/**
|
|
3
|
+
* worktree-add.ts
|
|
4
|
+
*
|
|
5
|
+
* Create or reuse a Git worktree for <branch> as a sibling of the current worktree.
|
|
6
|
+
* Note: the destination is placed next to whichever worktree you run this from,
|
|
7
|
+
* not necessarily the original "main" checkout.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
13
|
+
import { git, getRepositoryName, normalizeBranchName, toSafePathSegment, findWorktreeByBranchName, exitWithMessage, } from "./git/git.js";
|
|
14
|
+
import { copyUntrackedFiles } from "./worktree/untracked-file-copy.js";
|
|
15
|
+
import { fetchRemoteBranch, createWorktree } from "./git/worktree-creation.js";
|
|
16
|
+
import { handleExistingDirectory } from "./worktree/destination-directory.js";
|
|
17
|
+
import { setupProject } from "./project/setup.js";
|
|
18
|
+
async function main(branchRaw) {
|
|
19
|
+
const branch = normalizeBranchName(branchRaw);
|
|
20
|
+
// Prevent attempting to add a worktree for a branch that is already checked out
|
|
21
|
+
const existingWorktree = findWorktreeByBranchName(branch);
|
|
22
|
+
if (existingWorktree) {
|
|
23
|
+
exitWithMessage(`Branch '${branch}' is already checked out in: ${existingWorktree}\n` +
|
|
24
|
+
"You cannot add another worktree for the same branch.\n" +
|
|
25
|
+
"Open that worktree instead or remove it before retrying.");
|
|
26
|
+
}
|
|
27
|
+
// Determine destination directory for the new worktree
|
|
28
|
+
const repoRoot = git("rev-parse", "--show-toplevel");
|
|
29
|
+
const repoName = getRepositoryName();
|
|
30
|
+
const branchDirectorySegment = toSafePathSegment(branch);
|
|
31
|
+
const destinationDirectory = path.join(path.dirname(repoRoot), `${repoName}-${branchDirectorySegment}`);
|
|
32
|
+
// Check if destination directory already exists
|
|
33
|
+
await handleExistingDirectory(destinationDirectory);
|
|
34
|
+
// Step 1: Fetch remote branch if it exists
|
|
35
|
+
fetchRemoteBranch(branch);
|
|
36
|
+
// Step 2: Create the worktree
|
|
37
|
+
createWorktree(branch, destinationDirectory);
|
|
38
|
+
// Step 3: Copy untracked files, excluding any on the denylist
|
|
39
|
+
await copyUntrackedFiles(repoRoot, destinationDirectory);
|
|
40
|
+
// Step 4: Install dependencies and run project-specific setup
|
|
41
|
+
await setupProject(destinationDirectory);
|
|
42
|
+
// Step 6: Open the new worktree in Cursor
|
|
43
|
+
console.log("➤ Opening Cursor …");
|
|
44
|
+
spawnSync("cursor", [destinationDirectory], { stdio: "inherit" });
|
|
45
|
+
}
|
|
46
|
+
const program = new Command()
|
|
47
|
+
.name(packageJson.name)
|
|
48
|
+
.description("Create or reuse a Git worktree for a branch as a sibling of the current worktree")
|
|
49
|
+
.version(packageJson.version)
|
|
50
|
+
.argument("<branch>", "branch name for the worktree")
|
|
51
|
+
.action(async (branch) => {
|
|
52
|
+
try {
|
|
53
|
+
await main(branch);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error(error);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
program.parse();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a git command synchronously and return its trimmed stdout.
|
|
3
|
+
* Any failure causes the script to throw.
|
|
4
|
+
* @param args Git command arguments. If the last argument is an object with a `cwd` property,
|
|
5
|
+
* it will be used as the working directory for the command.
|
|
6
|
+
*/
|
|
7
|
+
export declare function git(...arguments_: [...string[], {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
}] | string[]): string;
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a Git branch reference to a simple branch name.
|
|
12
|
+
*
|
|
13
|
+
* This helper trims surrounding whitespace and removes common prefixes so the
|
|
14
|
+
* resulting string matches the short local branch name used by refs under
|
|
15
|
+
* `refs/heads/`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeBranchName(name: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Check if a local branch exists by verifying its ref.
|
|
20
|
+
*/
|
|
21
|
+
export declare function localBranchExists(branch: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a remote branch exists on origin.
|
|
24
|
+
*/
|
|
25
|
+
export declare function remoteBranchExists(branch: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Convert an arbitrary string to a filesystem-safe single path segment.
|
|
28
|
+
*
|
|
29
|
+
* Use this helper when constructing directory names from branch names or other
|
|
30
|
+
* user-provided identifiers.
|
|
31
|
+
*/
|
|
32
|
+
export declare function toSafePathSegment(input: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Check if a file exists at the given path.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fileExists(p: string): Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Print an error message and exit the process with status 1.
|
|
39
|
+
*/
|
|
40
|
+
export declare function exitWithMessage(message: string): never;
|
|
41
|
+
/**
|
|
42
|
+
* Prompts user for yes/no confirmation
|
|
43
|
+
* @param message The message to display to the user
|
|
44
|
+
* @returns Promise<boolean> - true if user confirms, false otherwise
|
|
45
|
+
*/
|
|
46
|
+
export declare function confirm(message: string): Promise<boolean>;
|
|
47
|
+
export { getRepositoryName, findWorktreeByBranchName, } from "./worktree-discovery.js";
|
package/dist/git/git.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as readline from "node:readline/promises";
|
|
4
|
+
/**
|
|
5
|
+
* Run a git command synchronously and return its trimmed stdout.
|
|
6
|
+
* Any failure causes the script to throw.
|
|
7
|
+
* @param args Git command arguments. If the last argument is an object with a `cwd` property,
|
|
8
|
+
* it will be used as the working directory for the command.
|
|
9
|
+
*/
|
|
10
|
+
export function git(...arguments_) {
|
|
11
|
+
let cwd;
|
|
12
|
+
let gitArguments;
|
|
13
|
+
// Check if the last argument is an options object
|
|
14
|
+
const lastArgument = arguments_.at(-1);
|
|
15
|
+
if (lastArgument &&
|
|
16
|
+
typeof lastArgument === "object" &&
|
|
17
|
+
"cwd" in lastArgument) {
|
|
18
|
+
cwd = lastArgument.cwd;
|
|
19
|
+
gitArguments = arguments_.slice(0, -1);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
gitArguments = arguments_;
|
|
23
|
+
}
|
|
24
|
+
const result = spawnSync("git", gitArguments, {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
maxBuffer: 100 * 1024 * 1024, // 100 MB to accommodate large outputs
|
|
27
|
+
...(cwd && { cwd }),
|
|
28
|
+
});
|
|
29
|
+
if (result.error)
|
|
30
|
+
throw result.error;
|
|
31
|
+
if (result.status !== 0) {
|
|
32
|
+
throw new Error(result.stderr || `git ${gitArguments[0] ?? "command"} failed`);
|
|
33
|
+
}
|
|
34
|
+
return result.stdout.trim();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a Git branch reference to a simple branch name.
|
|
38
|
+
*
|
|
39
|
+
* This helper trims surrounding whitespace and removes common prefixes so the
|
|
40
|
+
* resulting string matches the short local branch name used by refs under
|
|
41
|
+
* `refs/heads/`.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeBranchName(name) {
|
|
44
|
+
const trimmed = name.trim();
|
|
45
|
+
return trimmed
|
|
46
|
+
.replace(/^refs\/heads\//u, "")
|
|
47
|
+
.replace(/^refs\/remotes\/origin\//u, "")
|
|
48
|
+
.replace(/^remotes\/origin\//u, "")
|
|
49
|
+
.replace(/^origin\//u, "");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if a local branch exists by verifying its ref.
|
|
53
|
+
*/
|
|
54
|
+
export function localBranchExists(branch) {
|
|
55
|
+
try {
|
|
56
|
+
const normalized = normalizeBranchName(branch);
|
|
57
|
+
git("show-ref", "--verify", "--quiet", `refs/heads/${normalized}`);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if a remote branch exists on origin.
|
|
66
|
+
*/
|
|
67
|
+
export function remoteBranchExists(branch) {
|
|
68
|
+
try {
|
|
69
|
+
const normalized = normalizeBranchName(branch);
|
|
70
|
+
return !!git("ls-remote", "--heads", "origin", normalized);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Convert an arbitrary string to a filesystem-safe single path segment.
|
|
78
|
+
*
|
|
79
|
+
* Use this helper when constructing directory names from branch names or other
|
|
80
|
+
* user-provided identifiers.
|
|
81
|
+
*/
|
|
82
|
+
export function toSafePathSegment(input) {
|
|
83
|
+
return input
|
|
84
|
+
.trim()
|
|
85
|
+
.replaceAll(/[\\/:*?"<>|]/gu, "-")
|
|
86
|
+
.replaceAll(/\s+/gu, "-")
|
|
87
|
+
.replaceAll(/\.+$/gu, "")
|
|
88
|
+
.replaceAll(/-+/gu, "-");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if a file exists at the given path.
|
|
92
|
+
*/
|
|
93
|
+
export async function fileExists(p) {
|
|
94
|
+
try {
|
|
95
|
+
await fs.access(p);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Print an error message and exit the process with status 1.
|
|
104
|
+
*/
|
|
105
|
+
export function exitWithMessage(message) {
|
|
106
|
+
console.error(message);
|
|
107
|
+
// eslint-disable-next-line unicorn/no-process-exit -- This is a CLI helper function
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Prompts user for yes/no confirmation
|
|
112
|
+
* @param message The message to display to the user
|
|
113
|
+
* @returns Promise<boolean> - true if user confirms, false otherwise
|
|
114
|
+
*/
|
|
115
|
+
export async function confirm(message) {
|
|
116
|
+
const rl = readline.createInterface({
|
|
117
|
+
input: process.stdin,
|
|
118
|
+
output: process.stdout,
|
|
119
|
+
});
|
|
120
|
+
try {
|
|
121
|
+
const answer = await rl.question(`${message} [y/N] `);
|
|
122
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
rl.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Re-export worktree parsing functions for backward compatibility
|
|
129
|
+
export { getRepositoryName, findWorktreeByBranchName, } from "./worktree-discovery.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-creation.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for creating Git worktrees
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Fetch a remote branch if it exists locally but not remotely
|
|
8
|
+
*/
|
|
9
|
+
export declare function fetchRemoteBranch(branch: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Create a worktree for the given branch at the specified destination
|
|
12
|
+
*/
|
|
13
|
+
export declare function createWorktree(branch: string, destinationDirectory: string): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-creation.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for creating Git worktrees
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { git, localBranchExists, remoteBranchExists } from "./git.js";
|
|
8
|
+
/**
|
|
9
|
+
* Fetch a remote branch if it exists locally but not remotely
|
|
10
|
+
*/
|
|
11
|
+
export function fetchRemoteBranch(branch) {
|
|
12
|
+
if (!localBranchExists(branch) && remoteBranchExists(branch)) {
|
|
13
|
+
console.log(`➤ Fetching origin/${branch} …`);
|
|
14
|
+
spawnSync("git", ["fetch", "origin", branch], {
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create a worktree for the given branch at the specified destination
|
|
21
|
+
*/
|
|
22
|
+
export function createWorktree(branch, destinationDirectory) {
|
|
23
|
+
if (localBranchExists(branch)) {
|
|
24
|
+
// Reuse existing local branch
|
|
25
|
+
console.log(`➤ git worktree add ${destinationDirectory} ${branch}`);
|
|
26
|
+
git("worktree", "add", destinationDirectory, branch);
|
|
27
|
+
}
|
|
28
|
+
else if (remoteBranchExists(branch)) {
|
|
29
|
+
// Create new local branch tracking the remote branch
|
|
30
|
+
console.log(`➤ git worktree add --track -b ${branch} ${destinationDirectory} origin/${branch}`);
|
|
31
|
+
git("worktree", "add", "--track", "-b", branch, destinationDirectory, `origin/${branch}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Create new branch in the worktree from current HEAD
|
|
35
|
+
console.log(`➤ git worktree add -b ${branch} ${destinationDirectory}`);
|
|
36
|
+
git("worktree", "add", "-b", branch, destinationDirectory);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-discovery.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for parsing Git worktree information
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Get the repository name from the main repository directory
|
|
8
|
+
* Works universally across all Git setups (worktrees, bare repos, no remotes, etc.)
|
|
9
|
+
*/
|
|
10
|
+
export declare function getRepositoryName(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Find the worktree path where the given branch is currently checked out.
|
|
13
|
+
* Returns the worktree path if found, otherwise undefined.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findWorktreeByBranchName(branchName: string): string | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-discovery.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for parsing Git worktree information
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { git } from "./git.js";
|
|
8
|
+
/**
|
|
9
|
+
* Get the repository name from the main repository directory
|
|
10
|
+
* Works universally across all Git setups (worktrees, bare repos, no remotes, etc.)
|
|
11
|
+
*/
|
|
12
|
+
export function getRepositoryName() {
|
|
13
|
+
// Get the main worktree path from git worktree list
|
|
14
|
+
const wtList = git("worktree", "list", "--porcelain");
|
|
15
|
+
const mainLine = wtList.split(/\n/u).find((l) => l.startsWith("worktree "));
|
|
16
|
+
if (mainLine) {
|
|
17
|
+
// Extract path from "worktree /path/to/main/repo"
|
|
18
|
+
const mainWorktreePath = mainLine.replace(/^worktree\s+/u, "").trim();
|
|
19
|
+
return path.basename(mainWorktreePath);
|
|
20
|
+
}
|
|
21
|
+
// Fallback to current directory if worktree list fails
|
|
22
|
+
const repoRoot = git("rev-parse", "--show-toplevel");
|
|
23
|
+
return path.basename(repoRoot);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Find the worktree path where the given branch is currently checked out.
|
|
27
|
+
* Returns the worktree path if found, otherwise undefined.
|
|
28
|
+
*/
|
|
29
|
+
export function findWorktreeByBranchName(branchName) {
|
|
30
|
+
const wtList = git("worktree", "list", "--porcelain");
|
|
31
|
+
const wtLines = wtList.split(/\n/u);
|
|
32
|
+
let currentWorktreePath;
|
|
33
|
+
for (const line of wtLines) {
|
|
34
|
+
if (line.startsWith("worktree ")) {
|
|
35
|
+
currentWorktreePath = line.replace(/^worktree\s+/u, "").trim();
|
|
36
|
+
}
|
|
37
|
+
else if (line.startsWith("branch ") && currentWorktreePath) {
|
|
38
|
+
const referenceOrName = line.replace(/^branch\s+/u, "").trim();
|
|
39
|
+
const shortName = referenceOrName.startsWith("refs/heads/")
|
|
40
|
+
? referenceOrName.slice("refs/heads/".length)
|
|
41
|
+
: referenceOrName;
|
|
42
|
+
if (shortName === branchName) {
|
|
43
|
+
return currentWorktreePath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-commands.ts
|
|
3
|
+
*
|
|
4
|
+
* Package manager-specific install command logic
|
|
5
|
+
*/
|
|
6
|
+
type PackageManager = "npm" | "yarn" | "pnpm" | "bun" | "deno" | undefined;
|
|
7
|
+
interface InstallCommand {
|
|
8
|
+
readonly command: string;
|
|
9
|
+
readonly args: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get the install command for the given package manager
|
|
13
|
+
*/
|
|
14
|
+
export declare function getInstallCommand(pm: PackageManager, cwd: string): Promise<InstallCommand>;
|
|
15
|
+
/**
|
|
16
|
+
* Get the display message for an install command
|
|
17
|
+
*/
|
|
18
|
+
export declare function getInstallMessage(cmd: InstallCommand): string;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-commands.ts
|
|
3
|
+
*
|
|
4
|
+
* Package manager-specific install command logic
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileExists } from "../git/git.js";
|
|
8
|
+
import { formatCommand, getYarnInstallArguments, } from "./package-manager-commands.js";
|
|
9
|
+
/**
|
|
10
|
+
* Get the install command for the given package manager
|
|
11
|
+
*/
|
|
12
|
+
export async function getInstallCommand(pm, cwd) {
|
|
13
|
+
switch (pm) {
|
|
14
|
+
case "pnpm": {
|
|
15
|
+
return { command: "pnpm", args: ["install", "--frozen-lockfile"] };
|
|
16
|
+
}
|
|
17
|
+
case "yarn": {
|
|
18
|
+
const yarnArguments = getYarnInstallArguments(cwd);
|
|
19
|
+
return { command: "yarn", args: yarnArguments };
|
|
20
|
+
}
|
|
21
|
+
case "bun": {
|
|
22
|
+
return { command: "bun", args: ["install", "--frozen-lockfile"] };
|
|
23
|
+
}
|
|
24
|
+
case "deno": {
|
|
25
|
+
return { command: "deno", args: ["install", "--frozen"] };
|
|
26
|
+
}
|
|
27
|
+
case "npm":
|
|
28
|
+
case undefined: {
|
|
29
|
+
// Use npm ci if lockfile exists, otherwise npm install
|
|
30
|
+
const hasLockfile = await fileExists(path.join(cwd, "package-lock.json"));
|
|
31
|
+
if (hasLockfile) {
|
|
32
|
+
return { command: "npm", args: ["ci"] };
|
|
33
|
+
}
|
|
34
|
+
return { command: "npm", args: ["install"] };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get the display message for an install command
|
|
40
|
+
*/
|
|
41
|
+
export function getInstallMessage(cmd) {
|
|
42
|
+
if (cmd.command === "npm" && cmd.args[0] === "ci") {
|
|
43
|
+
return "➤ Running npm ci (using package-lock.json) …";
|
|
44
|
+
}
|
|
45
|
+
return `➤ Running ${formatCommand(cmd.command, cmd.args)} …`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* next.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for working with Next.js projects
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { runPackageManagerBinary } from "./package-manager.js";
|
|
9
|
+
const loadPackageJson = async (directory) => {
|
|
10
|
+
const packageJsonPath = path.join(directory, "package.json");
|
|
11
|
+
try {
|
|
12
|
+
const content = await fs.readFile(packageJsonPath, "utf8");
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === "ENOENT") {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export const isNextProject = async (directory) => {
|
|
23
|
+
const packageJson = await loadPackageJson(directory);
|
|
24
|
+
if (!packageJson) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return Boolean(packageJson.dependencies?.next ?? packageJson.devDependencies?.next);
|
|
28
|
+
};
|
|
29
|
+
export const isNextTypegenSupported = async (directory) => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await runPackageManagerBinary(directory, "next", ["--help"], { stdio: "pipe" });
|
|
32
|
+
if (!result) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return result.stdout
|
|
36
|
+
.split("\n")
|
|
37
|
+
.some((line) => line.trimStart().startsWith("typegen "));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package-manager-commands.ts
|
|
3
|
+
*
|
|
4
|
+
* Internal utilities for package manager operations
|
|
5
|
+
*/
|
|
6
|
+
export declare const formatCommand: (command: string, arguments_: string[]) => string;
|
|
7
|
+
export declare const getYarnInstallArguments: (cwd: string) => string[];
|
|
8
|
+
export declare const getBinaryRunCommand: (pm: "npm" | "yarn" | "pnpm" | "bun" | "deno" | undefined, binary: string, arguments_: string[]) => {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package-manager-commands.ts
|
|
3
|
+
*
|
|
4
|
+
* Internal utilities for package manager operations
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
export const formatCommand = (command, arguments_) => {
|
|
8
|
+
return [command, ...arguments_].join(" ").trim();
|
|
9
|
+
};
|
|
10
|
+
const parseMajorVersion = (value) => {
|
|
11
|
+
const match = value.trim().match(/^(\d+)/u);
|
|
12
|
+
if (!match) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const major = Number.parseInt(match[1] ?? "");
|
|
16
|
+
return Number.isNaN(major) ? undefined : major;
|
|
17
|
+
};
|
|
18
|
+
export const getYarnInstallArguments = (cwd) => {
|
|
19
|
+
const result = spawnSync("yarn", ["--version"], {
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
});
|
|
23
|
+
if (!result.error && result.status === 0) {
|
|
24
|
+
const versionOutput = result.stdout;
|
|
25
|
+
const major = parseMajorVersion(versionOutput);
|
|
26
|
+
if (major !== undefined && major >= 2) {
|
|
27
|
+
return ["install", "--immutable"];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return ["install", "--frozen-lockfile"];
|
|
31
|
+
};
|
|
32
|
+
export const getBinaryRunCommand = (pm, binary, arguments_) => {
|
|
33
|
+
switch (pm) {
|
|
34
|
+
case "pnpm": {
|
|
35
|
+
return { command: "pnpm", args: ["exec", binary, ...arguments_] };
|
|
36
|
+
}
|
|
37
|
+
case "yarn": {
|
|
38
|
+
return { command: "yarn", args: [binary, ...arguments_] };
|
|
39
|
+
}
|
|
40
|
+
case "bun": {
|
|
41
|
+
return { command: "bun", args: ["x", binary, ...arguments_] };
|
|
42
|
+
}
|
|
43
|
+
case "npm":
|
|
44
|
+
case "deno":
|
|
45
|
+
case undefined: {
|
|
46
|
+
return { command: "npx", args: [binary, ...arguments_] };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package-manager.ts
|
|
3
|
+
*
|
|
4
|
+
* Helpers for detecting and running package managers
|
|
5
|
+
*/
|
|
6
|
+
interface RunOptions {
|
|
7
|
+
readonly stdio?: "inherit" | "pipe";
|
|
8
|
+
}
|
|
9
|
+
interface RunBinaryResult {
|
|
10
|
+
readonly stdout: string;
|
|
11
|
+
readonly stderr: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Install dependencies for a project using the detected package manager.
|
|
15
|
+
*/
|
|
16
|
+
export declare function installDependencies(cwd: string): Promise<void>;
|
|
17
|
+
export declare function runPackageManagerBinary(cwd: string, binary: string, arguments_?: string[], options?: RunOptions): Promise<RunBinaryResult | undefined>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package-manager.ts
|
|
3
|
+
*
|
|
4
|
+
* Helpers for detecting and running package managers
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { detect } from "package-manager-detector/detect";
|
|
8
|
+
import { formatCommand, getBinaryRunCommand, } from "./package-manager-commands.js";
|
|
9
|
+
import { getInstallCommand, getInstallMessage } from "./install-commands.js";
|
|
10
|
+
const runOrThrow = (command, arguments_, cwd, options = {}) => {
|
|
11
|
+
const result = spawnSync(command, arguments_, {
|
|
12
|
+
cwd,
|
|
13
|
+
stdio: options.stdio ?? "inherit",
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
});
|
|
16
|
+
if (result.error) {
|
|
17
|
+
throw result.error;
|
|
18
|
+
}
|
|
19
|
+
if (result.status === null) {
|
|
20
|
+
const signal = result.signal ? ` (signal: ${result.signal})` : "";
|
|
21
|
+
throw new Error(`Command "${formatCommand(command, arguments_)}" terminated unexpectedly${signal}.`);
|
|
22
|
+
}
|
|
23
|
+
if (result.status !== 0) {
|
|
24
|
+
throw new Error(`Command "${formatCommand(command, arguments_)}" exited with code ${String(result.status)}.`);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Detect the package manager for a given project directory.
|
|
30
|
+
* Checks the packageManager field in package.json first, then falls back to lockfiles.
|
|
31
|
+
*
|
|
32
|
+
* @param cwd The project directory to detect the package manager for
|
|
33
|
+
* @returns The detected package manager name, or undefined if none detected
|
|
34
|
+
*/
|
|
35
|
+
async function detectPackageManager(cwd) {
|
|
36
|
+
const result = await detect({
|
|
37
|
+
cwd,
|
|
38
|
+
strategies: ["packageManager-field", "lockfile"],
|
|
39
|
+
});
|
|
40
|
+
return result?.name;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Install dependencies for a project using the detected package manager.
|
|
44
|
+
*/
|
|
45
|
+
export async function installDependencies(cwd) {
|
|
46
|
+
const pm = await detectPackageManager(cwd);
|
|
47
|
+
const cmd = await getInstallCommand(pm, cwd);
|
|
48
|
+
console.log(getInstallMessage(cmd));
|
|
49
|
+
runOrThrow(cmd.command, cmd.args, cwd);
|
|
50
|
+
}
|
|
51
|
+
export async function runPackageManagerBinary(cwd, binary, arguments_ = [], options = {}) {
|
|
52
|
+
const pm = await detectPackageManager(cwd);
|
|
53
|
+
const { command, args: commandArguments } = getBinaryRunCommand(pm, binary, arguments_);
|
|
54
|
+
console.log(`➤ Running ${formatCommand(command, commandArguments)} …`);
|
|
55
|
+
const result = runOrThrow(command, commandArguments, cwd, options);
|
|
56
|
+
if (options.stdio === "pipe") {
|
|
57
|
+
return {
|
|
58
|
+
stdout: result.stdout,
|
|
59
|
+
stderr: result.stderr,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for setting up project dependencies and build artifacts
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileExists } from "../git/git.js";
|
|
8
|
+
import { installDependencies, runPackageManagerBinary, } from "./package-manager.js";
|
|
9
|
+
import { isNextProject, isNextTypegenSupported } from "./next.js";
|
|
10
|
+
/**
|
|
11
|
+
* Install dependencies and run Next.js typegen if applicable
|
|
12
|
+
*/
|
|
13
|
+
export async function setupProject(destinationDirectory) {
|
|
14
|
+
const packageJsonPath = path.join(destinationDirectory, "package.json");
|
|
15
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
await installDependencies(destinationDirectory);
|
|
19
|
+
// Run Next.js type generation if applicable
|
|
20
|
+
if (await isNextProject(destinationDirectory)) {
|
|
21
|
+
console.log("➤ Checking Next.js CLI typegen support …");
|
|
22
|
+
const hasTypegenSupport = await isNextTypegenSupported(destinationDirectory);
|
|
23
|
+
if (hasTypegenSupport) {
|
|
24
|
+
console.log("➤ Running next typegen …");
|
|
25
|
+
await runPackageManagerBinary(destinationDirectory, "next", ["typegen"]);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.warn("! Skipping next typegen: installed Next.js CLI does not support this command.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* destination-directory.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for managing worktree directories
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Check if directory exists and prompt for removal if needed
|
|
8
|
+
* Returns true if the directory should be created
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleExistingDirectory(destinationDirectory: string): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* destination-directory.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for managing worktree directories
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileExists, confirm, exitWithMessage } from "../git/git.js";
|
|
9
|
+
/**
|
|
10
|
+
* Check if directory exists and prompt for removal if needed
|
|
11
|
+
* Returns true if the directory should be created
|
|
12
|
+
*/
|
|
13
|
+
export async function handleExistingDirectory(destinationDirectory) {
|
|
14
|
+
if (!(await fileExists(destinationDirectory))) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const proceed = await confirm(`Directory '${path.basename(destinationDirectory)}' already exists. Remove and recreate?`);
|
|
18
|
+
if (!proceed) {
|
|
19
|
+
console.log("Operation cancelled.");
|
|
20
|
+
// eslint-disable-next-line unicorn/no-process-exit -- User requested cancellation
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
// Remove the existing directory
|
|
24
|
+
console.log(`➤ Removing existing directory...`);
|
|
25
|
+
const result = spawnSync("rm", ["-rf", destinationDirectory], {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
});
|
|
28
|
+
if (result.error)
|
|
29
|
+
throw result.error;
|
|
30
|
+
if (result.status !== 0) {
|
|
31
|
+
exitWithMessage(`Failed to remove existing directory: ${result.stderr}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-patterns.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for worktree file selection
|
|
5
|
+
*/
|
|
6
|
+
export declare const EXTRA_IGNORED_PATTERNS: string[];
|
|
7
|
+
export declare const globToRegExp: (globPattern: string) => RegExp;
|
|
8
|
+
export declare const toPosixPath: (value: string) => string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-patterns.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for worktree file selection
|
|
5
|
+
*/
|
|
6
|
+
// Patterns for untracked files that should NOT be copied to new worktrees
|
|
7
|
+
export const EXTRA_IGNORED_PATTERNS = [
|
|
8
|
+
"node_modules/**",
|
|
9
|
+
".next/**",
|
|
10
|
+
".nuxt/**",
|
|
11
|
+
".output/**",
|
|
12
|
+
"dist/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"**/*.tsbuildinfo",
|
|
15
|
+
".turbo/**",
|
|
16
|
+
".cache/**",
|
|
17
|
+
"coverage/**",
|
|
18
|
+
".venv/**",
|
|
19
|
+
"venv/**",
|
|
20
|
+
"__pycache__/**",
|
|
21
|
+
".pytest_cache/**",
|
|
22
|
+
"target/**", // Rust
|
|
23
|
+
".dart_tool/**", // Dart
|
|
24
|
+
];
|
|
25
|
+
const escapeForRegExp = (value) => value.replaceAll(/[\\^$*+?.()|[\]{}]/gu, String.raw `\$&`);
|
|
26
|
+
export const globToRegExp = (globPattern) => {
|
|
27
|
+
let regexBody = "";
|
|
28
|
+
for (let index = 0; index < globPattern.length; index += 1) {
|
|
29
|
+
const character = globPattern.charAt(index);
|
|
30
|
+
if (character === "*") {
|
|
31
|
+
const nextCharacter = globPattern.charAt(index + 1);
|
|
32
|
+
if (nextCharacter === "*") {
|
|
33
|
+
regexBody += ".*";
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
regexBody += "[^/]*";
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (character === "?") {
|
|
42
|
+
regexBody += "[^/]";
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (character === "/") {
|
|
46
|
+
regexBody += String.raw `\/`;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
regexBody += escapeForRegExp(character);
|
|
50
|
+
}
|
|
51
|
+
return new RegExp(`^${regexBody}$`, "u");
|
|
52
|
+
};
|
|
53
|
+
export const toPosixPath = (value) => value.replaceAll("\\", "/");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* untracked-file-copy.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for copying untracked files to worktrees
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Copy untracked files from source to destination, excluding ignored patterns
|
|
8
|
+
*/
|
|
9
|
+
export declare function copyUntrackedFiles(repoRoot: string, destinationDirectory: string): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* untracked-file-copy.ts
|
|
3
|
+
*
|
|
4
|
+
* Utilities for copying untracked files to worktrees
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { git } from "../git/git.js";
|
|
9
|
+
import { EXTRA_IGNORED_PATTERNS, globToRegExp, toPosixPath, } from "./file-patterns.js";
|
|
10
|
+
/**
|
|
11
|
+
* Copy untracked files from source to destination, excluding ignored patterns
|
|
12
|
+
*/
|
|
13
|
+
export async function copyUntrackedFiles(repoRoot, destinationDirectory) {
|
|
14
|
+
const extraIgnoredRegexes = EXTRA_IGNORED_PATTERNS.map((pattern) => globToRegExp(pattern));
|
|
15
|
+
const untrackedEntries = git("ls-files", "--others", // untracked files
|
|
16
|
+
"--ignored", // ignored files
|
|
17
|
+
"--exclude-standard", // respect .gitignore rules
|
|
18
|
+
"-z")
|
|
19
|
+
.split("\0")
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
for (const relativePath of untrackedEntries) {
|
|
22
|
+
const posixPath = toPosixPath(relativePath);
|
|
23
|
+
if (extraIgnoredRegexes.some((regex) => regex.test(posixPath))) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const sourcePath = path.resolve(repoRoot, relativePath);
|
|
27
|
+
const destinationPath = path.join(destinationDirectory, relativePath);
|
|
28
|
+
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
29
|
+
await fs.cp(sourcePath, destinationPath, {
|
|
30
|
+
recursive: true,
|
|
31
|
+
errorOnExist: false,
|
|
32
|
+
});
|
|
33
|
+
console.log(` • copied ${relativePath}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "worktree-add",
|
|
3
|
+
"author": "Łukasz Jerciński",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Jercik/worktree-add.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"worktree-add": "bin/worktree-add"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"dist/",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "pnpm run rebuild && node bin/worktree-add",
|
|
23
|
+
"build": "tsc -p tsconfig.app.json",
|
|
24
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
25
|
+
"rebuild": "pnpm run clean && pnpm run build",
|
|
26
|
+
"prepare": "husky",
|
|
27
|
+
"prepublishOnly": "pnpm run rebuild",
|
|
28
|
+
"typecheck": "tsc -b --noEmit",
|
|
29
|
+
"format": "prettier --write .",
|
|
30
|
+
"format:check": "prettier --check .",
|
|
31
|
+
"lint": "eslint",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"test:coverage": "vitest run --coverage",
|
|
35
|
+
"knip": "knip",
|
|
36
|
+
"fta:check": "fta-check --threshold 55"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [],
|
|
39
|
+
"packageManager": "pnpm@10.22.0",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"commander": "^14.0.2",
|
|
45
|
+
"package-manager-detector": "^1.5.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@commitlint/cli": "^20.1.0",
|
|
49
|
+
"@commitlint/config-conventional": "^20.0.0",
|
|
50
|
+
"@eslint/compat": "^1.3.2",
|
|
51
|
+
"@eslint/js": "^9.31.0",
|
|
52
|
+
"@total-typescript/ts-reset": "^0.6.1",
|
|
53
|
+
"@types/node": "^24.0.15",
|
|
54
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
55
|
+
"@vitest/eslint-plugin": "^1.3.4",
|
|
56
|
+
"eslint": "^9.31.0",
|
|
57
|
+
"eslint-config-prettier": "^10.1.8",
|
|
58
|
+
"eslint-plugin-unicorn": "^62.0.0",
|
|
59
|
+
"fta-check": "^1.1.0",
|
|
60
|
+
"fta-cli": "^3.0.0",
|
|
61
|
+
"globals": "^16.5.0",
|
|
62
|
+
"husky": "^9.1.7",
|
|
63
|
+
"knip": "^5.66.4",
|
|
64
|
+
"prettier": "3.6.2",
|
|
65
|
+
"semantic-release": "^25.0.2",
|
|
66
|
+
"typescript": "^5.8.3",
|
|
67
|
+
"typescript-eslint": "^8.37.0",
|
|
68
|
+
"vitest": "^3.2.4"
|
|
69
|
+
}
|
|
70
|
+
}
|