shiplocal 0.1.4 → 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 +558 -90
- 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,10 +4239,32 @@ 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()
|
|
4111
4263
|
});
|
|
4264
|
+
var terminatedMessageSchema = external_exports.object({
|
|
4265
|
+
type: external_exports.literal("terminated"),
|
|
4266
|
+
message: external_exports.string()
|
|
4267
|
+
});
|
|
4112
4268
|
var tunnelMessageSchema = external_exports.discriminatedUnion("type", [
|
|
4113
4269
|
registerMessageSchema,
|
|
4114
4270
|
registeredMessageSchema,
|
|
@@ -4116,7 +4272,11 @@ var tunnelMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
4116
4272
|
pongMessageSchema,
|
|
4117
4273
|
tunnelRequestMessageSchema,
|
|
4118
4274
|
tunnelResponseMessageSchema,
|
|
4119
|
-
|
|
4275
|
+
tunnelWebSocketOpenMessageSchema,
|
|
4276
|
+
tunnelWebSocketMessageSchema,
|
|
4277
|
+
tunnelWebSocketCloseMessageSchema,
|
|
4278
|
+
errorMessageSchema,
|
|
4279
|
+
terminatedMessageSchema
|
|
4120
4280
|
]);
|
|
4121
4281
|
function parseTunnelMessage(data) {
|
|
4122
4282
|
const parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
@@ -4148,12 +4308,22 @@ var healthResponseSchema = external_exports.object({
|
|
|
4148
4308
|
timestamp: external_exports.string()
|
|
4149
4309
|
});
|
|
4150
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
|
+
|
|
4151
4316
|
// ../tunnel-client/dist/client.js
|
|
4152
4317
|
import { WebSocket } from "ws";
|
|
4153
4318
|
|
|
4154
4319
|
// ../tunnel-client/dist/local-proxy.js
|
|
4155
4320
|
import http from "node:http";
|
|
4321
|
+
import { promisify } from "node:util";
|
|
4322
|
+
import { brotliCompress, constants as zlibConstants, gzip } from "node:zlib";
|
|
4156
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;
|
|
4157
4327
|
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
4158
4328
|
"connection",
|
|
4159
4329
|
"keep-alive",
|
|
@@ -4164,11 +4334,19 @@ var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
|
4164
4334
|
"transfer-encoding",
|
|
4165
4335
|
"upgrade"
|
|
4166
4336
|
]);
|
|
4167
|
-
var STRIP_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
4168
|
-
|
|
4169
|
-
"
|
|
4170
|
-
"
|
|
4171
|
-
|
|
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
|
+
];
|
|
4172
4350
|
function isConnectionRefused(err) {
|
|
4173
4351
|
return err instanceof Error && "code" in err && err.code === "ECONNREFUSED";
|
|
4174
4352
|
}
|
|
@@ -4176,7 +4354,7 @@ function sanitizeRequestHeaders(headers, localPort) {
|
|
|
4176
4354
|
const result = {};
|
|
4177
4355
|
for (const [key, value] of Object.entries(headers)) {
|
|
4178
4356
|
const lower = key.toLowerCase();
|
|
4179
|
-
if (HOP_BY_HOP_HEADERS.has(lower) || lower === "host"
|
|
4357
|
+
if (HOP_BY_HOP_HEADERS.has(lower) || lower === "host") {
|
|
4180
4358
|
continue;
|
|
4181
4359
|
}
|
|
4182
4360
|
result[key] = value;
|
|
@@ -4196,6 +4374,73 @@ function sanitizeResponseHeaders(headers) {
|
|
|
4196
4374
|
}
|
|
4197
4375
|
return result;
|
|
4198
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
|
+
}
|
|
4199
4444
|
function forwardToHost(hostname, localPort, message, body, pathWithQuery) {
|
|
4200
4445
|
return new Promise((resolve, reject) => {
|
|
4201
4446
|
const req = http.request({
|
|
@@ -4217,14 +4462,17 @@ function forwardToHost(hostname, localPort, message, body, pathWithQuery) {
|
|
|
4217
4462
|
chunks.push(chunk);
|
|
4218
4463
|
});
|
|
4219
4464
|
res.on("end", () => {
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
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);
|
|
4228
4476
|
});
|
|
4229
4477
|
});
|
|
4230
4478
|
req.on("error", (err) => {
|
|
@@ -4251,7 +4499,10 @@ async function forwardToLocal(localPort, message) {
|
|
|
4251
4499
|
throw err;
|
|
4252
4500
|
}
|
|
4253
4501
|
}
|
|
4254
|
-
|
|
4502
|
+
if (lastError instanceof Error) {
|
|
4503
|
+
throw lastError;
|
|
4504
|
+
}
|
|
4505
|
+
throw new Error("Local server unreachable");
|
|
4255
4506
|
}
|
|
4256
4507
|
|
|
4257
4508
|
// ../tunnel-client/dist/client.js
|
|
@@ -4285,16 +4536,65 @@ function rawDataToString(raw) {
|
|
|
4285
4536
|
return Buffer.concat(raw).toString("utf8");
|
|
4286
4537
|
return Buffer.from(raw).toString("utf8");
|
|
4287
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
|
+
}
|
|
4288
4585
|
function createTunnelClient(options) {
|
|
4289
4586
|
let ws = null;
|
|
4290
4587
|
let reconnectTimer = null;
|
|
4291
4588
|
let reconnectAttempt = 0;
|
|
4292
4589
|
let intentionalClose = false;
|
|
4293
4590
|
let publicUrl = null;
|
|
4591
|
+
let registeredTunnelId = options.tunnelId ?? null;
|
|
4294
4592
|
let connectPromise = null;
|
|
4295
4593
|
let connectResolve = null;
|
|
4296
4594
|
let connectReject = null;
|
|
4297
4595
|
let hasRegistered = false;
|
|
4596
|
+
const localWebSockets = /* @__PURE__ */ new Map();
|
|
4597
|
+
const pendingLocalWebSocketMessages = /* @__PURE__ */ new Map();
|
|
4298
4598
|
const clearReconnect = () => {
|
|
4299
4599
|
if (reconnectTimer) {
|
|
4300
4600
|
clearTimeout(reconnectTimer);
|
|
@@ -4307,6 +4607,72 @@ function createTunnelClient(options) {
|
|
|
4307
4607
|
connectReject = reject;
|
|
4308
4608
|
});
|
|
4309
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
|
+
};
|
|
4310
4676
|
const scheduleReconnect = () => {
|
|
4311
4677
|
if (intentionalClose)
|
|
4312
4678
|
return;
|
|
@@ -4328,6 +4694,7 @@ function createTunnelClient(options) {
|
|
|
4328
4694
|
}
|
|
4329
4695
|
if (message.type === "registered") {
|
|
4330
4696
|
publicUrl = message.publicUrl;
|
|
4697
|
+
registeredTunnelId = message.tunnelId;
|
|
4331
4698
|
reconnectAttempt = 0;
|
|
4332
4699
|
options.onRegistered?.(message);
|
|
4333
4700
|
if (!hasRegistered) {
|
|
@@ -4349,6 +4716,13 @@ function createTunnelClient(options) {
|
|
|
4349
4716
|
}
|
|
4350
4717
|
return;
|
|
4351
4718
|
}
|
|
4719
|
+
if (message.type === "terminated") {
|
|
4720
|
+
intentionalClose = true;
|
|
4721
|
+
publicUrl = null;
|
|
4722
|
+
options.onTerminated?.(message.message);
|
|
4723
|
+
ws?.close();
|
|
4724
|
+
return;
|
|
4725
|
+
}
|
|
4352
4726
|
if (message.type === "ping") {
|
|
4353
4727
|
ws?.send(JSON.stringify({ type: "pong" }));
|
|
4354
4728
|
return;
|
|
@@ -4356,7 +4730,7 @@ function createTunnelClient(options) {
|
|
|
4356
4730
|
if (message.type === "request") {
|
|
4357
4731
|
try {
|
|
4358
4732
|
const response = await forwardToLocal(options.localPort, message);
|
|
4359
|
-
|
|
4733
|
+
sendControlMessage(response);
|
|
4360
4734
|
} catch (err) {
|
|
4361
4735
|
const messageText = formatLocalProxyError(err, options.localPort);
|
|
4362
4736
|
const errorResponse = {
|
|
@@ -4366,8 +4740,26 @@ function createTunnelClient(options) {
|
|
|
4366
4740
|
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
4367
4741
|
body: Buffer.from(messageText).toString("base64")
|
|
4368
4742
|
};
|
|
4369
|
-
|
|
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);
|
|
4370
4758
|
}
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
if (message.type === "ws-close") {
|
|
4762
|
+
closeLocalWebSocket(message);
|
|
4371
4763
|
}
|
|
4372
4764
|
};
|
|
4373
4765
|
const openConnection = () => {
|
|
@@ -4389,7 +4781,11 @@ function createTunnelClient(options) {
|
|
|
4389
4781
|
type: "register",
|
|
4390
4782
|
localPort: options.localPort,
|
|
4391
4783
|
token: options.token,
|
|
4392
|
-
...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 } : {}
|
|
4393
4789
|
}));
|
|
4394
4790
|
});
|
|
4395
4791
|
socket.on("message", (data) => {
|
|
@@ -4397,6 +4793,7 @@ function createTunnelClient(options) {
|
|
|
4397
4793
|
});
|
|
4398
4794
|
socket.on("close", () => {
|
|
4399
4795
|
publicUrl = null;
|
|
4796
|
+
closeLocalWebSockets();
|
|
4400
4797
|
options.onDisconnect?.();
|
|
4401
4798
|
if (!intentionalClose && !hasRegistered && connectReject) {
|
|
4402
4799
|
connectReject(new Error("Connection closed before registration"));
|
|
@@ -4426,6 +4823,7 @@ function createTunnelClient(options) {
|
|
|
4426
4823
|
disconnect() {
|
|
4427
4824
|
intentionalClose = true;
|
|
4428
4825
|
clearReconnect();
|
|
4826
|
+
closeLocalWebSockets();
|
|
4429
4827
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
4430
4828
|
ws.close();
|
|
4431
4829
|
}
|
|
@@ -4448,6 +4846,9 @@ import { homedir } from "node:os";
|
|
|
4448
4846
|
import { join } from "node:path";
|
|
4449
4847
|
var CONFIG_DIR = join(homedir(), ".shiplocal");
|
|
4450
4848
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
4849
|
+
function tunnelConfigKey(projectSlug, targetName) {
|
|
4850
|
+
return `${projectSlug}:${targetName}`;
|
|
4851
|
+
}
|
|
4451
4852
|
async function loadConfig() {
|
|
4452
4853
|
try {
|
|
4453
4854
|
const raw = await readFile(CONFIG_FILE, "utf8");
|
|
@@ -4462,12 +4863,26 @@ async function saveConfig(config) {
|
|
|
4462
4863
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
4463
4864
|
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
|
|
4464
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
|
+
}
|
|
4465
4877
|
async function clearConfig() {
|
|
4466
4878
|
try {
|
|
4467
4879
|
await writeFile(CONFIG_FILE, "{}", "utf8");
|
|
4468
4880
|
} catch {
|
|
4469
4881
|
}
|
|
4470
4882
|
}
|
|
4883
|
+
function resolveApiUrl() {
|
|
4884
|
+
return process.env["SHIPLOCAL_API_URL"] ?? "http://localhost:4000";
|
|
4885
|
+
}
|
|
4471
4886
|
var DEFAULT_CLOUD_API_URL = "https://shiplocal.cloud";
|
|
4472
4887
|
async function resolveApiUrlAsync() {
|
|
4473
4888
|
if (process.env["SHIPLOCAL_API_URL"]) {
|
|
@@ -4561,7 +4976,7 @@ async function isLocalPortOpen(port, timeoutMs = 2e3) {
|
|
|
4561
4976
|
|
|
4562
4977
|
// src/index.ts
|
|
4563
4978
|
var program = new Command();
|
|
4564
|
-
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");
|
|
4565
4980
|
program.command("login").description("Authenticate with ShipLocal Cloud").action(async () => {
|
|
4566
4981
|
const rl = createInterface({ input, output });
|
|
4567
4982
|
const apiUrl = await resolveApiUrlAsync();
|
|
@@ -4592,78 +5007,131 @@ program.command("logout").description("Remove saved credentials").action(async (
|
|
|
4592
5007
|
await clearConfig();
|
|
4593
5008
|
console.log("Logged out.");
|
|
4594
5009
|
});
|
|
4595
|
-
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(
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
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
|
-
|
|
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
|
+
);
|
|
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}`);
|
|
4634
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.");
|
|
4635
5111
|
console.log("");
|
|
4636
|
-
console.log("Share this with your client.");
|
|
4637
|
-
console.log("Press Ctrl+C to stop.");
|
|
4638
|
-
printed = true;
|
|
4639
|
-
} else {
|
|
4640
|
-
console.log(`Reconnected: ${info.publicUrl}`);
|
|
4641
5112
|
}
|
|
4642
|
-
},
|
|
4643
|
-
onReconnecting: (attempt) => {
|
|
4644
|
-
console.log(`Reconnecting\u2026 (attempt ${String(attempt)})`);
|
|
4645
|
-
}
|
|
4646
|
-
});
|
|
4647
|
-
const shutdown = () => {
|
|
4648
|
-
void client.disconnect().then(() => {
|
|
4649
|
-
process.exit(0);
|
|
4650
5113
|
});
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
"
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
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
|
+
}
|
|
4666
5134
|
}
|
|
4667
|
-
|
|
5135
|
+
);
|
|
4668
5136
|
program.parse();
|
|
4669
5137
|
//# sourceMappingURL=index.js.map
|