@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.
@@ -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 { createServer } from 'vite'
6
- import type { ViteDevServer } from 'vite'
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
- const uiRoot = findUiRoot(path.resolve(here, '..', '..'))
51
- const prevCwd = process.cwd()
52
- process.chdir(uiRoot)
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
- const vite: ViteDevServer = await createServer({
55
- root: uiRoot,
56
- configFile: path.join(uiRoot, 'vite.config.ts'),
57
- cacheDir: path.join(os.tmpdir(), 'wtui-vite-cache'),
58
- server: {
59
- host: '127.0.0.1',
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'))
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
- await vite.listen()
68
- const url = vite.resolvedUrls?.local?.[0] || `http://127.0.0.1:${uiPort}/`
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 vite.close()
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 { createServer } from 'vite';
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
- process.env.VITE_WTUI_API_URL = process.env.WTUI_API_URL;
18
+ // Setup static file serving for UI
36
19
  const here = path.dirname(fileURLToPath(import.meta.url));
37
- const uiRoot = findUiRoot(path.resolve(here, '..', '..'));
38
- const prevCwd = process.cwd();
39
- process.chdir(uiRoot);
40
- const vite = await createServer({
41
- root: uiRoot,
42
- configFile: path.join(uiRoot, 'vite.config.ts'),
43
- cacheDir: path.join(os.tmpdir(), 'wtui-vite-cache'),
44
- server: {
45
- host: '127.0.0.1',
46
- port: uiPort,
47
- strictPort: false,
48
- },
49
- clearScreen: false,
50
- appType: 'spa',
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
- await vite.listen();
53
- const url = vite.resolvedUrls?.local?.[0] || `http://127.0.0.1:${uiPort}/`;
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 vite.close();
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.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",