@fatdoge/wtree 0.1.10 → 0.2.1

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.
@@ -5,12 +5,32 @@ import inquirer from 'inquirer';
5
5
  import chalk from 'chalk';
6
6
  import { execSync } from 'node:child_process';
7
7
  import fs from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
8
9
  import { getRepoRoot } from '../core/git.js';
9
10
  import { git, gitOrThrow } from '../core/git.js';
10
11
  import { listWorktrees, parseWorktreePorcelain } from '../core/worktree.js';
11
12
  import { openPath } from '../core/open.js';
12
13
  import { readConfig, writeConfig, getConfigPaths } from '../core/config.js';
13
14
  import { startUiDevServer } from '../ui/startUiDev.js';
15
+ function getVersion() {
16
+ try {
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ // 从 api/cli/ 或 dist-node/api/cli/ 向上查找 package.json
19
+ let dir = __dirname;
20
+ for (let i = 0; i < 5; i++) {
21
+ const pkgPath = path.join(dir, 'package.json');
22
+ if (fs.existsSync(pkgPath)) {
23
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
24
+ return pkg.version || 'unknown';
25
+ }
26
+ dir = path.dirname(dir);
27
+ }
28
+ return 'unknown';
29
+ }
30
+ catch {
31
+ return 'unknown';
32
+ }
33
+ }
14
34
  function errMsg(e) {
15
35
  return e instanceof Error ? e.message : String(e);
16
36
  }
@@ -21,6 +41,15 @@ function parseArgs(argv) {
21
41
  noOpen: false,
22
42
  repo: '',
23
43
  port: undefined,
44
+ json: false,
45
+ yes: false,
46
+ force: false,
47
+ dir: '',
48
+ base: '',
49
+ editor: undefined,
50
+ noEditor: false,
51
+ noInstall: false,
52
+ version: false,
24
53
  };
25
54
  const positional = [];
26
55
  while (args.length) {
@@ -43,6 +72,42 @@ function parseArgs(argv) {
43
72
  flags.port = v;
44
73
  continue;
45
74
  }
75
+ if (a === '--json') {
76
+ flags.json = true;
77
+ continue;
78
+ }
79
+ if (a === '--yes' || a === '-y') {
80
+ flags.yes = true;
81
+ continue;
82
+ }
83
+ if (a === '--force' || a === '-f') {
84
+ flags.force = true;
85
+ continue;
86
+ }
87
+ if (a === '--dir') {
88
+ flags.dir = String(args.shift() || '');
89
+ continue;
90
+ }
91
+ if (a === '--base') {
92
+ flags.base = String(args.shift() || '');
93
+ continue;
94
+ }
95
+ if (a === '--editor') {
96
+ flags.editor = String(args.shift() || '');
97
+ continue;
98
+ }
99
+ if (a === '--no-editor') {
100
+ flags.noEditor = true;
101
+ continue;
102
+ }
103
+ if (a === '--no-install') {
104
+ flags.noInstall = true;
105
+ continue;
106
+ }
107
+ if (a === '--version' || a === '-v') {
108
+ flags.version = true;
109
+ continue;
110
+ }
46
111
  if (a.startsWith('--'))
47
112
  continue;
48
113
  positional.push(a);
@@ -80,8 +145,12 @@ function parseCommand(positional) {
80
145
  }
81
146
  return { command: 'interactive', rest: positional };
82
147
  }
