portmore 1.0.0
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.md +52 -0
- package/dist/dashboard.d.ts +2 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +724 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/demo.d.ts +2 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +100 -0
- package/dist/demo.js.map +1 -0
- package/dist/find-free-port.d.ts +2 -0
- package/dist/find-free-port.d.ts.map +1 -0
- package/dist/find-free-port.js +47 -0
- package/dist/find-free-port.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/dist/open-in-browser.d.ts +2 -0
- package/dist/open-in-browser.d.ts.map +1 -0
- package/dist/open-in-browser.js +18 -0
- package/dist/open-in-browser.js.map +1 -0
- package/dist/portless.d.ts +2 -0
- package/dist/portless.d.ts.map +1 -0
- package/dist/portless.js +17 -0
- package/dist/portless.js.map +1 -0
- package/dist/types/portmore.d.ts +9 -0
- package/dist/types/portmore.d.ts.map +1 -0
- package/dist/types/portmore.js +2 -0
- package/dist/types/portmore.js.map +1 -0
- package/docs/dashboard.png +0 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# portmore
|
|
2
|
+
|
|
3
|
+
Local multi-service helper with automatic free ports, `portless` aliases, and a small dashboard.
|
|
4
|
+
|
|
5
|
+
> Disclaimer: Primarily coded by Codex, worktree/branch naming not supported, https not tested.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js 22+
|
|
10
|
+
- [`portless`](https://github.com/vercel-labs/portless) installed and available in `PATH`
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install portmore
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createServer } from "node:http";
|
|
22
|
+
import { portmore } from "portmore";
|
|
23
|
+
|
|
24
|
+
await portmore({
|
|
25
|
+
name: "server-a",
|
|
26
|
+
title: "Server A",
|
|
27
|
+
metrics: async () => ({ visits: 42 }),
|
|
28
|
+
start: async (port) => {
|
|
29
|
+
const server = createServer((_, res) => {
|
|
30
|
+
res.statusCode = 200;
|
|
31
|
+
res.end("Server A\n");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return server.listen(port);
|
|
35
|
+
},
|
|
36
|
+
stop: async (server) => {
|
|
37
|
+
await new Promise<void>((resolve, reject) => {
|
|
38
|
+
server.close((err) => {
|
|
39
|
+
if (err) {
|
|
40
|
+
reject(err);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
resolve();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Dashboard
|
|
51
|
+
|
|
52
|
+

|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAqrBA,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAwDpD"}
|
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { portless } from "./portless.js";
|
|
4
|
+
import { findFreePort } from "./find-free-port.js";
|
|
5
|
+
import { aliases, toggleAlias } from "./index.js";
|
|
6
|
+
function escapeHtml(value) {
|
|
7
|
+
return value
|
|
8
|
+
.replace(/&/g, "&")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">")
|
|
11
|
+
.replace(/"/g, """)
|
|
12
|
+
.replace(/'/g, "'");
|
|
13
|
+
}
|
|
14
|
+
function formatStartTime(input) {
|
|
15
|
+
return input.toLocaleTimeString("en-US", {
|
|
16
|
+
hour: "2-digit",
|
|
17
|
+
minute: "2-digit",
|
|
18
|
+
second: "2-digit",
|
|
19
|
+
hour12: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function isSvgMarkup(value) {
|
|
23
|
+
return /^\s*<svg[\s>]/i.test(value);
|
|
24
|
+
}
|
|
25
|
+
function svgToDataUri(svg) {
|
|
26
|
+
const encoded = Buffer.from(svg, "utf8").toString("base64");
|
|
27
|
+
return `data:image/svg+xml;base64,${encoded}`;
|
|
28
|
+
}
|
|
29
|
+
function dashboardFaviconSvg() {
|
|
30
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Portmore">
|
|
31
|
+
<rect width="64" height="64" rx="14" fill="#0f1724"/>
|
|
32
|
+
<circle cx="32" cy="32" r="18" fill="#35b6ff"/>
|
|
33
|
+
<circle cx="32" cy="32" r="8" fill="#0f1724"/>
|
|
34
|
+
</svg>`;
|
|
35
|
+
}
|
|
36
|
+
function renderIcon(icon) {
|
|
37
|
+
if (!icon) {
|
|
38
|
+
return `<span class="service-icon" aria-hidden="true">◉</span>`;
|
|
39
|
+
}
|
|
40
|
+
if (isSvgMarkup(icon)) {
|
|
41
|
+
const src = escapeHtml(svgToDataUri(icon));
|
|
42
|
+
return `<img class="service-icon service-icon-svg" src="${src}" alt="" aria-hidden="true" />`;
|
|
43
|
+
}
|
|
44
|
+
return `<span class="service-icon" aria-hidden="true">${escapeHtml(icon)}</span>`;
|
|
45
|
+
}
|
|
46
|
+
async function resolveAliasMetrics(alias) {
|
|
47
|
+
if (!alias.metrics) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return await alias.metrics();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { Status: "error" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function renderAliases(aliases) {
|
|
58
|
+
const renderedRows = await Promise.all(aliases.map(async (alias) => {
|
|
59
|
+
const escapedName = escapeHtml(alias.name);
|
|
60
|
+
const escapedTitle = escapeHtml(alias.title);
|
|
61
|
+
const renderedIcon = renderIcon(alias.icon);
|
|
62
|
+
const localUrl = await portless.getUrl(alias.name).catch(() => `http://localhost:${alias.port}`);
|
|
63
|
+
const startedAt = alias.started;
|
|
64
|
+
const metricsRecord = await resolveAliasMetrics(alias);
|
|
65
|
+
const metricsCount = Object.keys(metricsRecord).length;
|
|
66
|
+
let statusClass;
|
|
67
|
+
let statusText;
|
|
68
|
+
switch (alias.status) {
|
|
69
|
+
case "Starting":
|
|
70
|
+
statusClass = "warning";
|
|
71
|
+
statusText = "Starting";
|
|
72
|
+
break;
|
|
73
|
+
case "Running":
|
|
74
|
+
statusClass = "running";
|
|
75
|
+
statusText = "Running";
|
|
76
|
+
break;
|
|
77
|
+
case "Stopping":
|
|
78
|
+
statusClass = "warning";
|
|
79
|
+
statusText = "Stopping";
|
|
80
|
+
break;
|
|
81
|
+
case "Stopped":
|
|
82
|
+
statusClass = "muted";
|
|
83
|
+
statusText = "Stopped";
|
|
84
|
+
break;
|
|
85
|
+
case "Error":
|
|
86
|
+
statusClass = "error";
|
|
87
|
+
statusText = "Error";
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
statusClass = "muted";
|
|
91
|
+
statusText = "Unknown";
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
const isBusy = alias.status === "Starting" || alias.status === "Stopping";
|
|
95
|
+
const toggleLabel = alias.status === "Running" ? "Stop" : "Start";
|
|
96
|
+
const toggleAttributes = isBusy
|
|
97
|
+
? "disabled aria-disabled=\"true\""
|
|
98
|
+
: "";
|
|
99
|
+
return {
|
|
100
|
+
row: `<tr class="service-row" tabindex="0" role="button" data-service-name="${escapedName}" data-service-title="${escapedTitle}" data-metrics-count="${metricsCount}">
|
|
101
|
+
<td>
|
|
102
|
+
<div class="service-name">
|
|
103
|
+
${renderedIcon}
|
|
104
|
+
<strong>${escapedTitle}</strong>
|
|
105
|
+
</div>
|
|
106
|
+
</td>
|
|
107
|
+
<td>
|
|
108
|
+
<span class="pill ${statusClass}">${statusText}</span>
|
|
109
|
+
</td>
|
|
110
|
+
<td>${startedAt ? formatStartTime(startedAt) : ""}</td>
|
|
111
|
+
<td><code>${alias.port}</code></td>
|
|
112
|
+
<td>
|
|
113
|
+
<a href="${localUrl}" target="_blank" rel="noopener noreferrer">${localUrl}</a>
|
|
114
|
+
</td>
|
|
115
|
+
<td>
|
|
116
|
+
<div class="actions">
|
|
117
|
+
<button type="button" class="action-btn" data-toggle-service="${escapedName}" ${toggleAttributes}>${toggleLabel}</button>
|
|
118
|
+
<a href="${localUrl}" class="action-btn" target="_blank" rel="noopener noreferrer">Open</a>
|
|
119
|
+
</div>
|
|
120
|
+
</td>
|
|
121
|
+
</tr>`,
|
|
122
|
+
};
|
|
123
|
+
}));
|
|
124
|
+
const envPortlessUrl = process.env.PORTLESS_URL?.trim();
|
|
125
|
+
if (envPortlessUrl) {
|
|
126
|
+
let envName = "PORTLESS_URL";
|
|
127
|
+
try {
|
|
128
|
+
const parsedUrl = new URL(envPortlessUrl);
|
|
129
|
+
const labels = parsedUrl.hostname.split(".");
|
|
130
|
+
if (labels.length > 1) {
|
|
131
|
+
envName = labels.slice(0, -1).join(".");
|
|
132
|
+
}
|
|
133
|
+
else if (labels[0]) {
|
|
134
|
+
envName = labels[0];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Keep fallback label when PORTLESS_URL is not a valid absolute URL.
|
|
139
|
+
}
|
|
140
|
+
renderedRows.unshift({
|
|
141
|
+
row: `<tr>
|
|
142
|
+
<td>
|
|
143
|
+
<div class="service-name">
|
|
144
|
+
<strong>${envName}</strong>
|
|
145
|
+
</div>
|
|
146
|
+
</td>
|
|
147
|
+
<td>
|
|
148
|
+
<span class="pill muted">Main</span>
|
|
149
|
+
</td>
|
|
150
|
+
<td></td>
|
|
151
|
+
<td></td>
|
|
152
|
+
<td>
|
|
153
|
+
<a href="${envPortlessUrl}" target="_blank" rel="noopener noreferrer">${envPortlessUrl}</a>
|
|
154
|
+
</td>
|
|
155
|
+
<td>
|
|
156
|
+
<div class="actions">
|
|
157
|
+
<a href="${envPortlessUrl}" class="action-btn" target="_blank" rel="noopener noreferrer">Open</a>
|
|
158
|
+
</div>
|
|
159
|
+
</td>
|
|
160
|
+
</tr>`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const rows = renderedRows.map((entry) => entry.row).join("\n");
|
|
164
|
+
return `<!doctype html>
|
|
165
|
+
<html lang="en">
|
|
166
|
+
<head>
|
|
167
|
+
<meta charset="utf-8" />
|
|
168
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
169
|
+
<link rel="icon" href="/favicon.ico" type="image/svg+xml" />
|
|
170
|
+
<title>Portmore Dashboard</title>
|
|
171
|
+
<style>
|
|
172
|
+
:root {
|
|
173
|
+
color-scheme: dark;
|
|
174
|
+
--bg-0: #070a0f;
|
|
175
|
+
--bg-1: #0c1320;
|
|
176
|
+
--bg-2: #111a28;
|
|
177
|
+
--line: #273347;
|
|
178
|
+
--text-0: #e9edf6;
|
|
179
|
+
--text-1: #b5bfd2;
|
|
180
|
+
--accent: #35b6ff;
|
|
181
|
+
--running-bg: #15372b;
|
|
182
|
+
--running-fg: #67e6ad;
|
|
183
|
+
--starting-bg: #3d2b18;
|
|
184
|
+
--starting-fg: #ffbf73;
|
|
185
|
+
--muted-bg: #1f2937;
|
|
186
|
+
--muted-fg: #a7b3c9;
|
|
187
|
+
--error-bg: #3a1b24;
|
|
188
|
+
--error-fg: #ff8ba4;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
* {
|
|
192
|
+
box-sizing: border-box;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
body {
|
|
196
|
+
margin: 0;
|
|
197
|
+
min-height: 100vh;
|
|
198
|
+
font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;
|
|
199
|
+
color: var(--text-0);
|
|
200
|
+
background:
|
|
201
|
+
radial-gradient(circle at 12% 0%, #133657 0%, transparent 44%),
|
|
202
|
+
radial-gradient(circle at 88% 100%, #0f3e33 0%, transparent 36%),
|
|
203
|
+
linear-gradient(180deg, var(--bg-0) 0%, var(--bg-1) 60%, var(--bg-0) 100%);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.layout {
|
|
207
|
+
max-width: 1100px;
|
|
208
|
+
margin: 0 auto;
|
|
209
|
+
padding: 44px 20px 70px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.header {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-wrap: wrap;
|
|
215
|
+
gap: 14px;
|
|
216
|
+
align-items: end;
|
|
217
|
+
justify-content: space-between;
|
|
218
|
+
margin-bottom: 20px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.title {
|
|
222
|
+
margin: 0;
|
|
223
|
+
font-size: clamp(1.8rem, 2.4vw, 2.5rem);
|
|
224
|
+
letter-spacing: 0.02em;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.subtitle {
|
|
228
|
+
margin: 7px 0 0;
|
|
229
|
+
color: var(--text-1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.kpi {
|
|
233
|
+
border: 1px solid var(--line);
|
|
234
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent);
|
|
235
|
+
padding: 10px 14px;
|
|
236
|
+
border-radius: 12px;
|
|
237
|
+
min-width: 160px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.kpi-label {
|
|
241
|
+
display: block;
|
|
242
|
+
font-size: 0.78rem;
|
|
243
|
+
text-transform: uppercase;
|
|
244
|
+
letter-spacing: 0.08em;
|
|
245
|
+
color: var(--text-1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.kpi-value {
|
|
249
|
+
display: block;
|
|
250
|
+
font-size: 1.35rem;
|
|
251
|
+
margin-top: 4px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.table-shell {
|
|
255
|
+
border: 1px solid var(--line);
|
|
256
|
+
border-radius: 16px;
|
|
257
|
+
overflow: hidden;
|
|
258
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01));
|
|
259
|
+
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.35);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
table {
|
|
263
|
+
width: 100%;
|
|
264
|
+
border-collapse: collapse;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
thead th {
|
|
268
|
+
text-align: left;
|
|
269
|
+
font-size: 0.77rem;
|
|
270
|
+
text-transform: uppercase;
|
|
271
|
+
letter-spacing: 0.09em;
|
|
272
|
+
color: var(--text-1);
|
|
273
|
+
background: var(--bg-2);
|
|
274
|
+
border-bottom: 1px solid var(--line);
|
|
275
|
+
padding: 14px 16px;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
tbody td {
|
|
279
|
+
border-bottom: 1px solid rgba(74, 91, 118, 0.35);
|
|
280
|
+
padding: 15px 16px;
|
|
281
|
+
vertical-align: middle;
|
|
282
|
+
font-size: 0.95rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
tbody tr:last-child td {
|
|
286
|
+
border-bottom: none;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
tbody tr:hover {
|
|
290
|
+
background: rgba(56, 71, 94, 0.26);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.service-name {
|
|
294
|
+
display: inline-flex;
|
|
295
|
+
gap: 10px;
|
|
296
|
+
align-items: center;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.service-row {
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
outline: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.service-row:focus,
|
|
305
|
+
.service-row:focus-visible,
|
|
306
|
+
.service-row:active {
|
|
307
|
+
outline: none;
|
|
308
|
+
box-shadow: none;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.service-row:hover .service-name strong {
|
|
312
|
+
color: #9fdcff;
|
|
313
|
+
text-decoration: underline;
|
|
314
|
+
text-underline-offset: 3px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.service-icon {
|
|
318
|
+
width: 26px;
|
|
319
|
+
height: 26px;
|
|
320
|
+
border-radius: 999px;
|
|
321
|
+
display: inline-flex;
|
|
322
|
+
align-items: center;
|
|
323
|
+
justify-content: center;
|
|
324
|
+
font-size: 0.95rem;
|
|
325
|
+
line-height: 1;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.service-icon-svg {
|
|
329
|
+
display: inline-block;
|
|
330
|
+
object-fit: contain;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.pill {
|
|
334
|
+
display: inline-flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
gap: 8px;
|
|
337
|
+
font-size: 0.84rem;
|
|
338
|
+
padding: 4px 10px;
|
|
339
|
+
border-radius: 999px;
|
|
340
|
+
border: 1px solid transparent;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.pill::before {
|
|
344
|
+
content: "";
|
|
345
|
+
width: 7px;
|
|
346
|
+
height: 7px;
|
|
347
|
+
border-radius: 999px;
|
|
348
|
+
background: currentColor;
|
|
349
|
+
opacity: 0.9;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.pill.running {
|
|
353
|
+
color: var(--running-fg);
|
|
354
|
+
background: var(--running-bg);
|
|
355
|
+
border-color: rgba(103, 230, 173, 0.22);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.pill.warning {
|
|
359
|
+
color: var(--starting-fg);
|
|
360
|
+
background: var(--starting-bg);
|
|
361
|
+
border-color: rgba(255, 191, 115, 0.2);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.pill.muted {
|
|
365
|
+
color: var(--muted-fg);
|
|
366
|
+
background: var(--muted-bg);
|
|
367
|
+
border-color: rgba(167, 179, 201, 0.25);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.pill.error {
|
|
371
|
+
color: var(--error-fg);
|
|
372
|
+
background: var(--error-bg);
|
|
373
|
+
border-color: rgba(255, 139, 164, 0.28);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
code {
|
|
377
|
+
font-family: "Iosevka", "SF Mono", Menlo, monospace;
|
|
378
|
+
font-size: 0.84rem;
|
|
379
|
+
color: #c8d3e5;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
a {
|
|
383
|
+
color: #7bd0ff;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.meta {
|
|
387
|
+
margin-left: 8px;
|
|
388
|
+
font-size: 0.78rem;
|
|
389
|
+
color: var(--text-1);
|
|
390
|
+
background: rgba(95, 131, 180, 0.2);
|
|
391
|
+
border: 1px solid rgba(95, 131, 180, 0.35);
|
|
392
|
+
border-radius: 999px;
|
|
393
|
+
padding: 2px 7px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.actions {
|
|
397
|
+
display: inline-flex;
|
|
398
|
+
gap: 8px;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.action-btn {
|
|
402
|
+
appearance: none;
|
|
403
|
+
border: 1px solid var(--line);
|
|
404
|
+
border-radius: 8px;
|
|
405
|
+
padding: 6px 10px;
|
|
406
|
+
font: inherit;
|
|
407
|
+
color: var(--text-0);
|
|
408
|
+
text-decoration: none;
|
|
409
|
+
background: rgba(255, 255, 255, 0.03);
|
|
410
|
+
cursor: pointer;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.action-btn:hover {
|
|
414
|
+
border-color: #4b86ab;
|
|
415
|
+
background: rgba(53, 182, 255, 0.15);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.metrics-dialog {
|
|
419
|
+
border: 1px solid rgba(95, 131, 180, 0.38);
|
|
420
|
+
border-radius: 14px;
|
|
421
|
+
padding: 0;
|
|
422
|
+
width: min(560px, calc(100vw - 32px));
|
|
423
|
+
background: #0f1724;
|
|
424
|
+
color: var(--text-0);
|
|
425
|
+
box-shadow: 0 26px 50px rgba(0, 0, 0, 0.45);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.metrics-dialog::backdrop {
|
|
429
|
+
background: rgba(2, 7, 12, 0.62);
|
|
430
|
+
backdrop-filter: blur(2px);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.metrics-dialog-header {
|
|
434
|
+
display: flex;
|
|
435
|
+
justify-content: space-between;
|
|
436
|
+
align-items: center;
|
|
437
|
+
padding: 14px 16px;
|
|
438
|
+
border-bottom: 1px solid rgba(95, 131, 180, 0.25);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.metrics-dialog-title {
|
|
442
|
+
margin: 0;
|
|
443
|
+
font-size: 1.02rem;
|
|
444
|
+
color: #b7ddf4;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.metrics-dialog-close {
|
|
448
|
+
appearance: none;
|
|
449
|
+
border: 1px solid rgba(95, 131, 180, 0.45);
|
|
450
|
+
background: rgba(95, 131, 180, 0.14);
|
|
451
|
+
color: var(--text-0);
|
|
452
|
+
border-radius: 8px;
|
|
453
|
+
padding: 4px 9px;
|
|
454
|
+
cursor: pointer;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.metrics-grid {
|
|
458
|
+
display: grid;
|
|
459
|
+
gap: 8px;
|
|
460
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
461
|
+
padding: 14px 16px 16px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.metric-item {
|
|
465
|
+
border: 1px solid rgba(95, 131, 180, 0.28);
|
|
466
|
+
border-radius: 8px;
|
|
467
|
+
padding: 8px 10px;
|
|
468
|
+
background: rgba(95, 131, 180, 0.08);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.metric-item span {
|
|
472
|
+
display: block;
|
|
473
|
+
color: var(--text-1);
|
|
474
|
+
font-size: 0.78rem;
|
|
475
|
+
margin-bottom: 4px;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.metric-item strong {
|
|
479
|
+
font-size: 1rem;
|
|
480
|
+
color: var(--text-0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.empty {
|
|
484
|
+
padding: 34px 18px;
|
|
485
|
+
color: var(--text-1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@media (max-width: 840px) {
|
|
489
|
+
.layout {
|
|
490
|
+
padding-top: 24px;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.table-shell {
|
|
494
|
+
overflow-x: auto;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
table {
|
|
498
|
+
min-width: 800px;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
</style>
|
|
502
|
+
</head>
|
|
503
|
+
<body>
|
|
504
|
+
<main class="layout">
|
|
505
|
+
<header class="header">
|
|
506
|
+
<div>
|
|
507
|
+
<h1 class="title">Portmore Dashboard</h1>
|
|
508
|
+
<p class="subtitle">Live alias routing map for local development targets.</p>
|
|
509
|
+
</div>
|
|
510
|
+
</header>
|
|
511
|
+
|
|
512
|
+
<section class="table-shell">
|
|
513
|
+
<table>
|
|
514
|
+
<thead>
|
|
515
|
+
<tr>
|
|
516
|
+
<th>Name</th>
|
|
517
|
+
<th>Status</th>
|
|
518
|
+
<th>Start Time</th>
|
|
519
|
+
<th>Port</th>
|
|
520
|
+
<th>URL</th>
|
|
521
|
+
<th>Actions</th>
|
|
522
|
+
</tr>
|
|
523
|
+
</thead>
|
|
524
|
+
<tbody>${rows || `<tr><td colspan="6" class="empty">No aliases yet. Start a service with portmore to populate this table.</td></tr>`}</tbody>
|
|
525
|
+
</table>
|
|
526
|
+
</section>
|
|
527
|
+
</main>
|
|
528
|
+
|
|
529
|
+
<dialog class="metrics-dialog" id="metrics-dialog">
|
|
530
|
+
<header class="metrics-dialog-header">
|
|
531
|
+
<h2 class="metrics-dialog-title" id="metrics-dialog-title">Service metrics</h2>
|
|
532
|
+
<button type="button" class="metrics-dialog-close" id="metrics-dialog-close">Close</button>
|
|
533
|
+
</header>
|
|
534
|
+
<section id="metrics-dialog-content"></section>
|
|
535
|
+
</dialog>
|
|
536
|
+
|
|
537
|
+
<script>
|
|
538
|
+
const metricsDialog = document.getElementById("metrics-dialog");
|
|
539
|
+
const metricsDialogTitle = document.getElementById("metrics-dialog-title");
|
|
540
|
+
const metricsDialogContent = document.getElementById("metrics-dialog-content");
|
|
541
|
+
const metricsDialogClose = document.getElementById("metrics-dialog-close");
|
|
542
|
+
|
|
543
|
+
if (
|
|
544
|
+
metricsDialog instanceof HTMLDialogElement &&
|
|
545
|
+
metricsDialogTitle instanceof HTMLElement &&
|
|
546
|
+
metricsDialogContent instanceof HTMLElement
|
|
547
|
+
) {
|
|
548
|
+
const renderMetricsGrid = (metrics) => {
|
|
549
|
+
const entries = Object.entries(metrics || {});
|
|
550
|
+
|
|
551
|
+
if (entries.length === 0) {
|
|
552
|
+
return '<div class="metric-item"><span>Status</span><strong>No metrics available</strong></div>';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return entries
|
|
556
|
+
.map(([key, value]) => {
|
|
557
|
+
const safeKey = String(key)
|
|
558
|
+
.replace(/&/g, "&")
|
|
559
|
+
.replace(/</g, "<")
|
|
560
|
+
.replace(/>/g, ">")
|
|
561
|
+
.replace(/"/g, """)
|
|
562
|
+
.replace(/'/g, "'");
|
|
563
|
+
|
|
564
|
+
const normalizedValue = typeof value === "object" && value !== null
|
|
565
|
+
? JSON.stringify(value)
|
|
566
|
+
: String(value);
|
|
567
|
+
|
|
568
|
+
const safeValue = normalizedValue
|
|
569
|
+
.replace(/&/g, "&")
|
|
570
|
+
.replace(/</g, "<")
|
|
571
|
+
.replace(/>/g, ">")
|
|
572
|
+
.replace(/"/g, """)
|
|
573
|
+
.replace(/'/g, "'");
|
|
574
|
+
|
|
575
|
+
return '<div class="metric-item"><span>' + safeKey + '</span><strong>' + safeValue + '</strong></div>';
|
|
576
|
+
})
|
|
577
|
+
.join("");
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const openMetricsDialog = async (row) => {
|
|
581
|
+
const serviceName = row.getAttribute("data-service-name") || "Service";
|
|
582
|
+
const serviceTitle = row.getAttribute("data-service-title") || serviceName;
|
|
583
|
+
|
|
584
|
+
metricsDialogTitle.textContent = serviceTitle + " metrics";
|
|
585
|
+
metricsDialogContent.innerHTML = '<div class="metrics-grid"><div class="metric-item"><span>Status</span><strong>Loading...</strong></div></div>';
|
|
586
|
+
metricsDialog.showModal();
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const response = await fetch("/aliases/" + encodeURIComponent(serviceName) + "/metrics");
|
|
590
|
+
if (!response.ok) {
|
|
591
|
+
metricsDialogContent.innerHTML = '<div class="metrics-grid"><div class="metric-item"><span>Status</span><strong>Failed to load metrics</strong></div></div>';
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const payload = await response.json();
|
|
596
|
+
const metrics = payload && typeof payload === "object" && payload.metrics && typeof payload.metrics === "object"
|
|
597
|
+
? payload.metrics
|
|
598
|
+
: {};
|
|
599
|
+
|
|
600
|
+
metricsDialogTitle.textContent = serviceTitle + " metrics";
|
|
601
|
+
metricsDialogContent.innerHTML = '<div class="metrics-grid">' + renderMetricsGrid(metrics) + '</div>';
|
|
602
|
+
} catch {
|
|
603
|
+
metricsDialogContent.innerHTML = '<div class="metrics-grid"><div class="metric-item"><span>Status</span><strong>Failed to load metrics</strong></div></div>';
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
document.querySelectorAll(".service-row[data-service-name]").forEach((row) => {
|
|
608
|
+
row.addEventListener("click", (event) => {
|
|
609
|
+
const target = event.target;
|
|
610
|
+
if (target instanceof Element && target.closest("a, button")) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
void openMetricsDialog(row);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
row.addEventListener("keydown", (event) => {
|
|
618
|
+
if (event.key !== "Enter" && event.key !== " ") {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const target = event.target;
|
|
623
|
+
if (target instanceof Element && target.closest("a, button")) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
event.preventDefault();
|
|
628
|
+
void openMetricsDialog(row);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
if (metricsDialogClose instanceof HTMLButtonElement) {
|
|
633
|
+
metricsDialogClose.addEventListener("click", () => {
|
|
634
|
+
metricsDialog.close();
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
document.querySelectorAll(".action-btn[data-toggle-service]").forEach((button) => {
|
|
640
|
+
button.addEventListener("click", async () => {
|
|
641
|
+
const serviceName = button.getAttribute("data-toggle-service");
|
|
642
|
+
if (!serviceName) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
button.setAttribute("disabled", "true");
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
const response = await fetch("/aliases/" + encodeURIComponent(serviceName) + "/toggle", {
|
|
650
|
+
method: "POST",
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
if (!response.ok) {
|
|
654
|
+
button.textContent = "Failed";
|
|
655
|
+
setTimeout(() => {
|
|
656
|
+
window.location.reload();
|
|
657
|
+
}, 500);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
window.location.reload();
|
|
662
|
+
} catch {
|
|
663
|
+
button.textContent = "Failed";
|
|
664
|
+
setTimeout(() => {
|
|
665
|
+
window.location.reload();
|
|
666
|
+
}, 500);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
</script>
|
|
671
|
+
</body>
|
|
672
|
+
</html>`;
|
|
673
|
+
}
|
|
674
|
+
export async function startDashboard() {
|
|
675
|
+
const app = new Hono();
|
|
676
|
+
app.get("/", async (c) => c.html(await renderAliases(aliases)));
|
|
677
|
+
app.get("/favicon.ico", () => {
|
|
678
|
+
return new Response(dashboardFaviconSvg(), {
|
|
679
|
+
headers: {
|
|
680
|
+
"content-type": "image/svg+xml; charset=utf-8",
|
|
681
|
+
"cache-control": "public, max-age=86400",
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
app.get("/aliases", (c) => c.json(aliases));
|
|
686
|
+
app.get("/aliases/:name/metrics", async (c) => {
|
|
687
|
+
const name = c.req.param("name");
|
|
688
|
+
const alias = aliases.find((entry) => entry.name === name);
|
|
689
|
+
if (!alias) {
|
|
690
|
+
return c.json({ ok: false, error: `Alias ${name} not found` }, 404);
|
|
691
|
+
}
|
|
692
|
+
const metrics = await resolveAliasMetrics(alias);
|
|
693
|
+
return c.json({ ok: true, metrics });
|
|
694
|
+
});
|
|
695
|
+
app.post("/aliases/:name/toggle", async (c) => {
|
|
696
|
+
const name = c.req.param("name");
|
|
697
|
+
const alias = aliases.find((entry) => entry.name === name);
|
|
698
|
+
if (!alias) {
|
|
699
|
+
return c.json({ ok: false, error: `Alias ${name} not found` }, 404);
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
await toggleAlias(alias);
|
|
703
|
+
return c.json({ ok: true, status: alias.status });
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
alias.status = "Error";
|
|
707
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
708
|
+
return c.json({ ok: false, error: message }, 500);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
const port = await findFreePort();
|
|
712
|
+
await portless.alias("portmore", (port));
|
|
713
|
+
return new Promise((resolve, reject) => {
|
|
714
|
+
try {
|
|
715
|
+
serve({ fetch: app.fetch, port }, () => {
|
|
716
|
+
resolve();
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
reject(error);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"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"}
|
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../src/demo.ts"],"names":[],"mappings":""}
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createServer, } from "node:http";
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { portmore } from "./index.js";
|
|
5
|
+
import { openInBrowser } from "./open-in-browser.js";
|
|
6
|
+
let visitsA = 0;
|
|
7
|
+
let visitsB = 0;
|
|
8
|
+
await portmore({
|
|
9
|
+
name: "server-a",
|
|
10
|
+
title: "Server A",
|
|
11
|
+
icon: "🧪",
|
|
12
|
+
metrics: () => {
|
|
13
|
+
return Promise.resolve({
|
|
14
|
+
visits: visitsA,
|
|
15
|
+
server: "hono",
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
start: async (port) => {
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
app.get("/", (c) => {
|
|
21
|
+
visitsA += 1;
|
|
22
|
+
return c.text("Server A\n");
|
|
23
|
+
});
|
|
24
|
+
app.get("/server-b", async (c) => {
|
|
25
|
+
visitsA += 1;
|
|
26
|
+
const serverBUrl = await portmore("server-b");
|
|
27
|
+
if (!serverBUrl) {
|
|
28
|
+
return c.text("Server B alias not found\n", 404);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const upstream = await fetch(serverBUrl);
|
|
32
|
+
const body = await upstream.text();
|
|
33
|
+
return new Response(body, {
|
|
34
|
+
status: upstream.status,
|
|
35
|
+
headers: {
|
|
36
|
+
"content-type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return c.text("Failed to call Server B\n", 502);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return serve({ fetch: app.fetch, port });
|
|
45
|
+
},
|
|
46
|
+
stop: async (server) => {
|
|
47
|
+
await new Promise((resolve, reject) => {
|
|
48
|
+
server?.close((err) => {
|
|
49
|
+
if (err) {
|
|
50
|
+
reject(err);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
await portmore({
|
|
59
|
+
name: "server-b",
|
|
60
|
+
title: "Server B",
|
|
61
|
+
icon: "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='#7bd0ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M3 12h18'/><path d='M12 3c2.5 2.7 2.5 15.3 0 18'/><path d='M12 3c-2.5 2.7-2.5 15.3 0 18'/></svg>",
|
|
62
|
+
metrics: () => {
|
|
63
|
+
return Promise.resolve({
|
|
64
|
+
visits: visitsB,
|
|
65
|
+
server: "node",
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
start: async (port) => {
|
|
69
|
+
const server = createServer((_, res) => {
|
|
70
|
+
visitsB += 1;
|
|
71
|
+
res.statusCode = 200;
|
|
72
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
73
|
+
res.end("Server B\n");
|
|
74
|
+
});
|
|
75
|
+
return server?.listen(port);
|
|
76
|
+
},
|
|
77
|
+
stop: async (server) => {
|
|
78
|
+
await new Promise((resolve, reject) => {
|
|
79
|
+
server?.close((err) => {
|
|
80
|
+
if (err) {
|
|
81
|
+
reject(err);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const port = Number(process.env.PORT) || 3000;
|
|
90
|
+
const server = createServer((_, res) => {
|
|
91
|
+
res.statusCode = 200;
|
|
92
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
93
|
+
res.end("Hello, World!\n");
|
|
94
|
+
});
|
|
95
|
+
server.listen(port);
|
|
96
|
+
console.log(`Demo running on port: ` + port);
|
|
97
|
+
if (process.env.PORTLESS_URL) {
|
|
98
|
+
openInBrowser("http://portmore.localhost:1355");
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=demo.js.map
|
package/dist/demo.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo.js","sourceRoot":"","sources":["../src/demo.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-free-port.d.ts","sourceRoot":"","sources":["../src/find-free-port.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
// From:
|
|
3
|
+
// https://github.com/vercel-labs/portless/blob/28b3f6d807a7c831b80a2a03ab3f38049e5c0653/packages/portless/src/cli-utils.ts#L184
|
|
4
|
+
/** Minimum app port when finding a free port. */
|
|
5
|
+
const MIN_APP_PORT = 4000;
|
|
6
|
+
/** Maximum app port when finding a free port. */
|
|
7
|
+
const MAX_APP_PORT = 4999;
|
|
8
|
+
/** Number of random port attempts before sequential scan. */
|
|
9
|
+
const RANDOM_PORT_ATTEMPTS = 50;
|
|
10
|
+
/**
|
|
11
|
+
* Find a free port in the given range (default 4000-4999).
|
|
12
|
+
* Tries random ports first for speed, then falls back to sequential scan.
|
|
13
|
+
*
|
|
14
|
+
* Note: There is an inherent TOCTOU race between verifying a port is free
|
|
15
|
+
* and the child process actually binding to it. The random-first strategy
|
|
16
|
+
* minimizes the window.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
|
|
20
|
+
if (minPort > maxPort) {
|
|
21
|
+
throw new Error(`minPort (${minPort}) must be <= maxPort (${maxPort})`);
|
|
22
|
+
}
|
|
23
|
+
const tryPort = (port) => {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const server = net.createServer();
|
|
26
|
+
server.listen(port, () => {
|
|
27
|
+
server.close(() => resolve(true));
|
|
28
|
+
});
|
|
29
|
+
server.on("error", () => resolve(false));
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
// Try random ports first
|
|
33
|
+
for (let i = 0; i < RANDOM_PORT_ATTEMPTS; i++) {
|
|
34
|
+
const port = minPort + Math.floor(Math.random() * (maxPort - minPort + 1));
|
|
35
|
+
if (await tryPort(port)) {
|
|
36
|
+
return port;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Fall back to sequential
|
|
40
|
+
for (let port = minPort; port <= maxPort; port++) {
|
|
41
|
+
if (await tryPort(port)) {
|
|
42
|
+
return port;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`No free port found in range ${minPort}-${maxPort}`);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=find-free-port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-free-port.js","sourceRoot":"","sources":["../src/find-free-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAEhC,QAAQ;AACR,gIAAgI;AAEhI,iDAAiD;AACjD,MAAM,YAAY,GAAG,IAAI,CAAC;AAE1B,iDAAiD;AACjD,MAAM,YAAY,GAAG,IAAI,CAAC;AAE1B,6DAA6D;AAC7D,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAO,GAAG,YAAY,EACtB,OAAO,GAAG,YAAY;IAEtB,IAAI,OAAO,GAAG,OAAO,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,YAAY,OAAO,yBAAyB,OAAO,GAAG,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,IAAY,EAAoB,EAAE;QACjD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;gBACvB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YACpC,CAAC,CAAC,CAAC;YACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,yBAAyB;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,oBAAoB,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9C,MAAM,IAAI,GAAG,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3E,IAAI,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,0BAA0B;IAC1B,KAAK,IAAI,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;QACjD,IAAI,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,+BAA+B,OAAO,IAAI,OAAO,EAAE,CAAC,CAAC;AACvE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PortMoreAlias, PortMoreOptions } from "./types/portmore.js";
|
|
2
|
+
export declare const aliases: Array<PortMoreAlias>;
|
|
3
|
+
export declare function startAlias(alias: PortMoreAlias<unknown>): Promise<void>;
|
|
4
|
+
export declare function stopAlias(alias: PortMoreAlias<unknown>): Promise<void>;
|
|
5
|
+
export declare function toggleAlias(alias: PortMoreAlias<unknown>): Promise<void>;
|
|
6
|
+
export declare function portmore<T>(options: PortMoreOptions<T>): Promise<void>;
|
|
7
|
+
export declare function portmore(aliasName: string): Promise<string | undefined>;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE1E,eAAO,MAAM,OAAO,EAAE,KAAK,CAAC,aAAa,CAAM,CAAC;AAEhD,wBAAsB,UAAU,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7E;AAED,wBAAsB,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB5E;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAO9E;AAGD,wBAAsB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAC9E,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { findFreePort } from "./find-free-port.js";
|
|
2
|
+
import { startDashboard } from "./dashboard.js";
|
|
3
|
+
import { portless } from "./portless.js";
|
|
4
|
+
export const aliases = [];
|
|
5
|
+
export async function startAlias(alias) {
|
|
6
|
+
if (alias.status === "Running" || alias.status === "Starting") {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
alias.status = "Starting";
|
|
10
|
+
try {
|
|
11
|
+
alias.server = await alias.start(alias.port);
|
|
12
|
+
alias.started = new Date();
|
|
13
|
+
alias.status = "Running";
|
|
14
|
+
await portless.alias(alias.name, alias.port);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
alias.status = "Error";
|
|
18
|
+
// If alias creation fails after the server started, clean up to avoid orphan listeners.
|
|
19
|
+
if (alias.server !== undefined) {
|
|
20
|
+
try {
|
|
21
|
+
await alias.stop(alias.server);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Preserve original error path; alias is already marked Error.
|
|
25
|
+
}
|
|
26
|
+
alias.server = undefined;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function stopAlias(alias) {
|
|
32
|
+
if (alias.status === "Stopped" || alias.status === "Stopping") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (alias.server === undefined) {
|
|
36
|
+
alias.status = "Stopped";
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
alias.status = "Stopping";
|
|
40
|
+
try {
|
|
41
|
+
await alias.stop(alias.server);
|
|
42
|
+
alias.server = undefined;
|
|
43
|
+
await portless.removeAlias(alias.name);
|
|
44
|
+
alias.status = "Stopped";
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
alias.status = "Error";
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function toggleAlias(alias) {
|
|
52
|
+
if (alias.status === "Running" || alias.status === "Starting") {
|
|
53
|
+
await stopAlias(alias);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await startAlias(alias);
|
|
57
|
+
}
|
|
58
|
+
export async function portmore(optionsOrAliasName) {
|
|
59
|
+
if (typeof optionsOrAliasName === "string") {
|
|
60
|
+
const alias = aliases.find((entry) => entry.name === optionsOrAliasName);
|
|
61
|
+
if (!alias) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return portless.getUrl(alias.name).catch(() => `http://localhost:${alias.port}`);
|
|
65
|
+
}
|
|
66
|
+
const options = optionsOrAliasName;
|
|
67
|
+
const port = await findFreePort();
|
|
68
|
+
const alias = Object.assign({}, { port }, options);
|
|
69
|
+
aliases.push(alias);
|
|
70
|
+
if (aliases.length === 1) {
|
|
71
|
+
await startDashboard();
|
|
72
|
+
}
|
|
73
|
+
await startAlias(alias);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-in-browser.d.ts","sourceRoot":"","sources":["../src/open-in-browser.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/** @internal */
|
|
3
|
+
export function openInBrowser(url) {
|
|
4
|
+
const openCommand = process.platform === "darwin"
|
|
5
|
+
? { command: "open", args: [url] }
|
|
6
|
+
: process.platform === "win32"
|
|
7
|
+
? { command: "cmd", args: ["/c", "start", "", url] }
|
|
8
|
+
: { command: "xdg-open", args: [url] };
|
|
9
|
+
const child = spawn(openCommand.command, openCommand.args, {
|
|
10
|
+
detached: true,
|
|
11
|
+
stdio: "ignore",
|
|
12
|
+
});
|
|
13
|
+
child.on("error", () => {
|
|
14
|
+
// Ignore failures to open a browser in demo mode.
|
|
15
|
+
});
|
|
16
|
+
child.unref();
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=open-in-browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open-in-browser.js","sourceRoot":"","sources":["../src/open-in-browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,gBAAgB;AAChB,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,WAAW,GACf,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC3B,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE;QAClC,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC5B,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE;YACpD,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IAE7C,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,EAAE;QACzD,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,QAAQ;KAChB,CAAC,CAAC;IAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QACrB,kDAAkD;IACpD,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portless.d.ts","sourceRoot":"","sources":["../src/portless.ts"],"names":[],"mappings":""}
|
package/dist/portless.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/** @internal */
|
|
5
|
+
export const portless = {
|
|
6
|
+
async alias(name, port) {
|
|
7
|
+
await execFileAsync("portless", ["alias", name, String(port)]);
|
|
8
|
+
},
|
|
9
|
+
async getUrl(name) {
|
|
10
|
+
const { stdout } = await execFileAsync("portless", ["get", name, "--no-worktree"]);
|
|
11
|
+
return stdout.trim();
|
|
12
|
+
},
|
|
13
|
+
async removeAlias(name) {
|
|
14
|
+
await execFileAsync("portless", ["alias", "--remove", name]);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=portless.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portless.js","sourceRoot":"","sources":["../src/portless.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,gBAAgB;AAChB,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,IAAY;QACpC,MAAM,aAAa,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,aAAa,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC;QACnF,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAY;QAC5B,MAAM,aAAa,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC;IAC/D,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portmore.d.ts","sourceRoot":"","sources":["../../src/types/portmore.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,CAAC,CAAC,IAAI;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACpC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portmore.js","sourceRoot":"","sources":["../../src/types/portmore.ts"],"names":[],"mappings":""}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "portmore",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A portless process wrapper that assigns and aliases free ports for local in process service orchestration.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/allanhvam/portmore.git"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"docs",
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"dev": "npm run build && portless run --name demo node dist/demo.js",
|
|
25
|
+
"lint": "eslint .",
|
|
26
|
+
"lint:fix": "eslint . --fix",
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"test": "echo \"No tests yet\""
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"portless",
|
|
32
|
+
"ports",
|
|
33
|
+
"local-development",
|
|
34
|
+
"service-orchestration",
|
|
35
|
+
"dashboard"
|
|
36
|
+
],
|
|
37
|
+
"author": "Allan Hvam",
|
|
38
|
+
"license": "ISC",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "^10.0.1",
|
|
41
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
42
|
+
"@types/node": "^25.4.0",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
44
|
+
"@typescript-eslint/parser": "^8.57.0",
|
|
45
|
+
"eslint": "^10.0.3",
|
|
46
|
+
"globals": "^17.4.0",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@hono/node-server": "^1.19.11",
|
|
54
|
+
"hono": "^4.12.7"
|
|
55
|
+
}
|
|
56
|
+
}
|