sakuraai 0.0.8 → 0.0.9
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 +385 -84
- package/package.json +3 -1
- package/scripts/download-tsnet.js +77 -0
package/dist/index.js
CHANGED
|
@@ -14,6 +14,13 @@ import os from "os";
|
|
|
14
14
|
import path from "path";
|
|
15
15
|
import fs from "fs";
|
|
16
16
|
import { randomUUID } from "crypto";
|
|
17
|
+
function readTsnetState() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(TSNET_PATH, "utf8"));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
17
24
|
function ensureDir(dir) {
|
|
18
25
|
fs.mkdirSync(dir, { recursive: true });
|
|
19
26
|
}
|
|
@@ -74,7 +81,7 @@ function ensureSakuraDirs() {
|
|
|
74
81
|
ensureDir(SAKURA_DIR);
|
|
75
82
|
ensureDir(LOG_DIR);
|
|
76
83
|
}
|
|
77
|
-
var SAKURA_DIR, CONFIG_PATH, AUTH_PATH, DAEMON_PATH, PID_PATH, LOG_DIR, LOG_PATH, DEFAULT_PORT, DEFAULT_HOST, _config;
|
|
84
|
+
var SAKURA_DIR, CONFIG_PATH, AUTH_PATH, DAEMON_PATH, PID_PATH, LOG_DIR, LOG_PATH, DEFAULT_PORT, DEFAULT_HOST, TSNET_PATH, _config;
|
|
78
85
|
var init_config = __esm({
|
|
79
86
|
"src/config.ts"() {
|
|
80
87
|
"use strict";
|
|
@@ -87,38 +94,134 @@ var init_config = __esm({
|
|
|
87
94
|
LOG_PATH = path.join(LOG_DIR, "daemon.log");
|
|
88
95
|
DEFAULT_PORT = Number(process.env.SAKURA_PORT ?? 4787);
|
|
89
96
|
DEFAULT_HOST = process.env.SAKURA_HOST ?? "127.0.0.1";
|
|
97
|
+
TSNET_PATH = path.join(SAKURA_DIR, "tsnet.json");
|
|
90
98
|
_config = null;
|
|
91
99
|
}
|
|
92
100
|
});
|
|
93
101
|
|
|
102
|
+
// src/cloud.ts
|
|
103
|
+
function apiBase() {
|
|
104
|
+
return (loadAuth().serverUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
105
|
+
}
|
|
106
|
+
async function postJson(base, path10, body, token) {
|
|
107
|
+
const res = await fetch(`${base}${path10}`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(body ?? {})
|
|
114
|
+
});
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
const text = await res.text().catch(() => "");
|
|
117
|
+
throw new Error(`${path10} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
|
|
118
|
+
}
|
|
119
|
+
return await res.json();
|
|
120
|
+
}
|
|
121
|
+
function deviceStart(base, machineName) {
|
|
122
|
+
return postJson(base, "/cli/device/start", { machineName });
|
|
123
|
+
}
|
|
124
|
+
function devicePoll(base, deviceCode) {
|
|
125
|
+
return postJson(base, "/cli/device/poll", { deviceCode });
|
|
126
|
+
}
|
|
127
|
+
function provisionTailnet(base, serverToken, hostname2) {
|
|
128
|
+
return postJson(base, "/tailnet/provision", { hostname: hostname2 }, serverToken);
|
|
129
|
+
}
|
|
130
|
+
var DEFAULT_API_URL;
|
|
131
|
+
var init_cloud = __esm({
|
|
132
|
+
"src/cloud.ts"() {
|
|
133
|
+
"use strict";
|
|
134
|
+
init_config();
|
|
135
|
+
DEFAULT_API_URL = process.env.SAKURA_SERVER_URL || "https://api.sakura.mom";
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
94
139
|
// src/auth.ts
|
|
95
140
|
var auth_exports = {};
|
|
96
141
|
__export(auth_exports, {
|
|
142
|
+
getServerToken: () => getServerToken,
|
|
97
143
|
getToken: () => getToken,
|
|
98
144
|
isLoggedIn: () => isLoggedIn,
|
|
99
145
|
login: () => login,
|
|
146
|
+
loginInteractive: () => loginInteractive,
|
|
100
147
|
logout: () => logout,
|
|
101
148
|
requireToken: () => requireToken
|
|
102
149
|
});
|
|
103
150
|
import { randomBytes } from "crypto";
|
|
151
|
+
import { spawn } from "child_process";
|
|
104
152
|
import os2 from "os";
|
|
105
153
|
function getToken() {
|
|
106
154
|
return process.env.SAKURA_AUTH || loadAuth().token;
|
|
107
155
|
}
|
|
156
|
+
function getServerToken() {
|
|
157
|
+
return process.env.SAKURA_SERVER_TOKEN || loadAuth().serverToken;
|
|
158
|
+
}
|
|
108
159
|
function isLoggedIn() {
|
|
109
160
|
return !!getToken();
|
|
110
161
|
}
|
|
111
162
|
function generateToken() {
|
|
112
163
|
return "sk_" + randomBytes(24).toString("hex");
|
|
113
164
|
}
|
|
165
|
+
function openBrowser(url) {
|
|
166
|
+
try {
|
|
167
|
+
if (process.platform === "darwin") {
|
|
168
|
+
spawn("open", [url], { stdio: "ignore", detached: true }).unref();
|
|
169
|
+
} else if (process.platform === "win32") {
|
|
170
|
+
spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true }).unref();
|
|
171
|
+
} else {
|
|
172
|
+
spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
114
177
|
function login(opts = {}) {
|
|
115
|
-
const
|
|
178
|
+
const existing = loadAuth();
|
|
116
179
|
const machineName = opts.machineName?.trim() || os2.hostname();
|
|
117
180
|
const state = {
|
|
118
|
-
|
|
119
|
-
|
|
181
|
+
...existing,
|
|
182
|
+
token: opts.auth?.trim() || existing.token || generateToken(),
|
|
183
|
+
serverUrl: opts.serverUrl || existing.serverUrl,
|
|
184
|
+
machineName,
|
|
185
|
+
login: existing.login || process.env.USER || machineName,
|
|
186
|
+
loggedInAt: Date.now()
|
|
187
|
+
};
|
|
188
|
+
saveAuth(state);
|
|
189
|
+
updateConfig((cfg) => {
|
|
190
|
+
cfg.machineName = machineName;
|
|
191
|
+
});
|
|
192
|
+
return state;
|
|
193
|
+
}
|
|
194
|
+
async function loginInteractive(opts = {}, cb = {}) {
|
|
195
|
+
const base = (opts.serverUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
196
|
+
const machineName = opts.machineName?.trim() || os2.hostname();
|
|
197
|
+
const start2 = await deviceStart(base, machineName);
|
|
198
|
+
cb.onPrompt?.({ verifyUrl: start2.verifyUrl, userCode: start2.userCode });
|
|
199
|
+
openBrowser(start2.verifyUrl);
|
|
200
|
+
const deadline = Date.now() + start2.expiresIn * 1e3;
|
|
201
|
+
const intervalMs = Math.max(1, start2.interval) * 1e3;
|
|
202
|
+
let approved = null;
|
|
203
|
+
while (Date.now() < deadline) {
|
|
204
|
+
await sleep(intervalMs);
|
|
205
|
+
const poll = await devicePoll(base, start2.deviceCode);
|
|
206
|
+
if (poll.status === "approved") {
|
|
207
|
+
approved = poll;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (poll.status === "expired" || poll.status === "unknown") {
|
|
211
|
+
throw new Error("login code expired \u2014 run `sakura login` again.");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!approved?.token) throw new Error("login timed out \u2014 run `sakura login` again.");
|
|
215
|
+
const existing = loadAuth();
|
|
216
|
+
const state = {
|
|
217
|
+
...existing,
|
|
218
|
+
token: existing.token || generateToken(),
|
|
219
|
+
// keep the phone↔daemon secret
|
|
220
|
+
serverToken: approved.token,
|
|
221
|
+
serverUrl: base,
|
|
222
|
+
userId: approved.user?._id,
|
|
223
|
+
login: approved.user?.login || approved.user?.email || machineName,
|
|
120
224
|
machineName,
|
|
121
|
-
login: process.env.USER || machineName,
|
|
122
225
|
loggedInAt: Date.now()
|
|
123
226
|
};
|
|
124
227
|
saveAuth(state);
|
|
@@ -134,15 +237,18 @@ function requireToken() {
|
|
|
134
237
|
const t = getToken();
|
|
135
238
|
if (!t) {
|
|
136
239
|
throw new Error(
|
|
137
|
-
"Not signed in. Run `
|
|
240
|
+
"Not signed in. Run `sakura login` first."
|
|
138
241
|
);
|
|
139
242
|
}
|
|
140
243
|
return t;
|
|
141
244
|
}
|
|
245
|
+
var sleep;
|
|
142
246
|
var init_auth = __esm({
|
|
143
247
|
"src/auth.ts"() {
|
|
144
248
|
"use strict";
|
|
145
249
|
init_config();
|
|
250
|
+
init_cloud();
|
|
251
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
146
252
|
}
|
|
147
253
|
});
|
|
148
254
|
|
|
@@ -296,7 +402,7 @@ var init_util = __esm({
|
|
|
296
402
|
// src/runtime/claude.ts
|
|
297
403
|
import fs3 from "fs";
|
|
298
404
|
import path2 from "path";
|
|
299
|
-
import { spawn } from "child_process";
|
|
405
|
+
import { spawn as spawn2 } from "child_process";
|
|
300
406
|
function* jsonlEntries(file) {
|
|
301
407
|
let content;
|
|
302
408
|
try {
|
|
@@ -440,7 +546,7 @@ function send(sessionId, message, onData, images) {
|
|
|
440
546
|
|
|
441
547
|
${refs}` : refs;
|
|
442
548
|
}
|
|
443
|
-
const
|
|
549
|
+
const child2 = spawn2(
|
|
444
550
|
bin,
|
|
445
551
|
[
|
|
446
552
|
"--resume",
|
|
@@ -460,7 +566,7 @@ ${refs}` : refs;
|
|
|
460
566
|
let buf = "";
|
|
461
567
|
let finalText = "";
|
|
462
568
|
let err = "";
|
|
463
|
-
|
|
569
|
+
child2.stdout.on("data", (d) => {
|
|
464
570
|
buf += d.toString();
|
|
465
571
|
let nl;
|
|
466
572
|
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
@@ -477,9 +583,9 @@ ${refs}` : refs;
|
|
|
477
583
|
}
|
|
478
584
|
}
|
|
479
585
|
});
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
586
|
+
child2.stderr.on("data", (d) => err += d.toString());
|
|
587
|
+
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
588
|
+
child2.on(
|
|
483
589
|
"close",
|
|
484
590
|
(code) => resolve({
|
|
485
591
|
ok: code === 0,
|
|
@@ -508,7 +614,7 @@ var init_claude = __esm({
|
|
|
508
614
|
// src/runtime/codex.ts
|
|
509
615
|
import fs4 from "fs";
|
|
510
616
|
import path3 from "path";
|
|
511
|
-
import { spawn as
|
|
617
|
+
import { spawn as spawn3 } from "child_process";
|
|
512
618
|
function codexBin() {
|
|
513
619
|
if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
|
|
514
620
|
if (fs4.existsSync(MAC_APP_BIN)) return MAC_APP_BIN;
|
|
@@ -620,19 +726,19 @@ function messages2(sessionId) {
|
|
|
620
726
|
function send2(sessionId, message, onData, images) {
|
|
621
727
|
return new Promise((resolve) => {
|
|
622
728
|
const imageArgs = (images ?? []).flatMap((p) => ["-i", p]);
|
|
623
|
-
const
|
|
729
|
+
const child2 = spawn3(codexBin(), ["exec", "resume", sessionId, ...imageArgs, message], {
|
|
624
730
|
stdio: ["ignore", "pipe", "pipe"],
|
|
625
731
|
env: process.env
|
|
626
732
|
});
|
|
627
733
|
let out = "";
|
|
628
734
|
let err = "";
|
|
629
|
-
|
|
735
|
+
child2.stdout.on("data", (d) => {
|
|
630
736
|
out += d.toString();
|
|
631
737
|
onData?.(d.toString());
|
|
632
738
|
});
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
739
|
+
child2.stderr.on("data", (d) => err += d.toString());
|
|
740
|
+
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
741
|
+
child2.on(
|
|
636
742
|
"close",
|
|
637
743
|
(code) => resolve({
|
|
638
744
|
ok: code === 0,
|
|
@@ -657,7 +763,7 @@ var init_codex = __esm({
|
|
|
657
763
|
// src/runtime/opencode.ts
|
|
658
764
|
import path4 from "path";
|
|
659
765
|
import fs5 from "fs";
|
|
660
|
-
import { spawn as
|
|
766
|
+
import { spawn as spawn4 } from "child_process";
|
|
661
767
|
function opencodeBin() {
|
|
662
768
|
if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN;
|
|
663
769
|
if (fs5.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
|
|
@@ -767,19 +873,19 @@ async function messages3(sessionId) {
|
|
|
767
873
|
function send3(sessionId, message, onData, images) {
|
|
768
874
|
return new Promise((resolve) => {
|
|
769
875
|
const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
|
|
770
|
-
const
|
|
876
|
+
const child2 = spawn4(opencodeBin(), ["run", "-s", sessionId, ...fileArgs, message], {
|
|
771
877
|
stdio: ["ignore", "pipe", "pipe"],
|
|
772
878
|
env: process.env
|
|
773
879
|
});
|
|
774
880
|
let out = "";
|
|
775
881
|
let err = "";
|
|
776
|
-
|
|
882
|
+
child2.stdout.on("data", (d) => {
|
|
777
883
|
out += d.toString();
|
|
778
884
|
onData?.(d.toString());
|
|
779
885
|
});
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
886
|
+
child2.stderr.on("data", (d) => err += d.toString());
|
|
887
|
+
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
888
|
+
child2.on(
|
|
783
889
|
"close",
|
|
784
890
|
(code) => resolve({
|
|
785
891
|
ok: code === 0,
|
|
@@ -837,7 +943,7 @@ var init_meta = __esm({
|
|
|
837
943
|
});
|
|
838
944
|
|
|
839
945
|
// src/runtime/sessions.ts
|
|
840
|
-
import { spawn as
|
|
946
|
+
import { spawn as spawn5 } from "child_process";
|
|
841
947
|
async function list(opts = {}) {
|
|
842
948
|
let sessions = [];
|
|
843
949
|
if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
|
|
@@ -918,16 +1024,16 @@ function create(prompt, opts = {}) {
|
|
|
918
1024
|
}
|
|
919
1025
|
return new Promise((resolve) => {
|
|
920
1026
|
const bin = process.env.CLAUDE_BIN || "claude";
|
|
921
|
-
const
|
|
1027
|
+
const child2 = spawn5(bin, ["-p", prompt], {
|
|
922
1028
|
cwd: opts.cwd || process.cwd(),
|
|
923
1029
|
stdio: ["ignore", "pipe", "pipe"]
|
|
924
1030
|
});
|
|
925
1031
|
let out = "";
|
|
926
1032
|
let err = "";
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1033
|
+
child2.stdout.on("data", (d) => out += d.toString());
|
|
1034
|
+
child2.stderr.on("data", (d) => err += d.toString());
|
|
1035
|
+
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
1036
|
+
child2.on(
|
|
931
1037
|
"close",
|
|
932
1038
|
(code) => resolve({
|
|
933
1039
|
ok: code === 0,
|
|
@@ -1289,7 +1395,7 @@ var init_fsops = __esm({
|
|
|
1289
1395
|
|
|
1290
1396
|
// src/tailscale.ts
|
|
1291
1397
|
import fs9 from "fs";
|
|
1292
|
-
import { spawn as
|
|
1398
|
+
import { spawn as spawn6 } from "child_process";
|
|
1293
1399
|
function tsBin() {
|
|
1294
1400
|
const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
1295
1401
|
if (fs9.existsSync(macPath)) return macPath;
|
|
@@ -1297,11 +1403,11 @@ function tsBin() {
|
|
|
1297
1403
|
}
|
|
1298
1404
|
function runTs(args, timeoutMs = 6e4) {
|
|
1299
1405
|
return new Promise((resolve) => {
|
|
1300
|
-
const
|
|
1406
|
+
const child2 = spawn6(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1301
1407
|
let out = "";
|
|
1302
1408
|
let err = "";
|
|
1303
1409
|
const timer = setTimeout(() => {
|
|
1304
|
-
|
|
1410
|
+
child2.kill("SIGKILL");
|
|
1305
1411
|
resolve({
|
|
1306
1412
|
ok: false,
|
|
1307
1413
|
data: null,
|
|
@@ -1310,9 +1416,9 @@ function runTs(args, timeoutMs = 6e4) {
|
|
|
1310
1416
|
code: "timeout"
|
|
1311
1417
|
});
|
|
1312
1418
|
}, timeoutMs);
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1419
|
+
child2.stdout.on("data", (d) => out += d.toString());
|
|
1420
|
+
child2.stderr.on("data", (d) => err += d.toString());
|
|
1421
|
+
child2.on("error", (e) => {
|
|
1316
1422
|
clearTimeout(timer);
|
|
1317
1423
|
if (e.code === "ENOENT") {
|
|
1318
1424
|
resolve({
|
|
@@ -1332,7 +1438,7 @@ function runTs(args, timeoutMs = 6e4) {
|
|
|
1332
1438
|
});
|
|
1333
1439
|
}
|
|
1334
1440
|
});
|
|
1335
|
-
|
|
1441
|
+
child2.on("close", (code) => {
|
|
1336
1442
|
clearTimeout(timer);
|
|
1337
1443
|
const stdout = out.trim();
|
|
1338
1444
|
const stderr = err.trim();
|
|
@@ -1506,7 +1612,7 @@ var init_stream = __esm({
|
|
|
1506
1612
|
|
|
1507
1613
|
// src/daemon/terminal.ts
|
|
1508
1614
|
import os6 from "os";
|
|
1509
|
-
import { spawn as
|
|
1615
|
+
import { spawn as spawn7 } from "child_process";
|
|
1510
1616
|
function cleanOutput(t, chunk) {
|
|
1511
1617
|
let s = t.buf + chunk;
|
|
1512
1618
|
s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
|
|
@@ -1524,7 +1630,7 @@ function defaultShell() {
|
|
|
1524
1630
|
}
|
|
1525
1631
|
function openTerminal(ws, opts) {
|
|
1526
1632
|
const shell = defaultShell();
|
|
1527
|
-
const
|
|
1633
|
+
const child2 = spawn7(shell, ["-i"], {
|
|
1528
1634
|
cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os6.homedir(),
|
|
1529
1635
|
env: {
|
|
1530
1636
|
...process.env,
|
|
@@ -1534,7 +1640,7 @@ function openTerminal(ws, opts) {
|
|
|
1534
1640
|
ITERM_SHELL_INTEGRATION_INSTALLED: ""
|
|
1535
1641
|
}
|
|
1536
1642
|
});
|
|
1537
|
-
const term = { child, buf: "" };
|
|
1643
|
+
const term = { child: child2, buf: "" };
|
|
1538
1644
|
terminals.set(ws, term);
|
|
1539
1645
|
const sendOut = (raw) => {
|
|
1540
1646
|
const data = cleanOutput(term, raw);
|
|
@@ -1544,15 +1650,15 @@ function openTerminal(ws, opts) {
|
|
|
1544
1650
|
} catch {
|
|
1545
1651
|
}
|
|
1546
1652
|
};
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1653
|
+
child2.stdout.on("data", (d) => sendOut(d.toString()));
|
|
1654
|
+
child2.stderr.on("data", (d) => sendOut(d.toString()));
|
|
1655
|
+
child2.on("exit", (code) => {
|
|
1550
1656
|
try {
|
|
1551
1657
|
ws.send(JSON.stringify({ type: "exit", code }));
|
|
1552
1658
|
} catch {
|
|
1553
1659
|
}
|
|
1554
1660
|
});
|
|
1555
|
-
|
|
1661
|
+
child2.on("error", (e) => sendOut(`
|
|
1556
1662
|
[shell error: ${e.message}]
|
|
1557
1663
|
`));
|
|
1558
1664
|
try {
|
|
@@ -1597,10 +1703,10 @@ import os7 from "os";
|
|
|
1597
1703
|
import path8 from "path";
|
|
1598
1704
|
import { URL } from "url";
|
|
1599
1705
|
import { WebSocketServer } from "ws";
|
|
1600
|
-
function add(method,
|
|
1706
|
+
function add(method, path10, handler) {
|
|
1601
1707
|
const keys = [];
|
|
1602
1708
|
const pattern = new RegExp(
|
|
1603
|
-
"^" +
|
|
1709
|
+
"^" + path10.replace(/:[^/]+/g, (m) => {
|
|
1604
1710
|
keys.push(m.slice(1));
|
|
1605
1711
|
return "([^/]+)";
|
|
1606
1712
|
}) + "/?$"
|
|
@@ -2024,12 +2130,121 @@ var init_server = __esm({
|
|
|
2024
2130
|
}
|
|
2025
2131
|
});
|
|
2026
2132
|
|
|
2027
|
-
// src/daemon/
|
|
2133
|
+
// src/daemon/tsnet.ts
|
|
2028
2134
|
import fs11 from "fs";
|
|
2029
|
-
import
|
|
2135
|
+
import path9 from "path";
|
|
2136
|
+
import { fileURLToPath } from "url";
|
|
2137
|
+
import { spawn as spawn8 } from "child_process";
|
|
2138
|
+
function resolveBinary() {
|
|
2139
|
+
const envBin = process.env.SAKURA_TSNET_BIN;
|
|
2140
|
+
if (envBin && fs11.existsSync(envBin)) return envBin;
|
|
2141
|
+
const here = path9.dirname(fileURLToPath(import.meta.url));
|
|
2142
|
+
const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
|
|
2143
|
+
const candidates = [
|
|
2144
|
+
path9.join(here, "..", "bin", name),
|
|
2145
|
+
// package/bin (postinstall download / build-sidecar.sh)
|
|
2146
|
+
path9.join(here, "bin", name),
|
|
2147
|
+
path9.join(process.cwd(), "bin", name),
|
|
2148
|
+
path9.join(SAKURA_DIR, name)
|
|
2149
|
+
// user-dropped
|
|
2150
|
+
];
|
|
2151
|
+
return candidates.find((p) => fs11.existsSync(p)) ?? null;
|
|
2152
|
+
}
|
|
2153
|
+
function startTsnet(port2) {
|
|
2154
|
+
const cfg = loadConfig();
|
|
2155
|
+
const ts = cfg.tailscale;
|
|
2156
|
+
if (!ts?.controlUrl || !ts?.authKey) return;
|
|
2157
|
+
if (child) return;
|
|
2158
|
+
const bin = resolveBinary();
|
|
2159
|
+
if (!bin) {
|
|
2160
|
+
log.warn(
|
|
2161
|
+
"embedded Tailscale is configured but the sakura-tsnet binary was not found \u2014 build it with modules/sakura-tailscale/scripts/build-sidecar.sh (LAN access still works)."
|
|
2162
|
+
);
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
stopping = false;
|
|
2166
|
+
spawnSidecar(bin, ts.controlUrl, ts.authKey, ts.hostname || cfg.machineName, port2);
|
|
2167
|
+
}
|
|
2168
|
+
function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
2169
|
+
const env = {
|
|
2170
|
+
...process.env,
|
|
2171
|
+
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
2172
|
+
SAKURA_TS_AUTH_KEY: authKey,
|
|
2173
|
+
SAKURA_TS_HOSTNAME: hostname2,
|
|
2174
|
+
SAKURA_TS_STATE_DIR: path9.join(SAKURA_DIR, "tsnet-state"),
|
|
2175
|
+
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
2176
|
+
SAKURA_TS_PORT: String(port2)
|
|
2177
|
+
};
|
|
2178
|
+
log.info(`starting embedded Tailscale sidecar (${path9.basename(bin)})`);
|
|
2179
|
+
const c = spawn8(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
2180
|
+
child = c;
|
|
2181
|
+
const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
|
|
2182
|
+
c.stdout?.on("data", pipe);
|
|
2183
|
+
c.stderr?.on("data", pipe);
|
|
2184
|
+
c.on("exit", (code) => {
|
|
2185
|
+
child = null;
|
|
2186
|
+
if (stopping) return;
|
|
2187
|
+
log.warn(`tsnet sidecar exited (code ${code}); restarting in 5s`);
|
|
2188
|
+
restartTimer = setTimeout(() => {
|
|
2189
|
+
if (!stopping) spawnSidecar(bin, controlUrl, authKey, hostname2, port2);
|
|
2190
|
+
}, 5e3);
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
function stopTsnet() {
|
|
2194
|
+
stopping = true;
|
|
2195
|
+
if (restartTimer) {
|
|
2196
|
+
clearTimeout(restartTimer);
|
|
2197
|
+
restartTimer = null;
|
|
2198
|
+
}
|
|
2199
|
+
if (child) {
|
|
2200
|
+
try {
|
|
2201
|
+
child.kill("SIGTERM");
|
|
2202
|
+
} catch {
|
|
2203
|
+
}
|
|
2204
|
+
child = null;
|
|
2205
|
+
}
|
|
2206
|
+
try {
|
|
2207
|
+
fs11.rmSync(TSNET_PATH, { force: true });
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
var child, stopping, restartTimer;
|
|
2212
|
+
var init_tsnet = __esm({
|
|
2213
|
+
"src/daemon/tsnet.ts"() {
|
|
2214
|
+
"use strict";
|
|
2215
|
+
init_config();
|
|
2216
|
+
init_logger();
|
|
2217
|
+
child = null;
|
|
2218
|
+
stopping = false;
|
|
2219
|
+
restartTimer = null;
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
// src/daemon/manager.ts
|
|
2224
|
+
import fs12 from "fs";
|
|
2225
|
+
import { spawn as spawn9 } from "child_process";
|
|
2226
|
+
async function ensureTailnetProvisioned() {
|
|
2227
|
+
const serverToken = getServerToken();
|
|
2228
|
+
if (!serverToken) return;
|
|
2229
|
+
try {
|
|
2230
|
+
const cfg = loadConfig();
|
|
2231
|
+
const prov = await provisionTailnet(apiBase(), serverToken, cfg.machineName);
|
|
2232
|
+
if (!prov?.controlUrl || !prov?.authKey) return;
|
|
2233
|
+
updateConfig((c) => {
|
|
2234
|
+
c.tailscale = {
|
|
2235
|
+
controlUrl: prov.controlUrl,
|
|
2236
|
+
authKey: prov.authKey,
|
|
2237
|
+
hostname: prov.hostname || c.machineName
|
|
2238
|
+
};
|
|
2239
|
+
});
|
|
2240
|
+
log.info("provisioned embedded-Tailscale credentials for this account");
|
|
2241
|
+
} catch (e) {
|
|
2242
|
+
log.warn(`tailnet provisioning skipped (${e.message}) \u2014 serving over LAN`);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2030
2245
|
function readDaemonInfo() {
|
|
2031
2246
|
try {
|
|
2032
|
-
return JSON.parse(
|
|
2247
|
+
return JSON.parse(fs12.readFileSync(DAEMON_PATH, "utf8"));
|
|
2033
2248
|
} catch {
|
|
2034
2249
|
return null;
|
|
2035
2250
|
}
|
|
@@ -2046,8 +2261,8 @@ function running() {
|
|
|
2046
2261
|
const info2 = readDaemonInfo();
|
|
2047
2262
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
2048
2263
|
if (info2) {
|
|
2049
|
-
|
|
2050
|
-
|
|
2264
|
+
fs12.rmSync(DAEMON_PATH, { force: true });
|
|
2265
|
+
fs12.rmSync(PID_PATH, { force: true });
|
|
2051
2266
|
}
|
|
2052
2267
|
return null;
|
|
2053
2268
|
}
|
|
@@ -2070,17 +2285,20 @@ async function runServer(version) {
|
|
|
2070
2285
|
url: `http://${host}:${port2}`,
|
|
2071
2286
|
startedAt: Date.now()
|
|
2072
2287
|
};
|
|
2073
|
-
|
|
2074
|
-
|
|
2288
|
+
fs12.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
2289
|
+
fs12.writeFileSync(PID_PATH, String(process.pid));
|
|
2290
|
+
await ensureTailnetProvisioned();
|
|
2291
|
+
startTsnet(port2);
|
|
2075
2292
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
2076
2293
|
log.info(`sakuraai runtime listening on http://0.0.0.0:${port2}`);
|
|
2077
2294
|
if (tailnet) log.info(`reachable over Tailscale at ${tailnet}`);
|
|
2078
2295
|
log.info("point the Sakura app at the tailnet URL above (Settings \u2192 Connectivity)");
|
|
2079
2296
|
const shutdown = () => {
|
|
2080
2297
|
log.info("shutting down");
|
|
2298
|
+
stopTsnet();
|
|
2081
2299
|
server.close();
|
|
2082
|
-
|
|
2083
|
-
|
|
2300
|
+
fs12.rmSync(DAEMON_PATH, { force: true });
|
|
2301
|
+
fs12.rmSync(PID_PATH, { force: true });
|
|
2084
2302
|
process.exit(0);
|
|
2085
2303
|
};
|
|
2086
2304
|
process.on("SIGINT", shutdown);
|
|
@@ -2091,14 +2309,14 @@ function start() {
|
|
|
2091
2309
|
if (existing) return existing;
|
|
2092
2310
|
ensureSakuraDirs();
|
|
2093
2311
|
const entry = process.argv[1] ?? "";
|
|
2094
|
-
const out =
|
|
2095
|
-
const err =
|
|
2096
|
-
const
|
|
2312
|
+
const out = fs12.openSync(LOG_PATH, "a");
|
|
2313
|
+
const err = fs12.openSync(LOG_PATH, "a");
|
|
2314
|
+
const child2 = spawn9(process.execPath, [entry, "__run-daemon"], {
|
|
2097
2315
|
detached: true,
|
|
2098
2316
|
stdio: ["ignore", out, err],
|
|
2099
2317
|
env: process.env
|
|
2100
2318
|
});
|
|
2101
|
-
|
|
2319
|
+
child2.unref();
|
|
2102
2320
|
const deadline = Date.now() + 5e3;
|
|
2103
2321
|
while (Date.now() < deadline) {
|
|
2104
2322
|
const info2 = readDaemonInfo();
|
|
@@ -2114,8 +2332,8 @@ function stop() {
|
|
|
2114
2332
|
process.kill(info2.pid, "SIGTERM");
|
|
2115
2333
|
} catch {
|
|
2116
2334
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
2335
|
+
fs12.rmSync(DAEMON_PATH, { force: true });
|
|
2336
|
+
fs12.rmSync(PID_PATH, { force: true });
|
|
2119
2337
|
return true;
|
|
2120
2338
|
}
|
|
2121
2339
|
function restart() {
|
|
@@ -2125,7 +2343,7 @@ function restart() {
|
|
|
2125
2343
|
}
|
|
2126
2344
|
function logs(lines = 50) {
|
|
2127
2345
|
try {
|
|
2128
|
-
const all2 =
|
|
2346
|
+
const all2 = fs12.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
2129
2347
|
return all2.slice(-lines).join("\n");
|
|
2130
2348
|
} catch {
|
|
2131
2349
|
return "";
|
|
@@ -2138,6 +2356,9 @@ var init_manager = __esm({
|
|
|
2138
2356
|
init_logger();
|
|
2139
2357
|
init_server();
|
|
2140
2358
|
init_tailscale();
|
|
2359
|
+
init_tsnet();
|
|
2360
|
+
init_auth();
|
|
2361
|
+
init_cloud();
|
|
2141
2362
|
}
|
|
2142
2363
|
});
|
|
2143
2364
|
|
|
@@ -2148,10 +2369,23 @@ async function buildPairing() {
|
|
|
2148
2369
|
const token = requireToken();
|
|
2149
2370
|
const cfg = loadConfig();
|
|
2150
2371
|
const port2 = Number(process.env.SAKURA_PORT || cfg.daemon.port);
|
|
2151
|
-
const
|
|
2152
|
-
const
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2372
|
+
const tsnet = readTsnetState();
|
|
2373
|
+
const tsnetUrl = tsnet?.tailscaleIp && tsnet.backendState === "Running" ? `http://${tsnet.tailscaleIp}:${port2}` : null;
|
|
2374
|
+
const cliTailnet = tsnetUrl ? null : await discoverBaseUrl(port2).catch(() => null);
|
|
2375
|
+
const url = tsnetUrl ?? cliTailnet ?? `http://${localIp()}:${port2}`;
|
|
2376
|
+
const params = /* @__PURE__ */ new Map([
|
|
2377
|
+
["url", url],
|
|
2378
|
+
["token", token]
|
|
2379
|
+
]);
|
|
2380
|
+
const ts = cfg.tailscale;
|
|
2381
|
+
const embeddedTailscale = !!(ts?.controlUrl && ts?.authKey);
|
|
2382
|
+
if (embeddedTailscale) {
|
|
2383
|
+
params.set("cn", ts.controlUrl);
|
|
2384
|
+
params.set("tskey", ts.authKey);
|
|
2385
|
+
}
|
|
2386
|
+
const query = [...params].map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
2387
|
+
const deepLink = `sakura://pair?${query}`;
|
|
2388
|
+
return { url, token, deepLink, embeddedTailscale };
|
|
2155
2389
|
}
|
|
2156
2390
|
function renderQr(deepLink) {
|
|
2157
2391
|
return new Promise((resolve) => {
|
|
@@ -2193,6 +2427,11 @@ async function showPairing(opts = {}) {
|
|
|
2193
2427
|
info(` URL: ${pairing.url}`);
|
|
2194
2428
|
info(` Token: ${pairing.token}`);
|
|
2195
2429
|
info("");
|
|
2430
|
+
if (pairing.embeddedTailscale) {
|
|
2431
|
+
info("Embedded Tailscale: the QR also carries the tailnet join key, so the");
|
|
2432
|
+
info("phone connects privately over the tunnel with no Tailscale app needed.");
|
|
2433
|
+
info("");
|
|
2434
|
+
}
|
|
2196
2435
|
info("Or paste the token into the app (Settings \u2192 Connect).");
|
|
2197
2436
|
}
|
|
2198
2437
|
function registerPair(program) {
|
|
@@ -2222,7 +2461,7 @@ var init_pair = __esm({
|
|
|
2222
2461
|
import { Command } from "commander";
|
|
2223
2462
|
|
|
2224
2463
|
// src/version.ts
|
|
2225
|
-
var VERSION = "0.0.
|
|
2464
|
+
var VERSION = "0.0.9";
|
|
2226
2465
|
|
|
2227
2466
|
// src/index.ts
|
|
2228
2467
|
init_config();
|
|
@@ -2232,21 +2471,39 @@ init_auth();
|
|
|
2232
2471
|
init_config();
|
|
2233
2472
|
init_output();
|
|
2234
2473
|
function registerAuth(program) {
|
|
2235
|
-
program.command("login").description("Sign in
|
|
2236
|
-
|
|
2237
|
-
auth: opts.auth,
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2474
|
+
program.command("login").description("Sign in with your Sakura account (opens a browser) so this machine can join your tailnet").option("--auth <token>", "headless: set a pre-created daemon token instead of signing in").option("--server-url <url>", "override the Sakura cloud API URL").option("--machine-name <name>", "override the machine name (defaults to hostname)").option("--json", "output as JSON").action(async (opts) => {
|
|
2475
|
+
if (opts.auth) {
|
|
2476
|
+
const state = login({ auth: opts.auth, serverUrl: opts.serverUrl, machineName: opts.machineName });
|
|
2477
|
+
if (opts.json) return printJson(state);
|
|
2478
|
+
success(`Daemon token set for "${state.machineName}".`);
|
|
2479
|
+
info("Start the runtime: sakura daemon start");
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
try {
|
|
2483
|
+
const state = await loginInteractive(
|
|
2484
|
+
{ serverUrl: opts.serverUrl, machineName: opts.machineName },
|
|
2485
|
+
{
|
|
2486
|
+
onPrompt: ({ verifyUrl, userCode }) => {
|
|
2487
|
+
info("Opening your browser to finish sign-in\u2026");
|
|
2488
|
+
info("");
|
|
2489
|
+
info(` ${verifyUrl}`);
|
|
2490
|
+
info("");
|
|
2491
|
+
info(`If it didn't open, visit the link above and confirm the code: ${userCode}`);
|
|
2492
|
+
info("");
|
|
2493
|
+
info("Waiting for approval\u2026");
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
);
|
|
2497
|
+
if (opts.json) return printJson(state);
|
|
2498
|
+
success(`Signed in as ${state.login} on "${state.machineName}".`);
|
|
2499
|
+
info("");
|
|
2500
|
+
info("Start the runtime \u2014 it auto-joins your tailnet, then show the QR:");
|
|
2501
|
+
info(" sakura daemon start");
|
|
2502
|
+
info(" sakura pair");
|
|
2503
|
+
} catch (e) {
|
|
2504
|
+
warn(e?.message || "login failed");
|
|
2505
|
+
process.exitCode = 1;
|
|
2506
|
+
}
|
|
2250
2507
|
});
|
|
2251
2508
|
program.command("logout").description("Clear local credentials").action(() => {
|
|
2252
2509
|
logout();
|
|
@@ -2313,7 +2570,7 @@ function registerRuntime(program, version) {
|
|
|
2313
2570
|
// src/commands/session.ts
|
|
2314
2571
|
init_sessions();
|
|
2315
2572
|
init_output();
|
|
2316
|
-
import
|
|
2573
|
+
import fs13 from "fs";
|
|
2317
2574
|
function resolveSessionId(arg) {
|
|
2318
2575
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
2319
2576
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -2322,7 +2579,7 @@ function resolveSessionId(arg) {
|
|
|
2322
2579
|
async function resolvePrompt(positional, opts) {
|
|
2323
2580
|
if (positional) return positional;
|
|
2324
2581
|
if (opts.prompt) return opts.prompt;
|
|
2325
|
-
if (opts.promptFile) return
|
|
2582
|
+
if (opts.promptFile) return fs13.readFileSync(opts.promptFile, "utf8");
|
|
2326
2583
|
if (!process.stdin.isTTY) {
|
|
2327
2584
|
const chunks = [];
|
|
2328
2585
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -2550,8 +2807,52 @@ init_output();
|
|
|
2550
2807
|
function port() {
|
|
2551
2808
|
return Number(process.env.SAKURA_PORT || loadConfig().daemon.port);
|
|
2552
2809
|
}
|
|
2810
|
+
function maskKey(key) {
|
|
2811
|
+
if (key.length <= 14) return "\u2022\u2022\u2022\u2022";
|
|
2812
|
+
return `${key.slice(0, 12)}\u2026${key.slice(-4)}`;
|
|
2813
|
+
}
|
|
2814
|
+
function redact(cfg) {
|
|
2815
|
+
if (!cfg) return cfg;
|
|
2816
|
+
return { ...cfg, authKey: maskKey(cfg.authKey) };
|
|
2817
|
+
}
|
|
2553
2818
|
function registerConnectivity(program) {
|
|
2554
2819
|
const ts = program.command("tailscale").alias("ts").description("Tailscale connectivity for private PC \u2194 mobile access");
|
|
2820
|
+
ts.command("init").description("Configure embedded Tailscale (Headscale control URL + reusable key)").requiredOption("--control-url <url>", "Headscale control-plane URL (https://headscale.\u2026)").requiredOption("--auth-key <key>", "reusable pre-auth key (tskey-auth-\u2026)").option("--hostname <name>", "tailnet hostname for this machine").action((opts) => {
|
|
2821
|
+
const controlUrl = String(opts.controlUrl).trim();
|
|
2822
|
+
const authKey = String(opts.authKey).trim();
|
|
2823
|
+
if (!/^https?:\/\//i.test(controlUrl)) die("control URL must start with http(s)://");
|
|
2824
|
+
if (!authKey) die("auth key is required");
|
|
2825
|
+
updateConfig((cfg) => {
|
|
2826
|
+
cfg.tailscale = {
|
|
2827
|
+
controlUrl: controlUrl.replace(/\/+$/, ""),
|
|
2828
|
+
authKey,
|
|
2829
|
+
...opts.hostname ? { hostname: String(opts.hostname).trim() } : {}
|
|
2830
|
+
};
|
|
2831
|
+
});
|
|
2832
|
+
success("Embedded Tailscale configured.");
|
|
2833
|
+
info(`Control URL: ${controlUrl}`);
|
|
2834
|
+
info("Run `sakuraai daemon restart`, then `sakuraai pair` to show the QR.");
|
|
2835
|
+
});
|
|
2836
|
+
ts.command("config").description("Show the embedded-Tailscale config + live tsnet node state").option("--json", "output as JSON").action((opts) => {
|
|
2837
|
+
const cfg = loadConfig().tailscale ?? null;
|
|
2838
|
+
const node = readTsnetState();
|
|
2839
|
+
if (opts.json) return printJson({ config: redact(cfg), node });
|
|
2840
|
+
if (!cfg) {
|
|
2841
|
+
warn("Embedded Tailscale is not configured. Run `sakuraai tailscale init`.");
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
success("Embedded Tailscale configured.");
|
|
2845
|
+
info(`Control URL: ${cfg.controlUrl}`);
|
|
2846
|
+
info(`Auth key: ${maskKey(cfg.authKey)}`);
|
|
2847
|
+
if (cfg.hostname) info(`Hostname: ${cfg.hostname}`);
|
|
2848
|
+
if (node) {
|
|
2849
|
+
info(`Node state: ${node.backendState}`);
|
|
2850
|
+
if (node.tailscaleIp) info(`Tailnet IP: ${node.tailscaleIp}`);
|
|
2851
|
+
if (node.dnsName) info(`DNS name: ${node.dnsName}`);
|
|
2852
|
+
} else {
|
|
2853
|
+
warn("tsnet sidecar not running \u2014 start it with `sakuraai daemon start`.");
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2555
2856
|
ts.command("status").description("Show tailnet status + the base URL the app should use").option("--json", "output as JSON").action(async (opts) => {
|
|
2556
2857
|
const r = await status(port());
|
|
2557
2858
|
if (opts.json) return printJson(r);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sakuraai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Sakura Agent CLI + local runtime for managing AI coding sessions (Claude Code, Codex, OpenCode) and reaching them privately from the Sakura mobile app over Tailscale",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
29
|
"!dist/**/*.map",
|
|
30
|
+
"scripts/download-tsnet.js",
|
|
30
31
|
"README.md"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"dev": "tsx src/index.ts",
|
|
36
37
|
"clean": "rimraf dist",
|
|
37
38
|
"typecheck": "tsc --noEmit",
|
|
39
|
+
"postinstall": "node scripts/download-tsnet.js",
|
|
38
40
|
"prepublishOnly": "npm run clean && npm run build"
|
|
39
41
|
},
|
|
40
42
|
"dependencies": {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// postinstall: fetch the prebuilt `sakura-tsnet` sidecar for this OS/arch from
|
|
2
|
+
// the GitHub Release matching this package version. Best-effort by design — if
|
|
3
|
+
// anything fails (offline, unsupported platform, missing asset) we print a hint
|
|
4
|
+
// and exit 0, so `npm install` never breaks. The daemon then serves over LAN and
|
|
5
|
+
// `sakura daemon start` logs how to build the binary locally.
|
|
6
|
+
//
|
|
7
|
+
// Skips: SAKURA_SKIP_TSNET_DOWNLOAD=1, SAKURA_TSNET_BIN set, or already present.
|
|
8
|
+
// Override source: SAKURA_TSNET_TAG, SAKURA_TSNET_BASE_URL.
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import https from "node:https";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(here, "..", "package.json"), "utf8"));
|
|
17
|
+
|
|
18
|
+
const PLAT = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform];
|
|
19
|
+
const ARCH = { x64: "amd64", arm64: "arm64" }[process.arch];
|
|
20
|
+
const isWin = process.platform === "win32";
|
|
21
|
+
|
|
22
|
+
const binDir = path.join(here, "..", "bin");
|
|
23
|
+
const outPath = path.join(binDir, isWin ? "sakura-tsnet.exe" : "sakura-tsnet");
|
|
24
|
+
|
|
25
|
+
function skip(msg) {
|
|
26
|
+
if (msg) console.warn(`[sakura] ${msg}`);
|
|
27
|
+
console.warn(
|
|
28
|
+
"[sakura] embedded-Tailscale sidecar not installed — the daemon will still " +
|
|
29
|
+
"work over your LAN. To enable cross-network access, build it with " +
|
|
30
|
+
"modules/sakura-tailscale/scripts/build-sidecar.sh, or set SAKURA_TSNET_BIN.",
|
|
31
|
+
);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (process.env.SAKURA_SKIP_TSNET_DOWNLOAD === "1" || process.env.SAKURA_TSNET_BIN) process.exit(0);
|
|
36
|
+
if (fs.existsSync(outPath)) process.exit(0);
|
|
37
|
+
if (!PLAT || !ARCH) skip(`unsupported platform ${process.platform}/${process.arch}.`);
|
|
38
|
+
|
|
39
|
+
const asset = `sakura-tsnet-${PLAT}-${ARCH}${isWin ? ".exe" : ""}`;
|
|
40
|
+
const tag = process.env.SAKURA_TSNET_TAG || `v${pkg.version}`;
|
|
41
|
+
const base = process.env.SAKURA_TSNET_BASE_URL || `https://github.com/Nishu0/sakura-tsnet/releases/download/${tag}`;
|
|
42
|
+
const url = `${base}/${asset}`;
|
|
43
|
+
|
|
44
|
+
function download(u, redirectsLeft = 5) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
https
|
|
47
|
+
.get(u, { headers: { "User-Agent": "sakuraai-postinstall" } }, (res) => {
|
|
48
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
49
|
+
if (redirectsLeft <= 0) return reject(new Error("too many redirects"));
|
|
50
|
+
res.resume();
|
|
51
|
+
return resolve(download(res.headers.location, redirectsLeft - 1));
|
|
52
|
+
}
|
|
53
|
+
if (res.statusCode !== 200) {
|
|
54
|
+
res.resume();
|
|
55
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${u}`));
|
|
56
|
+
}
|
|
57
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
58
|
+
const tmp = `${outPath}.download`;
|
|
59
|
+
const file = fs.createWriteStream(tmp);
|
|
60
|
+
res.pipe(file);
|
|
61
|
+
file.on("finish", () => file.close(() => {
|
|
62
|
+
fs.renameSync(tmp, outPath);
|
|
63
|
+
if (!isWin) fs.chmodSync(outPath, 0o755);
|
|
64
|
+
resolve();
|
|
65
|
+
}));
|
|
66
|
+
file.on("error", reject);
|
|
67
|
+
})
|
|
68
|
+
.on("error", reject);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await download(url);
|
|
74
|
+
console.log(`[sakura] installed embedded-Tailscale sidecar (${asset}).`);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
skip(`could not download ${asset} (${e.message}).`);
|
|
77
|
+
}
|