83
- function printWorktreeList(rootDir) {
148
+ function printWorktreeList(rootDir, json = false) {
84
149
  const items = listWorktrees(rootDir);
150
+ if (json) {
151
+ console.info(JSON.stringify(items, null, 2));
152
+ return;
153
+ }
85
154
  if (items.length === 0) {
86
155
  console.info('未读取到 worktree。');
87
156
  return;
@@ -98,7 +167,7 @@ function printHelp() {
98
167
  console.info(' wtree');
99
168
  console.info(' wtree list');
100
169
  console.info(' wtree create [branch]');
101
- console.info(' wtree delete');
170
+ console.info(' wtree delete [branch|path ...]');
102
171
  console.info(' wtree open [path|branch]');
103
172
  console.info(' wtree lock [path|branch]');
104
173
  console.info(' wtree unlock [path|branch]');
@@ -107,8 +176,25 @@ function printHelp() {
107
176
  console.info(' wtree config get <key>');
108
177
  console.info(' wtree config set <key> <value>');
109
178
  console.info(' wtree --ui [--repo <path>] [--no-open] [--port <number>]');
179
+ console.info(' wtree --version, -v');
180
+ console.info('');
181
+ console.info('选项:');
182
+ console.info(' --json 以 JSON 格式输出 (适合脚本/agent 使用)');
183
+ console.info(' --yes, -y 自动确认所有提示');
184
+ console.info(' --force, -f 强制操作 (如强制删除有未提交更改的 worktree)');
185
+ console.info(' --dir <path> 指定 worktree 目录路径 (相对于 git 根目录)');
186
+ console.info(' --base <ref> 创建新分支时的基准引用 (如 main, origin/main)');
187
+ console.info(' --editor <name> 创建后使用指定编辑器打开 (trae, cursor, code, none)');
188
+ console.info(' --no-editor 创建后不打开编辑器');
189
+ console.info(' --no-install 创建后不自动安装依赖');
110
190
  console.info('');
111
191
  console.info('可用配置 key: baseDir, openCommand, editorCommand');
192
+ console.info('');
193
+ console.info('非交互示例:');
194
+ console.info(' wtree list --json');
195
+ console.info(' wtree create feat/x --yes --no-editor --no-install --json');
196
+ console.info(' wtree create feat/new --base main --yes --dir worktrees/feat-new --json');
197
+ console.info(' wtree delete feat/old --yes --force --json');
112
198
  }
113
199
  function resolveWorktree(rootDir, key) {
114
200
  const items = listWorktrees(rootDir);
@@ -258,6 +344,10 @@ async function pruneWorktrees(rootDir) {
258
344
  }
259
345
  async function main() {
260
346
  const { flags, positional } = parseArgs(process.argv.slice(2));
347
+ if (flags.version || positional[0] === 'version') {
348
+ console.info(getVersion());
349
+ return;
350
+ }
261
351
  const cwd = flags.repo ? path.resolve(flags.repo) : process.cwd();
262
352
  const rootDir = getRepoRoot(cwd);
263
353
  if (flags.ui) {
@@ -276,18 +366,20 @@ async function main() {
276
366
  process.on('SIGTERM', close);
277
367
  return;
278
368
  }
279
- console.info(chalk.blue(`检测到git repo根目录 ${rootDir},将在这里运行git命令`));
369
+ if (!flags.json) {
370
+ console.info(chalk.blue(`检测到git repo根目录 ${rootDir},将在这里运行git命令`));
371
+ }
280
372
  const { command, rest } = parseCommand(positional);
281
373
  if (command === 'list') {
282
- printWorktreeList(rootDir);
374
+ printWorktreeList(rootDir, flags.json);
283
375
  return;
284
376
  }
285
377
  if (command === 'create') {
286
- await createWorktree({ rootDir }, rest[0]);
378
+ await createWorktree({ rootDir, flags }, rest[0]);
287
379
  return;
288
380
  }
289
381
  if (command === 'delete') {
290
- await deleteWorktree({ rootDir });
382
+ await deleteWorktree({ rootDir, flags }, rest);
291
383
  return;
292
384
  }
293
385
  if (command === 'open') {
@@ -329,7 +421,7 @@ async function main() {
329
421
  }
330
422
  const directBranch = rest[0];
331
423
  const action = await getUserAction(directBranch);
332
- const ctx = { rootDir };
424
+ const ctx = { rootDir, flags };
333
425
  if (action === 'create') {
334
426
  await createWorktree(ctx, directBranch);
335
427
  }
@@ -374,21 +466,28 @@ async function getUserAction(directBranch) {
374
466
  return action;
375
467
  }
376
468
  async function createWorktree(ctx, directBranch) {
377
- const { rootDir } = ctx;
469
+ const { rootDir, flags } = ctx;
378
470
  const defaultBranch = git(rootDir, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']).stdout
379
471
  .replace(/^origin\//, '')
380
472
  .trim() || 'master';
381
473
  const { sourceType, selection } = await selectSource(rootDir, directBranch, defaultBranch);
382
- const { targetBranch, baseRef, isNewBranch } = await resolveBranchInfo(rootDir, sourceType, selection, directBranch, defaultBranch);
383
- const { targetDir, dirName } = await selectTargetDir(rootDir, targetBranch);
384
- console.info(chalk.green(`\n准备创建 Worktree:`));
385
- console.info(` 分支: ${targetBranch}`);
386
- console.info(` 目录: ${targetDir}`);
387
- console.info(` 来源: ${baseRef || 'Existing Local'}`);
474
+ const { targetBranch, baseRef, isNewBranch } = await resolveBranchInfo(rootDir, sourceType, selection, directBranch, defaultBranch, flags);
475
+ const { targetDir, dirName } = await selectTargetDir(rootDir, targetBranch, flags);
476
+ if (!flags.json) {
477
+ console.info(chalk.green(`\n准备创建 Worktree:`));
478
+ console.info(` 分支: ${targetBranch}`);
479
+ console.info(` 目录: ${targetDir}`);
480
+ console.info(` 来源: ${baseRef || 'Existing Local'}`);
481
+ }
388
482
  await createGitWorktree(rootDir, targetDir, targetBranch, baseRef, isNewBranch, sourceType, defaultBranch);
389
483
  await setupWorktreeEnv(rootDir, targetDir, dirName);
390
- await installDependencies(targetDir);
391
- await openInIDE(targetDir);
484
+ await installDependencies(targetDir, flags.noInstall);
485
+ await openInIDE(targetDir, flags);
486
+ if (flags.json) {
487
+ const items = listWorktrees(rootDir);
488
+ const created = items.find(x => path.resolve(x.path) === path.resolve(targetDir));
489
+ console.info(JSON.stringify({ ok: true, data: created || null }));
490
+ }
392
491
  }
393
492
  async function selectSource(rootDir, directBranch, defaultBranch) {
394
493
  let sourceType;
@@ -430,7 +529,7 @@ async function selectSource(rootDir, directBranch, defaultBranch) {
430
529
  }
431
530
  return { sourceType, selection };
432
531
  }
433
- async function resolveBranchInfo(rootDir, sourceType, selection, directBranch, defaultBranch) {
532
+ async function resolveBranchInfo(rootDir, sourceType, selection, directBranch, defaultBranch, flags) {
434
533
  let targetBranch = '';
435
534
  let baseRef = '';
436
535
  let isNewBranch = false;
@@ -470,20 +569,26 @@ async function resolveBranchInfo(rootDir, sourceType, selection, directBranch, d
470
569
  isNewBranch = true;
471
570
  }
472
571
  else {
473
- const { createNew } = await inquirer.prompt([
474
- {
475
- type: 'confirm',
476
- name: 'createNew',
477
- message: `分支 ${targetBranch} 不存在。是否基于 ${defaultBranch} 创建新分支?`,
478
- default: true,
479
- },
480
- ]);
481
- if (createNew) {
482
- baseRef = defaultBranch;
572
+ if (flags.yes) {
573
+ baseRef = flags.base || defaultBranch;
483
574
  isNewBranch = true;
484
575
  }
485
576
  else {
486
- process.exit(1);
577
+ const { createNew } = await inquirer.prompt([
578
+ {
579
+ type: 'confirm',
580
+ name: 'createNew',
581
+ message: `分支 ${targetBranch} 不存在。是否基于 ${defaultBranch} 创建新分支?`,
582
+ default: true,
583
+ },
584
+ ]);
585
+ if (createNew) {
586
+ baseRef = defaultBranch;
587
+ isNewBranch = true;
588
+ }
589
+ else {
590
+ process.exit(1);
591
+ }
487
592
  }
488
593
  }
489
594
  }
@@ -539,16 +644,26 @@ async function resolveBranchInfo(rootDir, sourceType, selection, directBranch, d
539
644
  }
540
645
  return { targetBranch, baseRef, isNewBranch };
541
646
  }
542
- async function selectTargetDir(rootDir, targetBranch) {
647
+ async function selectTargetDir(rootDir, targetBranch, flags) {
543
648
  const defaultDirName = `worktrees/${targetBranch.split('/').join('-')}`;
544
- const { dirName } = await inquirer.prompt([
545
- {
546
- type: 'input',
547
- name: 'dirName',
548
- message: `请输入 Worktree 目录路径 (相对于 Git 根目录, 默认: ${defaultDirName}):`,
549
- default: defaultDirName,
550
- },
551
- ]);
649
+ let dirName;
650
+ if (flags.dir) {
651
+ dirName = flags.dir;
652
+ }
653
+ else if (flags.yes) {
654
+ dirName = defaultDirName;
655
+ }
656
+ else {
657
+ const result = await inquirer.prompt([
658
+ {
659
+ type: 'input',
660
+ name: 'dirName',
661
+ message: `请输入 Worktree 目录路径 (相对于 Git 根目录, 默认: ${defaultDirName}):`,
662
+ default: defaultDirName,
663
+ },
664
+ ]);
665
+ dirName = result.dirName;
666
+ }
552
667
  const targetDir = path.resolve(rootDir, dirName);
553
668
  if (fs.existsSync(targetDir)) {
554
669
  console.error(chalk.red(`目录 ${targetDir} 已存在!`));
@@ -594,7 +709,9 @@ async function setupWorktreeEnv(rootDir, targetDir, dirName) {
594
709
  }
595
710
  }
596
711
  }
597
- async function installDependencies(targetDir) {
712
+ async function installDependencies(targetDir, skip = false) {
713
+ if (skip)
714
+ return;
598
715
  if (!fs.existsSync(path.join(targetDir, 'package.json')))
599
716
  return;
600
717
  try {
@@ -615,7 +732,20 @@ function hasCommand(cmd) {
615
732
  return false;
616
733
  }
617
734
  }
618
- async function openInIDE(targetDir) {
735
+ async function openInIDE(targetDir, flags) {
736
+ if (flags.noEditor)
737
+ return;
738
+ if (flags.editor !== undefined) {
739
+ if (flags.editor === 'none' || flags.editor === '')
740
+ return;
741
+ try {
742
+ execSync(`${flags.editor} "${targetDir}"`, { stdio: 'ignore' });
743
+ }
744
+ catch (e) {
745
+ void e;
746
+ }
747
+ return;
748
+ }
619
749
  const editors = [];
620
750
  if (hasCommand('trae'))
621
751
  editors.push({ name: `在 Trae 中打开 (trae ${targetDir})`, value: 'trae' });
@@ -644,34 +774,63 @@ async function openInIDE(targetDir) {
644
774
  void e;
645
775
  }
646
776
  }
647
- async function deleteWorktree(ctx) {
648
- const worktrees = getWorktreeList(ctx.rootDir);
649
- const choices = getDeletableWorktrees(ctx.rootDir, worktrees);
777
+ async function deleteWorktree(ctx, targets = []) {
778
+ const { rootDir, flags } = ctx;
779
+ const worktrees = getWorktreeList(rootDir);
780
+ const choices = getDeletableWorktrees(rootDir, worktrees);
650
781
  if (choices.length === 0) {
651
- console.warn(chalk.yellow('没有可删除的 Worktree (除了主 Worktree)'));
782
+ if (flags.json) {
783
+ console.info(JSON.stringify({ ok: true, data: [], message: 'No deletable worktrees' }));
784
+ }
785
+ else {
786
+ console.warn(chalk.yellow('没有可删除的 Worktree (除了主 Worktree)'));
787
+ }
652
788
  return;
653
789
  }
654
- const { targetPaths } = await inquirer.prompt([
655
- {
656
- type: 'checkbox',
657
- name: 'targetPaths',
658
- message: '请选择要删除的 Worktree:',
659
- choices,
660
- validate: (answer) => (answer.length > 0 ? true : '请至少选择一个'),
661
- },
662
- ]);
663
- const { confirmDelete } = await inquirer.prompt([
664
- {
665
- type: 'confirm',
666
- name: 'confirmDelete',
667
- message: `确定要删除这 ${targetPaths.length} 个 Worktree 吗?`,
668
- default: false,
669
- },
670
- ]);
671
- if (!confirmDelete)
672
- return;
790
+ let targetPaths;
791
+ if (targets.length > 0) {
792
+ // Non-interactive: resolve each target to a worktree path
793
+ targetPaths = [];
794
+ for (const key of targets) {
795
+ const wt = resolveWorktree(rootDir, key);
796
+ if (!wt) {
797
+ console.error(chalk.red(`未找到 worktree: ${key}`));
798
+ process.exit(1);
799
+ }
800
+ if (path.resolve(wt.path) === path.resolve(rootDir)) {
801
+ console.error(chalk.red(`不能删除主 worktree: ${key}`));
802
+ process.exit(1);
803
+ }
804
+ targetPaths.push(wt.path);
805
+ }
806
+ }
807
+ else {
808
+ // Interactive: checkbox prompt
809
+ const result = await inquirer.prompt([
810
+ {
811
+ type: 'checkbox',
812
+ name: 'targetPaths',
813
+ message: '请选择要删除的 Worktree:',
814
+ choices,
815
+ validate: (answer) => (answer.length > 0 ? true : '请至少选择一个'),
816
+ },
817
+ ]);
818
+ targetPaths = result.targetPaths;
819
+ }
820
+ if (!flags.yes) {
821
+ const { confirmDelete } = await inquirer.prompt([
822
+ {
823
+ type: 'confirm',
824
+ name: 'confirmDelete',
825
+ message: `确定要删除这 ${targetPaths.length} 个 Worktree 吗?`,
826
+ default: false,
827
+ },
828
+ ]);
829
+ if (!confirmDelete)
830
+ return;
831
+ }
673
832
  for (const targetPath of targetPaths) {
674
- await deleteSingleWorktree(ctx.rootDir, targetPath);
833
+ await deleteSingleWorktree(rootDir, targetPath, flags);
675
834
  }
676
835
  }
677
836
  function getWorktreeList(rootDir) {
@@ -690,29 +849,55 @@ function getDeletableWorktrees(rootDir, worktrees) {
690
849
  return { name: `${wt.branch || 'HEAD'} (${relativePath})`, value: wt.path };
691
850
  });
692
851
  }
693
- async function deleteSingleWorktree(rootDir, targetPath) {
852
+ async function deleteSingleWorktree(rootDir, targetPath, flags) {
694
853
  try {
695
854
  gitOrThrow(rootDir, ['worktree', 'remove', targetPath], 'WORKTREE_REMOVE');
696
- console.info(chalk.green(`成功删除: ${targetPath}`));
855
+ if (flags.json) {
856
+ console.info(JSON.stringify({ ok: true, removed: targetPath }));
857
+ }
858
+ else {
859
+ console.info(chalk.green(`成功删除: ${targetPath}`));
860
+ }
697
861
  }
698
862
  catch (e) {
699
- console.error(chalk.red(`删除失败: ${errMsg(e)}`));
700
- const { force } = await inquirer.prompt([
701
- {
702
- type: 'confirm',
703
- name: 'force',
704
- message: '删除失败 (可能有未提交的更改). 强制删除吗?',
705
- default: false,
706
- },
707
- ]);
708
- if (!force)
709
- return;
710
- try {
711
- gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE');
712
- console.info(chalk.green(`成功强制删除: ${targetPath}`));
863
+ if (flags.force) {
864
+ try {
865
+ gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE');
866
+ if (flags.json) {
867
+ console.info(JSON.stringify({ ok: true, removed: targetPath, forced: true }));
868
+ }
869
+ else {
870
+ console.info(chalk.green(`成功强制删除: ${targetPath}`));
871
+ }
872
+ }
873
+ catch (forceErr) {
874
+ if (flags.json) {
875
+ console.error(JSON.stringify({ ok: false, error: errMsg(forceErr), path: targetPath }));
876
+ }
877
+ else {
878
+ console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`));
879
+ }
880
+ }
713
881
  }
714
- catch (forceErr) {
715
- console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`));
882
+ else {
883
+ console.error(chalk.red(`删除失败: ${errMsg(e)}`));
884
+ const { force } = await inquirer.prompt([
885
+ {
886
+ type: 'confirm',
887
+ name: 'force',
888
+ message: '删除失败 (可能有未提交的更改). 强制删除吗?',
889
+ default: false,
890
+ },
891
+ ]);
892
+ if (!force)
893
+ return;
894
+ try {
895
+ gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE');
896
+ console.info(chalk.green(`成功强制删除: ${targetPath}`));
897
+ }
898
+ catch (forceErr) {
899
+ console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`));
900
+ }
716
901
  }
717
902
  }
718
903
  }
File without changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fatdoge/wtree",
3
3
  "private": false,
4
- "version": "0.1.10",
4
+ "version": "0.2.1",
5
5
  "description": "CLI + UI tool for managing git worktrees",
6
6
  "keywords": [
7
7
  "git",
@@ -30,6 +30,7 @@
30
30
  "api",
31
31
  "src",
32
32
  "shared",
33
+ "skills",
33
34
  "index.html",
34
35
  "vite.config.ts",
35
36
  "tailwind.config.js",
@@ -37,6 +38,19 @@
37
38
  "README.md",
38
39
  "LICENSE"
39
40
  ],
41
+ "scripts": {
42
+ "client:dev": "vite",
43
+ "build:ui": "vite build",
44
+ "build:cli": "tsc -p tsconfig.node.json",
45
+ "build": "pnpm run build:cli && pnpm run build:ui",
46
+ "lint": "eslint .",
47
+ "preview": "vite preview",
48
+ "check": "tsc --noEmit && tsc -p tsconfig.node.json --noEmit",
49
+ "server:dev": "nodemon",
50
+ "dev": "concurrently \"pnpm run client:dev\" \"pnpm run server:dev\"",
51
+ "wtree": "tsx api/cli/wtree.ts",
52
+ "test": "vitest run"
53
+ },
40
54
  "dependencies": {
41
55
  "@vitejs/plugin-react": "^4.4.1",
42
56
  "autoprefixer": "^10.4.21",
@@ -82,18 +96,5 @@
82
96
  "typescript": "~5.8.3",
83
97
  "typescript-eslint": "^8.30.1",
84
98
  "vitest": "^2.1.9"
85
- },
86
- "scripts": {
87
- "client:dev": "vite",
88
- "build:ui": "vite build",
89
- "build:cli": "tsc -p tsconfig.node.json",
90
- "build": "pnpm run build:cli && pnpm run build:ui",
91
- "lint": "eslint .",
92
- "preview": "vite preview",
93
- "check": "tsc --noEmit && tsc -p tsconfig.node.json --noEmit",
94
- "server:dev": "nodemon",
95
- "dev": "concurrently \"pnpm run client:dev\" \"pnpm run server:dev\"",
96
- "wtree": "tsx api/cli/wtree.ts",
97
- "test": "vitest run"
98
99
  }
99
- }
100
+ }