shiplocal 0.1.5 → 0.1.6
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/dist/index.js +545 -95
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,121 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
3
7
|
var __export = (target, all) => {
|
|
4
8
|
for (var name in all)
|
|
5
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
10
|
};
|
|
7
11
|
|
|
12
|
+
// src/env-rewrite.ts
|
|
13
|
+
var env_rewrite_exports = {};
|
|
14
|
+
__export(env_rewrite_exports, {
|
|
15
|
+
runEnvRewrite: () => runEnvRewrite
|
|
16
|
+
});
|
|
17
|
+
import { copyFile, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
18
|
+
import { join as join2 } from "node:path";
|
|
19
|
+
function localhostPattern(port) {
|
|
20
|
+
return new RegExp(`https?://(?:127\\.0\\.0\\.1|localhost):${String(port)}(?:/|$)`, "i");
|
|
21
|
+
}
|
|
22
|
+
function findReplacements(file, content, port, publicUrl, siblingUrls) {
|
|
23
|
+
const replacements = [];
|
|
24
|
+
const lines = content.split("\n");
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
28
|
+
const eqIndex = trimmed.indexOf("=");
|
|
29
|
+
if (eqIndex === -1) continue;
|
|
30
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
31
|
+
const value = trimmed.slice(eqIndex + 1).trim().replace(/^["']|["']$/g, "");
|
|
32
|
+
if (!ENV_KEY_PATTERN.test(key)) continue;
|
|
33
|
+
if (localhostPattern(port).test(value)) {
|
|
34
|
+
replacements.push({ file, key, from: value, to: publicUrl });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
for (const sibling of siblingUrls) {
|
|
38
|
+
if (localhostPattern(sibling.port).test(value)) {
|
|
39
|
+
replacements.push({ file, key, from: value, to: sibling.publicUrl });
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return replacements;
|
|
45
|
+
}
|
|
46
|
+
function applyReplacements(content, replacements) {
|
|
47
|
+
let updated = content;
|
|
48
|
+
for (const replacement of replacements) {
|
|
49
|
+
const pattern = new RegExp(
|
|
50
|
+
`^(${replacement.key}\\s*=\\s*["']?)${replacement.from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(["']?)`,
|
|
51
|
+
"m"
|
|
52
|
+
);
|
|
53
|
+
updated = updated.replace(pattern, `$1${replacement.to}$2`);
|
|
54
|
+
}
|
|
55
|
+
return updated;
|
|
56
|
+
}
|
|
57
|
+
async function runEnvRewrite(options) {
|
|
58
|
+
const allReplacements = [];
|
|
59
|
+
for (const fileName of ENV_FILES) {
|
|
60
|
+
const filePath = join2(process.cwd(), fileName);
|
|
61
|
+
try {
|
|
62
|
+
const content = await readFile2(filePath, "utf8");
|
|
63
|
+
allReplacements.push(
|
|
64
|
+
...findReplacements(
|
|
65
|
+
fileName,
|
|
66
|
+
content,
|
|
67
|
+
options.port,
|
|
68
|
+
options.publicUrl,
|
|
69
|
+
options.siblingUrls
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (allReplacements.length === 0) {
|
|
76
|
+
if (options.write) {
|
|
77
|
+
console.log("No matching localhost env vars found to rewrite.");
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log("");
|
|
82
|
+
console.log(options.write ? "Applying .env updates:" : "Suggested .env updates:");
|
|
83
|
+
for (const replacement of allReplacements) {
|
|
84
|
+
console.log(` ${replacement.file}: ${replacement.key}`);
|
|
85
|
+
console.log(` ${replacement.from}`);
|
|
86
|
+
console.log(` \u2192 ${replacement.to}`);
|
|
87
|
+
}
|
|
88
|
+
if (!options.write) {
|
|
89
|
+
console.log("");
|
|
90
|
+
console.log("Run with --rewrite-env to apply these changes.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const replacement of allReplacements) {
|
|
95
|
+
const list = byFile.get(replacement.file) ?? [];
|
|
96
|
+
list.push(replacement);
|
|
97
|
+
byFile.set(replacement.file, list);
|
|
98
|
+
}
|
|
99
|
+
for (const [fileName, replacements] of byFile) {
|
|
100
|
+
const filePath = join2(process.cwd(), fileName);
|
|
101
|
+
const content = await readFile2(filePath, "utf8");
|
|
102
|
+
const backupPath = `${filePath}.shiplocal.bak`;
|
|
103
|
+
await copyFile(filePath, backupPath);
|
|
104
|
+
const updated = applyReplacements(content, replacements);
|
|
105
|
+
await writeFile2(filePath, updated, "utf8");
|
|
106
|
+
console.log(` Updated ${fileName} (backup: ${fileName}.shiplocal.bak)`);
|
|
107
|
+
}
|
|
108
|
+
console.log("");
|
|
109
|
+
}
|
|
110
|
+
var ENV_FILES, ENV_KEY_PATTERN;
|
|
111
|
+
var init_env_rewrite = __esm({
|
|
112
|
+
"src/env-rewrite.ts"() {
|
|
113
|
+
"use strict";
|
|
114
|
+
ENV_FILES = [".env", ".env.local", ".env.development"];
|
|
115
|
+
ENV_KEY_PATTERN = /^(NEXT_PUBLIC_|VITE_|REACT_APP_|PUBLIC_|API_|BACKEND_|FRONTEND_)/;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
8
119
|
// src/index.ts
|
|
9
120
|
import { createInterface } from "node:readline/promises";
|
|
10
121
|
import { stdin as input, stdout as output } from "node:process";
|
|
@@ -4061,8 +4172,20 @@ var loginSchema = external_exports.object({
|
|
|
4061
4172
|
email: external_exports.string().email(),
|
|
4062
4173
|
password: external_exports.string().min(1).max(128)
|
|
4063
4174
|
});
|
|
4175
|
+
var forgotPasswordSchema = external_exports.object({
|
|
4176
|
+
email: external_exports.string().email()
|
|
4177
|
+
});
|
|
4178
|
+
var resetPasswordSchema = external_exports.object({
|
|
4179
|
+
token: external_exports.string().min(1),
|
|
4180
|
+
password: external_exports.string().min(8).max(128)
|
|
4181
|
+
});
|
|
4182
|
+
var changePasswordSchema = external_exports.object({
|
|
4183
|
+
currentPassword: external_exports.string().min(1).max(128).optional(),
|
|
4184
|
+
newPassword: external_exports.string().min(8).max(128)
|
|
4185
|
+
});
|
|
4064
4186
|
var createProjectSchema = external_exports.object({
|
|
4065
|
-
name: external_exports.string().min(1).max(100)
|
|
4187
|
+
name: external_exports.string().min(1).max(100),
|
|
4188
|
+
slug: external_exports.string().min(2).max(48).optional()
|
|
4066
4189
|
});
|
|
4067
4190
|
|
|
4068
4191
|
// ../shared/dist/constants.js
|
|
@@ -4073,11 +4196,19 @@ var TUNNEL_WS_PATH = "/tunnel";
|
|
|
4073
4196
|
var DEFAULT_TUNNEL_EXPIRY_MS = 2 * 60 * 60 * 1e3;
|
|
4074
4197
|
var MAX_BODY_BYTES = 50 * 1024 * 1024;
|
|
4075
4198
|
var headersSchema = external_exports.record(external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]));
|
|
4199
|
+
var siblingTunnelSchema = external_exports.object({
|
|
4200
|
+
name: external_exports.string(),
|
|
4201
|
+
publicUrl: external_exports.string().url(),
|
|
4202
|
+
port: external_exports.number().int()
|
|
4203
|
+
});
|
|
4076
4204
|
var registerMessageSchema = external_exports.object({
|
|
4077
4205
|
type: external_exports.literal("register"),
|
|
4078
4206
|
localPort: external_exports.number().int().min(1).max(65535),
|
|
4079
4207
|
token: external_exports.string().optional(),
|
|
4080
4208
|
projectId: external_exports.string().optional(),
|
|
4209
|
+
projectSlug: external_exports.string().optional(),
|
|
4210
|
+
targetName: external_exports.string().optional(),
|
|
4211
|
+
tunnelId: external_exports.string().optional(),
|
|
4081
4212
|
password: external_exports.string().min(4).max(128).optional()
|
|
4082
4213
|
});
|
|
4083
4214
|
var registeredMessageSchema = external_exports.object({
|
|
@@ -4085,7 +4216,10 @@ var registeredMessageSchema = external_exports.object({
|
|
|
4085
4216
|
tunnelId: external_exports.string(),
|
|
4086
4217
|
subdomain: external_exports.string(),
|
|
4087
4218
|
publicUrl: external_exports.string().url(),
|
|
4088
|
-
expiresAt: external_exports.string()
|
|
4219
|
+
expiresAt: external_exports.string(),
|
|
4220
|
+
projectSlug: external_exports.string().optional(),
|
|
4221
|
+
targetName: external_exports.string().optional(),
|
|
4222
|
+
siblingUrls: external_exports.array(siblingTunnelSchema).optional()
|
|
4089
4223
|
});
|
|
4090
4224
|
var pingMessageSchema = external_exports.object({ type: external_exports.literal("ping") });
|
|
4091
4225
|
var pongMessageSchema = external_exports.object({ type: external_exports.literal("pong") });
|
|
@@ -4105,6 +4239,24 @@ var tunnelResponseMessageSchema = external_exports.object({
|
|
|
4105
4239
|
headers: headersSchema,
|
|
4106
4240
|
body: external_exports.string().optional()
|
|
4107
4241
|
});
|
|
4242
|
+
var tunnelWebSocketOpenMessageSchema = external_exports.object({
|
|
4243
|
+
type: external_exports.literal("ws-open"),
|
|
4244
|
+
id: external_exports.string(),
|
|
4245
|
+
path: external_exports.string(),
|
|
4246
|
+
query: external_exports.string(),
|
|
4247
|
+
headers: headersSchema
|
|
4248
|
+
});
|
|
4249
|
+
var tunnelWebSocketMessageSchema = external_exports.object({
|
|
4250
|
+
type: external_exports.literal("ws-message"),
|
|
4251
|
+
id: external_exports.string(),
|
|
4252
|
+
body: external_exports.string()
|
|
4253
|
+
});
|
|
4254
|
+
var tunnelWebSocketCloseMessageSchema = external_exports.object({
|
|
4255
|
+
type: external_exports.literal("ws-close"),
|
|
4256
|
+
id: external_exports.string(),
|
|
4257
|
+
code: external_exports.number().int().optional(),
|
|
4258
|
+
reason: external_exports.string().optional()
|
|
4259
|
+
});
|
|
4108
4260
|
var errorMessageSchema = external_exports.object({
|
|
4109
4261
|
type: external_exports.literal("error"),
|
|
4110
4262
|
message: external_exports.string()
|
|
@@ -4120,6 +4272,9 @@ var tunnelMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
4120
4272
|
pongMessageSchema,
|
|
4121
4273
|
tunnelRequestMessageSchema,
|
|
4122
4274
|
tunnelResponseMessageSchema,
|
|
4275
|
+
tunnelWebSocketOpenMessageSchema,
|
|
4276
|
+
tunnelWebSocketMessageSchema,
|
|
4277
|
+
tunnelWebSocketCloseMessageSchema,
|
|
4123
4278
|
errorMessageSchema,
|
|
4124
4279
|
terminatedMessageSchema
|
|
4125
4280
|
]);
|
|
@@ -4153,12 +4308,22 @@ var healthResponseSchema = external_exports.object({
|
|
|
4153
4308
|
timestamp: external_exports.string()
|
|
4154
4309
|
});
|
|
4155
4310
|
|
|
4311
|
+
// ../shared/dist/slug.js
|
|
4312
|
+
function normalizeTargetName(name) {
|
|
4313
|
+
return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 32);
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4156
4316
|
// ../tunnel-client/dist/client.js
|
|
4157
4317
|
import { WebSocket } from "ws";
|
|
4158
4318
|
|
|
4159
4319
|
// ../tunnel-client/dist/local-proxy.js
|
|
4160
4320
|
import http from "node:http";
|
|
4321
|
+
import { promisify } from "node:util";
|
|
4322
|
+
import { brotliCompress, constants as zlibConstants, gzip } from "node:zlib";
|
|
4161
4323
|
var LOOPBACK_HOSTS = ["127.0.0.1", "::1"];
|
|
4324
|
+
var gzipAsync = promisify(gzip);
|
|
4325
|
+
var brotliCompressAsync = promisify(brotliCompress);
|
|
4326
|
+
var MIN_COMPRESS_BYTES = 1024;
|
|
4162
4327
|
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
4163
4328
|
"connection",
|
|
4164
4329
|
"keep-alive",
|
|
@@ -4169,11 +4334,19 @@ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
|
4169
4334
|
"transfer-encoding",
|
|
4170
4335
|
"upgrade"
|
|
4171
4336
|
]);
|
|
4172
|
-
var STRIP_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
4173
|
-
|
|
4174
|
-
"
|
|
4175
|
-
"
|
|
4176
|
-
|
|
4337
|
+
var STRIP_RESPONSE_HEADERS = /* @__PURE__ */ new Set([...HOP_BY_HOP_HEADERS, "content-length"]);
|
|
4338
|
+
var COMPRESSIBLE_CONTENT_TYPES = [
|
|
4339
|
+
"application/javascript",
|
|
4340
|
+
"application/json",
|
|
4341
|
+
"application/manifest+json",
|
|
4342
|
+
"application/rss+xml",
|
|
4343
|
+
"application/vnd.apple.mpegurl",
|
|
4344
|
+
"application/wasm",
|
|
4345
|
+
"application/x-javascript",
|
|
4346
|
+
"application/xml",
|
|
4347
|
+
"image/svg+xml",
|
|
4348
|
+
"text/"
|
|
4349
|
+
];
|
|
4177
4350
|
function isConnectionRefused(err) {
|
|
4178
4351
|
return err instanceof Error && "code" in err && err.code === "ECONNREFUSED";
|
|
4179
4352
|
}
|
|
@@ -4181,7 +4354,7 @@ function sanitizeRequestHeaders(headers, localPort) {
|
|
|
4181
4354
|
const result = {};
|
|
4182
4355
|
for (const [key, value] of Object.entries(headers)) {
|
|
4183
4356
|
const lower = key.toLowerCase();
|
|
4184
|
-
if (HOP_BY_HOP_HEADERS.has(lower) || lower === "host"
|
|
4357
|
+
if (HOP_BY_HOP_HEADERS.has(lower) || lower === "host") {
|
|
4185
4358
|
continue;
|
|
4186
4359
|
}
|
|
4187
4360
|
result[key] = value;
|
|
@@ -4201,6 +4374,73 @@ function sanitizeResponseHeaders(headers) {
|
|
|
4201
4374
|
}
|
|
4202
4375
|
return result;
|
|
4203
4376
|
}
|
|
4377
|
+
function headerValue(value) {
|
|
4378
|
+
if (value === void 0)
|
|
4379
|
+
return void 0;
|
|
4380
|
+
return Array.isArray(value) ? value[0] : value;
|
|
4381
|
+
}
|
|
4382
|
+
function getHeader(headers, name) {
|
|
4383
|
+
const lowerName = name.toLowerCase();
|
|
4384
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4385
|
+
if (key.toLowerCase() === lowerName)
|
|
4386
|
+
return headerValue(value);
|
|
4387
|
+
}
|
|
4388
|
+
return void 0;
|
|
4389
|
+
}
|
|
4390
|
+
function mergeVary(existing, value) {
|
|
4391
|
+
if (!existing)
|
|
4392
|
+
return value;
|
|
4393
|
+
const parts = existing.split(",").map((part) => part.trim().toLowerCase());
|
|
4394
|
+
if (parts.includes(value.toLowerCase()))
|
|
4395
|
+
return existing;
|
|
4396
|
+
return `${existing}, ${value}`;
|
|
4397
|
+
}
|
|
4398
|
+
function setHeader(headers, name, value) {
|
|
4399
|
+
const next = Object.fromEntries(Object.entries(headers).filter(([key]) => key.toLowerCase() !== name.toLowerCase()));
|
|
4400
|
+
next[name] = value;
|
|
4401
|
+
return next;
|
|
4402
|
+
}
|
|
4403
|
+
function stripHeader(headers, name) {
|
|
4404
|
+
return Object.fromEntries(Object.entries(headers).filter(([key]) => key.toLowerCase() !== name.toLowerCase()));
|
|
4405
|
+
}
|
|
4406
|
+
function isCompressibleContentType(contentType) {
|
|
4407
|
+
if (!contentType)
|
|
4408
|
+
return false;
|
|
4409
|
+
const normalized = contentType.toLowerCase();
|
|
4410
|
+
if (normalized.includes("text/html"))
|
|
4411
|
+
return false;
|
|
4412
|
+
return COMPRESSIBLE_CONTENT_TYPES.some((type) => normalized.includes(type));
|
|
4413
|
+
}
|
|
4414
|
+
async function maybeCompressResponse(body, responseHeaders, requestHeaders) {
|
|
4415
|
+
const headers = sanitizeResponseHeaders(responseHeaders);
|
|
4416
|
+
if (body.length < MIN_COMPRESS_BYTES || getHeader(responseHeaders, "content-encoding") || getHeader(responseHeaders, "content-range") || !isCompressibleContentType(getHeader(responseHeaders, "content-type"))) {
|
|
4417
|
+
return { body, headers };
|
|
4418
|
+
}
|
|
4419
|
+
const acceptEncoding = getHeader(requestHeaders, "accept-encoding") ?? "";
|
|
4420
|
+
if (/\bbr\b/.test(acceptEncoding)) {
|
|
4421
|
+
const compressed = await brotliCompressAsync(body, {
|
|
4422
|
+
params: {
|
|
4423
|
+
[zlibConstants.BROTLI_PARAM_QUALITY]: 4
|
|
4424
|
+
}
|
|
4425
|
+
});
|
|
4426
|
+
const vary = mergeVary(getHeader(headers, "vary"), "Accept-Encoding");
|
|
4427
|
+
const compressedHeaders = setHeader(stripHeader(headers, "etag"), "content-encoding", "br");
|
|
4428
|
+
return {
|
|
4429
|
+
body: compressed,
|
|
4430
|
+
headers: setHeader(compressedHeaders, "vary", vary)
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
if (/\bgzip\b/.test(acceptEncoding)) {
|
|
4434
|
+
const compressed = await gzipAsync(body, { level: 6 });
|
|
4435
|
+
const vary = mergeVary(getHeader(headers, "vary"), "Accept-Encoding");
|
|
4436
|
+
const compressedHeaders = setHeader(stripHeader(headers, "etag"), "content-encoding", "gzip");
|
|
4437
|
+
return {
|
|
4438
|
+
body: compressed,
|
|
4439
|
+
headers: setHeader(compressedHeaders, "vary", vary)
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
return { body, headers };
|
|
4443
|
+
}
|
|
4204
4444
|
function forwardToHost(hostname, localPort, message, body, pathWithQuery) {
|
|
4205
4445
|
return new Promise((resolve, reject) => {
|
|
4206
4446
|
const req = http.request({
|
|
@@ -4222,14 +4462,17 @@ function forwardToHost(hostname, localPort, message, body, pathWithQuery) {
|
|
|
4222
4462
|
chunks.push(chunk);
|
|
4223
4463
|
});
|
|
4224
4464
|
res.on("end", () => {
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4465
|
+
void (async () => {
|
|
4466
|
+
const responseBody = Buffer.concat(chunks);
|
|
4467
|
+
const { body: encodedBody, headers } = await maybeCompressResponse(responseBody, res.headers, message.headers);
|
|
4468
|
+
resolve({
|
|
4469
|
+
type: "response",
|
|
4470
|
+
id: message.id,
|
|
4471
|
+
status: res.statusCode ?? 502,
|
|
4472
|
+
headers,
|
|
4473
|
+
body: encodedBody.length > 0 ? encodeBody(encodedBody) : void 0
|
|
4474
|
+
});
|
|
4475
|
+
})().catch(reject);
|
|
4233
4476
|
});
|
|
4234
4477
|
});
|
|
4235
4478
|
req.on("error", (err) => {
|
|
@@ -4256,7 +4499,10 @@ async function forwardToLocal(localPort, message) {
|
|
|
4256
4499
|
throw err;
|
|
4257
4500
|
}
|
|
4258
4501
|
}
|
|
4259
|
-
|
|
4502
|
+
if (lastError instanceof Error) {
|
|
4503
|
+
throw lastError;
|
|
4504
|
+
}
|
|
4505
|
+
throw new Error("Local server unreachable");
|
|
4260
4506
|
}
|
|
4261
4507
|
|
|
4262
4508
|
// ../tunnel-client/dist/client.js
|
|
@@ -4290,16 +4536,65 @@ function rawDataToString(raw) {
|
|
|
4290
4536
|
return Buffer.concat(raw).toString("utf8");
|
|
4291
4537
|
return Buffer.from(raw).toString("utf8");
|
|
4292
4538
|
}
|
|
4539
|
+
function rawDataToBuffer(raw) {
|
|
4540
|
+
if (typeof raw === "string")
|
|
4541
|
+
return Buffer.from(raw, "utf8");
|
|
4542
|
+
if (Buffer.isBuffer(raw))
|
|
4543
|
+
return raw;
|
|
4544
|
+
if (Array.isArray(raw))
|
|
4545
|
+
return Buffer.concat(raw);
|
|
4546
|
+
return Buffer.from(raw);
|
|
4547
|
+
}
|
|
4548
|
+
function sanitizeWebSocketHeaders(headers, localPort) {
|
|
4549
|
+
const result = {};
|
|
4550
|
+
const skip = /* @__PURE__ */ new Set([
|
|
4551
|
+
"connection",
|
|
4552
|
+
"host",
|
|
4553
|
+
"keep-alive",
|
|
4554
|
+
"proxy-authenticate",
|
|
4555
|
+
"proxy-authorization",
|
|
4556
|
+
"sec-websocket-accept",
|
|
4557
|
+
"sec-websocket-extensions",
|
|
4558
|
+
"sec-websocket-key",
|
|
4559
|
+
"sec-websocket-version",
|
|
4560
|
+
"te",
|
|
4561
|
+
"trailers",
|
|
4562
|
+
"transfer-encoding",
|
|
4563
|
+
"upgrade"
|
|
4564
|
+
]);
|
|
4565
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
4566
|
+
const lower = key.toLowerCase();
|
|
4567
|
+
if (skip.has(lower))
|
|
4568
|
+
continue;
|
|
4569
|
+
result[key] = lower === "origin" ? `http://localhost:${String(localPort)}` : value;
|
|
4570
|
+
}
|
|
4571
|
+
result["host"] = `localhost:${String(localPort)}`;
|
|
4572
|
+
return result;
|
|
4573
|
+
}
|
|
4574
|
+
function toLocalWebSocketUrl(localPort, message) {
|
|
4575
|
+
const url = new URL(`ws://localhost:${String(localPort)}`);
|
|
4576
|
+
url.pathname = message.path;
|
|
4577
|
+
url.search = message.query ? `?${message.query}` : "";
|
|
4578
|
+
return url.toString();
|
|
4579
|
+
}
|
|
4580
|
+
function validCloseCode(code) {
|
|
4581
|
+
if (code === void 0)
|
|
4582
|
+
return void 0;
|
|
4583
|
+
return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006 || code >= 3e3 && code <= 4999 ? code : void 0;
|
|
4584
|
+
}
|
|
4293
4585
|
function createTunnelClient(options) {
|
|
4294
4586
|
let ws = null;
|
|
4295
4587
|
let reconnectTimer = null;
|
|
4296
4588
|
let reconnectAttempt = 0;
|
|
4297
4589
|
let intentionalClose = false;
|
|
4298
4590
|
let publicUrl = null;
|
|
4591
|
+
let registeredTunnelId = options.tunnelId ?? null;
|
|
4299
4592
|
let connectPromise = null;
|
|
4300
4593
|
let connectResolve = null;
|
|
4301
4594
|
let connectReject = null;
|
|
4302
4595
|
let hasRegistered = false;
|
|
4596
|
+
const localWebSockets = /* @__PURE__ */ new Map();
|
|
4597
|
+
const pendingLocalWebSocketMessages = /* @__PURE__ */ new Map();
|
|
4303
4598
|
const clearReconnect = () => {
|
|
4304
4599
|
if (reconnectTimer) {
|
|
4305
4600
|
clearTimeout(reconnectTimer);
|
|
@@ -4312,6 +4607,72 @@ function createTunnelClient(options) {
|
|
|
4312
4607
|
connectReject = reject;
|
|
4313
4608
|
});
|
|
4314
4609
|
};
|
|
4610
|
+
const sendControlMessage = (message) => {
|
|
4611
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
4612
|
+
ws.send(JSON.stringify(message));
|
|
4613
|
+
}
|
|
4614
|
+
};
|
|
4615
|
+
const closeLocalWebSockets = () => {
|
|
4616
|
+
for (const socket of localWebSockets.values()) {
|
|
4617
|
+
socket.removeAllListeners();
|
|
4618
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
4619
|
+
socket.close();
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
localWebSockets.clear();
|
|
4623
|
+
};
|
|
4624
|
+
const closeLocalWebSocket = (message) => {
|
|
4625
|
+
const socket = localWebSockets.get(message.id);
|
|
4626
|
+
if (!socket)
|
|
4627
|
+
return;
|
|
4628
|
+
localWebSockets.delete(message.id);
|
|
4629
|
+
pendingLocalWebSocketMessages.delete(message.id);
|
|
4630
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
4631
|
+
socket.close(validCloseCode(message.code), message.reason);
|
|
4632
|
+
}
|
|
4633
|
+
};
|
|
4634
|
+
const openLocalWebSocket = (message) => {
|
|
4635
|
+
const localSocket = new WebSocket(toLocalWebSocketUrl(options.localPort, message), {
|
|
4636
|
+
headers: sanitizeWebSocketHeaders(message.headers, options.localPort),
|
|
4637
|
+
maxPayload: 64 * 1024 * 1024
|
|
4638
|
+
});
|
|
4639
|
+
localWebSockets.set(message.id, localSocket);
|
|
4640
|
+
pendingLocalWebSocketMessages.set(message.id, []);
|
|
4641
|
+
localSocket.on("open", () => {
|
|
4642
|
+
const pendingMessages = pendingLocalWebSocketMessages.get(message.id) ?? [];
|
|
4643
|
+
pendingLocalWebSocketMessages.delete(message.id);
|
|
4644
|
+
for (const pendingMessage of pendingMessages) {
|
|
4645
|
+
localSocket.send(pendingMessage);
|
|
4646
|
+
}
|
|
4647
|
+
});
|
|
4648
|
+
localSocket.on("message", (data) => {
|
|
4649
|
+
sendControlMessage({
|
|
4650
|
+
type: "ws-message",
|
|
4651
|
+
id: message.id,
|
|
4652
|
+
body: encodeBody(rawDataToBuffer(data))
|
|
4653
|
+
});
|
|
4654
|
+
});
|
|
4655
|
+
localSocket.on("close", (code, reason) => {
|
|
4656
|
+
localWebSockets.delete(message.id);
|
|
4657
|
+
pendingLocalWebSocketMessages.delete(message.id);
|
|
4658
|
+
sendControlMessage({
|
|
4659
|
+
type: "ws-close",
|
|
4660
|
+
id: message.id,
|
|
4661
|
+
code,
|
|
4662
|
+
reason: reason.toString("utf8")
|
|
4663
|
+
});
|
|
4664
|
+
});
|
|
4665
|
+
localSocket.on("error", () => {
|
|
4666
|
+
localWebSockets.delete(message.id);
|
|
4667
|
+
pendingLocalWebSocketMessages.delete(message.id);
|
|
4668
|
+
sendControlMessage({
|
|
4669
|
+
type: "ws-close",
|
|
4670
|
+
id: message.id,
|
|
4671
|
+
code: 1011,
|
|
4672
|
+
reason: "Local WebSocket connection failed"
|
|
4673
|
+
});
|
|
4674
|
+
});
|
|
4675
|
+
};
|
|
4315
4676
|
const scheduleReconnect = () => {
|
|
4316
4677
|
if (intentionalClose)
|
|
4317
4678
|
return;
|
|
@@ -4333,6 +4694,7 @@ function createTunnelClient(options) {
|
|
|
4333
4694
|
}
|
|
4334
4695
|
if (message.type === "registered") {
|
|
4335
4696
|
publicUrl = message.publicUrl;
|
|
4697
|
+
registeredTunnelId = message.tunnelId;
|
|
4336
4698
|
reconnectAttempt = 0;
|
|
4337
4699
|
options.onRegistered?.(message);
|
|
4338
4700
|
if (!hasRegistered) {
|
|
@@ -4368,7 +4730,7 @@ function createTunnelClient(options) {
|
|
|
4368
4730
|
if (message.type === "request") {
|
|
4369
4731
|
try {
|
|
4370
4732
|
const response = await forwardToLocal(options.localPort, message);
|
|
4371
|
-
|
|
4733
|
+
sendControlMessage(response);
|
|
4372
4734
|
} catch (err) {
|
|
4373
4735
|
const messageText = formatLocalProxyError(err, options.localPort);
|
|
4374
4736
|
const errorResponse = {
|
|
@@ -4378,8 +4740,26 @@ function createTunnelClient(options) {
|
|
|
4378
4740
|
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
4379
4741
|
body: Buffer.from(messageText).toString("base64")
|
|
4380
4742
|
};
|
|
4381
|
-
|
|
4743
|
+
sendControlMessage(errorResponse);
|
|
4744
|
+
}
|
|
4745
|
+
return;
|
|
4746
|
+
}
|
|
4747
|
+
if (message.type === "ws-open") {
|
|
4748
|
+
openLocalWebSocket(message);
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
if (message.type === "ws-message") {
|
|
4752
|
+
const localSocket = localWebSockets.get(message.id);
|
|
4753
|
+
const body = decodeBody(message.body);
|
|
4754
|
+
if (localSocket?.readyState === WebSocket.OPEN) {
|
|
4755
|
+
localSocket.send(body);
|
|
4756
|
+
} else if (localSocket?.readyState === WebSocket.CONNECTING) {
|
|
4757
|
+
pendingLocalWebSocketMessages.get(message.id)?.push(body);
|
|
4382
4758
|
}
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
if (message.type === "ws-close") {
|
|
4762
|
+
closeLocalWebSocket(message);
|
|
4383
4763
|
}
|
|
4384
4764
|
};
|
|
4385
4765
|
const openConnection = () => {
|
|
@@ -4401,7 +4781,11 @@ function createTunnelClient(options) {
|
|
|
4401
4781
|
type: "register",
|
|
4402
4782
|
localPort: options.localPort,
|
|
4403
4783
|
token: options.token,
|
|
4404
|
-
...options.password ? { password: options.password } : {}
|
|
4784
|
+
...options.password ? { password: options.password } : {},
|
|
4785
|
+
...options.projectSlug ? { projectSlug: options.projectSlug } : {},
|
|
4786
|
+
...options.projectId ? { projectId: options.projectId } : {},
|
|
4787
|
+
...options.targetName ? { targetName: options.targetName } : {},
|
|
4788
|
+
...registeredTunnelId ? { tunnelId: registeredTunnelId } : {}
|
|
4405
4789
|
}));
|
|
4406
4790
|
});
|
|
4407
4791
|
socket.on("message", (data) => {
|
|
@@ -4409,6 +4793,7 @@ function createTunnelClient(options) {
|
|
|
4409
4793
|
});
|
|
4410
4794
|
socket.on("close", () => {
|
|
4411
4795
|
publicUrl = null;
|
|
4796
|
+
closeLocalWebSockets();
|
|
4412
4797
|
options.onDisconnect?.();
|
|
4413
4798
|
if (!intentionalClose && !hasRegistered && connectReject) {
|
|
4414
4799
|
connectReject(new Error("Connection closed before registration"));
|
|
@@ -4438,6 +4823,7 @@ function createTunnelClient(options) {
|
|
|
4438
4823
|
disconnect() {
|
|
4439
4824
|
intentionalClose = true;
|
|
4440
4825
|
clearReconnect();
|
|
4826
|
+
closeLocalWebSockets();
|
|
4441
4827
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
4442
4828
|
ws.close();
|
|
4443
4829
|
}
|
|
@@ -4460,6 +4846,9 @@ import { homedir } from "node:os";
|
|
|
4460
4846
|
import { join } from "node:path";
|
|
4461
4847
|
var CONFIG_DIR = join(homedir(), ".shiplocal");
|
|
4462
4848
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
4849
|
+
function tunnelConfigKey(projectSlug, targetName) {
|
|
4850
|
+
return `${projectSlug}:${targetName}`;
|
|
4851
|
+
}
|
|
4463
4852
|
async function loadConfig() {
|
|
4464
4853
|
try {
|
|
4465
4854
|
const raw = await readFile(CONFIG_FILE, "utf8");
|
|
@@ -4474,12 +4863,26 @@ async function saveConfig(config) {
|
|
|
4474
4863
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
4475
4864
|
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
|
|
4476
4865
|
}
|
|
4866
|
+
async function updateTunnelConfig(projectSlug, targetName, tunnelId, publicUrl) {
|
|
4867
|
+
const config = await loadConfig() ?? { apiUrl: resolveApiUrl(), token: "" };
|
|
4868
|
+
config.defaultProjectSlug = projectSlug;
|
|
4869
|
+
config.tunnels ??= {};
|
|
4870
|
+
config.tunnels[tunnelConfigKey(projectSlug, targetName)] = { tunnelId, publicUrl };
|
|
4871
|
+
await saveConfig(config);
|
|
4872
|
+
}
|
|
4873
|
+
async function getSavedTunnelId(projectSlug, targetName) {
|
|
4874
|
+
const config = await loadConfig();
|
|
4875
|
+
return config?.tunnels?.[tunnelConfigKey(projectSlug, targetName)]?.tunnelId;
|
|
4876
|
+
}
|
|
4477
4877
|
async function clearConfig() {
|
|
4478
4878
|
try {
|
|
4479
4879
|
await writeFile(CONFIG_FILE, "{}", "utf8");
|
|
4480
4880
|
} catch {
|
|
4481
4881
|
}
|
|
4482
4882
|
}
|
|
4883
|
+
function resolveApiUrl() {
|
|
4884
|
+
return process.env["SHIPLOCAL_API_URL"] ?? "http://localhost:4000";
|
|
4885
|
+
}
|
|
4483
4886
|
var DEFAULT_CLOUD_API_URL = "https://shiplocal.cloud";
|
|
4484
4887
|
async function resolveApiUrlAsync() {
|
|
4485
4888
|
if (process.env["SHIPLOCAL_API_URL"]) {
|
|
@@ -4573,7 +4976,7 @@ async function isLocalPortOpen(port, timeoutMs = 2e3) {
|
|
|
4573
4976
|
|
|
4574
4977
|
// src/index.ts
|
|
4575
4978
|
var program = new Command();
|
|
4576
|
-
program.name("shiplocal").description("Share localhost with clients in seconds").version("0.1.
|
|
4979
|
+
program.name("shiplocal").description("Share localhost with clients in seconds").version("0.1.6");
|
|
4577
4980
|
program.command("login").description("Authenticate with ShipLocal Cloud").action(async () => {
|
|
4578
4981
|
const rl = createInterface({ input, output });
|
|
4579
4982
|
const apiUrl = await resolveApiUrlAsync();
|
|
@@ -4604,84 +5007,131 @@ program.command("logout").description("Remove saved credentials").action(async (
|
|
|
4604
5007
|
await clearConfig();
|
|
4605
5008
|
console.log("Logged out.");
|
|
4606
5009
|
});
|
|
4607
|
-
program.argument("[port]", "Local port to expose", String(DEFAULT_TUNNEL_PORT)).option("-p, --password <password>", "Require a password to open the public URL").description("Start a tunnel to your local server").action(
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
5010
|
+
program.argument("[port]", "Local port to expose", String(DEFAULT_TUNNEL_PORT)).option("-p, --password <password>", "Require a password to open the public URL").option("--project <slug>", "Project slug for coordinated multi-target URLs").option("--name <target>", "Target name within project (default: web)", "web").option("--rewrite-env", "Suggest or apply .env URL rewrites for tunnel URLs").description("Start a tunnel to your local server").action(
|
|
5011
|
+
async (portArg, options) => {
|
|
5012
|
+
const port = Number.parseInt(portArg, 10);
|
|
5013
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
5014
|
+
console.error("Error: port must be a number between 1 and 65535");
|
|
5015
|
+
process.exit(1);
|
|
5016
|
+
}
|
|
5017
|
+
const serverUrl = await resolveApiUrlAsync();
|
|
5018
|
+
const token = await resolveToken();
|
|
5019
|
+
if (!token) {
|
|
5020
|
+
console.error("Not authenticated. Run `shiplocal login` first.");
|
|
5021
|
+
process.exit(1);
|
|
5022
|
+
}
|
|
5023
|
+
if (!token.startsWith("sl_")) {
|
|
5024
|
+
console.error("Invalid saved token. Run `shiplocal login` again.");
|
|
5025
|
+
process.exit(1);
|
|
5026
|
+
}
|
|
5027
|
+
const projectSlug = options.project?.toLowerCase();
|
|
5028
|
+
const targetName = normalizeTargetName(options.name);
|
|
5029
|
+
const savedTunnelId = projectSlug !== void 0 ? await getSavedTunnelId(projectSlug, targetName) : void 0;
|
|
5030
|
+
const portOpen = await isLocalPortOpen(port);
|
|
5031
|
+
if (!portOpen) {
|
|
5032
|
+
console.warn("");
|
|
5033
|
+
console.warn(`Warning: nothing is listening on http://localhost:${String(port)}`);
|
|
5034
|
+
console.warn("Start your local app first, or pass the port it uses.");
|
|
5035
|
+
console.warn("Example: pnpm tunnel 3001 (dashboard in this repo runs on 3001)");
|
|
5036
|
+
console.warn("");
|
|
5037
|
+
}
|
|
5038
|
+
let printed = false;
|
|
5039
|
+
const client = createTunnelClient({
|
|
5040
|
+
serverUrl,
|
|
5041
|
+
localPort: port,
|
|
5042
|
+
token,
|
|
5043
|
+
password: options.password,
|
|
5044
|
+
projectSlug,
|
|
5045
|
+
targetName: projectSlug ? targetName : void 0,
|
|
5046
|
+
tunnelId: savedTunnelId,
|
|
5047
|
+
onRegistered: (info) => {
|
|
5048
|
+
if (info.projectSlug && info.targetName) {
|
|
5049
|
+
void updateTunnelConfig(
|
|
5050
|
+
info.projectSlug,
|
|
5051
|
+
info.targetName,
|
|
5052
|
+
info.tunnelId,
|
|
5053
|
+
info.publicUrl
|
|
5054
|
+
);
|
|
4646
5055
|
}
|
|
5056
|
+
if (!printed) {
|
|
5057
|
+
console.log("");
|
|
5058
|
+
console.log("\u{1F680} ShipLocal running");
|
|
5059
|
+
console.log("");
|
|
5060
|
+
console.log(`Local: http://localhost:${String(port)}`);
|
|
5061
|
+
console.log(`Public: ${info.publicUrl}`);
|
|
5062
|
+
if (info.projectSlug) {
|
|
5063
|
+
console.log(`Project: ${info.projectSlug} (${info.targetName ?? targetName})`);
|
|
5064
|
+
}
|
|
5065
|
+
if (info.siblingUrls && info.siblingUrls.length > 0) {
|
|
5066
|
+
console.log("");
|
|
5067
|
+
console.log("Other targets in this project:");
|
|
5068
|
+
for (const sibling of info.siblingUrls) {
|
|
5069
|
+
console.log(
|
|
5070
|
+
` ${sibling.name}: ${sibling.publicUrl} (port ${String(sibling.port)})`
|
|
5071
|
+
);
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
if (options.password) {
|
|
5075
|
+
console.log(`Password: ${options.password} (share with your client)`);
|
|
5076
|
+
}
|
|
5077
|
+
console.log("");
|
|
5078
|
+
console.log("Share this with your client.");
|
|
5079
|
+
console.log("Press Ctrl+C to stop.");
|
|
5080
|
+
printed = true;
|
|
5081
|
+
if (options.rewriteEnv) {
|
|
5082
|
+
void Promise.resolve().then(() => (init_env_rewrite(), env_rewrite_exports)).then(
|
|
5083
|
+
({ runEnvRewrite: runEnvRewrite2 }) => runEnvRewrite2({
|
|
5084
|
+
port,
|
|
5085
|
+
publicUrl: info.publicUrl,
|
|
5086
|
+
siblingUrls: info.siblingUrls ?? [],
|
|
5087
|
+
write: true
|
|
5088
|
+
})
|
|
5089
|
+
);
|
|
5090
|
+
} else if (projectSlug) {
|
|
5091
|
+
void Promise.resolve().then(() => (init_env_rewrite(), env_rewrite_exports)).then(
|
|
5092
|
+
({ runEnvRewrite: runEnvRewrite2 }) => runEnvRewrite2({
|
|
5093
|
+
port,
|
|
5094
|
+
publicUrl: info.publicUrl,
|
|
5095
|
+
siblingUrls: info.siblingUrls ?? [],
|
|
5096
|
+
write: false
|
|
5097
|
+
})
|
|
5098
|
+
);
|
|
5099
|
+
}
|
|
5100
|
+
} else {
|
|
5101
|
+
console.log(`Reconnected: ${info.publicUrl}`);
|
|
5102
|
+
}
|
|
5103
|
+
},
|
|
5104
|
+
onReconnecting: (attempt) => {
|
|
5105
|
+
console.log(`Reconnecting\u2026 (attempt ${String(attempt)})`);
|
|
5106
|
+
},
|
|
5107
|
+
onTerminated: (message) => {
|
|
5108
|
+
console.log("");
|
|
5109
|
+
console.log(message);
|
|
5110
|
+
console.log("Run shiplocal again to start a new tunnel.");
|
|
4647
5111
|
console.log("");
|
|
4648
|
-
console.log("Share this with your client.");
|
|
4649
|
-
console.log("Press Ctrl+C to stop.");
|
|
4650
|
-
printed = true;
|
|
4651
|
-
} else {
|
|
4652
|
-
console.log(`Reconnected: ${info.publicUrl}`);
|
|
4653
5112
|
}
|
|
4654
|
-
},
|
|
4655
|
-
onReconnecting: (attempt) => {
|
|
4656
|
-
console.log(`Reconnecting\u2026 (attempt ${String(attempt)})`);
|
|
4657
|
-
},
|
|
4658
|
-
onTerminated: (message) => {
|
|
4659
|
-
console.log("");
|
|
4660
|
-
console.log(message);
|
|
4661
|
-
console.log("Run shiplocal again to start a new tunnel.");
|
|
4662
|
-
console.log("");
|
|
4663
|
-
}
|
|
4664
|
-
});
|
|
4665
|
-
const shutdown = () => {
|
|
4666
|
-
void client.disconnect().then(() => {
|
|
4667
|
-
process.exit(0);
|
|
4668
5113
|
});
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
"
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
5114
|
+
const shutdown = () => {
|
|
5115
|
+
void client.disconnect().then(() => {
|
|
5116
|
+
process.exit(0);
|
|
5117
|
+
});
|
|
5118
|
+
};
|
|
5119
|
+
process.on("SIGINT", shutdown);
|
|
5120
|
+
process.on("SIGTERM", shutdown);
|
|
5121
|
+
try {
|
|
5122
|
+
await client.connect();
|
|
5123
|
+
} catch (err) {
|
|
5124
|
+
const apiUrl = await resolveApiUrlAsync();
|
|
5125
|
+
const message = err instanceof Error && err.message.includes("ECONNREFUSED") ? [
|
|
5126
|
+
`Cannot reach ShipLocal server at ${apiUrl}.`,
|
|
5127
|
+
"",
|
|
5128
|
+
"Start the API server first:",
|
|
5129
|
+
" pnpm dev"
|
|
5130
|
+
].join("\n") : err instanceof Error ? err.message : String(err);
|
|
5131
|
+
console.error(message);
|
|
5132
|
+
process.exit(1);
|
|
5133
|
+
}
|
|
4684
5134
|
}
|
|
4685
|
-
|
|
5135
|
+
);
|
|
4686
5136
|
program.parse();
|
|
4687
5137
|
//# sourceMappingURL=index.js.map
|