@fatdoge/wtree 0.1.1 → 0.1.3
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/api/ui/startUiDev.ts +99 -41
- package/dist-node/api/ui/startUiDev.js +84 -40
- package/package.json +3 -1
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,100 @@ 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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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'))
|
|
56
|
+
})
|
|
57
|
+
|
|
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'))
|
|
65
74
|
})
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
// Close the temporary apiServer we created earlier since we are using combinedApp now
|
|
77
|
+
apiServer.close()
|
|
78
|
+
|
|
79
|
+
// Use port 0 if uiPort is not specified or if user wants random port (handled by caller passing 0?)
|
|
80
|
+
// If uiPort is 5173 (default), we might want to try it, and failover?
|
|
81
|
+
// But standard express behavior for port 0 is random port.
|
|
82
|
+
// If user explicitly passed a port, we should use it and fail if taken.
|
|
83
|
+
// If default (5173), we should probably try it, but if taken, maybe random?
|
|
84
|
+
// Let's implement simple retry logic or just use random port if default fails?
|
|
85
|
+
// The current error EADDRINUSE suggests we are forcing 5173.
|
|
86
|
+
|
|
87
|
+
// Let's modify the logic:
|
|
88
|
+
// If user provided a specific port (not default 5173 logic), use it.
|
|
89
|
+
// If we are using default 5173, we can try it, but to be safe and avoid conflicts,
|
|
90
|
+
// maybe we should just use port 0 (random) by default unless specified?
|
|
91
|
+
// Or keep 5173 as preference but fallback to 0 if busy.
|
|
92
|
+
|
|
93
|
+
let finalPort = uiPort
|
|
94
|
+
let combinedServer: ReturnType<typeof combinedApp.listen>
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
combinedServer = await startServer(combinedApp, uiPort)
|
|
98
|
+
} catch (e: unknown) {
|
|
99
|
+
const err = e as { code?: string }
|
|
100
|
+
if (err.code === 'EADDRINUSE' && uiPort === 5173 && !options.uiPort) {
|
|
101
|
+
// Retry with random port if default port 5173 is taken and user didn't force it
|
|
102
|
+
combinedServer = await startServer(combinedApp, 0)
|
|
103
|
+
const addr = combinedServer.address()
|
|
104
|
+
if (typeof addr === 'object' && addr) {
|
|
105
|
+
finalPort = addr.port
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
throw e
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// const combinedServer = combinedApp.listen(uiPort, '127.0.0.1')
|
|
113
|
+
// await new Promise<void>((resolve) => combinedServer.once('listening', () => resolve()))
|
|
114
|
+
|
|
115
|
+
const address = combinedServer.address()
|
|
116
|
+
if (typeof address === 'object' && address) {
|
|
117
|
+
finalPort = address.port
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const url = `http://127.0.0.1:${finalPort}/`
|
|
69
121
|
|
|
70
122
|
if (open) {
|
|
71
123
|
openPath(url)
|
|
@@ -74,9 +126,15 @@ export async function startUiDevServer(options: {
|
|
|
74
126
|
return {
|
|
75
127
|
uiUrl: url,
|
|
76
128
|
close: async () => {
|
|
77
|
-
await
|
|
78
|
-
await new Promise<void>((resolve) => apiServer.close(() => resolve()))
|
|
79
|
-
process.chdir(prevCwd)
|
|
129
|
+
await new Promise<void>((resolve) => combinedServer.close(() => resolve()))
|
|
80
130
|
},
|
|
81
131
|
}
|
|
82
132
|
}
|
|
133
|
+
|
|
134
|
+
function startServer(app: express.Express, port: number): Promise<ReturnType<typeof app.listen>> {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const server = app.listen(port, '127.0.0.1')
|
|
137
|
+
server.once('listening', () => resolve(server))
|
|
138
|
+
server.once('error', (err) => reject(err))
|
|
139
|
+
})
|
|
140
|
+
}
|
|
@@ -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,97 @@ 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
|
+
// Use port 0 if uiPort is not specified or if user wants random port (handled by caller passing 0?)
|
|
56
|
+
// If uiPort is 5173 (default), we might want to try it, and failover?
|
|
57
|
+
// But standard express behavior for port 0 is random port.
|
|
58
|
+
// If user explicitly passed a port, we should use it and fail if taken.
|
|
59
|
+
// If default (5173), we should probably try it, but if taken, maybe random?
|
|
60
|
+
// Let's implement simple retry logic or just use random port if default fails?
|
|
61
|
+
// The current error EADDRINUSE suggests we are forcing 5173.
|
|
62
|
+
// Let's modify the logic:
|
|
63
|
+
// If user provided a specific port (not default 5173 logic), use it.
|
|
64
|
+
// If we are using default 5173, we can try it, but to be safe and avoid conflicts,
|
|
65
|
+
// maybe we should just use port 0 (random) by default unless specified?
|
|
66
|
+
// Or keep 5173 as preference but fallback to 0 if busy.
|
|
67
|
+
let finalPort = uiPort;
|
|
68
|
+
let combinedServer;
|
|
69
|
+
try {
|
|
70
|
+
combinedServer = await startServer(combinedApp, uiPort);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
const err = e;
|
|
74
|
+
if (err.code === 'EADDRINUSE' && uiPort === 5173 && !options.uiPort) {
|
|
75
|
+
// Retry with random port if default port 5173 is taken and user didn't force it
|
|
76
|
+
combinedServer = await startServer(combinedApp, 0);
|
|
77
|
+
const addr = combinedServer.address();
|
|
78
|
+
if (typeof addr === 'object' && addr) {
|
|
79
|
+
finalPort = addr.port;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
throw e;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// const combinedServer = combinedApp.listen(uiPort, '127.0.0.1')
|
|
87
|
+
// await new Promise<void>((resolve) => combinedServer.once('listening', () => resolve()))
|
|
88
|
+
const address = combinedServer.address();
|
|
89
|
+
if (typeof address === 'object' && address) {
|
|
90
|
+
finalPort = address.port;
|
|
91
|
+
}
|
|
92
|
+
const url = `http://127.0.0.1:${finalPort}/`;
|
|
54
93
|
if (open) {
|
|
55
94
|
openPath(url);
|
|
56
95
|
}
|
|
57
96
|
return {
|
|
58
97
|
uiUrl: url,
|
|
59
98
|
close: async () => {
|
|
60
|
-
await
|
|
61
|
-
await new Promise((resolve) => apiServer.close(() => resolve()));
|
|
62
|
-
process.chdir(prevCwd);
|
|
99
|
+
await new Promise((resolve) => combinedServer.close(() => resolve()));
|
|
63
100
|
},
|
|
64
101
|
};
|
|
65
102
|
}
|
|
103
|
+
function startServer(app, port) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const server = app.listen(port, '127.0.0.1');
|
|
106
|
+
server.once('listening', () => resolve(server));
|
|
107
|
+
server.once('error', (err) => reject(err));
|
|
108
|
+
});
|
|
109
|
+
}
|
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.3",
|
|
5
5
|
"description": "CLI + UI tool for managing git worktrees",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"git",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"react-dom": "^18.3.1",
|
|
68
68
|
"react-i18next": "^16.5.7",
|
|
69
69
|
"react-router-dom": "^7.3.0",
|
|
70
|
+
"serve-static": "^2.2.1",
|
|
70
71
|
"tailwind-merge": "^3.0.2",
|
|
71
72
|
"tailwindcss": "^3.4.17",
|
|
72
73
|
"vite": "^6.3.5",
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
"@types/node": "^22.15.30",
|
|
82
83
|
"@types/react": "^18.3.12",
|
|
83
84
|
"@types/react-dom": "^18.3.1",
|
|
85
|
+
"@types/serve-static": "^2.2.0",
|
|
84
86
|
"@vercel/node": "^5.3.6",
|
|
85
87
|
"babel-plugin-react-dev-locator": "^1.0.0",
|
|
86
88
|
"concurrently": "^9.2.0",
|