pinggy 0.5.0 → 0.5.2
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 +2 -0
- package/dist/{chunk-FVLXFHBL.js → chunk-C3ZMLIC4.js} +21 -5
- package/dist/{chunk-YJQC6LQN.js → chunk-LFK74BMU.js} +60 -25
- package/dist/{chunk-HUP6YWH6.js → chunk-WM7I57AA.js} +119 -20
- package/dist/{configStore-TSGRNOE3.js → configStore-HP3NXPH4.js} +13 -3
- package/dist/{daemonChild-E2CORSSB.js → daemonChild-6QA4AVAN.js} +3 -3
- package/dist/index.cjs +243 -50
- package/dist/index.d.cts +157 -3
- package/dist/index.d.ts +157 -3
- package/dist/index.js +51 -4
- package/dist/{main-F4U5R4SW.js → main-K6ZEWZQ7.js} +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -24,6 +24,8 @@ Create secure, shareable tunnels to your localhost and manage them from the comm
|
|
|
24
24
|
## Architecture at a glance
|
|
25
25
|
The CLI runs as two processes. A short-lived foreground process is what you invoke. A long-running daemon owns every tunnel and the `@pinggy/pinggy` SDK. They talk over HTTP and WebSocket on `127.0.0.1`.
|
|
26
26
|
|
|
27
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full breakdown of the two-process model, IPC routes, daemon discovery, session ownership, and filesystem layout.
|
|
28
|
+
|
|
27
29
|
## Requirements
|
|
28
30
|
- Node.js 18+ (recommended). The CLI uses modern ESM and WebSocket features.
|
|
29
31
|
- A network connection that allows outgoing WebSocket/HTTPS traffic.
|
|
@@ -209,11 +209,23 @@ var ForwardingEntryV2Schema = z.object({
|
|
|
209
209
|
address: z.string(),
|
|
210
210
|
type: z.enum([TunnelType.Http, TunnelType.Tcp, TunnelType.Udp, TunnelType.Tls, TunnelType.TlsTcp])
|
|
211
211
|
});
|
|
212
|
+
var OptionalSchema = z.object({
|
|
213
|
+
sniServerName: z.string().optional(),
|
|
214
|
+
ssl: z.boolean().optional(),
|
|
215
|
+
additionalArguments: z.string().optional(),
|
|
216
|
+
serve: z.string().optional(),
|
|
217
|
+
manage: z.string().optional(),
|
|
218
|
+
remoteManagement: z.object({ serverUrl: z.string(), apiKey: z.string() }).optional(),
|
|
219
|
+
noTui: z.boolean().optional()
|
|
220
|
+
}).catchall(z.unknown());
|
|
212
221
|
var TunnelConfigV1Schema = z.object({
|
|
213
222
|
// Meta Info
|
|
214
223
|
version: z.string(),
|
|
215
224
|
name: z.string(),
|
|
216
225
|
configId: z.string(),
|
|
226
|
+
hostKeyCheck: z.boolean().optional(),
|
|
227
|
+
platformValue: z.string().optional(),
|
|
228
|
+
isQRCode: z.boolean().optional(),
|
|
217
229
|
// General tunnel configurations
|
|
218
230
|
serverAddress: z.string().optional(),
|
|
219
231
|
token: z.string().optional(),
|
|
@@ -239,9 +251,11 @@ var TunnelConfigV1Schema = z.object({
|
|
|
239
251
|
httpsOnly: z.boolean().optional(),
|
|
240
252
|
originalRequestUrl: z.boolean().optional(),
|
|
241
253
|
allowPreflight: z.boolean().optional(),
|
|
242
|
-
|
|
243
|
-
optional: z.record(z.string(), z.unknown()).optional()
|
|
254
|
+
optional: OptionalSchema.optional()
|
|
244
255
|
});
|
|
256
|
+
var TUNNEL_CONFIG_V1_KEYS = Object.keys(
|
|
257
|
+
TunnelConfigV1Schema.shape
|
|
258
|
+
);
|
|
245
259
|
var StartV2Schema = z.object({
|
|
246
260
|
tunnelID: z.string().nullable().optional(),
|
|
247
261
|
tunnelConfig: TunnelConfigV1Schema
|
|
@@ -490,11 +504,11 @@ var TunnelOperations = class {
|
|
|
490
504
|
startPromise.catch((err) => {
|
|
491
505
|
logger.error("No-wait startTunnel failed", { tunnelid, err: String(err) });
|
|
492
506
|
});
|
|
493
|
-
return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.serve);
|
|
507
|
+
return this.buildPendingTunnelResponseV2(tunnelid, tunnelConfig, config, config.configId, config.name, config.optional?.serve);
|
|
494
508
|
}
|
|
495
509
|
await startPromise;
|
|
496
510
|
const tunnelPconfig = await this.tunnelManager.getTunnelConfig("", tunnelid);
|
|
497
|
-
return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.serve);
|
|
511
|
+
return this.buildTunnelResponseV2(tunnelid, tunnelPconfig, config, config.configId, config.name, config.optional?.serve);
|
|
498
512
|
} catch (err) {
|
|
499
513
|
if (err instanceof TunnelAlreadyRunningError) {
|
|
500
514
|
return this.error(ErrorCode.TunnelAlreadyRunningError, err, err.message);
|
|
@@ -1491,7 +1505,7 @@ async function ensureDaemonRunning() {
|
|
|
1491
1505
|
const existing = getDaemonInfo();
|
|
1492
1506
|
if (existing) return existing;
|
|
1493
1507
|
if (process.versions.electron) {
|
|
1494
|
-
const { runDaemonChild } = await import("./daemonChild-
|
|
1508
|
+
const { runDaemonChild } = await import("./daemonChild-6QA4AVAN.js");
|
|
1495
1509
|
const handle = await runDaemonChild({
|
|
1496
1510
|
installSignalHandlers: false,
|
|
1497
1511
|
exitOnFailure: false
|
|
@@ -2139,6 +2153,8 @@ export {
|
|
|
2139
2153
|
TunnelWarningCode,
|
|
2140
2154
|
ErrorCode,
|
|
2141
2155
|
isErrorResponse,
|
|
2156
|
+
TunnelConfigV1Schema,
|
|
2157
|
+
TUNNEL_CONFIG_V1_KEYS,
|
|
2142
2158
|
TunnelOperations,
|
|
2143
2159
|
RemoteManagementUnauthorizedError,
|
|
2144
2160
|
buildRemoteManagementWsUrl,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
startDaemon,
|
|
11
11
|
startRemoteManagement,
|
|
12
12
|
stopDaemon
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-C3ZMLIC4.js";
|
|
14
14
|
import {
|
|
15
15
|
readDaemonConfig
|
|
16
16
|
} from "./chunk-MT44NAXX.js";
|
|
@@ -24,15 +24,17 @@ import {
|
|
|
24
24
|
Subcommand,
|
|
25
25
|
deleteConfig,
|
|
26
26
|
findConfig,
|
|
27
|
+
findConfigByName,
|
|
27
28
|
getAutoStartConfigs,
|
|
28
29
|
listSavedConfigs,
|
|
29
30
|
printConfigDetail,
|
|
30
31
|
printConfigList,
|
|
32
|
+
sanitizeName,
|
|
31
33
|
saveConfig,
|
|
32
34
|
updateConfigAutoStart,
|
|
33
35
|
updateTunnelConfig,
|
|
34
36
|
validateName
|
|
35
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-WM7I57AA.js";
|
|
36
38
|
import {
|
|
37
39
|
TunnelManager,
|
|
38
40
|
detachAllTunnelLoggers,
|
|
@@ -274,7 +276,7 @@ import { isIP } from "net";
|
|
|
274
276
|
function parseExtendedOptions(options, config, localServerTls) {
|
|
275
277
|
if (!options) return localServerTls;
|
|
276
278
|
for (const opt of options) {
|
|
277
|
-
const [key, value] = opt.replace(/^"|"$/g, "").split(/:(
|
|
279
|
+
const [key, value] = opt.replace(/^"|"$/g, "").split(/:(.*)/).filter(Boolean);
|
|
278
280
|
switch (key) {
|
|
279
281
|
case "x":
|
|
280
282
|
switch (value) {
|
|
@@ -335,7 +337,7 @@ function parseExtendedOptions(options, config, localServerTls) {
|
|
|
335
337
|
break;
|
|
336
338
|
case "b":
|
|
337
339
|
if (value && value.includes(":")) {
|
|
338
|
-
const [username, password] = value.split(/:(
|
|
340
|
+
const [username, password] = value.split(/:(.*)/);
|
|
339
341
|
if (!config.basicAuth) config.basicAuth = [];
|
|
340
342
|
config.basicAuth.push({ username, password });
|
|
341
343
|
} else {
|
|
@@ -345,7 +347,7 @@ function parseExtendedOptions(options, config, localServerTls) {
|
|
|
345
347
|
break;
|
|
346
348
|
case "a":
|
|
347
349
|
if (value && value.includes(":")) {
|
|
348
|
-
const [key2, val] = value.split(/:(
|
|
350
|
+
const [key2, val] = value.split(/:(.*)/);
|
|
349
351
|
if (!config.headerModification) config.headerModification = [];
|
|
350
352
|
config.headerModification.push({ type: "add", key: key2, value: [val] });
|
|
351
353
|
} else {
|
|
@@ -355,7 +357,7 @@ function parseExtendedOptions(options, config, localServerTls) {
|
|
|
355
357
|
break;
|
|
356
358
|
case "u":
|
|
357
359
|
if (value && value.includes(":")) {
|
|
358
|
-
const [key2, val] = value.split(/:(
|
|
360
|
+
const [key2, val] = value.split(/:(.*)/);
|
|
359
361
|
if (!config.headerModification) config.headerModification = [];
|
|
360
362
|
config.headerModification.push({ type: "update", key: key2, value: [val] });
|
|
361
363
|
} else {
|
|
@@ -757,6 +759,13 @@ function parseToken(finalConfig, explicitToken) {
|
|
|
757
759
|
finalConfig.token = explicitToken;
|
|
758
760
|
}
|
|
759
761
|
}
|
|
762
|
+
function normalizeForwardingToArray(finalConfig, type) {
|
|
763
|
+
const fwd = finalConfig.forwarding;
|
|
764
|
+
if (typeof fwd !== "string") {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
finalConfig.forwarding = fwd.trim() === "" ? [] : [{ type: type || TunnelType.Http, address: fwd.trim() }];
|
|
768
|
+
}
|
|
760
769
|
function parseArgs2(finalConfig, remainingPositionals) {
|
|
761
770
|
let localserverTls = "";
|
|
762
771
|
localserverTls = parseExtendedOptions(remainingPositionals, finalConfig, localserverTls);
|
|
@@ -877,6 +886,7 @@ function buildFinalConfig(values, positionals, baseConfig) {
|
|
|
877
886
|
if (forceFlag || values.force) {
|
|
878
887
|
finalConfig.force = true;
|
|
879
888
|
}
|
|
889
|
+
normalizeForwardingToArray(finalConfig, type);
|
|
880
890
|
parseArgs2(finalConfig, remainingPositionals);
|
|
881
891
|
storeJson(finalConfig, saveconf);
|
|
882
892
|
return finalConfig;
|
|
@@ -1298,7 +1308,7 @@ async function startBackgroundTunnels(configs, values, positionals) {
|
|
|
1298
1308
|
client.close();
|
|
1299
1309
|
}
|
|
1300
1310
|
async function startAutoStartTunnels() {
|
|
1301
|
-
const { getAutoStartConfigs: getAutoStartConfigs2 } = await import("./configStore-
|
|
1311
|
+
const { getAutoStartConfigs: getAutoStartConfigs2 } = await import("./configStore-HP3NXPH4.js");
|
|
1302
1312
|
const configs = getAutoStartConfigs2();
|
|
1303
1313
|
if (configs.length === 0) {
|
|
1304
1314
|
printer_default.warn("No configs marked for auto-start. Use: pinggy config auto <name>");
|
|
@@ -2217,10 +2227,10 @@ function handleConfig(args) {
|
|
|
2217
2227
|
printConfigList();
|
|
2218
2228
|
return;
|
|
2219
2229
|
case ConfigVerb.Show: {
|
|
2220
|
-
const names = requireNames(rest, "config show");
|
|
2230
|
+
const names = clubSpacedName(requireNames(rest, "config show"));
|
|
2221
2231
|
for (const name of names) {
|
|
2222
|
-
const
|
|
2223
|
-
if (
|
|
2232
|
+
const saved = resolveConfig(name, "config show");
|
|
2233
|
+
if (saved) printConfigDetail(saved);
|
|
2224
2234
|
}
|
|
2225
2235
|
return;
|
|
2226
2236
|
}
|
|
@@ -2230,7 +2240,7 @@ function handleConfig(args) {
|
|
|
2230
2240
|
return;
|
|
2231
2241
|
}
|
|
2232
2242
|
case ConfigVerb.Delete: {
|
|
2233
|
-
const names = requireNames(rest, "config delete");
|
|
2243
|
+
const names = clubSpacedName(requireNames(rest, "config delete"));
|
|
2234
2244
|
for (const name of names) {
|
|
2235
2245
|
const deletedName = deleteConfig(name);
|
|
2236
2246
|
if (deletedName) {
|
|
@@ -2247,7 +2257,7 @@ function handleConfig(args) {
|
|
|
2247
2257
|
return;
|
|
2248
2258
|
}
|
|
2249
2259
|
case ConfigVerb.Auto: {
|
|
2250
|
-
const names = requireNames(rest, "config auto");
|
|
2260
|
+
const names = clubSpacedName(requireNames(rest, "config auto"));
|
|
2251
2261
|
for (const name of names) {
|
|
2252
2262
|
const updated = updateConfigAutoStart(name, true);
|
|
2253
2263
|
if (updated) {
|
|
@@ -2259,7 +2269,7 @@ function handleConfig(args) {
|
|
|
2259
2269
|
return;
|
|
2260
2270
|
}
|
|
2261
2271
|
case ConfigVerb.Noauto: {
|
|
2262
|
-
const names = requireNames(rest, "config noauto");
|
|
2272
|
+
const names = clubSpacedName(requireNames(rest, "config noauto"));
|
|
2263
2273
|
for (const name of names) {
|
|
2264
2274
|
const updated = updateConfigAutoStart(name, false);
|
|
2265
2275
|
if (updated) {
|
|
@@ -2270,10 +2280,14 @@ function handleConfig(args) {
|
|
|
2270
2280
|
}
|
|
2271
2281
|
return;
|
|
2272
2282
|
}
|
|
2273
|
-
default:
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2283
|
+
default: {
|
|
2284
|
+
const names = clubSpacedName([verb, ...rest]);
|
|
2285
|
+
for (const name of names) {
|
|
2286
|
+
const saved = resolveConfig(name, "config");
|
|
2287
|
+
if (saved) printConfigDetail(saved);
|
|
2288
|
+
}
|
|
2276
2289
|
return;
|
|
2290
|
+
}
|
|
2277
2291
|
}
|
|
2278
2292
|
}
|
|
2279
2293
|
function handleConfigSave(name, remainingArgs) {
|
|
@@ -2291,7 +2305,7 @@ function handleConfigSave(name, remainingArgs) {
|
|
|
2291
2305
|
printer_default.success(`Config "${name}" saved.`);
|
|
2292
2306
|
}
|
|
2293
2307
|
function handleConfigUpdate(nameOrId, remainingArgs) {
|
|
2294
|
-
const saved = resolveConfig(nameOrId);
|
|
2308
|
+
const saved = resolveConfig(nameOrId, "config update");
|
|
2295
2309
|
if (!saved) return;
|
|
2296
2310
|
const { values, positionals } = parseCliArgs(cliOptions, remainingArgs);
|
|
2297
2311
|
logger.debug("Building updated config", { nameOrId, values, positionals });
|
|
@@ -2325,7 +2339,7 @@ async function handleStart(args) {
|
|
|
2325
2339
|
return;
|
|
2326
2340
|
}
|
|
2327
2341
|
const resolved = [];
|
|
2328
|
-
for (const name of names) {
|
|
2342
|
+
for (const name of clubSpacedName(names)) {
|
|
2329
2343
|
const saved = resolveConfig(name);
|
|
2330
2344
|
if (!saved) return;
|
|
2331
2345
|
resolved.push(saved);
|
|
@@ -2350,13 +2364,34 @@ async function handleStart(args) {
|
|
|
2350
2364
|
await startMultipleForegroundViaDaemon(resolved, values, positionals);
|
|
2351
2365
|
}
|
|
2352
2366
|
}
|
|
2353
|
-
function
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2367
|
+
function clubSpacedName(names) {
|
|
2368
|
+
if (names.length > 1) {
|
|
2369
|
+
const joined = names.join(" ");
|
|
2370
|
+
if (findConfig(joined) ?? findConfigByName(joined)) {
|
|
2371
|
+
return [joined];
|
|
2372
|
+
}
|
|
2358
2373
|
}
|
|
2359
|
-
return
|
|
2374
|
+
return names;
|
|
2375
|
+
}
|
|
2376
|
+
function resolveConfig(nameOrId, command = "start") {
|
|
2377
|
+
const saved = findConfig(nameOrId) ?? findConfigByName(nameOrId);
|
|
2378
|
+
if (saved) return saved;
|
|
2379
|
+
const sanitizedQuery = sanitizeName(nameOrId);
|
|
2380
|
+
const suggestions = listSavedConfigs().filter(
|
|
2381
|
+
(c) => c.name !== nameOrId && sanitizeName(c.name) === sanitizedQuery
|
|
2382
|
+
);
|
|
2383
|
+
printer_default.error(`No config found matching "${nameOrId}".`);
|
|
2384
|
+
if (suggestions.length > 0) {
|
|
2385
|
+
for (const c of suggestions.slice(0, 3)) {
|
|
2386
|
+
printer_default.info(` Did you mean "${c.name}"? \u2192 pinggy ${command} "${c.name}"`);
|
|
2387
|
+
}
|
|
2388
|
+
if (suggestions.length > 3) {
|
|
2389
|
+
printer_default.info(` \u2026and ${suggestions.length - 3} more. Use: pinggy config list`);
|
|
2390
|
+
}
|
|
2391
|
+
} else {
|
|
2392
|
+
printer_default.info(" Use: pinggy config list");
|
|
2393
|
+
}
|
|
2394
|
+
return null;
|
|
2360
2395
|
}
|
|
2361
2396
|
function requireName(args, command) {
|
|
2362
2397
|
if (args.length === 0 || args[0].startsWith("-")) {
|
|
@@ -3364,7 +3399,7 @@ async function main() {
|
|
|
3364
3399
|
const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
|
|
3365
3400
|
configureLogger(values);
|
|
3366
3401
|
if (values["_daemon-child"]) {
|
|
3367
|
-
const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-
|
|
3402
|
+
const { runDaemonChild: runDaemonChild2 } = await import("./daemonChild-6QA4AVAN.js");
|
|
3368
3403
|
await runDaemonChild2();
|
|
3369
3404
|
return;
|
|
3370
3405
|
}
|
|
@@ -10,6 +10,14 @@ import {
|
|
|
10
10
|
import fs from "fs";
|
|
11
11
|
import path from "path";
|
|
12
12
|
import pico from "picocolors";
|
|
13
|
+
var storageLogger = {
|
|
14
|
+
info: (message, ...meta) => logger.info(message, ...meta),
|
|
15
|
+
warn: (message, ...meta) => logger.warn(message, ...meta),
|
|
16
|
+
error: (message, ...meta) => logger.error(message, ...meta)
|
|
17
|
+
};
|
|
18
|
+
function configureStorageLogger(l) {
|
|
19
|
+
storageLogger = l;
|
|
20
|
+
}
|
|
13
21
|
function buildFilename(name, configId) {
|
|
14
22
|
return `${name}_${configId}.json`;
|
|
15
23
|
}
|
|
@@ -41,7 +49,7 @@ var ConfigVerb = {
|
|
|
41
49
|
var SUBCOMMANDS = Object.values(Subcommand);
|
|
42
50
|
var CONFIG_VERBS = Object.values(ConfigVerb);
|
|
43
51
|
var RESERVED_NAMES = /* @__PURE__ */ new Set([...SUBCOMMANDS, ...CONFIG_VERBS]);
|
|
44
|
-
function
|
|
52
|
+
function validateNameStrict(name) {
|
|
45
53
|
if (!name || name.trim().length === 0) {
|
|
46
54
|
return new Error("Tunnel name cannot be empty.");
|
|
47
55
|
}
|
|
@@ -56,17 +64,39 @@ function validateName(name) {
|
|
|
56
64
|
}
|
|
57
65
|
return null;
|
|
58
66
|
}
|
|
67
|
+
function validateNameForStorage(name) {
|
|
68
|
+
if (!name || name.trim().length === 0) {
|
|
69
|
+
return new Error("Tunnel name cannot be empty.");
|
|
70
|
+
}
|
|
71
|
+
if (/[\u0000-\u001F\/\\]/.test(name)) {
|
|
72
|
+
return new Error("Tunnel name cannot contain path separators or control characters.");
|
|
73
|
+
}
|
|
74
|
+
if (RESERVED_NAMES.has(name.toLowerCase())) {
|
|
75
|
+
return new Error(`"${name}" is a reserved subcommand name. Use a different name.`);
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
var validateName = validateNameStrict;
|
|
59
80
|
function readConfigFile(filePath) {
|
|
60
81
|
try {
|
|
61
82
|
const data = fs.readFileSync(filePath, { encoding: "utf-8" });
|
|
62
83
|
return JSON.parse(data);
|
|
63
84
|
} catch (err) {
|
|
64
|
-
|
|
85
|
+
storageLogger.warn(`Failed to read config file ${filePath}: ${String(err)}`);
|
|
65
86
|
return null;
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
function writeConfigFile(filePath, config) {
|
|
69
|
-
|
|
90
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
91
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2), { encoding: "utf-8" });
|
|
92
|
+
fs.renameSync(tmp, filePath);
|
|
93
|
+
}
|
|
94
|
+
function nextTimestamp(prev) {
|
|
95
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
96
|
+
if (prev && ts <= prev) {
|
|
97
|
+
return new Date(new Date(prev).getTime() + 1).toISOString();
|
|
98
|
+
}
|
|
99
|
+
return ts;
|
|
70
100
|
}
|
|
71
101
|
function listSavedConfigs() {
|
|
72
102
|
const dir = getTunnelConfigDir();
|
|
@@ -88,15 +118,15 @@ function findConfigFile(nameOrId) {
|
|
|
88
118
|
if (!fs.existsSync(dir)) return null;
|
|
89
119
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
90
120
|
const sanitized = sanitizeName(nameOrId);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const filePath = path.join(dir,
|
|
121
|
+
const nameCandidates = files.filter((f) => f.startsWith(sanitized + "_"));
|
|
122
|
+
for (const f of nameCandidates) {
|
|
123
|
+
const filePath = path.join(dir, f);
|
|
94
124
|
const config = readConfigFile(filePath);
|
|
95
125
|
if (config && config.name === nameOrId) return { filePath, config };
|
|
96
126
|
}
|
|
97
127
|
const idCandidates = files.filter((f) => {
|
|
98
128
|
const withoutExt = f.replace(/\.json$/, "");
|
|
99
|
-
const lastUnderscore = withoutExt.
|
|
129
|
+
const lastUnderscore = withoutExt.lastIndexOf("_");
|
|
100
130
|
if (lastUnderscore === -1) return false;
|
|
101
131
|
const idPart = withoutExt.slice(lastUnderscore + 1);
|
|
102
132
|
return idPart.startsWith(nameOrId);
|
|
@@ -108,15 +138,33 @@ function findConfigFile(nameOrId) {
|
|
|
108
138
|
}
|
|
109
139
|
return null;
|
|
110
140
|
}
|
|
141
|
+
function findConfigFileByConfigId(configId) {
|
|
142
|
+
const dir = getTunnelConfigDir();
|
|
143
|
+
if (!fs.existsSync(dir)) return null;
|
|
144
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const filePath = path.join(dir, file);
|
|
147
|
+
const config = readConfigFile(filePath);
|
|
148
|
+
if (config && config.configId === configId) {
|
|
149
|
+
return { filePath, config };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
111
154
|
function findConfigByName(name) {
|
|
112
|
-
const
|
|
113
|
-
|
|
155
|
+
const dir = getTunnelConfigDir();
|
|
156
|
+
if (!fs.existsSync(dir)) return null;
|
|
157
|
+
const prefix = `${sanitizeName(name)}_`;
|
|
158
|
+
const matches = fs.readdirSync(dir).filter((f) => f.endsWith(".json") && f.startsWith(prefix)).map((f) => readConfigFile(path.join(dir, f))).filter((c) => c !== null && c.name === name);
|
|
159
|
+
if (matches.length === 0) return null;
|
|
160
|
+
return matches.sort((a, b) => (b.updatedAt || "").localeCompare(a.updatedAt || ""))[0];
|
|
114
161
|
}
|
|
115
162
|
function findConfig(nameOrId) {
|
|
116
163
|
return findConfigFile(nameOrId)?.config ?? null;
|
|
117
164
|
}
|
|
118
|
-
function saveConfig(name, configId, tunnelConfig, autoStart = false) {
|
|
119
|
-
const
|
|
165
|
+
function saveConfig(name, configId, tunnelConfig, autoStart = false, options = {}) {
|
|
166
|
+
const { strict = true } = options;
|
|
167
|
+
const nameErr = strict ? validateNameStrict(name) : validateNameForStorage(name);
|
|
120
168
|
if (nameErr) {
|
|
121
169
|
throw nameErr;
|
|
122
170
|
}
|
|
@@ -136,12 +184,58 @@ function saveConfig(name, configId, tunnelConfig, autoStart = false) {
|
|
|
136
184
|
updatedAt: now,
|
|
137
185
|
tunnelConfig
|
|
138
186
|
};
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
logger.info(`Config "${name}" saved to ${filePath}`);
|
|
187
|
+
const filePath = path.join(dir, buildFilename(sanitizeName(name), configId));
|
|
188
|
+
writeConfigFile(filePath, saved);
|
|
189
|
+
storageLogger.info(`Config "${name}" saved to ${filePath}`);
|
|
143
190
|
return saved;
|
|
144
191
|
}
|
|
192
|
+
function upsertConfig(saved, options = {}) {
|
|
193
|
+
const { strict = true } = options;
|
|
194
|
+
const dir = ensureTunnelConfigDir();
|
|
195
|
+
const targetPath = path.join(dir, buildFilename(sanitizeName(saved.name), saved.configId));
|
|
196
|
+
const existing = findConfigFileByConfigId(saved.configId);
|
|
197
|
+
if (existing) {
|
|
198
|
+
if (path.resolve(existing.filePath) !== path.resolve(targetPath)) {
|
|
199
|
+
try {
|
|
200
|
+
fs.unlinkSync(existing.filePath);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
storageLogger.warn(`upsertConfig: failed to remove old file ${existing.filePath}: ${String(err)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
writeConfigFile(targetPath, saved);
|
|
206
|
+
storageLogger.info(`Config "${saved.name}" (${saved.configId}) updated at ${targetPath}`);
|
|
207
|
+
return saved;
|
|
208
|
+
}
|
|
209
|
+
const nameErr = strict ? validateNameStrict(saved.name) : validateNameForStorage(saved.name);
|
|
210
|
+
if (nameErr) {
|
|
211
|
+
throw nameErr;
|
|
212
|
+
}
|
|
213
|
+
writeConfigFile(targetPath, saved);
|
|
214
|
+
storageLogger.info(`Config "${saved.name}" (${saved.configId}) saved to ${targetPath}`);
|
|
215
|
+
return saved;
|
|
216
|
+
}
|
|
217
|
+
function bulkReplace(configs, knownConfigIds, options = {}) {
|
|
218
|
+
const written = [];
|
|
219
|
+
const deleted = [];
|
|
220
|
+
const incomingIds = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const cfg of configs) {
|
|
222
|
+
upsertConfig(cfg, options);
|
|
223
|
+
written.push(cfg.configId);
|
|
224
|
+
incomingIds.add(cfg.configId);
|
|
225
|
+
}
|
|
226
|
+
for (const id of knownConfigIds) {
|
|
227
|
+
if (incomingIds.has(id)) continue;
|
|
228
|
+
const existing = findConfigFileByConfigId(id);
|
|
229
|
+
if (!existing) continue;
|
|
230
|
+
try {
|
|
231
|
+
fs.unlinkSync(existing.filePath);
|
|
232
|
+
deleted.push(id);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
storageLogger.warn(`bulkReplace: failed to delete ${existing.filePath}: ${String(err)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { written, deleted };
|
|
238
|
+
}
|
|
145
239
|
function loadConfigByName(name) {
|
|
146
240
|
return findConfigByName(name);
|
|
147
241
|
}
|
|
@@ -149,25 +243,25 @@ function deleteConfig(nameOrId) {
|
|
|
149
243
|
const resolved = findConfigFile(nameOrId);
|
|
150
244
|
if (!resolved) return null;
|
|
151
245
|
fs.unlinkSync(resolved.filePath);
|
|
152
|
-
|
|
246
|
+
storageLogger.info(`Config "${resolved.config.name}" deleted.`);
|
|
153
247
|
return resolved.config.name;
|
|
154
248
|
}
|
|
155
249
|
function updateConfigAutoStart(nameOrId, autoStart) {
|
|
156
250
|
const resolved = findConfigFile(nameOrId);
|
|
157
251
|
if (!resolved) return null;
|
|
158
252
|
resolved.config.autoStart = autoStart;
|
|
159
|
-
resolved.config.updatedAt = (
|
|
253
|
+
resolved.config.updatedAt = nextTimestamp(resolved.config.updatedAt);
|
|
160
254
|
writeConfigFile(resolved.filePath, resolved.config);
|
|
161
|
-
|
|
255
|
+
storageLogger.info(`Config "${resolved.config.name}" auto-start set to ${autoStart}`);
|
|
162
256
|
return resolved.config;
|
|
163
257
|
}
|
|
164
258
|
function updateTunnelConfig(nameOrId, tunnelConfig) {
|
|
165
259
|
const resolved = findConfigFile(nameOrId);
|
|
166
260
|
if (!resolved) return null;
|
|
167
261
|
resolved.config.tunnelConfig = tunnelConfig;
|
|
168
|
-
resolved.config.updatedAt = (
|
|
262
|
+
resolved.config.updatedAt = nextTimestamp(resolved.config.updatedAt);
|
|
169
263
|
writeConfigFile(resolved.filePath, resolved.config);
|
|
170
|
-
|
|
264
|
+
storageLogger.info(`Config "${resolved.config.name}" tunnel configuration updated`);
|
|
171
265
|
return resolved.config;
|
|
172
266
|
}
|
|
173
267
|
function getAutoStartConfigs() {
|
|
@@ -248,17 +342,22 @@ Tunnel Config: ${pico.cyanBright(config.name)}`));
|
|
|
248
342
|
}
|
|
249
343
|
|
|
250
344
|
export {
|
|
345
|
+
configureStorageLogger,
|
|
251
346
|
sanitizeName,
|
|
252
347
|
Subcommand,
|
|
253
348
|
ConfigVerb,
|
|
254
349
|
SUBCOMMANDS,
|
|
255
350
|
CONFIG_VERBS,
|
|
256
351
|
RESERVED_NAMES,
|
|
352
|
+
validateNameStrict,
|
|
353
|
+
validateNameForStorage,
|
|
257
354
|
validateName,
|
|
258
355
|
listSavedConfigs,
|
|
259
356
|
findConfigByName,
|
|
260
357
|
findConfig,
|
|
261
358
|
saveConfig,
|
|
359
|
+
upsertConfig,
|
|
360
|
+
bulkReplace,
|
|
262
361
|
loadConfigByName,
|
|
263
362
|
deleteConfig,
|
|
264
363
|
updateConfigAutoStart,
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
RESERVED_NAMES,
|
|
5
5
|
SUBCOMMANDS,
|
|
6
6
|
Subcommand,
|
|
7
|
+
bulkReplace,
|
|
8
|
+
configureStorageLogger,
|
|
7
9
|
deleteConfig,
|
|
8
10
|
findConfig,
|
|
9
11
|
findConfigByName,
|
|
@@ -16,8 +18,11 @@ import {
|
|
|
16
18
|
saveConfig,
|
|
17
19
|
updateConfigAutoStart,
|
|
18
20
|
updateTunnelConfig,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
upsertConfig,
|
|
22
|
+
validateName,
|
|
23
|
+
validateNameForStorage,
|
|
24
|
+
validateNameStrict
|
|
25
|
+
} from "./chunk-WM7I57AA.js";
|
|
21
26
|
import "./chunk-7G6SJEEA.js";
|
|
22
27
|
import "./chunk-GBYF2H4H.js";
|
|
23
28
|
export {
|
|
@@ -26,6 +31,8 @@ export {
|
|
|
26
31
|
RESERVED_NAMES,
|
|
27
32
|
SUBCOMMANDS,
|
|
28
33
|
Subcommand,
|
|
34
|
+
bulkReplace,
|
|
35
|
+
configureStorageLogger,
|
|
29
36
|
deleteConfig,
|
|
30
37
|
findConfig,
|
|
31
38
|
findConfigByName,
|
|
@@ -38,5 +45,8 @@ export {
|
|
|
38
45
|
saveConfig,
|
|
39
46
|
updateConfigAutoStart,
|
|
40
47
|
updateTunnelConfig,
|
|
41
|
-
|
|
48
|
+
upsertConfig,
|
|
49
|
+
validateName,
|
|
50
|
+
validateNameForStorage,
|
|
51
|
+
validateNameStrict
|
|
42
52
|
};
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
trackIPCTunnelStart,
|
|
6
6
|
trackTunnelStart,
|
|
7
7
|
trackTunnelStop
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-LFK74BMU.js";
|
|
9
|
+
import "./chunk-C3ZMLIC4.js";
|
|
10
10
|
import "./chunk-MT44NAXX.js";
|
|
11
11
|
import "./chunk-BFARGPGP.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-WM7I57AA.js";
|
|
13
13
|
import "./chunk-DLNUDW6G.js";
|
|
14
14
|
import "./chunk-UB26QJ4T.js";
|
|
15
15
|
import "./chunk-7G6SJEEA.js";
|