@reopt-ai/dev-proxy 1.1.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.
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/README_KO.md +371 -0
- package/bin/dev-proxy.js +3 -0
- package/dist/bootstrap.js +3 -0
- package/dist/cli/config-io.js +110 -0
- package/dist/cli/output.js +37 -0
- package/dist/cli.js +78 -0
- package/dist/commands/config.js +60 -0
- package/dist/commands/doctor.js +334 -0
- package/dist/commands/help.js +7 -0
- package/dist/commands/init.js +199 -0
- package/dist/commands/project.js +69 -0
- package/dist/commands/status.js +30 -0
- package/dist/commands/version.js +10 -0
- package/dist/commands/worktree.js +292 -0
- package/dist/components/app.js +394 -0
- package/dist/components/detail-panel.js +122 -0
- package/dist/components/footer-bar.js +62 -0
- package/dist/components/request-list.js +104 -0
- package/dist/components/splash.js +32 -0
- package/dist/components/status-bar.js +19 -0
- package/dist/hooks/use-mouse.js +66 -0
- package/dist/index.js +153 -0
- package/dist/proxy/certs.js +68 -0
- package/dist/proxy/config.js +78 -0
- package/dist/proxy/routes.js +70 -0
- package/dist/proxy/server.js +403 -0
- package/dist/proxy/types.js +1 -0
- package/dist/proxy/worktrees.js +116 -0
- package/dist/store.js +567 -0
- package/dist/utils/format.js +121 -0
- package/dist/utils/list-layout.js +48 -0
- package/package.json +83 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import * as dns from "node:dns";
|
|
7
|
+
import * as net from "node:net";
|
|
8
|
+
import { Box, Text, render, useApp } from "ink";
|
|
9
|
+
import { config, CONFIG_DIR, GLOBAL_CONFIG_PATH } from "../proxy/config.js";
|
|
10
|
+
import { getEntryPorts, readProjectConfig } from "../cli/config-io.js";
|
|
11
|
+
import { Header, Check, Section } from "../cli/output.js";
|
|
12
|
+
function checkConfigSection() {
|
|
13
|
+
const results = [];
|
|
14
|
+
// config.json exists + valid JSON
|
|
15
|
+
let configExists = false;
|
|
16
|
+
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
17
|
+
try {
|
|
18
|
+
JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
19
|
+
configExists = true;
|
|
20
|
+
results.push({ ok: true, label: "config.json exists and is valid JSON" });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
results.push({ ok: false, label: "config.json exists but is not valid JSON" });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
results.push({ ok: false, label: "config.json not found" });
|
|
28
|
+
}
|
|
29
|
+
// domain is set
|
|
30
|
+
if (configExists && config.domain && config.domain !== "localhost") {
|
|
31
|
+
results.push({ ok: true, label: `domain is set: ${config.domain}` });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
results.push({
|
|
35
|
+
ok: false,
|
|
36
|
+
warn: true,
|
|
37
|
+
label: `domain: ${config.domain || "not set"}`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// projects count
|
|
41
|
+
results.push({
|
|
42
|
+
ok: config.projects.length > 0,
|
|
43
|
+
warn: config.projects.length === 0,
|
|
44
|
+
label: `${String(config.projects.length)} project(s) registered`,
|
|
45
|
+
});
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
function checkProjectsSection() {
|
|
49
|
+
const results = [];
|
|
50
|
+
for (const project of config.projects) {
|
|
51
|
+
const exists = existsSync(project.configPath);
|
|
52
|
+
results.push({
|
|
53
|
+
ok: exists,
|
|
54
|
+
label: exists
|
|
55
|
+
? `.dev-proxy.json exists: ${project.path}`
|
|
56
|
+
: `.dev-proxy.json missing: ${project.path}`,
|
|
57
|
+
});
|
|
58
|
+
if (exists) {
|
|
59
|
+
const routeCount = Object.keys(project.routes).length;
|
|
60
|
+
const worktreeCount = Object.keys(project.worktrees).length;
|
|
61
|
+
results.push({
|
|
62
|
+
ok: routeCount > 0,
|
|
63
|
+
warn: routeCount === 0,
|
|
64
|
+
label: ` ${String(routeCount)} route(s), ${String(worktreeCount)} worktree(s)`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
function checkTlsSection() {
|
|
71
|
+
const results = [];
|
|
72
|
+
// mkcert installed
|
|
73
|
+
try {
|
|
74
|
+
execFileSync("which", ["mkcert"], { stdio: "pipe" });
|
|
75
|
+
results.push({ ok: true, label: "mkcert is installed" });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
results.push({ ok: false, label: "mkcert is not installed" });
|
|
79
|
+
}
|
|
80
|
+
// cert files
|
|
81
|
+
const certsDir = resolve(CONFIG_DIR, "certs");
|
|
82
|
+
const certFile = resolve(certsDir, "cert.pem");
|
|
83
|
+
const keyFile = resolve(certsDir, "key.pem");
|
|
84
|
+
const certExists = existsSync(certFile);
|
|
85
|
+
const keyExists = existsSync(keyFile);
|
|
86
|
+
results.push({
|
|
87
|
+
ok: certExists && keyExists,
|
|
88
|
+
label: certExists && keyExists
|
|
89
|
+
? `cert files exist in ${certsDir}`
|
|
90
|
+
: `cert files missing in ${certsDir}`,
|
|
91
|
+
});
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
function collectSubdomains(projects) {
|
|
95
|
+
const subs = new Set();
|
|
96
|
+
for (const project of projects) {
|
|
97
|
+
for (const sub of Object.keys(project.routes)) {
|
|
98
|
+
if (sub !== "*") {
|
|
99
|
+
subs.add(sub);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...subs];
|
|
104
|
+
}
|
|
105
|
+
function withTimeout(promise, ms) {
|
|
106
|
+
return Promise.race([
|
|
107
|
+
promise,
|
|
108
|
+
new Promise((_, reject) => {
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
reject(new Error("timeout"));
|
|
111
|
+
}, ms);
|
|
112
|
+
}),
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
async function checkDns(subdomains, domain) {
|
|
116
|
+
const results = [];
|
|
117
|
+
for (const sub of subdomains) {
|
|
118
|
+
const hostname = `${sub}.${domain}`;
|
|
119
|
+
try {
|
|
120
|
+
const { address } = await withTimeout(dns.promises.lookup(hostname, { family: 4 }), 5000);
|
|
121
|
+
results.push({
|
|
122
|
+
ok: address === "127.0.0.1",
|
|
123
|
+
warn: address !== "127.0.0.1",
|
|
124
|
+
label: address === "127.0.0.1"
|
|
125
|
+
? `${hostname} → 127.0.0.1`
|
|
126
|
+
: `${hostname} → ${address} (expected 127.0.0.1)`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
results.push({ ok: false, label: `${hostname} does not resolve` });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
function checkPort(port) {
|
|
136
|
+
return new Promise((res) => {
|
|
137
|
+
const server = net.createServer();
|
|
138
|
+
server.once("error", () => {
|
|
139
|
+
res({ ok: false, label: `:${String(port)} is in use` });
|
|
140
|
+
});
|
|
141
|
+
server.listen(port, () => {
|
|
142
|
+
server.close(() => {
|
|
143
|
+
res({ ok: true, label: `:${String(port)} is available` });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// ── Worktree checks ──────────────────────────────────────────
|
|
149
|
+
function checkWorktreeConfig(projects) {
|
|
150
|
+
const results = [];
|
|
151
|
+
for (const project of projects) {
|
|
152
|
+
const cfg = readProjectConfig(project.path);
|
|
153
|
+
const worktrees = cfg.worktrees ?? {};
|
|
154
|
+
const wtConfig = cfg.worktreeConfig;
|
|
155
|
+
const entries = Object.entries(worktrees);
|
|
156
|
+
if (entries.length === 0)
|
|
157
|
+
continue;
|
|
158
|
+
// Port conflict check — across all worktrees in this project
|
|
159
|
+
const portMap = new Map();
|
|
160
|
+
for (const [branch, entry] of entries) {
|
|
161
|
+
for (const p of getEntryPorts(entry)) {
|
|
162
|
+
const existing = portMap.get(p) ?? [];
|
|
163
|
+
existing.push(branch);
|
|
164
|
+
portMap.set(p, existing);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const [port, branches] of portMap) {
|
|
168
|
+
if (branches.length > 1) {
|
|
169
|
+
results.push({
|
|
170
|
+
ok: false,
|
|
171
|
+
label: `port ${port} used by multiple worktrees: ${branches.join(", ")}`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if ([...portMap.values()].every((b) => b.length === 1)) {
|
|
176
|
+
results.push({ ok: true, label: `no port conflicts in ${project.path}` });
|
|
177
|
+
}
|
|
178
|
+
// worktreeConfig validation
|
|
179
|
+
if (wtConfig) {
|
|
180
|
+
const [min, max] = wtConfig.portRange;
|
|
181
|
+
if (min >= max) {
|
|
182
|
+
results.push({
|
|
183
|
+
ok: false,
|
|
184
|
+
label: `invalid portRange [${min}, ${max}] — min must be less than max`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
results.push({ ok: true, label: `portRange [${min}, ${max}] is valid` });
|
|
189
|
+
}
|
|
190
|
+
// services vs routes cross-check
|
|
191
|
+
if (wtConfig.services) {
|
|
192
|
+
const routeKeys = new Set(Object.keys(cfg.routes ?? {}));
|
|
193
|
+
for (const svc of Object.keys(wtConfig.services)) {
|
|
194
|
+
if (!routeKeys.has(svc) && svc !== "*") {
|
|
195
|
+
results.push({
|
|
196
|
+
ok: false,
|
|
197
|
+
warn: true,
|
|
198
|
+
label: `service "${svc}" not found in routes`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Per-worktree directory + env file checks
|
|
205
|
+
if (wtConfig) {
|
|
206
|
+
for (const [branch, entry] of entries) {
|
|
207
|
+
const dirPattern = wtConfig.directory.replace("{branch}", branch);
|
|
208
|
+
const worktreeDir = resolve(project.path, dirPattern);
|
|
209
|
+
// Directory exists
|
|
210
|
+
if (existsSync(worktreeDir)) {
|
|
211
|
+
results.push({ ok: true, label: `${branch}: directory exists` });
|
|
212
|
+
// .env.local exists (if services defined)
|
|
213
|
+
if (wtConfig.services) {
|
|
214
|
+
const envFile = wtConfig.envFile ?? ".env.local";
|
|
215
|
+
const envPath = resolve(worktreeDir, envFile);
|
|
216
|
+
if (existsSync(envPath)) {
|
|
217
|
+
results.push({ ok: true, label: `${branch}: ${envFile} exists` });
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
results.push({
|
|
221
|
+
ok: false,
|
|
222
|
+
warn: true,
|
|
223
|
+
label: `${branch}: ${envFile} missing — run 'dev-proxy worktree create' to regenerate`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Skip "main" — it's the project root, not a worktree directory
|
|
230
|
+
if (branch !== "main") {
|
|
231
|
+
results.push({
|
|
232
|
+
ok: false,
|
|
233
|
+
warn: true,
|
|
234
|
+
label: `${branch}: directory not found at ${worktreeDir}`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Check if worktree ports are reachable
|
|
239
|
+
// (done async below)
|
|
240
|
+
void entry; // used in async check
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
function checkWorktreePort(port, branch, service) {
|
|
247
|
+
const label = service ? `${branch}/${service} :${port}` : `${branch} :${port}`;
|
|
248
|
+
return new Promise((res) => {
|
|
249
|
+
const socket = net.createConnection({ port, host: "127.0.0.1" }, () => {
|
|
250
|
+
socket.destroy();
|
|
251
|
+
res({ ok: true, label: `${label} is responding` });
|
|
252
|
+
});
|
|
253
|
+
socket.on("error", () => {
|
|
254
|
+
socket.destroy();
|
|
255
|
+
res({ ok: false, warn: true, label: `${label} is not responding` });
|
|
256
|
+
});
|
|
257
|
+
socket.setTimeout(2000, () => {
|
|
258
|
+
socket.destroy();
|
|
259
|
+
res({ ok: false, warn: true, label: `${label} timed out` });
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
async function checkWorktreePorts(projects) {
|
|
264
|
+
const checks = [];
|
|
265
|
+
for (const project of projects) {
|
|
266
|
+
const cfg = readProjectConfig(project.path);
|
|
267
|
+
const worktrees = cfg.worktrees ?? {};
|
|
268
|
+
for (const [branch, entry] of Object.entries(worktrees)) {
|
|
269
|
+
if ("ports" in entry) {
|
|
270
|
+
for (const [svc, port] of Object.entries(entry.ports)) {
|
|
271
|
+
checks.push(checkWorktreePort(port, branch, svc));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
checks.push(checkWorktreePort(entry.port, branch));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (checks.length === 0)
|
|
280
|
+
return [];
|
|
281
|
+
return Promise.all(checks);
|
|
282
|
+
}
|
|
283
|
+
function Doctor() {
|
|
284
|
+
const { exit } = useApp();
|
|
285
|
+
const [asyncChecks, setAsyncChecks] = useState(null);
|
|
286
|
+
const configChecks = checkConfigSection();
|
|
287
|
+
const projectChecks = checkProjectsSection();
|
|
288
|
+
const tlsChecks = checkTlsSection();
|
|
289
|
+
const worktreeChecks = checkWorktreeConfig(config.projects);
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
let cancelled = false;
|
|
292
|
+
void (async () => {
|
|
293
|
+
const subdomains = collectSubdomains(config.projects);
|
|
294
|
+
const [dnsResults, httpPort, httpsPort, wtPorts] = await Promise.all([
|
|
295
|
+
checkDns(subdomains, config.domain),
|
|
296
|
+
checkPort(config.port),
|
|
297
|
+
checkPort(config.httpsPort),
|
|
298
|
+
checkWorktreePorts(config.projects),
|
|
299
|
+
]);
|
|
300
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- mutated in cleanup
|
|
301
|
+
if (!cancelled) {
|
|
302
|
+
setAsyncChecks({
|
|
303
|
+
dns: dnsResults,
|
|
304
|
+
ports: [httpPort, httpsPort],
|
|
305
|
+
worktreePorts: wtPorts,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
return () => {
|
|
310
|
+
cancelled = true;
|
|
311
|
+
};
|
|
312
|
+
}, []);
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (asyncChecks) {
|
|
315
|
+
setTimeout(exit, 0);
|
|
316
|
+
}
|
|
317
|
+
}, [asyncChecks, exit]);
|
|
318
|
+
const allChecks = [
|
|
319
|
+
...configChecks,
|
|
320
|
+
...projectChecks,
|
|
321
|
+
...tlsChecks,
|
|
322
|
+
...worktreeChecks,
|
|
323
|
+
...(asyncChecks?.dns ?? []),
|
|
324
|
+
...(asyncChecks?.ports ?? []),
|
|
325
|
+
...(asyncChecks?.worktreePorts ?? []),
|
|
326
|
+
];
|
|
327
|
+
const passed = allChecks.filter((c) => c.ok).length;
|
|
328
|
+
const warnings = allChecks.filter((c) => !c.ok && c.warn).length;
|
|
329
|
+
const failed = allChecks.filter((c) => !c.ok && !c.warn).length;
|
|
330
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { text: "dev-proxy doctor" }), _jsx(Section, { title: "Config", children: configChecks.map((c) => (_jsx(Check, { ok: c.ok, warn: c.warn, label: c.label }, c.label))) }), _jsx(Section, { title: "Projects", children: projectChecks.length > 0 ? (projectChecks.map((c) => (_jsx(Check, { ok: c.ok, warn: c.warn, label: c.label }, c.label)))) : (_jsx(Check, { ok: false, warn: true, label: "no projects registered" })) }), _jsx(Section, { title: "DNS", children: asyncChecks ? (asyncChecks.dns.length > 0 ? (asyncChecks.dns.map((c) => (_jsx(Check, { ok: c.ok, warn: c.warn, label: c.label }, c.label)))) : (_jsx(Check, { ok: true, label: "no subdomains to check" }))) : (_jsx(Text, { dimColor: true, children: " checking..." })) }), _jsx(Section, { title: "TLS", children: tlsChecks.map((c) => (_jsx(Check, { ok: c.ok, warn: c.warn, label: c.label }, c.label))) }), _jsx(Section, { title: "Ports", children: asyncChecks ? (asyncChecks.ports.map((c) => (_jsx(Check, { ok: c.ok, warn: c.warn, label: c.label }, c.label)))) : (_jsx(Text, { dimColor: true, children: " checking..." })) }), (worktreeChecks.length > 0 || (asyncChecks?.worktreePorts.length ?? 0) > 0) && (_jsxs(Section, { title: "Worktrees", children: [worktreeChecks.map((c) => (_jsx(Check, { ok: c.ok, warn: c.warn, label: c.label }, c.label))), asyncChecks
|
|
331
|
+
? asyncChecks.worktreePorts.map((c) => (_jsx(Check, { ok: c.ok, warn: c.warn, label: c.label }, c.label)))
|
|
332
|
+
: worktreeChecks.length > 0 && (_jsx(Text, { dimColor: true, children: " checking ports..." }))] })), asyncChecks && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [" ", String(allChecks.length), " checks:", " ", _jsxs(Text, { color: "green", children: [String(passed), " passed"] }), warnings > 0 && (_jsxs(Text, { children: [", ", _jsxs(Text, { color: "yellow", children: [String(warnings), " warnings"] })] })), failed > 0 && (_jsxs(Text, { children: [", ", _jsxs(Text, { color: "red", children: [String(failed), " failed"] })] }))] }) }))] }));
|
|
333
|
+
}
|
|
334
|
+
render(_jsx(Doctor, {}));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, render } from "ink";
|
|
3
|
+
import { ExitOnRender } from "../cli/output.js";
|
|
4
|
+
function Help() {
|
|
5
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, children: [_jsx(ExitOnRender, {}), _jsx(Text, { bold: true, children: "dev-proxy" }), _jsx(Text, { dimColor: true, children: " subdomain-based reverse proxy with traffic inspector" }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: "Usage" }), _jsxs(Text, { children: [" ", "$ dev-proxy [command]"] }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: "Commands" }), _jsxs(Text, { children: [" (none) ", _jsx(Text, { dimColor: true, children: "Start proxy and open traffic inspector" })] }), _jsxs(Text, { children: [" init ", _jsx(Text, { dimColor: true, children: "Interactive setup wizard" })] }), _jsxs(Text, { children: [" status ", _jsx(Text, { dimColor: true, children: "Show current configuration and routing table" })] }), _jsxs(Text, { children: [" doctor ", _jsx(Text, { dimColor: true, children: "Run environment diagnostics" })] }), _jsxs(Text, { children: [" config ", _jsx(Text, { dimColor: true, children: "View or modify global settings" })] }), _jsxs(Text, { children: [" project ", _jsx(Text, { dimColor: true, children: "Manage registered projects" })] }), _jsxs(Text, { children: [" worktree ", _jsx(Text, { dimColor: true, children: "Manage worktree port mappings" })] }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: "Options" }), _jsxs(Text, { children: [" --help, -h ", _jsx(Text, { dimColor: true, children: "Show this help" })] }), _jsxs(Text, { children: [" --version, -v ", _jsx(Text, { dimColor: true, children: "Show version" })] }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: "Examples" }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "$ dev-proxy" }), " ", _jsx(Text, { dimColor: true, children: "Start the proxy" })] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "$ dev-proxy init" }), " ", _jsx(Text, { dimColor: true, children: "Set up a new project" })] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "$ dev-proxy project add ." }), " ", _jsx(Text, { dimColor: true, children: "Register current directory" })] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "$ dev-proxy worktree create feature-auth" })] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "$ dev-proxy worktree destroy feature-auth" })] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: "$ dev-proxy doctor" }), " ", _jsx(Text, { dimColor: true, children: "Check your setup" })] }), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, children: "Documentation" }), _jsxs(Text, { children: [" ", "https://github.com/reopt-ai/dev-proxy"] })] }));
|
|
6
|
+
}
|
|
7
|
+
render(_jsx(Help, {}));
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { render, Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { resolve, isAbsolute } from "node:path";
|
|
7
|
+
import { platform } from "node:os";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { CONFIG_DIR, GLOBAL_CONFIG_PATH, PROJECT_CONFIG_NAME, isValidPort, isValidSubdomain, readGlobalConfig, writeGlobalConfig, writeProjectConfig, } from "../cli/config-io.js";
|
|
10
|
+
import { ExitOnRender } from "../cli/output.js";
|
|
11
|
+
// ── Completed Step Display ───────────────────────────────────
|
|
12
|
+
function CompletedStep({ label, value }) {
|
|
13
|
+
return (_jsxs(Text, { children: [" ", _jsx(Text, { color: "green", children: "\u2713" }), _jsx(Text, { bold: true, children: ` ${label}: ` }), _jsx(Text, { children: value })] }));
|
|
14
|
+
}
|
|
15
|
+
// ── Route Input ──────────────────────────────────────────────
|
|
16
|
+
function RouteInput({ onAdd, onDone, }) {
|
|
17
|
+
const [value, setValue] = useState("");
|
|
18
|
+
const [error, setError] = useState("");
|
|
19
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(TextInput, { value: value, placeholder: "subdomain=port (empty to finish)", onChange: (v) => {
|
|
20
|
+
setValue(v);
|
|
21
|
+
setError("");
|
|
22
|
+
}, onSubmit: (v) => {
|
|
23
|
+
const trimmed = v.trim();
|
|
24
|
+
if (!trimmed) {
|
|
25
|
+
onDone();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const eq = trimmed.indexOf("=");
|
|
29
|
+
if (eq === -1) {
|
|
30
|
+
setError("Expected format: subdomain=port (e.g. api=4000)");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const sub = trimmed.slice(0, eq).trim();
|
|
34
|
+
if (!isValidSubdomain(sub)) {
|
|
35
|
+
setError(`Invalid subdomain "${sub}" — use lowercase alphanumeric and hyphens only`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const portStr = trimmed.slice(eq + 1).trim();
|
|
39
|
+
const portNum = parseInt(portStr, 10);
|
|
40
|
+
if (!isValidPort(portNum)) {
|
|
41
|
+
setError(`Invalid port "${portStr}" — must be 1-65535`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
onAdd(sub, portStr);
|
|
45
|
+
setValue("");
|
|
46
|
+
setError("");
|
|
47
|
+
} })] }), error && (_jsxs(Text, { color: "red", children: [" ", error] }))] }));
|
|
48
|
+
}
|
|
49
|
+
// ── Prompt ───────────────────────────────────────────────────
|
|
50
|
+
function Prompt({ label, defaultValue, onSubmit, }) {
|
|
51
|
+
const [value, setValue] = useState("");
|
|
52
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: label }), defaultValue && _jsx(Text, { dimColor: true, children: ` (${defaultValue})` }), _jsx(Text, { children: ": " }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: (v) => {
|
|
53
|
+
const trimmed = v.trim();
|
|
54
|
+
onSubmit(trimmed ? trimmed : (defaultValue ?? ""));
|
|
55
|
+
} })] }));
|
|
56
|
+
}
|
|
57
|
+
// ── Confirm Step ─────────────────────────────────────────────
|
|
58
|
+
function ConfirmOverwrite({ path, onConfirm, }) {
|
|
59
|
+
useInput((input) => {
|
|
60
|
+
const key = input.toLowerCase();
|
|
61
|
+
if (key === "y")
|
|
62
|
+
onConfirm(true);
|
|
63
|
+
else if (key === "n")
|
|
64
|
+
onConfirm(false);
|
|
65
|
+
});
|
|
66
|
+
return (_jsxs(Text, { children: [" ", _jsx(Text, { color: "yellow", children: "\u26A0" }), _jsx(Text, { children: ` ${path} exists. Overwrite? ` }), _jsx(Text, { dimColor: true, children: "[y/N]" })] }));
|
|
67
|
+
}
|
|
68
|
+
// ── Main Wizard ──────────────────────────────────────────────
|
|
69
|
+
function InitWizard() {
|
|
70
|
+
const [step, setStep] = useState("domain");
|
|
71
|
+
const [domain, setDomain] = useState("");
|
|
72
|
+
const [httpPort, setHttpPort] = useState("");
|
|
73
|
+
const [httpsPort, setHttpsPort] = useState("");
|
|
74
|
+
const [projectPath, setProjectPath] = useState("");
|
|
75
|
+
const [routes, setRoutes] = useState([]);
|
|
76
|
+
const [wildcard, setWildcard] = useState("");
|
|
77
|
+
const [messages, setMessages] = useState([]);
|
|
78
|
+
const addMessage = (msg) => {
|
|
79
|
+
setMessages((prev) => [...prev, msg]);
|
|
80
|
+
};
|
|
81
|
+
const writeConfigs = (overwriteProject) => {
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
addMessage(`Failed to create ${CONFIG_DIR}: ${err.message}`);
|
|
87
|
+
setStep("done");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const absPath = isAbsolute(projectPath)
|
|
91
|
+
? projectPath
|
|
92
|
+
: resolve(process.cwd(), projectPath);
|
|
93
|
+
let globalConfig;
|
|
94
|
+
const existing = readGlobalConfig();
|
|
95
|
+
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
96
|
+
globalConfig = {
|
|
97
|
+
domain: existing.domain ?? domain,
|
|
98
|
+
port: existing.port ?? parseInt(httpPort, 10),
|
|
99
|
+
httpsPort: existing.httpsPort ?? parseInt(httpsPort, 10),
|
|
100
|
+
projects: existing.projects ?? [],
|
|
101
|
+
};
|
|
102
|
+
if (!globalConfig.projects.includes(absPath)) {
|
|
103
|
+
globalConfig.projects.push(absPath);
|
|
104
|
+
addMessage(`Added project to ${GLOBAL_CONFIG_PATH}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
addMessage(`Project already registered`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
globalConfig = {
|
|
112
|
+
domain,
|
|
113
|
+
port: parseInt(httpPort, 10),
|
|
114
|
+
httpsPort: parseInt(httpsPort, 10),
|
|
115
|
+
projects: [absPath],
|
|
116
|
+
};
|
|
117
|
+
addMessage(`Created ${GLOBAL_CONFIG_PATH}`);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
writeGlobalConfig(globalConfig);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
addMessage(`Failed to write ${GLOBAL_CONFIG_PATH}: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
// Project config
|
|
126
|
+
const projectConfigPath = resolve(absPath, PROJECT_CONFIG_NAME);
|
|
127
|
+
const routeMap = {};
|
|
128
|
+
for (const r of routes) {
|
|
129
|
+
routeMap[r.subdomain] = `http://localhost:${r.port}`;
|
|
130
|
+
}
|
|
131
|
+
if (wildcard) {
|
|
132
|
+
routeMap["*"] = `http://localhost:${wildcard}`;
|
|
133
|
+
}
|
|
134
|
+
if (!existsSync(projectConfigPath) || overwriteProject) {
|
|
135
|
+
try {
|
|
136
|
+
writeProjectConfig(absPath, { routes: routeMap, worktrees: {} });
|
|
137
|
+
addMessage(`Created ${projectConfigPath}`);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
addMessage(`Failed to write ${projectConfigPath}: ${err.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
addMessage(`Skipped ${projectConfigPath}`);
|
|
145
|
+
}
|
|
146
|
+
setStep("done");
|
|
147
|
+
};
|
|
148
|
+
// DNS + mkcert info for done screen
|
|
149
|
+
const hasMkcert = (() => {
|
|
150
|
+
try {
|
|
151
|
+
execFileSync("which", ["mkcert"], { stdio: "ignore" });
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Expected: mkcert is simply not installed
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
const absProjectPath = projectPath
|
|
160
|
+
? isAbsolute(projectPath)
|
|
161
|
+
? projectPath
|
|
162
|
+
: resolve(process.cwd(), projectPath)
|
|
163
|
+
: "";
|
|
164
|
+
const projectConfigExists = absProjectPath && existsSync(resolve(absProjectPath, PROJECT_CONFIG_NAME));
|
|
165
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingBottom: 1, children: [_jsx(Text, { bold: true, children: " dev-proxy init" }), _jsx(Text, { dimColor: true, children: " " + "\u2500".repeat(44) }), _jsx(Text, { children: "" }), domain && step !== "domain" && _jsx(CompletedStep, { label: "Domain", value: domain }), httpPort && step !== "httpPort" && (_jsx(CompletedStep, { label: "HTTP", value: `:${httpPort}` })), httpsPort && step !== "httpsPort" && (_jsx(CompletedStep, { label: "HTTPS", value: `:${httpsPort}` })), projectPath && step !== "projectPath" && (_jsx(CompletedStep, { label: "Project", value: absProjectPath })), routes.length > 0 && step !== "routes" && (_jsx(Box, { flexDirection: "column", children: routes.map((r) => (_jsx(CompletedStep, { label: r.subdomain, value: `:${r.port}` }, r.subdomain))) })), wildcard && step !== "wildcard" && (_jsx(CompletedStep, { label: "*", value: `:${wildcard}` })), step === "domain" && (_jsx(Prompt, { label: "Domain", defaultValue: "localhost", onSubmit: (v) => {
|
|
166
|
+
setDomain(v);
|
|
167
|
+
setStep("httpPort");
|
|
168
|
+
} })), step === "httpPort" && (_jsx(Prompt, { label: "HTTP port", defaultValue: "3000", onSubmit: (v) => {
|
|
169
|
+
const num = parseInt(v, 10);
|
|
170
|
+
if (!isValidPort(num))
|
|
171
|
+
return;
|
|
172
|
+
setHttpPort(v);
|
|
173
|
+
setStep("httpsPort");
|
|
174
|
+
} })), step === "httpsPort" && (_jsx(Prompt, { label: "HTTPS port", defaultValue: "3443", onSubmit: (v) => {
|
|
175
|
+
const num = parseInt(v, 10);
|
|
176
|
+
if (!isValidPort(num))
|
|
177
|
+
return;
|
|
178
|
+
setHttpsPort(v);
|
|
179
|
+
setStep("projectPath");
|
|
180
|
+
} })), step === "projectPath" && (_jsx(Prompt, { label: "Project path", defaultValue: process.cwd(), onSubmit: (v) => {
|
|
181
|
+
setProjectPath(v);
|
|
182
|
+
setStep("routes");
|
|
183
|
+
} })), step === "routes" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: " Add routes (subdomain=port, empty to finish):" }), routes.map((r) => (_jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", children: r.subdomain }), _jsx(Text, { dimColor: true, children: " \u279C " }), _jsx(Text, { children: `:${r.port}` })] }, r.subdomain))), _jsx(RouteInput, { onAdd: (sub, port) => {
|
|
184
|
+
setRoutes((prev) => [...prev, { subdomain: sub, port }]);
|
|
185
|
+
}, onDone: () => {
|
|
186
|
+
setStep("wildcard");
|
|
187
|
+
} })] })), step === "wildcard" && (_jsx(Prompt, { label: "Default port for unmatched subdomains (empty to skip)", onSubmit: (v) => {
|
|
188
|
+
setWildcard(v);
|
|
189
|
+
if (projectConfigExists) {
|
|
190
|
+
setStep("confirm");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
writeConfigs(false);
|
|
194
|
+
}
|
|
195
|
+
} })), step === "confirm" && (_jsx(ConfirmOverwrite, { path: resolve(absProjectPath, PROJECT_CONFIG_NAME), onConfirm: (yes) => {
|
|
196
|
+
writeConfigs(yes);
|
|
197
|
+
} })), step === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "" }), messages.map((msg, i) => (_jsxs(Text, { children: [" ", _jsx(Text, { color: "green", children: "\u2713" }), _jsx(Text, { children: ` ${msg}` })] }, i))), domain !== "localhost" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: " DNS Setup" }), _jsx(Text, { dimColor: true, children: " " + "\u2500".repeat(44) }), platform() === "darwin" ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: " Recommended: dnsmasq (automatic wildcard)" }), _jsx(Text, { color: "cyan", children: ` brew install dnsmasq` }), _jsx(Text, { color: "cyan", children: ` echo "address=/${domain}/127.0.0.1" >> $(brew --prefix)/etc/dnsmasq.conf` }), _jsx(Text, { color: "cyan", children: ` sudo brew services start dnsmasq` }), _jsx(Text, { color: "cyan", children: ` sudo mkdir -p /etc/resolver` }), _jsx(Text, { color: "cyan", children: ` echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/${domain}` })] })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: " Add to /etc/hosts:" }), routes.map((r) => (_jsx(Text, { color: "cyan", children: ` 127.0.0.1 ${r.subdomain}.${domain}` }, r.subdomain)))] }))] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: " TLS" }), _jsx(Text, { dimColor: true, children: " " + "\u2500".repeat(44) }), hasMkcert ? (_jsxs(Text, { children: [" ", _jsx(Text, { color: "green", children: "\u2713" }), _jsx(Text, { children: " mkcert detected \u2014 TLS certs auto-generated on first run" })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [" ", _jsx(Text, { color: "yellow", children: "\u26A0" }), _jsx(Text, { children: " mkcert not found \u2014 HTTPS will be disabled" })] }), _jsx(Text, { dimColor: true, children: " Install: brew install mkcert && mkcert -install" })] }))] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: " " + "\u2500".repeat(44) }), _jsxs(Text, { children: [" ", _jsx(Text, { bold: true, children: "Done!" }), _jsx(Text, { children: " Run " }), _jsx(Text, { color: "cyan", children: "dev-proxy" }), _jsx(Text, { children: " to start." })] })] }), _jsx(ExitOnRender, {})] }))] }));
|
|
198
|
+
}
|
|
199
|
+
render(_jsx(InitWizard, {}));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { Box, Text, render } from "ink";
|
|
5
|
+
import { PROJECT_CONFIG_NAME, readGlobalConfig, writeGlobalConfig, writeProjectConfig, readProjectConfig, } from "../cli/config-io.js";
|
|
6
|
+
import { Header, Row, SuccessMessage, ErrorMessage, ExitOnRender, } from "../cli/output.js";
|
|
7
|
+
// ── List projects ────────────────────────────────────────────
|
|
8
|
+
function ProjectList() {
|
|
9
|
+
const cfg = readGlobalConfig();
|
|
10
|
+
const projects = cfg.projects ?? [];
|
|
11
|
+
if (projects.length === 0) {
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ExitOnRender, {}), _jsx(Header, { text: "Registered Projects" }), _jsx(Text, { dimColor: true, children: " (none)" })] }));
|
|
13
|
+
}
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ExitOnRender, {}), _jsx(Header, { text: "Registered Projects" }), projects.map((p) => {
|
|
15
|
+
const pc = readProjectConfig(p);
|
|
16
|
+
const routeCount = Object.keys(pc.routes ?? {}).length;
|
|
17
|
+
const worktreeCount = Object.keys(pc.worktrees ?? {}).length;
|
|
18
|
+
return (_jsx(Row, { label: p, value: `${routeCount} route(s), ${worktreeCount} worktree(s)`, pad: 40 }, p));
|
|
19
|
+
})] }));
|
|
20
|
+
}
|
|
21
|
+
// ── Add project ──────────────────────────────────────────────
|
|
22
|
+
function ProjectAdd({ projectPath }) {
|
|
23
|
+
const absPath = resolve(projectPath);
|
|
24
|
+
const cfg = readGlobalConfig();
|
|
25
|
+
const projects = cfg.projects ?? [];
|
|
26
|
+
if (projects.includes(absPath)) {
|
|
27
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ExitOnRender, {}), _jsx(ErrorMessage, { message: `Project already registered: ${absPath}` })] }));
|
|
28
|
+
}
|
|
29
|
+
// Create .dev-proxy.json template if it doesn't exist
|
|
30
|
+
const projectConfigPath = resolve(absPath, PROJECT_CONFIG_NAME);
|
|
31
|
+
if (!existsSync(projectConfigPath)) {
|
|
32
|
+
writeProjectConfig(absPath, { routes: {}, worktrees: {} });
|
|
33
|
+
}
|
|
34
|
+
cfg.projects = [...projects, absPath];
|
|
35
|
+
writeGlobalConfig(cfg);
|
|
36
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ExitOnRender, {}), _jsx(SuccessMessage, { message: `Added project: ${absPath}` })] }));
|
|
37
|
+
}
|
|
38
|
+
// ── Remove project ───────────────────────────────────────────
|
|
39
|
+
function ProjectRemove({ projectPath }) {
|
|
40
|
+
const absPath = resolve(projectPath);
|
|
41
|
+
const cfg = readGlobalConfig();
|
|
42
|
+
const projects = cfg.projects ?? [];
|
|
43
|
+
if (!projects.includes(absPath)) {
|
|
44
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ExitOnRender, {}), _jsx(ErrorMessage, { message: `Project not found: ${absPath}`, hint: "Run 'dev-proxy project list' to see registered projects" })] }));
|
|
45
|
+
}
|
|
46
|
+
cfg.projects = projects.filter((p) => p !== absPath);
|
|
47
|
+
writeGlobalConfig(cfg);
|
|
48
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ExitOnRender, {}), _jsx(SuccessMessage, { message: `Removed project: ${absPath}` })] }));
|
|
49
|
+
}
|
|
50
|
+
// ── Entry point ──────────────────────────────────────────────
|
|
51
|
+
const args = process.argv.slice(3);
|
|
52
|
+
const subcommand = args[0];
|
|
53
|
+
if (subcommand === "add") {
|
|
54
|
+
const path = args[1] ?? process.cwd();
|
|
55
|
+
render(_jsx(ProjectAdd, { projectPath: path }));
|
|
56
|
+
}
|
|
57
|
+
else if (subcommand === "remove") {
|
|
58
|
+
const path = args[1];
|
|
59
|
+
if (!path) {
|
|
60
|
+
render(_jsx(ErrorMessage, { message: "Usage: dev-proxy project remove <path>" }));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
render(_jsx(ProjectRemove, { projectPath: path }));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// "list" or no subcommand → default to list
|
|
68
|
+
render(_jsx(ProjectList, {}));
|
|
69
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, render } from "ink";
|
|
3
|
+
import { config } from "../proxy/config.js";
|
|
4
|
+
import { Header, Section, Row, RouteRow, ExitOnRender } from "../cli/output.js";
|
|
5
|
+
function formatTarget(target) {
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(target);
|
|
8
|
+
if (url.protocol === "http:" && url.hostname === "localhost") {
|
|
9
|
+
return `localhost:${url.port || "80"}`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// not a URL, return as-is
|
|
14
|
+
}
|
|
15
|
+
return target;
|
|
16
|
+
}
|
|
17
|
+
function Status() {
|
|
18
|
+
const allRoutes = [];
|
|
19
|
+
const allWorktrees = [];
|
|
20
|
+
for (const project of config.projects) {
|
|
21
|
+
for (const [sub, target] of Object.entries(project.routes)) {
|
|
22
|
+
allRoutes.push({ sub, target: formatTarget(target) });
|
|
23
|
+
}
|
|
24
|
+
for (const [name, wt] of Object.entries(project.worktrees)) {
|
|
25
|
+
allWorktrees.push({ name, entry: wt });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ExitOnRender, {}), _jsx(Header, { text: "dev-proxy status" }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Row, { label: "Domain", value: config.domain }), _jsx(Row, { label: "HTTP", value: `:${String(config.port)}` }), _jsx(Row, { label: "HTTPS", value: `:${String(config.httpsPort)}` })] }), _jsx(Section, { title: `Routes (${String(allRoutes.length)})`, children: allRoutes.map((r) => (_jsx(RouteRow, { sub: r.sub, target: r.target }, `${r.sub}-${r.target}`))) }), _jsx(Section, { title: `Projects (${String(config.projects.length)})`, children: config.projects.map((p) => (_jsx(Text, { children: ` ${p.path}` }, p.path))) }), _jsx(Section, { title: `Worktrees (${String(allWorktrees.length)})`, children: allWorktrees.map((w) => "ports" in w.entry ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: ` ${w.name}` }), Object.entries(w.entry.ports).map(([svc, p]) => (_jsx(RouteRow, { sub: ` ${svc}`, target: `:${String(p)}` }, svc)))] }, w.name)) : (_jsx(RouteRow, { sub: w.name, target: `:${String(w.entry.port)}` }, w.name))) })] }));
|
|
29
|
+
}
|
|
30
|
+
render(_jsx(Status, {}));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Text, render } from "ink";
|
|
4
|
+
import { ExitOnRender } from "../cli/output.js";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const pkg = require("../../package.json");
|
|
7
|
+
function Version() {
|
|
8
|
+
return (_jsxs(Text, { children: [_jsx(ExitOnRender, {}), "dev-proxy v", pkg.version] }));
|
|
9
|
+
}
|
|
10
|
+
render(_jsx(Version, {}));
|