@fatdoge/wtree 0.1.0 → 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.
- package/README.en.md +5 -3
- package/README.md +10 -7
- package/api/ui/startUiDev.ts +53 -41
- package/dist-node/api/ui/startUiDev.js +42 -40
- package/package.json +11 -1
package/README.en.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wtree
|
|
2
2
|
|
|
3
|
-
English | [简体中文](
|
|
3
|
+
English | [简体中文](https://github.com/FatDoge/wtree/blob/main/README.md)
|
|
4
4
|
|
|
5
5
|
`wtree` is a local tool for managing git worktrees. It runs in an interactive command-line mode by default, and also supports a one-click local UI (TreeLab) for visual management.
|
|
6
6
|
|
|
@@ -15,14 +15,16 @@ English | [简体中文](./README.md)
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
+
Install globally via npm (specify the public registry if you are using a private one):
|
|
19
|
+
|
|
18
20
|
```bash
|
|
19
|
-
npm install -g wtree
|
|
21
|
+
npm install -g @fatdoge/wtree --registry=https://registry.npmjs.org/
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
Or run directly using `npx`:
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
npx wtree
|
|
27
|
+
npx --registry=https://registry.npmjs.org/ @fatdoge/wtree
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
## Usage
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wtree
|
|
2
2
|
|
|
3
|
-
[English](
|
|
3
|
+
[English](https://github.com/FatDoge/wtree/blob/main/README.en.md) | 简体中文
|
|
4
4
|
|
|
5
5
|
`wtree` 是一个本地工具,用于管理 git worktree。默认是交互式命令行模式,也支持一键启动 UI 页面 (TreeLab) 进行可视化操作。
|
|
6
6
|
|
|
@@ -15,16 +15,19 @@
|
|
|
15
15
|
|
|
16
16
|
## 安装
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
可以通过 npm 全局安装(如果使用了私有源,请指定官方源):
|
|
19
19
|
|
|
20
|
-
本地软链安装:
|
|
21
20
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
npm install -g @fatdoge/wtree --registry=https://registry.npmjs.org/
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
或者直接使用 `npx` 运行:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx --registry=https://registry.npmjs.org/ @fatdoge/wtree
|
|
25
28
|
```
|
|
26
29
|
|
|
27
|
-
##
|
|
30
|
+
## 运行与使用
|
|
28
31
|
|
|
29
32
|
### 交互式 CLI
|
|
30
33
|
|
package/api/ui/startUiDev.ts
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
import os from 'node:os'
|
|
2
1
|
import path from 'node:path'
|
|
3
|
-
import fs from 'node:fs'
|
|
4
2
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
import express from 'express'
|
|
4
|
+
import serveStatic from 'serve-static'
|
|
7
5
|
import { createApiApp } from '../createApiApp.js'
|
|
8
6
|
import { openPath } from '../core/open.js'
|
|
9
7
|
|
|
10
|
-
function findUiRoot(fromDir: string) {
|
|
11
|
-
let cur = fromDir
|
|
12
|
-
for (let i = 0; i < 10; i += 1) {
|
|
13
|
-
const indexHtml = path.join(cur, 'index.html')
|
|
14
|
-
const viteConfig = path.join(cur, 'vite.config.ts')
|
|
15
|
-
if (exists(indexHtml) && exists(viteConfig)) return cur
|
|
16
|
-
const parent = path.dirname(cur)
|
|
17
|
-
if (parent === cur) break
|
|
18
|
-
cur = parent
|
|
19
|
-
}
|
|
20
|
-
return fromDir
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function exists(p: string) {
|
|
24
|
-
return fs.existsSync(p)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
8
|
export type UiDevHandle = {
|
|
28
9
|
uiUrl: string
|
|
29
10
|
close: () => Promise<void>
|
|
@@ -43,29 +24,62 @@ export async function startUiDevServer(options: {
|
|
|
43
24
|
await new Promise<void>((resolve) => apiServer.once('listening', () => resolve()))
|
|
44
25
|
const apiAddress = apiServer.address()
|
|
45
26
|
const apiPort = typeof apiAddress === 'object' && apiAddress ? apiAddress.port : 0
|
|
27
|
+
|
|
28
|
+
// Set API URL for UI to consume
|
|
46
29
|
process.env.WTUI_API_URL = `http://127.0.0.1:${apiPort}`
|
|
47
|
-
process.env.VITE_WTUI_API_URL = process.env.WTUI_API_URL
|
|
48
30
|
|
|
31
|
+
// Setup static file serving for UI
|
|
49
32
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
33
|
+
// In production (dist-node/api/ui), UI assets are in ../../dist
|
|
34
|
+
const distPath = path.resolve(here, '..', '..', 'dist')
|
|
35
|
+
|
|
36
|
+
const uiApp = express()
|
|
37
|
+
|
|
38
|
+
// Proxy API requests to the API server
|
|
39
|
+
uiApp.use('/api', (req, res) => {
|
|
40
|
+
// Basic proxy implementation since we are in the same process
|
|
41
|
+
// But since we have createApiApp, we can just mount it?
|
|
42
|
+
// Actually createApiApp returns an express app, we can mount it directly.
|
|
43
|
+
// However, createApiApp includes cors and body parsers which might conflict if mounted twice?
|
|
44
|
+
// Let's just use the apiApp directly for API requests if possible,
|
|
45
|
+
// but here we are starting a separate UI server.
|
|
46
|
+
// A better approach: merge them into one server.
|
|
47
|
+
res.redirect(`http://127.0.0.1:${apiPort}/api${req.url}`)
|
|
48
|
+
})
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
port: uiPort,
|
|
61
|
-
strictPort: false,
|
|
62
|
-
},
|
|
63
|
-
clearScreen: false,
|
|
64
|
-
appType: 'spa',
|
|
50
|
+
// Serve static files
|
|
51
|
+
uiApp.use(serveStatic(distPath))
|
|
52
|
+
|
|
53
|
+
// SPA fallback
|
|
54
|
+
uiApp.get('*', (req, res) => {
|
|
55
|
+
res.sendFile(path.join(distPath, 'index.html'))
|
|
65
56
|
})
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
// Start UI server
|
|
59
|
+
// Note: We need to inject the API URL into the HTML or provide it via an endpoint
|
|
60
|
+
// The current UI implementation uses relative paths /api/..., so we need to proxy /api
|
|
61
|
+
// Let's rewrite the logic to use a single server for both API and UI
|
|
62
|
+
|
|
63
|
+
const combinedApp = express()
|
|
64
|
+
|
|
65
|
+
// 1. API routes
|
|
66
|
+
combinedApp.use('/api', createApiApp(() => repoRoot))
|
|
67
|
+
|
|
68
|
+
// 2. Static files
|
|
69
|
+
combinedApp.use(serveStatic(distPath))
|
|
70
|
+
|
|
71
|
+
// 3. SPA fallback
|
|
72
|
+
combinedApp.get('*', (req, res) => {
|
|
73
|
+
res.sendFile(path.join(distPath, 'index.html'))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Close the temporary apiServer we created earlier since we are using combinedApp now
|
|
77
|
+
apiServer.close()
|
|
78
|
+
|
|
79
|
+
const combinedServer = combinedApp.listen(uiPort, '127.0.0.1')
|
|
80
|
+
await new Promise<void>((resolve) => combinedServer.once('listening', () => resolve()))
|
|
81
|
+
|
|
82
|
+
const url = `http://127.0.0.1:${uiPort}/`
|
|
69
83
|
|
|
70
84
|
if (open) {
|
|
71
85
|
openPath(url)
|
|
@@ -74,9 +88,7 @@ export async function startUiDevServer(options: {
|
|
|
74
88
|
return {
|
|
75
89
|
uiUrl: url,
|
|
76
90
|
close: async () => {
|
|
77
|
-
await
|
|
78
|
-
await new Promise<void>((resolve) => apiServer.close(() => resolve()))
|
|
79
|
-
process.chdir(prevCwd)
|
|
91
|
+
await new Promise<void>((resolve) => combinedServer.close(() => resolve()))
|
|
80
92
|
},
|
|
81
93
|
}
|
|
82
94
|
}
|
|
@@ -1,27 +1,9 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
2
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import serveStatic from 'serve-static';
|
|
6
5
|
import { createApiApp } from '../createApiApp.js';
|
|
7
6
|
import { openPath } from '../core/open.js';
|
|
8
|
-
function findUiRoot(fromDir) {
|
|
9
|
-
let cur = fromDir;
|
|
10
|
-
for (let i = 0; i < 10; i += 1) {
|
|
11
|
-
const indexHtml = path.join(cur, 'index.html');
|
|
12
|
-
const viteConfig = path.join(cur, 'vite.config.ts');
|
|
13
|
-
if (exists(indexHtml) && exists(viteConfig))
|
|
14
|
-
return cur;
|
|
15
|
-
const parent = path.dirname(cur);
|
|
16
|
-
if (parent === cur)
|
|
17
|
-
break;
|
|
18
|
-
cur = parent;
|
|
19
|
-
}
|
|
20
|
-
return fromDir;
|
|
21
|
-
}
|
|
22
|
-
function exists(p) {
|
|
23
|
-
return fs.existsSync(p);
|
|
24
|
-
}
|
|
25
7
|
export async function startUiDevServer(options) {
|
|
26
8
|
const repoRoot = options.repoRoot;
|
|
27
9
|
const open = options.open !== false;
|
|
@@ -31,35 +13,55 @@ export async function startUiDevServer(options) {
|
|
|
31
13
|
await new Promise((resolve) => apiServer.once('listening', () => resolve()));
|
|
32
14
|
const apiAddress = apiServer.address();
|
|
33
15
|
const apiPort = typeof apiAddress === 'object' && apiAddress ? apiAddress.port : 0;
|
|
16
|
+
// Set API URL for UI to consume
|
|
34
17
|
process.env.WTUI_API_URL = `http://127.0.0.1:${apiPort}`;
|
|
35
|
-
|
|
18
|
+
// Setup static file serving for UI
|
|
36
19
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
20
|
+
// In production (dist-node/api/ui), UI assets are in ../../dist
|
|
21
|
+
const distPath = path.resolve(here, '..', '..', 'dist');
|
|
22
|
+
const uiApp = express();
|
|
23
|
+
// Proxy API requests to the API server
|
|
24
|
+
uiApp.use('/api', (req, res) => {
|
|
25
|
+
// Basic proxy implementation since we are in the same process
|
|
26
|
+
// But since we have createApiApp, we can just mount it?
|
|
27
|
+
// Actually createApiApp returns an express app, we can mount it directly.
|
|
28
|
+
// However, createApiApp includes cors and body parsers which might conflict if mounted twice?
|
|
29
|
+
// Let's just use the apiApp directly for API requests if possible,
|
|
30
|
+
// but here we are starting a separate UI server.
|
|
31
|
+
// A better approach: merge them into one server.
|
|
32
|
+
res.redirect(`http://127.0.0.1:${apiPort}/api${req.url}`);
|
|
33
|
+
});
|
|
34
|
+
// Serve static files
|
|
35
|
+
uiApp.use(serveStatic(distPath));
|
|
36
|
+
// SPA fallback
|
|
37
|
+
uiApp.get('*', (req, res) => {
|
|
38
|
+
res.sendFile(path.join(distPath, 'index.html'));
|
|
39
|
+
});
|
|
40
|
+
// Start UI server
|
|
41
|
+
// Note: We need to inject the API URL into the HTML or provide it via an endpoint
|
|
42
|
+
// The current UI implementation uses relative paths /api/..., so we need to proxy /api
|
|
43
|
+
// Let's rewrite the logic to use a single server for both API and UI
|
|
44
|
+
const combinedApp = express();
|
|
45
|
+
// 1. API routes
|
|
46
|
+
combinedApp.use('/api', createApiApp(() => repoRoot));
|
|
47
|
+
// 2. Static files
|
|
48
|
+
combinedApp.use(serveStatic(distPath));
|
|
49
|
+
// 3. SPA fallback
|
|
50
|
+
combinedApp.get('*', (req, res) => {
|
|
51
|
+
res.sendFile(path.join(distPath, 'index.html'));
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
// Close the temporary apiServer we created earlier since we are using combinedApp now
|
|
54
|
+
apiServer.close();
|
|
55
|
+
const combinedServer = combinedApp.listen(uiPort, '127.0.0.1');
|
|
56
|
+
await new Promise((resolve) => combinedServer.once('listening', () => resolve()));
|
|
57
|
+
const url = `http://127.0.0.1:${uiPort}/`;
|
|
54
58
|
if (open) {
|
|
55
59
|
openPath(url);
|
|
56
60
|
}
|
|
57
61
|
return {
|
|
58
62
|
uiUrl: url,
|
|
59
63
|
close: async () => {
|
|
60
|
-
await
|
|
61
|
-
await new Promise((resolve) => apiServer.close(() => resolve()));
|
|
62
|
-
process.chdir(prevCwd);
|
|
64
|
+
await new Promise((resolve) => combinedServer.close(() => resolve()));
|
|
63
65
|
},
|
|
64
66
|
};
|
|
65
67
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fatdoge/wtree",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "CLI + UI tool for managing git worktrees",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"git",
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
],
|
|
13
13
|
"author": "fatdoge",
|
|
14
14
|
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/FatDoge/wtree.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/FatDoge/wtree/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/FatDoge/wtree#readme",
|
|
15
23
|
"type": "module",
|
|
16
24
|
"bin": {
|
|
17
25
|
"wtree": "dist-node/api/cli/wtree.js"
|
|
@@ -59,6 +67,7 @@
|
|
|
59
67
|
"react-dom": "^18.3.1",
|
|
60
68
|
"react-i18next": "^16.5.7",
|
|
61
69
|
"react-router-dom": "^7.3.0",
|
|
70
|
+
"serve-static": "^2.2.1",
|
|
62
71
|
"tailwind-merge": "^3.0.2",
|
|
63
72
|
"tailwindcss": "^3.4.17",
|
|
64
73
|
"vite": "^6.3.5",
|
|
@@ -73,6 +82,7 @@
|
|
|
73
82
|
"@types/node": "^22.15.30",
|
|
74
83
|
"@types/react": "^18.3.12",
|
|
75
84
|
"@types/react-dom": "^18.3.1",
|
|
85
|
+
"@types/serve-static": "^2.2.0",
|
|
76
86
|
"@vercel/node": "^5.3.6",
|
|
77
87
|
"babel-plugin-react-dev-locator": "^1.0.0",
|
|
78
88
|
"concurrently": "^9.2.0",
|