@schuttdev/gigai 0.1.0-beta.9 → 0.2.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/dist/{chunk-4XUWD3DZ.js → chunk-7C3UYEKE.js} +1 -8
- package/dist/{dist-CU5WVKG2.js → dist-U7NYIMA4.js} +535 -143
- package/dist/index.js +508 -92
- package/package.json +3 -2
- package/dist/chunk-FN4LCKUA.js +0 -42
- package/dist/chunk-OMGUT7RM.js +0 -82
- package/dist/pairing-IGMDVOIZ-RA7GNFU7.js +0 -10
- package/dist/store-XDNMGPYX-5CGK2GXY.js +0 -7
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
AuthStore
|
|
4
|
-
} from "./chunk-OMGUT7RM.js";
|
|
5
|
-
import {
|
|
6
|
-
generatePairingCode,
|
|
7
|
-
validateAndPair
|
|
8
|
-
} from "./chunk-FN4LCKUA.js";
|
|
9
2
|
import {
|
|
10
3
|
ErrorCode,
|
|
11
4
|
GigaiConfigSchema,
|
|
@@ -13,7 +6,7 @@ import {
|
|
|
13
6
|
decrypt,
|
|
14
7
|
encrypt,
|
|
15
8
|
generateEncryptionKey
|
|
16
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-7C3UYEKE.js";
|
|
17
10
|
|
|
18
11
|
// ../server/dist/index.mjs
|
|
19
12
|
import { parseArgs } from "util";
|
|
@@ -22,7 +15,10 @@ import cors from "@fastify/cors";
|
|
|
22
15
|
import rateLimit from "@fastify/rate-limit";
|
|
23
16
|
import multipart from "@fastify/multipart";
|
|
24
17
|
import fp from "fastify-plugin";
|
|
18
|
+
import { nanoid } from "nanoid";
|
|
25
19
|
import { randomBytes } from "crypto";
|
|
20
|
+
import { hostname } from "os";
|
|
21
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
26
22
|
import fp2 from "fastify-plugin";
|
|
27
23
|
import fp3 from "fastify-plugin";
|
|
28
24
|
import { spawn } from "child_process";
|
|
@@ -38,18 +34,91 @@ import { spawn as spawn2 } from "child_process";
|
|
|
38
34
|
import { writeFile, readFile as readFile2, unlink, mkdir } from "fs/promises";
|
|
39
35
|
import { join as join2 } from "path";
|
|
40
36
|
import { tmpdir } from "os";
|
|
41
|
-
import { nanoid } from "nanoid";
|
|
37
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
38
|
+
import { execFile, spawn as spawn3 } from "child_process";
|
|
39
|
+
import { promisify } from "util";
|
|
42
40
|
import { readFile as readFile3 } from "fs/promises";
|
|
43
41
|
import { resolve as resolve3 } from "path";
|
|
42
|
+
import { spawn as spawn4 } from "child_process";
|
|
43
|
+
import { spawn as spawn5 } from "child_process";
|
|
44
44
|
import { input, select, checkbox, confirm } from "@inquirer/prompts";
|
|
45
|
-
import { writeFile as writeFile2
|
|
46
|
-
import { resolve as resolve4
|
|
47
|
-
import { execFile } from "child_process";
|
|
48
|
-
import { promisify } from "util";
|
|
49
|
-
import { randomBytes as randomBytes2 } from "crypto";
|
|
45
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
46
|
+
import { resolve as resolve4 } from "path";
|
|
47
|
+
import { execFile as execFile2, spawn as spawn6 } from "child_process";
|
|
48
|
+
import { promisify as promisify2 } from "util";
|
|
50
49
|
import { input as input2 } from "@inquirer/prompts";
|
|
51
50
|
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
52
51
|
import { resolve as resolve5 } from "path";
|
|
52
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
53
|
+
import { resolve as resolve6, join as join3 } from "path";
|
|
54
|
+
import { homedir, platform } from "os";
|
|
55
|
+
import { execFile as execFile3 } from "child_process";
|
|
56
|
+
import { promisify as promisify3 } from "util";
|
|
57
|
+
var AuthStore = class {
|
|
58
|
+
pairingCodes = /* @__PURE__ */ new Map();
|
|
59
|
+
sessions = /* @__PURE__ */ new Map();
|
|
60
|
+
cleanupInterval;
|
|
61
|
+
constructor() {
|
|
62
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 6e4);
|
|
63
|
+
}
|
|
64
|
+
// Pairing codes
|
|
65
|
+
addPairingCode(code, ttlSeconds) {
|
|
66
|
+
const entry = {
|
|
67
|
+
code,
|
|
68
|
+
expiresAt: Date.now() + ttlSeconds * 1e3,
|
|
69
|
+
used: false
|
|
70
|
+
};
|
|
71
|
+
this.pairingCodes.set(code, entry);
|
|
72
|
+
return entry;
|
|
73
|
+
}
|
|
74
|
+
getPairingCode(code) {
|
|
75
|
+
return this.pairingCodes.get(code);
|
|
76
|
+
}
|
|
77
|
+
markPairingCodeUsed(code) {
|
|
78
|
+
const entry = this.pairingCodes.get(code);
|
|
79
|
+
if (entry) {
|
|
80
|
+
entry.used = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Sessions
|
|
84
|
+
createSession(orgUuid, ttlSeconds) {
|
|
85
|
+
const session = {
|
|
86
|
+
id: nanoid(32),
|
|
87
|
+
orgUuid,
|
|
88
|
+
token: nanoid(48),
|
|
89
|
+
expiresAt: Date.now() + ttlSeconds * 1e3,
|
|
90
|
+
lastActivity: Date.now()
|
|
91
|
+
};
|
|
92
|
+
this.sessions.set(session.token, session);
|
|
93
|
+
return session;
|
|
94
|
+
}
|
|
95
|
+
getSession(token) {
|
|
96
|
+
const session = this.sessions.get(token);
|
|
97
|
+
if (session) {
|
|
98
|
+
session.lastActivity = Date.now();
|
|
99
|
+
}
|
|
100
|
+
return session;
|
|
101
|
+
}
|
|
102
|
+
// Cleanup
|
|
103
|
+
cleanup() {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
for (const [key, code] of this.pairingCodes) {
|
|
106
|
+
if (code.expiresAt < now) {
|
|
107
|
+
this.pairingCodes.delete(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const [key, session] of this.sessions) {
|
|
111
|
+
if (session.expiresAt < now) {
|
|
112
|
+
this.sessions.delete(key);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
destroy() {
|
|
117
|
+
clearInterval(this.cleanupInterval);
|
|
118
|
+
this.pairingCodes.clear();
|
|
119
|
+
this.sessions.clear();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
53
122
|
function connectWithToken(store, encryptedToken, orgUuid, encryptionKey, sessionTtlSeconds) {
|
|
54
123
|
let payload;
|
|
55
124
|
try {
|
|
@@ -63,16 +132,7 @@ function connectWithToken(store, encryptedToken, orgUuid, encryptionKey, session
|
|
|
63
132
|
} catch {
|
|
64
133
|
throw new GigaiError(ErrorCode.TOKEN_DECRYPT_FAILED, "Failed to decrypt token");
|
|
65
134
|
}
|
|
66
|
-
if (decrypted.orgUuid
|
|
67
|
-
const bound = store.getBoundOrg(decrypted.serverFingerprint);
|
|
68
|
-
if (bound) {
|
|
69
|
-
if (bound !== orgUuid) {
|
|
70
|
-
throw new GigaiError(ErrorCode.ORG_MISMATCH, "Organization UUID mismatch");
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
store.bindOrg(decrypted.serverFingerprint, orgUuid);
|
|
74
|
-
}
|
|
75
|
-
} else if (decrypted.orgUuid !== orgUuid) {
|
|
135
|
+
if (decrypted.orgUuid !== orgUuid) {
|
|
76
136
|
throw new GigaiError(ErrorCode.ORG_MISMATCH, "Organization UUID mismatch");
|
|
77
137
|
}
|
|
78
138
|
return store.createSession(orgUuid, sessionTtlSeconds);
|
|
@@ -98,8 +158,37 @@ function createAuthMiddleware(store) {
|
|
|
98
158
|
request.session = session;
|
|
99
159
|
};
|
|
100
160
|
}
|
|
161
|
+
var PAIRING_CODE_LENGTH = 8;
|
|
162
|
+
var PAIRING_CODE_CHARS = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
163
|
+
function generatePairingCode(store, ttlSeconds) {
|
|
164
|
+
let code = "";
|
|
165
|
+
const bytes = nanoid2(PAIRING_CODE_LENGTH);
|
|
166
|
+
for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
|
|
167
|
+
code += PAIRING_CODE_CHARS[bytes.charCodeAt(i) % PAIRING_CODE_CHARS.length];
|
|
168
|
+
}
|
|
169
|
+
store.addPairingCode(code, ttlSeconds);
|
|
170
|
+
return code;
|
|
171
|
+
}
|
|
172
|
+
function validateAndPair(store, code, orgUuid, encryptionKey, serverFingerprint) {
|
|
173
|
+
const entry = store.getPairingCode(code.toUpperCase());
|
|
174
|
+
if (!entry) {
|
|
175
|
+
throw new GigaiError(ErrorCode.PAIRING_INVALID, "Invalid pairing code");
|
|
176
|
+
}
|
|
177
|
+
if (entry.used) {
|
|
178
|
+
throw new GigaiError(ErrorCode.PAIRING_USED, "Pairing code already used");
|
|
179
|
+
}
|
|
180
|
+
if (entry.expiresAt < Date.now()) {
|
|
181
|
+
throw new GigaiError(ErrorCode.PAIRING_EXPIRED, "Pairing code expired");
|
|
182
|
+
}
|
|
183
|
+
store.markPairingCodeUsed(code.toUpperCase());
|
|
184
|
+
return encrypt(
|
|
185
|
+
{ orgUuid, serverFingerprint, createdAt: Date.now() },
|
|
186
|
+
encryptionKey
|
|
187
|
+
);
|
|
188
|
+
}
|
|
101
189
|
function registerAuthRoutes(server, store, config) {
|
|
102
190
|
const serverFingerprint = randomBytes(16).toString("hex");
|
|
191
|
+
const serverName = config.serverName ?? hostname();
|
|
103
192
|
server.post("/auth/pair", {
|
|
104
193
|
config: {
|
|
105
194
|
rateLimit: { max: 5, timeWindow: "1 hour" }
|
|
@@ -123,7 +212,7 @@ function registerAuthRoutes(server, store, config) {
|
|
|
123
212
|
config.auth.encryptionKey,
|
|
124
213
|
serverFingerprint
|
|
125
214
|
);
|
|
126
|
-
return { encryptedToken: JSON.stringify(encryptedToken) };
|
|
215
|
+
return { encryptedToken: JSON.stringify(encryptedToken), serverName };
|
|
127
216
|
});
|
|
128
217
|
server.post("/auth/connect", {
|
|
129
218
|
config: {
|
|
@@ -276,7 +365,7 @@ function executeTool(entry, args, timeout) {
|
|
|
276
365
|
`Cannot execute tool of type: ${entry.type}`
|
|
277
366
|
);
|
|
278
367
|
}
|
|
279
|
-
return new Promise((
|
|
368
|
+
return new Promise((resolve7, reject) => {
|
|
280
369
|
const start = Date.now();
|
|
281
370
|
const stdoutChunks = [];
|
|
282
371
|
const stderrChunks = [];
|
|
@@ -324,7 +413,7 @@ function executeTool(entry, args, timeout) {
|
|
|
324
413
|
reject(new GigaiError(ErrorCode.EXEC_TIMEOUT, `Tool execution timed out after ${effectiveTimeout}ms`));
|
|
325
414
|
return;
|
|
326
415
|
}
|
|
327
|
-
|
|
416
|
+
resolve7({
|
|
328
417
|
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
329
418
|
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
330
419
|
exitCode: exitCode ?? 1,
|
|
@@ -616,7 +705,7 @@ async function execCommandSafe(command, args, config) {
|
|
|
616
705
|
throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Null byte in argument");
|
|
617
706
|
}
|
|
618
707
|
}
|
|
619
|
-
return new Promise((
|
|
708
|
+
return new Promise((resolve7, reject) => {
|
|
620
709
|
const child = spawn2(command, args, {
|
|
621
710
|
shell: false,
|
|
622
711
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -638,7 +727,7 @@ async function execCommandSafe(command, args, config) {
|
|
|
638
727
|
reject(new GigaiError(ErrorCode.EXEC_FAILED, `Failed to spawn ${command}: ${err.message}`));
|
|
639
728
|
});
|
|
640
729
|
child.on("close", (exitCode) => {
|
|
641
|
-
|
|
730
|
+
resolve7({
|
|
642
731
|
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
643
732
|
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
644
733
|
exitCode: exitCode ?? 1
|
|
@@ -756,7 +845,7 @@ async function transferRoutes(server) {
|
|
|
756
845
|
if (!data) {
|
|
757
846
|
throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No file uploaded");
|
|
758
847
|
}
|
|
759
|
-
const id =
|
|
848
|
+
const id = nanoid3(16);
|
|
760
849
|
const buffer = await data.toBuffer();
|
|
761
850
|
const filePath = join2(TRANSFER_DIR, id);
|
|
762
851
|
await writeFile(filePath, buffer);
|
|
@@ -787,6 +876,48 @@ async function transferRoutes(server) {
|
|
|
787
876
|
reply.type(entry.mimeType).send(content);
|
|
788
877
|
});
|
|
789
878
|
}
|
|
879
|
+
var execFileAsync = promisify(execFile);
|
|
880
|
+
async function adminRoutes(server) {
|
|
881
|
+
server.post("/admin/update", async (_request, reply) => {
|
|
882
|
+
try {
|
|
883
|
+
const { stdout, stderr } = await execFileAsync(
|
|
884
|
+
"npm",
|
|
885
|
+
["install", "-g", "@schuttdev/gigai@latest"],
|
|
886
|
+
{ timeout: 12e4 }
|
|
887
|
+
);
|
|
888
|
+
server.log.info(`Update output: ${stdout}`);
|
|
889
|
+
if (stderr) server.log.warn(`Update stderr: ${stderr}`);
|
|
890
|
+
} catch (e) {
|
|
891
|
+
server.log.error(`Update failed: ${e.message}`);
|
|
892
|
+
reply.status(500);
|
|
893
|
+
return { updated: false, error: e.message };
|
|
894
|
+
}
|
|
895
|
+
setTimeout(async () => {
|
|
896
|
+
server.log.info("Restarting server after update...");
|
|
897
|
+
const args = ["server", "start"];
|
|
898
|
+
const configIdx = process.argv.indexOf("--config");
|
|
899
|
+
if (configIdx !== -1 && process.argv[configIdx + 1]) {
|
|
900
|
+
args.push("--config", process.argv[configIdx + 1]);
|
|
901
|
+
}
|
|
902
|
+
const shortIdx = process.argv.indexOf("-c");
|
|
903
|
+
if (shortIdx !== -1 && process.argv[shortIdx + 1]) {
|
|
904
|
+
args.push("--config", process.argv[shortIdx + 1]);
|
|
905
|
+
}
|
|
906
|
+
if (process.argv.includes("--dev")) {
|
|
907
|
+
args.push("--dev");
|
|
908
|
+
}
|
|
909
|
+
await server.close();
|
|
910
|
+
const child = spawn3("gigai", args, {
|
|
911
|
+
detached: true,
|
|
912
|
+
stdio: "ignore",
|
|
913
|
+
cwd: process.cwd()
|
|
914
|
+
});
|
|
915
|
+
child.unref();
|
|
916
|
+
process.exit(0);
|
|
917
|
+
}, 500);
|
|
918
|
+
return { updated: true, restarting: true };
|
|
919
|
+
});
|
|
920
|
+
}
|
|
790
921
|
async function createServer(opts) {
|
|
791
922
|
const { config, dev = false } = opts;
|
|
792
923
|
const server = Fastify({
|
|
@@ -796,7 +927,9 @@ async function createServer(opts) {
|
|
|
796
927
|
trustProxy: !dev
|
|
797
928
|
// Only trust proxy headers in production (behind HTTPS reverse proxy)
|
|
798
929
|
});
|
|
799
|
-
|
|
930
|
+
const httpsProvider = config.server.https?.provider;
|
|
931
|
+
const behindTunnel = httpsProvider === "tailscale" || httpsProvider === "cloudflare";
|
|
932
|
+
if (!dev && !behindTunnel) {
|
|
800
933
|
server.addHook("onRequest", async (request, _reply) => {
|
|
801
934
|
if (request.protocol !== "https") {
|
|
802
935
|
throw new GigaiError(ErrorCode.HTTPS_REQUIRED, "HTTPS is required");
|
|
@@ -814,6 +947,7 @@ async function createServer(opts) {
|
|
|
814
947
|
await server.register(toolRoutes);
|
|
815
948
|
await server.register(execRoutes);
|
|
816
949
|
await server.register(transferRoutes);
|
|
950
|
+
await server.register(adminRoutes);
|
|
817
951
|
server.setErrorHandler((error, _request, reply) => {
|
|
818
952
|
if (error instanceof GigaiError) {
|
|
819
953
|
reply.status(error.statusCode).send(error.toJSON());
|
|
@@ -839,10 +973,67 @@ async function loadConfig(path) {
|
|
|
839
973
|
const json = JSON.parse(raw);
|
|
840
974
|
return GigaiConfigSchema.parse(json);
|
|
841
975
|
}
|
|
842
|
-
|
|
976
|
+
function runCommand(command, args) {
|
|
977
|
+
return new Promise((resolve7, reject) => {
|
|
978
|
+
const child = spawn4(command, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
979
|
+
const chunks = [];
|
|
980
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
981
|
+
child.on("error", reject);
|
|
982
|
+
child.on("close", (exitCode) => {
|
|
983
|
+
resolve7({ stdout: Buffer.concat(chunks).toString("utf8").trim(), exitCode: exitCode ?? 1 });
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
async function getTailscaleStatus() {
|
|
988
|
+
try {
|
|
989
|
+
const { stdout, exitCode } = await runCommand("tailscale", ["status", "--json"]);
|
|
990
|
+
if (exitCode !== 0) return { online: false };
|
|
991
|
+
const status = JSON.parse(stdout);
|
|
992
|
+
return {
|
|
993
|
+
online: status.BackendState === "Running",
|
|
994
|
+
hostname: status.Self?.DNSName?.replace(/\.$/, "")
|
|
995
|
+
};
|
|
996
|
+
} catch {
|
|
997
|
+
return { online: false };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async function enableFunnel(port) {
|
|
1001
|
+
const status = await getTailscaleStatus();
|
|
1002
|
+
if (!status.online || !status.hostname) {
|
|
1003
|
+
throw new Error("Tailscale is not running or not connected");
|
|
1004
|
+
}
|
|
1005
|
+
const { exitCode, stdout } = await runCommand("tailscale", [
|
|
1006
|
+
"funnel",
|
|
1007
|
+
"--bg",
|
|
1008
|
+
`${port}`
|
|
1009
|
+
]);
|
|
1010
|
+
if (exitCode !== 0) {
|
|
1011
|
+
throw new Error(`Failed to enable Tailscale Funnel: ${stdout}`);
|
|
1012
|
+
}
|
|
1013
|
+
return `https://${status.hostname}`;
|
|
1014
|
+
}
|
|
1015
|
+
async function disableFunnel(port) {
|
|
1016
|
+
await runCommand("tailscale", ["funnel", "--bg", "off", `${port}`]);
|
|
1017
|
+
}
|
|
1018
|
+
function runTunnel(tunnelName, localPort) {
|
|
1019
|
+
const child = spawn5("cloudflared", [
|
|
1020
|
+
"tunnel",
|
|
1021
|
+
"--url",
|
|
1022
|
+
`http://localhost:${localPort}`,
|
|
1023
|
+
"run",
|
|
1024
|
+
tunnelName
|
|
1025
|
+
], {
|
|
1026
|
+
shell: false,
|
|
1027
|
+
stdio: "ignore",
|
|
1028
|
+
detached: true
|
|
1029
|
+
});
|
|
1030
|
+
child.unref();
|
|
1031
|
+
return child;
|
|
1032
|
+
}
|
|
1033
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
843
1034
|
async function getTailscaleDnsName() {
|
|
844
1035
|
try {
|
|
845
|
-
const { stdout } = await
|
|
1036
|
+
const { stdout } = await execFileAsync2("tailscale", ["status", "--json"]);
|
|
846
1037
|
const data = JSON.parse(stdout);
|
|
847
1038
|
const dnsName = data?.Self?.DNSName;
|
|
848
1039
|
if (dnsName) return dnsName.replace(/\.$/, "");
|
|
@@ -851,6 +1042,41 @@ async function getTailscaleDnsName() {
|
|
|
851
1042
|
return null;
|
|
852
1043
|
}
|
|
853
1044
|
}
|
|
1045
|
+
async function ensureTailscaleFunnel(port) {
|
|
1046
|
+
const dnsName = await getTailscaleDnsName();
|
|
1047
|
+
if (!dnsName) {
|
|
1048
|
+
throw new Error("Tailscale is not running or not connected. Install/start Tailscale first.");
|
|
1049
|
+
}
|
|
1050
|
+
console.log(" Enabling Tailscale Funnel...");
|
|
1051
|
+
try {
|
|
1052
|
+
const { stdout, stderr } = await execFileAsync2("tailscale", ["funnel", "--bg", `${port}`]);
|
|
1053
|
+
const output = stdout + stderr;
|
|
1054
|
+
if (output.includes("Funnel is not enabled")) {
|
|
1055
|
+
const urlMatch = output.match(/(https:\/\/login\.tailscale\.com\/\S+)/);
|
|
1056
|
+
const enableUrl = urlMatch?.[1] ?? "https://login.tailscale.com/admin/machines";
|
|
1057
|
+
console.log(`
|
|
1058
|
+
Funnel is not enabled on your tailnet.`);
|
|
1059
|
+
console.log(` Enable it here: ${enableUrl}
|
|
1060
|
+
`);
|
|
1061
|
+
await confirm({ message: "I've enabled Funnel in my Tailscale admin. Continue?", default: true });
|
|
1062
|
+
const retry = await execFileAsync2("tailscale", ["funnel", "--bg", `${port}`]);
|
|
1063
|
+
if ((retry.stdout + retry.stderr).includes("Funnel is not enabled")) {
|
|
1064
|
+
throw new Error("Funnel is still not enabled. Please enable it in your Tailscale admin and try again.");
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
if (e.message.includes("Funnel is still not enabled")) throw e;
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
const { stdout } = await execFileAsync2("tailscale", ["funnel", "status"]);
|
|
1072
|
+
if (stdout.includes("No serve config")) {
|
|
1073
|
+
throw new Error("Funnel setup failed. Run 'tailscale funnel --bg " + port + "' manually to debug.");
|
|
1074
|
+
}
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
console.log(` Tailscale Funnel active: https://${dnsName}`);
|
|
1078
|
+
return `https://${dnsName}`;
|
|
1079
|
+
}
|
|
854
1080
|
async function runInit() {
|
|
855
1081
|
console.log("\n gigai server setup\n");
|
|
856
1082
|
const httpsProvider = await select({
|
|
@@ -858,7 +1084,6 @@ async function runInit() {
|
|
|
858
1084
|
choices: [
|
|
859
1085
|
{ name: "Tailscale Funnel (recommended)", value: "tailscale" },
|
|
860
1086
|
{ name: "Cloudflare Tunnel", value: "cloudflare" },
|
|
861
|
-
{ name: "Let's Encrypt", value: "letsencrypt" },
|
|
862
1087
|
{ name: "Manual (provide certs)", value: "manual" },
|
|
863
1088
|
{ name: "None (dev mode only)", value: "none" }
|
|
864
1089
|
]
|
|
@@ -870,7 +1095,6 @@ async function runInit() {
|
|
|
870
1095
|
provider: "tailscale",
|
|
871
1096
|
funnelPort: 7443
|
|
872
1097
|
};
|
|
873
|
-
console.log(" Will use Tailscale Funnel for HTTPS.");
|
|
874
1098
|
break;
|
|
875
1099
|
case "cloudflare": {
|
|
876
1100
|
const tunnelName = await input({
|
|
@@ -887,22 +1111,6 @@ async function runInit() {
|
|
|
887
1111
|
};
|
|
888
1112
|
break;
|
|
889
1113
|
}
|
|
890
|
-
case "letsencrypt": {
|
|
891
|
-
const domain = await input({
|
|
892
|
-
message: "Domain name:",
|
|
893
|
-
required: true
|
|
894
|
-
});
|
|
895
|
-
const email = await input({
|
|
896
|
-
message: "Email for Let's Encrypt:",
|
|
897
|
-
required: true
|
|
898
|
-
});
|
|
899
|
-
httpsConfig = {
|
|
900
|
-
provider: "letsencrypt",
|
|
901
|
-
domain,
|
|
902
|
-
email
|
|
903
|
-
};
|
|
904
|
-
break;
|
|
905
|
-
}
|
|
906
1114
|
case "manual": {
|
|
907
1115
|
const certPath = await input({
|
|
908
1116
|
message: "Path to TLS certificate:",
|
|
@@ -970,8 +1178,25 @@ async function runInit() {
|
|
|
970
1178
|
config: { allowlist, allowSudo }
|
|
971
1179
|
});
|
|
972
1180
|
}
|
|
1181
|
+
let serverName;
|
|
1182
|
+
if (httpsProvider === "tailscale") {
|
|
1183
|
+
const dnsName = await getTailscaleDnsName();
|
|
1184
|
+
if (dnsName) {
|
|
1185
|
+
serverName = dnsName.split(".")[0];
|
|
1186
|
+
}
|
|
1187
|
+
} else if (httpsProvider === "cloudflare") {
|
|
1188
|
+
serverName = await input({
|
|
1189
|
+
message: "Server name (identifies this machine):",
|
|
1190
|
+
required: true
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
if (!serverName) {
|
|
1194
|
+
const { hostname: osHostname } = await import("os");
|
|
1195
|
+
serverName = osHostname();
|
|
1196
|
+
}
|
|
973
1197
|
const encryptionKey = generateEncryptionKey();
|
|
974
1198
|
const config = {
|
|
1199
|
+
serverName,
|
|
975
1200
|
server: {
|
|
976
1201
|
port,
|
|
977
1202
|
host: "0.0.0.0",
|
|
@@ -988,92 +1213,78 @@ async function runInit() {
|
|
|
988
1213
|
await writeFile2(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
989
1214
|
console.log(`
|
|
990
1215
|
Config written to: ${configPath}`);
|
|
991
|
-
|
|
992
|
-
await mkdir2(skillDir, { recursive: true });
|
|
993
|
-
let serverUrl = "<YOUR_SERVER_URL>";
|
|
1216
|
+
let serverUrl;
|
|
994
1217
|
if (httpsProvider === "tailscale") {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
console.
|
|
1218
|
+
try {
|
|
1219
|
+
serverUrl = await ensureTailscaleFunnel(port);
|
|
1220
|
+
} catch (e) {
|
|
1221
|
+
console.error(` ${e.message}`);
|
|
1222
|
+
console.log(" You can enable Funnel later and run 'gigai server start' manually.\n");
|
|
999
1223
|
}
|
|
1224
|
+
} else if (httpsProvider === "cloudflare" && httpsConfig && "domain" in httpsConfig && httpsConfig.domain) {
|
|
1225
|
+
serverUrl = `https://${httpsConfig.domain}`;
|
|
1226
|
+
console.log(` Cloudflare URL: ${serverUrl}`);
|
|
1227
|
+
}
|
|
1228
|
+
if (!serverUrl) {
|
|
1229
|
+
serverUrl = await input({
|
|
1230
|
+
message: "Server URL (how clients will reach this server):",
|
|
1231
|
+
required: true
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
console.log("\n Starting server...");
|
|
1235
|
+
const serverArgs = ["server", "start", "--config", configPath];
|
|
1236
|
+
if (!httpsConfig) serverArgs.push("--dev");
|
|
1237
|
+
const child = spawn6("gigai", serverArgs, {
|
|
1238
|
+
detached: true,
|
|
1239
|
+
stdio: "ignore",
|
|
1240
|
+
cwd: resolve4(".")
|
|
1241
|
+
});
|
|
1242
|
+
child.unref();
|
|
1243
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1244
|
+
try {
|
|
1245
|
+
const res = await fetch(`http://localhost:${port}/health`);
|
|
1246
|
+
if (res.ok) {
|
|
1247
|
+
console.log(` Server running on port ${port} (PID ${child.pid})`);
|
|
1248
|
+
}
|
|
1249
|
+
} catch {
|
|
1250
|
+
console.log(` Server starting in background (PID ${child.pid})`);
|
|
1251
|
+
}
|
|
1252
|
+
let code;
|
|
1253
|
+
const maxRetries = 5;
|
|
1254
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
1255
|
+
try {
|
|
1256
|
+
const res = await fetch(`http://localhost:${port}/auth/pair/generate`);
|
|
1257
|
+
if (res.ok) {
|
|
1258
|
+
const data = await res.json();
|
|
1259
|
+
code = data.code;
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
} catch {
|
|
1263
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
if (!code) {
|
|
1267
|
+
console.log("\n Server is starting but not ready yet.");
|
|
1268
|
+
console.log(" Run 'gigai server pair' once it's up to get a pairing code.\n");
|
|
1269
|
+
return;
|
|
1000
1270
|
}
|
|
1001
|
-
const skillMd = `---
|
|
1002
|
-
name: gigai
|
|
1003
|
-
description: Access tools on the user's machine via the gigai CLI
|
|
1004
|
-
---
|
|
1005
|
-
|
|
1006
|
-
# gigai Skill
|
|
1007
|
-
|
|
1008
|
-
This skill gives you access to tools running on the user's machine via the gigai CLI.
|
|
1009
|
-
|
|
1010
|
-
## Quick Start
|
|
1011
|
-
|
|
1012
|
-
1. Connect to the server:
|
|
1013
|
-
\`\`\`
|
|
1014
|
-
gigai connect
|
|
1015
|
-
\`\`\`
|
|
1016
|
-
|
|
1017
|
-
2. List available tools:
|
|
1018
|
-
\`\`\`
|
|
1019
|
-
gigai list
|
|
1020
|
-
\`\`\`
|
|
1021
|
-
|
|
1022
|
-
3. Get help on a specific tool:
|
|
1023
|
-
\`\`\`
|
|
1024
|
-
gigai help <tool-name>
|
|
1025
|
-
\`\`\`
|
|
1026
|
-
|
|
1027
|
-
4. Use a tool directly:
|
|
1028
|
-
\`\`\`
|
|
1029
|
-
gigai <tool-name> [args...]
|
|
1030
|
-
\`\`\`
|
|
1031
|
-
|
|
1032
|
-
## Commands
|
|
1033
|
-
|
|
1034
|
-
### Connection
|
|
1035
|
-
- \`gigai connect\` \u2014 Establish/renew a session with the server
|
|
1036
|
-
- \`gigai status\` \u2014 Check connection status
|
|
1037
|
-
- \`gigai pair <code> <server-url>\` \u2014 Pair with a new server
|
|
1038
|
-
|
|
1039
|
-
### Tool Usage
|
|
1040
|
-
- \`gigai list\` \u2014 List all available tools
|
|
1041
|
-
- \`gigai help <name>\` \u2014 Show detailed help for a tool
|
|
1042
|
-
- \`gigai <name> [args...]\` \u2014 Execute a tool by name
|
|
1043
|
-
|
|
1044
|
-
### File Transfer
|
|
1045
|
-
- \`gigai upload <file>\` \u2014 Upload a file to the server
|
|
1046
|
-
- \`gigai download <id> <dest>\` \u2014 Download a file from the server
|
|
1047
|
-
|
|
1048
|
-
## Notes
|
|
1049
|
-
|
|
1050
|
-
- The connection is authenticated and encrypted end-to-end
|
|
1051
|
-
- Tools are scoped to what the user has explicitly configured
|
|
1052
|
-
- Sessions auto-renew; if you get auth errors, run \`gigai connect\`
|
|
1053
|
-
- All tool execution happens on the user's machine, not in this container
|
|
1054
|
-
`;
|
|
1055
|
-
await writeFile2(join3(skillDir, "SKILL.md"), skillMd);
|
|
1056
|
-
const tokenPayload = {
|
|
1057
|
-
orgUuid: "*",
|
|
1058
|
-
serverFingerprint: randomBytes2(16).toString("hex"),
|
|
1059
|
-
createdAt: Date.now()
|
|
1060
|
-
};
|
|
1061
|
-
const encryptedToken = encrypt(tokenPayload, encryptionKey);
|
|
1062
|
-
const skillConfig = {
|
|
1063
|
-
server: serverUrl,
|
|
1064
|
-
token: JSON.stringify(encryptedToken)
|
|
1065
|
-
};
|
|
1066
|
-
await writeFile2(join3(skillDir, "config.json"), JSON.stringify(skillConfig, null, 2) + "\n", { mode: 384 });
|
|
1067
|
-
console.log(` Skill template written to: ${skillDir}/`);
|
|
1068
|
-
const store = new AuthStore();
|
|
1069
|
-
const code = generatePairingCode(store, config.auth.pairingTtlSeconds);
|
|
1070
1271
|
console.log(`
|
|
1071
|
-
|
|
1072
|
-
|
|
1272
|
+
Paste this into Claude to pair:
|
|
1273
|
+
`);
|
|
1274
|
+
console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
1275
|
+
console.log(` Install the gigai CLI and pair with my server:
|
|
1276
|
+
`);
|
|
1277
|
+
console.log(` \`\`\`bash`);
|
|
1278
|
+
console.log(` npm install -g @schuttdev/gigai`);
|
|
1279
|
+
console.log(` gigai pair ${code} ${serverUrl}`);
|
|
1280
|
+
console.log(` \`\`\`
|
|
1281
|
+
`);
|
|
1282
|
+
console.log(` Then show me the skill file output so I can save it.`);
|
|
1283
|
+
console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
1073
1284
|
console.log(`
|
|
1074
|
-
|
|
1075
|
-
console.log(`
|
|
1076
|
-
|
|
1285
|
+
Pairing code expires in ${config.auth.pairingTtlSeconds / 60} minutes.`);
|
|
1286
|
+
console.log(` Run 'gigai server pair' to generate a new one.
|
|
1287
|
+
`);
|
|
1077
1288
|
}
|
|
1078
1289
|
async function loadConfigFile(path) {
|
|
1079
1290
|
const configPath = resolve5(path ?? "gigai.config.json");
|
|
@@ -1186,17 +1397,165 @@ async function unwrapTool(name) {
|
|
|
1186
1397
|
}
|
|
1187
1398
|
async function generateServerPairingCode(configPath) {
|
|
1188
1399
|
const { config } = await loadConfigFile(configPath);
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1400
|
+
const port = config.server.port;
|
|
1401
|
+
try {
|
|
1402
|
+
const res = await fetch(`http://localhost:${port}/auth/pair/generate`);
|
|
1403
|
+
if (!res.ok) {
|
|
1404
|
+
const body = await res.text();
|
|
1405
|
+
throw new Error(`Server returned ${res.status}: ${body}`);
|
|
1406
|
+
}
|
|
1407
|
+
const data = await res.json();
|
|
1408
|
+
console.log(`
|
|
1409
|
+
Pairing code: ${data.code}`);
|
|
1410
|
+
console.log(`Expires in ${data.expiresIn / 60} minutes.`);
|
|
1411
|
+
} catch (e) {
|
|
1412
|
+
if (e.message.includes("fetch failed") || e.message.includes("ECONNREFUSED")) {
|
|
1413
|
+
console.error("Server is not running. Start it with: gigai server start");
|
|
1414
|
+
} else {
|
|
1415
|
+
console.error(`Error: ${e.message}`);
|
|
1416
|
+
}
|
|
1417
|
+
process.exitCode = 1;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
1421
|
+
function getGigaiBin() {
|
|
1422
|
+
return process.argv[1] ?? "gigai";
|
|
1423
|
+
}
|
|
1424
|
+
function getLaunchdPlist(configPath) {
|
|
1425
|
+
const bin = getGigaiBin();
|
|
1426
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1427
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1428
|
+
<plist version="1.0">
|
|
1429
|
+
<dict>
|
|
1430
|
+
<key>Label</key>
|
|
1431
|
+
<string>com.gigai.server</string>
|
|
1432
|
+
<key>ProgramArguments</key>
|
|
1433
|
+
<array>
|
|
1434
|
+
<string>${bin}</string>
|
|
1435
|
+
<string>server</string>
|
|
1436
|
+
<string>start</string>
|
|
1437
|
+
<string>--config</string>
|
|
1438
|
+
<string>${configPath}</string>
|
|
1439
|
+
</array>
|
|
1440
|
+
<key>RunAtLoad</key>
|
|
1441
|
+
<true/>
|
|
1442
|
+
<key>KeepAlive</key>
|
|
1443
|
+
<true/>
|
|
1444
|
+
<key>StandardOutPath</key>
|
|
1445
|
+
<string>${join3(homedir(), ".gigai", "server.log")}</string>
|
|
1446
|
+
<key>StandardErrorPath</key>
|
|
1447
|
+
<string>${join3(homedir(), ".gigai", "server.log")}</string>
|
|
1448
|
+
<key>WorkingDirectory</key>
|
|
1449
|
+
<string>${homedir()}</string>
|
|
1450
|
+
</dict>
|
|
1451
|
+
</plist>
|
|
1452
|
+
`;
|
|
1453
|
+
}
|
|
1454
|
+
function getSystemdUnit(configPath) {
|
|
1455
|
+
const bin = getGigaiBin();
|
|
1456
|
+
return `[Unit]
|
|
1457
|
+
Description=gigai server
|
|
1458
|
+
After=network.target
|
|
1459
|
+
|
|
1460
|
+
[Service]
|
|
1461
|
+
Type=simple
|
|
1462
|
+
ExecStart=${bin} server start --config ${configPath}
|
|
1463
|
+
Restart=always
|
|
1464
|
+
RestartSec=5
|
|
1465
|
+
WorkingDirectory=${homedir()}
|
|
1466
|
+
|
|
1467
|
+
[Install]
|
|
1468
|
+
WantedBy=default.target
|
|
1469
|
+
`;
|
|
1470
|
+
}
|
|
1471
|
+
async function installDaemon(configPath) {
|
|
1472
|
+
const config = resolve6(configPath ?? "gigai.config.json");
|
|
1473
|
+
const os = platform();
|
|
1474
|
+
if (os === "darwin") {
|
|
1475
|
+
const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
|
|
1476
|
+
await writeFile4(plistPath, getLaunchdPlist(config));
|
|
1477
|
+
console.log(` Wrote launchd plist: ${plistPath}`);
|
|
1478
|
+
try {
|
|
1479
|
+
await execFileAsync3("launchctl", ["load", plistPath]);
|
|
1480
|
+
console.log(" Service loaded and started.");
|
|
1481
|
+
} catch {
|
|
1482
|
+
console.log(` Load it with: launchctl load ${plistPath}`);
|
|
1483
|
+
}
|
|
1484
|
+
console.log(` Logs: ~/.gigai/server.log`);
|
|
1485
|
+
console.log(` Stop: launchctl unload ${plistPath}`);
|
|
1486
|
+
} else if (os === "linux") {
|
|
1487
|
+
const unitDir = join3(homedir(), ".config", "systemd", "user");
|
|
1488
|
+
const unitPath = join3(unitDir, "gigai.service");
|
|
1489
|
+
const { mkdir: mkdir2 } = await import("fs/promises");
|
|
1490
|
+
await mkdir2(unitDir, { recursive: true });
|
|
1491
|
+
await writeFile4(unitPath, getSystemdUnit(config));
|
|
1492
|
+
console.log(` Wrote systemd unit: ${unitPath}`);
|
|
1493
|
+
try {
|
|
1494
|
+
await execFileAsync3("systemctl", ["--user", "daemon-reload"]);
|
|
1495
|
+
await execFileAsync3("systemctl", ["--user", "enable", "--now", "gigai"]);
|
|
1496
|
+
console.log(" Service enabled and started.");
|
|
1497
|
+
} catch {
|
|
1498
|
+
console.log(" Enable it with: systemctl --user enable --now gigai");
|
|
1499
|
+
}
|
|
1500
|
+
console.log(` Logs: journalctl --user -u gigai -f`);
|
|
1501
|
+
console.log(` Stop: systemctl --user stop gigai`);
|
|
1502
|
+
console.log(` Remove: systemctl --user disable gigai`);
|
|
1503
|
+
} else {
|
|
1504
|
+
console.log(" Persistent daemon not supported on this platform.");
|
|
1505
|
+
console.log(" Run 'gigai server start' manually.");
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async function uninstallDaemon() {
|
|
1509
|
+
const os = platform();
|
|
1510
|
+
if (os === "darwin") {
|
|
1511
|
+
const plistPath = join3(homedir(), "Library", "LaunchAgents", "com.gigai.server.plist");
|
|
1512
|
+
try {
|
|
1513
|
+
await execFileAsync3("launchctl", ["unload", plistPath]);
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
1517
|
+
try {
|
|
1518
|
+
await unlink2(plistPath);
|
|
1519
|
+
console.log(" Service removed.");
|
|
1520
|
+
} catch {
|
|
1521
|
+
console.log(" No service found.");
|
|
1522
|
+
}
|
|
1523
|
+
} else if (os === "linux") {
|
|
1524
|
+
try {
|
|
1525
|
+
await execFileAsync3("systemctl", ["--user", "disable", "--now", "gigai"]);
|
|
1526
|
+
} catch {
|
|
1527
|
+
}
|
|
1528
|
+
const unitPath = join3(homedir(), ".config", "systemd", "user", "gigai.service");
|
|
1529
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
1530
|
+
try {
|
|
1531
|
+
await unlink2(unitPath);
|
|
1532
|
+
await execFileAsync3("systemctl", ["--user", "daemon-reload"]);
|
|
1533
|
+
console.log(" Service removed.");
|
|
1534
|
+
} catch {
|
|
1535
|
+
console.log(" No service found.");
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
async function stopServer() {
|
|
1540
|
+
const { execFileSync } = await import("child_process");
|
|
1541
|
+
let pids = [];
|
|
1542
|
+
try {
|
|
1543
|
+
const out = execFileSync("pgrep", ["-f", "gigai server start"], { encoding: "utf8" });
|
|
1544
|
+
pids = out.trim().split("\n").map(Number).filter((pid) => pid && pid !== process.pid);
|
|
1545
|
+
} catch {
|
|
1546
|
+
}
|
|
1547
|
+
if (pids.length === 0) {
|
|
1548
|
+
console.log("No running gigai server found.");
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
for (const pid of pids) {
|
|
1552
|
+
try {
|
|
1553
|
+
process.kill(pid, "SIGTERM");
|
|
1554
|
+
console.log(`Stopped gigai server (PID ${pid})`);
|
|
1555
|
+
} catch (e) {
|
|
1556
|
+
console.error(`Failed to stop PID ${pid}: ${e.message}`);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1200
1559
|
}
|
|
1201
1560
|
async function startServer() {
|
|
1202
1561
|
const { values } = parseArgs({
|
|
@@ -1212,8 +1571,38 @@ async function startServer() {
|
|
|
1212
1571
|
const host = config.server.host;
|
|
1213
1572
|
await server.listen({ port, host });
|
|
1214
1573
|
server.log.info(`gigai server listening on ${host}:${port}`);
|
|
1574
|
+
let cfTunnel;
|
|
1575
|
+
const httpsProvider = config.server.https?.provider;
|
|
1576
|
+
if (httpsProvider === "tailscale") {
|
|
1577
|
+
try {
|
|
1578
|
+
const funnelUrl = await enableFunnel(port);
|
|
1579
|
+
server.log.info(`Tailscale Funnel enabled: ${funnelUrl}`);
|
|
1580
|
+
} catch (e) {
|
|
1581
|
+
server.log.error(`Failed to enable Tailscale Funnel: ${e.message}`);
|
|
1582
|
+
}
|
|
1583
|
+
} else if (httpsProvider === "cloudflare") {
|
|
1584
|
+
try {
|
|
1585
|
+
const tunnelName = config.server.https.tunnelName;
|
|
1586
|
+
cfTunnel = runTunnel(tunnelName, port);
|
|
1587
|
+
server.log.info(`Cloudflare Tunnel started: ${tunnelName}`);
|
|
1588
|
+
} catch (e) {
|
|
1589
|
+
server.log.error(`Failed to start Cloudflare Tunnel: ${e.message}`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1215
1592
|
const shutdown = async () => {
|
|
1216
1593
|
server.log.info("Shutting down...");
|
|
1594
|
+
if (httpsProvider === "tailscale") {
|
|
1595
|
+
try {
|
|
1596
|
+
await disableFunnel(port);
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (cfTunnel) {
|
|
1601
|
+
try {
|
|
1602
|
+
cfTunnel.kill();
|
|
1603
|
+
} catch {
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1217
1606
|
await server.close();
|
|
1218
1607
|
process.exit(0);
|
|
1219
1608
|
};
|
|
@@ -1223,9 +1612,12 @@ async function startServer() {
|
|
|
1223
1612
|
export {
|
|
1224
1613
|
createServer,
|
|
1225
1614
|
generateServerPairingCode,
|
|
1615
|
+
installDaemon,
|
|
1226
1616
|
loadConfig,
|
|
1227
1617
|
runInit,
|
|
1228
1618
|
startServer,
|
|
1619
|
+
stopServer,
|
|
1620
|
+
uninstallDaemon,
|
|
1229
1621
|
unwrapTool,
|
|
1230
1622
|
wrapCli,
|
|
1231
1623
|
wrapImport,
|