@mbluemer_2/gittyup 0.1.2

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.
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BARE_REPO_DIRNAME = void 0;
7
+ exports.discoverProjects = discoverProjects;
8
+ exports.diagnoseProjects = diagnoseProjects;
9
+ exports.createProject = createProject;
10
+ exports.cloneProject = cloneProject;
11
+ exports.importProject = importProject;
12
+ exports.renameProject = renameProject;
13
+ exports.listProjectWorktrees = listProjectWorktrees;
14
+ exports.addProjectWorktree = addProjectWorktree;
15
+ exports.getProjectInit = getProjectInit;
16
+ exports.configureProjectInit = configureProjectInit;
17
+ exports.clearConfiguredProjectInit = clearConfiguredProjectInit;
18
+ exports.getProjectWorktree = getProjectWorktree;
19
+ exports.removeProjectWorktree = removeProjectWorktree;
20
+ exports.getProjectStatuses = getProjectStatuses;
21
+ const promises_1 = require("node:fs/promises");
22
+ const node_path_1 = __importDefault(require("node:path"));
23
+ const config_1 = require("./config");
24
+ Object.defineProperty(exports, "BARE_REPO_DIRNAME", { enumerable: true, get: function () { return config_1.BARE_REPO_DIRNAME; } });
25
+ const errors_1 = require("./errors");
26
+ const git_1 = require("./git");
27
+ const metadata_1 = require("./metadata");
28
+ const process_1 = require("./process");
29
+ const tmux_1 = require("./tmux");
30
+ async function pathExists(targetPath) {
31
+ try {
32
+ await (0, promises_1.access)(targetPath);
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ async function isDirectory(targetPath) {
40
+ try {
41
+ const entries = await (0, promises_1.readdir)(targetPath);
42
+ return Array.isArray(entries);
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ async function loadProjectRecord(rootDir, projectName) {
49
+ const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
50
+ if (!(await isDirectory(project.gitDir))) {
51
+ throw new errors_1.CliError(`Managed project '${project.name}' is missing ${config_1.BARE_REPO_DIRNAME} under ${project.rootPath}.`);
52
+ }
53
+ const [worktrees, hasCommits, defaultBranch, init] = await Promise.all([
54
+ (0, git_1.listLinkedWorktrees)(project.gitDir),
55
+ (0, git_1.repoHasCommits)(project.gitDir),
56
+ (0, git_1.getDefaultBranch)(project.gitDir),
57
+ (0, metadata_1.getProjectInitRecord)(rootDir, project.name),
58
+ ]);
59
+ void init;
60
+ return {
61
+ ...project,
62
+ defaultBranch,
63
+ hasCommits,
64
+ worktreeCount: worktrees.length,
65
+ };
66
+ }
67
+ async function inspectProject(rootDir, projectName) {
68
+ try {
69
+ return await loadProjectRecord(rootDir, projectName);
70
+ }
71
+ catch (error) {
72
+ const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
73
+ return {
74
+ name: project.name,
75
+ rootPath: project.rootPath,
76
+ ok: false,
77
+ error: error instanceof Error ? error.message : String(error),
78
+ };
79
+ }
80
+ }
81
+ async function runProjectInitCommand(rootDir, project, worktree) {
82
+ const init = await (0, metadata_1.getProjectInitRecord)(rootDir, project.name);
83
+ if (!init.initCommand) {
84
+ return;
85
+ }
86
+ try {
87
+ await (0, process_1.runShellCommand)(init.initCommand, { cwd: worktree.path });
88
+ }
89
+ catch (error) {
90
+ throw new errors_1.CliError(`Worktree '${worktree.alias}' was created, but project init command failed: ${init.initCommand}\n${error instanceof Error ? error.message : String(error)}`);
91
+ }
92
+ }
93
+ function sortProjects(projects) {
94
+ return projects.sort((left, right) => left.name.localeCompare(right.name));
95
+ }
96
+ function sortWorktrees(worktrees) {
97
+ return worktrees.sort((left, right) => {
98
+ const projectCompare = left.project.localeCompare(right.project);
99
+ if (projectCompare !== 0) {
100
+ return projectCompare;
101
+ }
102
+ return left.alias.localeCompare(right.alias);
103
+ });
104
+ }
105
+ async function discoverProjects(rootDir) {
106
+ const diagnostics = await diagnoseProjects(rootDir);
107
+ return sortProjects(diagnostics.filter((entry) => 'gitDir' in entry));
108
+ }
109
+ async function diagnoseProjects(rootDir) {
110
+ if (!(await pathExists(rootDir))) {
111
+ return [];
112
+ }
113
+ const entries = await (0, promises_1.readdir)(rootDir, { withFileTypes: true });
114
+ const projectNames = entries
115
+ .filter((entry) => entry.isDirectory())
116
+ .map((entry) => entry.name);
117
+ const projects = await Promise.all(projectNames.map((projectName) => inspectProject(rootDir, projectName)));
118
+ return sortProjects(projects);
119
+ }
120
+ async function createProject(rootDir, projectName) {
121
+ const project = (0, config_1.buildProjectPaths)(rootDir, projectName);
122
+ await (0, promises_1.mkdir)(rootDir, { recursive: true });
123
+ if (await pathExists(project.rootPath)) {
124
+ throw new errors_1.CliError(`Project path already exists: ${project.rootPath}`);
125
+ }
126
+ let createdRootPath = false;
127
+ try {
128
+ await (0, promises_1.mkdir)(project.rootPath, { recursive: false });
129
+ createdRootPath = true;
130
+ await (0, git_1.initBareRepo)(project.gitDir);
131
+ await (0, git_1.bootstrapBareRepo)(project.gitDir, config_1.MAIN_WORKTREE_ALIAS);
132
+ await (0, git_1.addWorktree)(project.gitDir, node_path_1.default.join(project.rootPath, config_1.MAIN_WORKTREE_ALIAS), config_1.MAIN_WORKTREE_ALIAS);
133
+ return loadProjectRecord(rootDir, project.name);
134
+ }
135
+ catch (error) {
136
+ if (createdRootPath) {
137
+ await (0, promises_1.rm)(project.rootPath, { recursive: true, force: true });
138
+ }
139
+ throw error;
140
+ }
141
+ }
142
+ async function cloneProject(rootDir, remote, projectName) {
143
+ const validatedRemote = (0, config_1.validateRemoteUrl)(remote);
144
+ const project = (0, config_1.buildProjectPaths)(rootDir, projectName ?? (0, config_1.inferProjectName)(validatedRemote));
145
+ await (0, promises_1.mkdir)(rootDir, { recursive: true });
146
+ if (await pathExists(project.rootPath)) {
147
+ throw new errors_1.CliError(`Project path already exists: ${project.rootPath}`);
148
+ }
149
+ let createdRootPath = false;
150
+ try {
151
+ await (0, promises_1.mkdir)(project.rootPath, { recursive: false });
152
+ createdRootPath = true;
153
+ await (0, git_1.cloneBareRepo)(validatedRemote, project.gitDir);
154
+ const defaultBranch = await (0, git_1.getDefaultBranch)(project.gitDir);
155
+ if ((await (0, git_1.repoHasCommits)(project.gitDir)) && defaultBranch) {
156
+ await (0, git_1.addWorktree)(project.gitDir, node_path_1.default.join(project.rootPath, config_1.MAIN_WORKTREE_ALIAS), defaultBranch);
157
+ }
158
+ return loadProjectRecord(rootDir, project.name);
159
+ }
160
+ catch (error) {
161
+ if (createdRootPath) {
162
+ await (0, promises_1.rm)(project.rootPath, { recursive: true, force: true });
163
+ }
164
+ throw error;
165
+ }
166
+ }
167
+ async function importProject(rootDir, sourcePath, projectName) {
168
+ const resolvedSourcePath = await (0, promises_1.realpath)(node_path_1.default.resolve(sourcePath));
169
+ const sourceTopLevel = await (0, promises_1.realpath)(node_path_1.default.resolve(await (0, git_1.getRepoTopLevel)(resolvedSourcePath)));
170
+ if (resolvedSourcePath !== sourceTopLevel) {
171
+ throw new errors_1.CliError(`Source path must be the repository root: ${sourceTopLevel}`);
172
+ }
173
+ if (await (0, git_1.isBareRepository)(resolvedSourcePath)) {
174
+ throw new errors_1.CliError('Source repository must be a non-bare working repository.');
175
+ }
176
+ const project = (0, config_1.buildProjectPaths)(rootDir, projectName ?? node_path_1.default.basename(sourceTopLevel));
177
+ await (0, promises_1.mkdir)(rootDir, { recursive: true });
178
+ if (await pathExists(project.rootPath)) {
179
+ throw new errors_1.CliError(`Project path already exists: ${project.rootPath}`);
180
+ }
181
+ let createdRootPath = false;
182
+ try {
183
+ await (0, promises_1.mkdir)(project.rootPath, { recursive: false });
184
+ createdRootPath = true;
185
+ await (0, git_1.cloneBareRepo)(sourceTopLevel, project.gitDir);
186
+ const defaultBranch = await (0, git_1.getDefaultBranch)(project.gitDir);
187
+ if ((await (0, git_1.repoHasCommits)(project.gitDir)) && defaultBranch) {
188
+ await (0, git_1.addWorktree)(project.gitDir, node_path_1.default.join(project.rootPath, config_1.MAIN_WORKTREE_ALIAS), defaultBranch);
189
+ }
190
+ return loadProjectRecord(rootDir, project.name);
191
+ }
192
+ catch (error) {
193
+ if (createdRootPath) {
194
+ await (0, promises_1.rm)(project.rootPath, { recursive: true, force: true });
195
+ }
196
+ throw error;
197
+ }
198
+ }
199
+ async function renameProject(rootDir, projectName, nextProjectName) {
200
+ const currentProject = await loadProjectRecord(rootDir, projectName);
201
+ const nextProject = (0, config_1.buildProjectPaths)(rootDir, nextProjectName);
202
+ if (currentProject.name === nextProject.name) {
203
+ throw new errors_1.CliError('New project name must be different from the current name.');
204
+ }
205
+ if (await pathExists(nextProject.rootPath)) {
206
+ throw new errors_1.CliError(`Project path already exists: ${nextProject.rootPath}`);
207
+ }
208
+ const currentRootPath = await (0, promises_1.realpath)(currentProject.rootPath);
209
+ const worktrees = await (0, git_1.listLinkedWorktrees)(currentProject.gitDir);
210
+ const currentWorktreePaths = worktrees.map((worktree) => worktree.path);
211
+ await (0, promises_1.rename)(currentProject.rootPath, nextProject.rootPath);
212
+ try {
213
+ const nextRootPath = await (0, promises_1.realpath)(nextProject.rootPath);
214
+ const repairedPaths = worktrees.map((worktree) => node_path_1.default.join(nextRootPath, node_path_1.default.relative(currentRootPath, worktree.path)));
215
+ if (repairedPaths.length > 0) {
216
+ await (0, git_1.repairWorktrees)(nextProject.gitDir, repairedPaths);
217
+ }
218
+ }
219
+ catch (error) {
220
+ await (0, promises_1.rename)(nextProject.rootPath, currentProject.rootPath);
221
+ if (currentWorktreePaths.length > 0) {
222
+ await (0, git_1.repairWorktrees)(currentProject.gitDir, currentWorktreePaths);
223
+ }
224
+ throw new errors_1.CliError(`Failed to rename project '${currentProject.name}' to '${nextProject.name}'. Changes were rolled back: ${error instanceof Error ? error.message : String(error)}`);
225
+ }
226
+ return loadProjectRecord(rootDir, nextProject.name);
227
+ }
228
+ async function listProjectWorktrees(rootDir, projectName) {
229
+ const projects = projectName
230
+ ? [await loadProjectRecord(rootDir, projectName)]
231
+ : await discoverProjects(rootDir);
232
+ const worktrees = await Promise.all(projects.map(async (project) => {
233
+ const entries = await (0, git_1.listLinkedWorktrees)(project.gitDir);
234
+ return entries.map((entry) => ({
235
+ ...entry,
236
+ project: project.name,
237
+ rootPath: project.rootPath,
238
+ gitDir: project.gitDir,
239
+ }));
240
+ }));
241
+ return sortWorktrees(worktrees.flat());
242
+ }
243
+ async function addProjectWorktree(rootDir, projectName, branch, alias, from) {
244
+ const validatedBranch = (0, config_1.validateBranchName)(branch);
245
+ const project = await loadProjectRecord(rootDir, projectName);
246
+ const worktreeAlias = alias
247
+ ? (0, config_1.validateAlias)(alias)
248
+ : (0, config_1.deriveAlias)(validatedBranch);
249
+ const worktreePath = node_path_1.default.join(project.rootPath, worktreeAlias);
250
+ const existingWorktrees = await (0, git_1.listLinkedWorktrees)(project.gitDir);
251
+ if (!project.hasCommits) {
252
+ throw new errors_1.CliError(`Project '${project.name}' has no commits yet. Create the first commit before adding linked worktrees.`);
253
+ }
254
+ if (await pathExists(worktreePath)) {
255
+ throw new errors_1.CliError(`Worktree path already exists: ${worktreePath}`);
256
+ }
257
+ if (existingWorktrees.some((entry) => entry.alias === worktreeAlias)) {
258
+ throw new errors_1.CliError(`Worktree alias '${worktreeAlias}' already exists in project '${project.name}'.`);
259
+ }
260
+ await (0, git_1.addWorktree)(project.gitDir, worktreePath, validatedBranch, from);
261
+ const created = (await listProjectWorktrees(rootDir, project.name)).find((entry) => entry.alias === worktreeAlias);
262
+ if (!created) {
263
+ throw new errors_1.CliError(`Worktree '${worktreeAlias}' was created but could not be rediscovered.`);
264
+ }
265
+ await runProjectInitCommand(rootDir, project, created);
266
+ return created;
267
+ }
268
+ async function getProjectInit(rootDir, projectName) {
269
+ const project = await loadProjectRecord(rootDir, projectName);
270
+ return (0, metadata_1.getProjectInitRecord)(rootDir, project.name);
271
+ }
272
+ async function configureProjectInit(rootDir, projectName, command) {
273
+ const project = await loadProjectRecord(rootDir, projectName);
274
+ return (0, metadata_1.setProjectInitCommand)(rootDir, project.name, command);
275
+ }
276
+ async function clearConfiguredProjectInit(rootDir, projectName) {
277
+ const project = await loadProjectRecord(rootDir, projectName);
278
+ return (0, metadata_1.clearProjectInitCommand)(rootDir, project.name);
279
+ }
280
+ async function getProjectWorktree(rootDir, projectName, alias) {
281
+ const worktreeAlias = (0, config_1.validateAlias)(alias);
282
+ const worktrees = await listProjectWorktrees(rootDir, projectName);
283
+ const target = worktrees.find((entry) => entry.alias === worktreeAlias);
284
+ if (!target) {
285
+ throw new errors_1.CliError(`Worktree '${worktreeAlias}' was not found in project '${projectName}'.`);
286
+ }
287
+ return target;
288
+ }
289
+ async function removeProjectWorktree(rootDir, projectName, alias, force) {
290
+ if (alias === config_1.MAIN_WORKTREE_ALIAS) {
291
+ throw new errors_1.CliError("Cannot remove the 'main' worktree. It is the canonical linked worktree for managed projects.");
292
+ }
293
+ const project = await loadProjectRecord(rootDir, projectName);
294
+ const target = await getProjectWorktree(rootDir, project.name, alias);
295
+ await (0, git_1.removeWorktree)(project.gitDir, target.path, force);
296
+ await (0, tmux_1.killTmuxSessionIfExists)((0, tmux_1.buildTmuxSessionName)(target.project, target.alias));
297
+ return target;
298
+ }
299
+ async function getProjectStatuses(rootDir, projectName) {
300
+ const worktrees = await listProjectWorktrees(rootDir, projectName);
301
+ const statuses = await Promise.all(worktrees.map(async (worktree) => ({
302
+ ...(await (0, git_1.getWorktreeStatus)(worktree)),
303
+ project: worktree.project,
304
+ rootPath: worktree.rootPath,
305
+ gitDir: worktree.gitDir,
306
+ })));
307
+ return sortWorktrees(statuses);
308
+ }
package/dist/tmux.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTmuxSessionName = buildTmuxSessionName;
4
+ exports.isInsideTmux = isInsideTmux;
5
+ exports.checkTmuxAvailable = checkTmuxAvailable;
6
+ exports.checkFzfAvailable = checkFzfAvailable;
7
+ exports.listTmuxSessions = listTmuxSessions;
8
+ exports.tmuxSessionExists = tmuxSessionExists;
9
+ exports.createDetachedTmuxSession = createDetachedTmuxSession;
10
+ exports.attachTmuxSession = attachTmuxSession;
11
+ exports.switchTmuxSession = switchTmuxSession;
12
+ exports.launchTmuxSession = launchTmuxSession;
13
+ exports.killTmuxSessionIfExists = killTmuxSessionIfExists;
14
+ exports.pickTmuxSession = pickTmuxSession;
15
+ const errors_1 = require("./errors");
16
+ const process_1 = require("./process");
17
+ function runTmux(args, options = {}) {
18
+ return (0, process_1.runCommand)('tmux', args, options);
19
+ }
20
+ function toTmuxSessionTarget(name) {
21
+ return `=${name}`;
22
+ }
23
+ function buildTmuxSessionName(project, alias) {
24
+ return `${project}_${alias}`;
25
+ }
26
+ function isInsideTmux() {
27
+ return Boolean(process.env.TMUX);
28
+ }
29
+ async function checkTmuxAvailable() {
30
+ try {
31
+ await runTmux(['-V']);
32
+ }
33
+ catch (error) {
34
+ if (error instanceof errors_1.CliError) {
35
+ throw new errors_1.CliError('tmux is not installed or not available in PATH. Please install tmux to use tmux session management.');
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ async function checkFzfAvailable() {
41
+ try {
42
+ await (0, process_1.runCommand)('fzf', ['--version']);
43
+ }
44
+ catch (error) {
45
+ if (error instanceof errors_1.CliError) {
46
+ throw new errors_1.CliError('fzf is not installed or not available in PATH. Please install fzf to use the tmux session picker.');
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+ async function listTmuxSessions() {
52
+ try {
53
+ const output = await runTmux([
54
+ 'list-sessions',
55
+ '-F',
56
+ '#{session_name}\t#{session_attached}',
57
+ ]);
58
+ if (!output.trim()) {
59
+ return [];
60
+ }
61
+ return output
62
+ .split('\n')
63
+ .filter(Boolean)
64
+ .map((line) => {
65
+ const [name, attached] = line.split('\t');
66
+ return {
67
+ name: name ?? '',
68
+ attached: attached === '1',
69
+ };
70
+ });
71
+ }
72
+ catch (error) {
73
+ const message = error instanceof errors_1.CliError ? error.message : '';
74
+ if (message.includes('no server running')) {
75
+ return [];
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+ async function tmuxSessionExists(name) {
81
+ const sessions = await listTmuxSessions();
82
+ return sessions.some((session) => session.name === name);
83
+ }
84
+ async function createDetachedTmuxSession(name, cwd) {
85
+ await runTmux(['new-session', '-d', '-s', name, '-c', cwd]);
86
+ }
87
+ async function attachTmuxSession(name) {
88
+ await runTmux(['attach-session', '-t', toTmuxSessionTarget(name)]);
89
+ }
90
+ async function switchTmuxSession(name) {
91
+ await runTmux(['switch-client', '-t', toTmuxSessionTarget(name)]);
92
+ }
93
+ async function launchTmuxSession(name, cwd) {
94
+ const exists = await tmuxSessionExists(name);
95
+ if (isInsideTmux()) {
96
+ if (!exists) {
97
+ await createDetachedTmuxSession(name, cwd);
98
+ }
99
+ await switchTmuxSession(name);
100
+ return;
101
+ }
102
+ if (exists) {
103
+ await attachTmuxSession(name);
104
+ return;
105
+ }
106
+ await runTmux(['new-session', '-s', name, '-c', cwd]);
107
+ }
108
+ async function killTmuxSessionIfExists(name) {
109
+ try {
110
+ await runTmux(['kill-session', '-t', toTmuxSessionTarget(name)]);
111
+ return true;
112
+ }
113
+ catch (error) {
114
+ const message = error instanceof errors_1.CliError ? error.message.toLowerCase() : '';
115
+ if (message.includes('no server running') ||
116
+ message.includes('failed to connect to server') ||
117
+ message.includes("can't find session") ||
118
+ message.includes('failed to run tmux')) {
119
+ return false;
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+ async function pickTmuxSession() {
125
+ const sessions = await listTmuxSessions();
126
+ if (sessions.length === 0) {
127
+ throw new errors_1.CliError('No tmux sessions are currently open.');
128
+ }
129
+ try {
130
+ const selection = await (0, process_1.runCommand)('fzf', ['--prompt', 'tmux session> '], {
131
+ stdin: `${sessions.map((session) => session.name).join('\n')}\n`,
132
+ });
133
+ const trimmed = selection.trim();
134
+ if (!trimmed) {
135
+ throw new errors_1.CliError('No tmux session selected.');
136
+ }
137
+ return trimmed;
138
+ }
139
+ catch (error) {
140
+ const message = error instanceof errors_1.CliError ? error.message : '';
141
+ if (message.includes('exit code 130')) {
142
+ throw new errors_1.CliError('No tmux session selected.');
143
+ }
144
+ throw error;
145
+ }
146
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@mbluemer_2/gittyup",
3
+ "version": "0.1.2",
4
+ "description": "Filesystem-driven CLI for managing bare git repositories and worktrees",
5
+ "main": "dist/cli.js",
6
+ "files": [
7
+ "dist",
8
+ "README.md"
9
+ ],
10
+ "bin": {
11
+ "gu": "dist/cli.js"
12
+ },
13
+ "scripts": {
14
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
15
+ "dev": "tsx src/cli.ts",
16
+ "format": "prettier . --write",
17
+ "format:check": "prettier . --check",
18
+ "lint": "eslint .",
19
+ "lint:fix": "eslint . --fix",
20
+ "prepare": "husky",
21
+ "test": "vitest run"
22
+ },
23
+ "keywords": [
24
+ "git",
25
+ "worktree",
26
+ "cli",
27
+ "typescript"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/mbluemer/gittyup.git"
32
+ },
33
+ "homepage": "https://github.com/mbluemer/gittyup#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/mbluemer/gittyup/issues"
36
+ },
37
+ "author": "",
38
+ "license": "ISC",
39
+ "packageManager": "pnpm@10.33.0",
40
+ "dependencies": {
41
+ "cli-table3": "^0.6.5",
42
+ "commander": "^14.0.3"
43
+ },
44
+ "devDependencies": {
45
+ "@eslint/js": "^10.0.1",
46
+ "@types/node": "^25.5.2",
47
+ "eslint": "^10.2.0",
48
+ "eslint-config-prettier": "^10.1.8",
49
+ "globals": "^17.4.0",
50
+ "husky": "^9.1.7",
51
+ "lint-staged": "^16.4.0",
52
+ "prettier": "^3.8.2",
53
+ "tsx": "^4.21.0",
54
+ "typescript": "^6.0.2",
55
+ "typescript-eslint": "^8.58.1",
56
+ "vitest": "^4.1.4"
57
+ },
58
+ "lint-staged": {
59
+ "*.{js,mjs,cjs,ts}": [
60
+ "eslint --fix --max-warnings=0",
61
+ "prettier --write"
62
+ ],
63
+ "*.{json,md,yml,yaml}": [
64
+ "prettier --write"
65
+ ]
66
+ }
67
+ }