sakuraai 0.0.14 → 0.0.16
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 +135 -44
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4651,6 +4651,7 @@ var init_server = __esm({
|
|
|
4651
4651
|
});
|
|
4652
4652
|
|
|
4653
4653
|
// src/daemon/tsnet.ts
|
|
4654
|
+
import crypto from "crypto";
|
|
4654
4655
|
import fs21 from "fs";
|
|
4655
4656
|
import path19 from "path";
|
|
4656
4657
|
import { fileURLToPath } from "url";
|
|
@@ -4678,35 +4679,63 @@ function startTsnet(port2) {
|
|
|
4678
4679
|
const bin = resolveBinary();
|
|
4679
4680
|
if (!bin) {
|
|
4680
4681
|
log.warn(
|
|
4681
|
-
"embedded Tailscale is configured but the sakura-tsnet binary was not found \u2014
|
|
4682
|
+
"embedded Tailscale is configured but the sakura-tsnet binary was not found \u2014 reinstall with `npm i -g sakuraai@latest --foreground-scripts` (LAN access still works)."
|
|
4682
4683
|
);
|
|
4683
4684
|
return;
|
|
4684
4685
|
}
|
|
4685
4686
|
stopping = false;
|
|
4686
|
-
|
|
4687
|
+
resetStateIfKeyChanged(ts.authKey);
|
|
4688
|
+
const fresh = !fs21.existsSync(path19.join(STATE_DIR, "tailscaled.state")) && !fs21.existsSync(JOINED_MARK);
|
|
4689
|
+
spawnSidecar(bin, ts.controlUrl, ts.authKey, ts.hostname || cfg.machineName, port2, fresh);
|
|
4687
4690
|
}
|
|
4688
|
-
function
|
|
4691
|
+
function resetStateIfKeyChanged(authKey) {
|
|
4692
|
+
const hash = crypto.createHash("sha256").update(authKey).digest("hex");
|
|
4693
|
+
let prev = null;
|
|
4694
|
+
try {
|
|
4695
|
+
prev = fs21.readFileSync(KEY_MARK, "utf8").trim();
|
|
4696
|
+
} catch {
|
|
4697
|
+
}
|
|
4698
|
+
if (prev && prev !== hash) {
|
|
4699
|
+
log.info("tailnet key changed \u2014 re-registering this Mac on the new key");
|
|
4700
|
+
fs21.rmSync(STATE_DIR, { recursive: true, force: true });
|
|
4701
|
+
}
|
|
4702
|
+
fs21.mkdirSync(STATE_DIR, { recursive: true });
|
|
4703
|
+
fs21.writeFileSync(KEY_MARK, hash);
|
|
4704
|
+
}
|
|
4705
|
+
function spawnSidecar(bin, controlUrl, authKey, hostname2, port2, forceLogin) {
|
|
4689
4706
|
const env = {
|
|
4690
4707
|
...process.env,
|
|
4691
4708
|
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
4692
4709
|
SAKURA_TS_AUTH_KEY: authKey,
|
|
4693
4710
|
SAKURA_TS_HOSTNAME: hostname2,
|
|
4694
|
-
SAKURA_TS_STATE_DIR:
|
|
4711
|
+
SAKURA_TS_STATE_DIR: STATE_DIR,
|
|
4695
4712
|
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
4696
4713
|
SAKURA_TS_PORT: String(port2)
|
|
4697
4714
|
};
|
|
4698
|
-
|
|
4715
|
+
if (forceLogin) env.TSNET_FORCE_LOGIN = "1";
|
|
4716
|
+
log.info(`starting embedded Tailscale sidecar (${path19.basename(bin)})${forceLogin ? " with a forced login" : ""}`);
|
|
4699
4717
|
const c = spawn11(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
4700
4718
|
child = c;
|
|
4701
|
-
|
|
4719
|
+
let joinFailed = false;
|
|
4720
|
+
const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => {
|
|
4721
|
+
log.info(`[tsnet] ${line}`);
|
|
4722
|
+
if (line.includes("failed to join tailnet")) joinFailed = true;
|
|
4723
|
+
if (line.includes("on tailnet as")) {
|
|
4724
|
+
try {
|
|
4725
|
+
fs21.writeFileSync(JOINED_MARK, (/* @__PURE__ */ new Date()).toISOString());
|
|
4726
|
+
} catch {
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
});
|
|
4702
4730
|
c.stdout?.on("data", pipe);
|
|
4703
4731
|
c.stderr?.on("data", pipe);
|
|
4704
4732
|
c.on("exit", (code) => {
|
|
4705
4733
|
child = null;
|
|
4706
4734
|
if (stopping) return;
|
|
4735
|
+
const force = forceLogin || joinFailed && !fs21.existsSync(JOINED_MARK);
|
|
4707
4736
|
log.warn(`tsnet sidecar exited (code ${code}); restarting in 5s`);
|
|
4708
4737
|
restartTimer = setTimeout(() => {
|
|
4709
|
-
if (!stopping) spawnSidecar(bin, controlUrl, authKey, hostname2, port2);
|
|
4738
|
+
if (!stopping) spawnSidecar(bin, controlUrl, authKey, hostname2, port2, force);
|
|
4710
4739
|
}, 5e3);
|
|
4711
4740
|
});
|
|
4712
4741
|
}
|
|
@@ -4728,7 +4757,7 @@ function stopTsnet() {
|
|
|
4728
4757
|
} catch {
|
|
4729
4758
|
}
|
|
4730
4759
|
}
|
|
4731
|
-
var child, stopping, restartTimer;
|
|
4760
|
+
var child, stopping, restartTimer, STATE_DIR, KEY_MARK, JOINED_MARK;
|
|
4732
4761
|
var init_tsnet = __esm({
|
|
4733
4762
|
"src/daemon/tsnet.ts"() {
|
|
4734
4763
|
"use strict";
|
|
@@ -4737,6 +4766,9 @@ var init_tsnet = __esm({
|
|
|
4737
4766
|
child = null;
|
|
4738
4767
|
stopping = false;
|
|
4739
4768
|
restartTimer = null;
|
|
4769
|
+
STATE_DIR = path19.join(SAKURA_DIR, "tsnet-state");
|
|
4770
|
+
KEY_MARK = path19.join(STATE_DIR, "joined-with");
|
|
4771
|
+
JOINED_MARK = path19.join(STATE_DIR, "joined");
|
|
4740
4772
|
}
|
|
4741
4773
|
});
|
|
4742
4774
|
|
|
@@ -4885,27 +4917,80 @@ var init_manager = __esm({
|
|
|
4885
4917
|
// src/pairing.ts
|
|
4886
4918
|
import os10 from "os";
|
|
4887
4919
|
import qrcode from "qrcode-terminal";
|
|
4920
|
+
import fs23 from "fs";
|
|
4921
|
+
function recentTsnetLog(max = 6) {
|
|
4922
|
+
let text = "";
|
|
4923
|
+
try {
|
|
4924
|
+
const fd = fs23.openSync(LOG_PATH, "r");
|
|
4925
|
+
const size = fs23.fstatSync(fd).size;
|
|
4926
|
+
const len = Math.min(size, 64 * 1024);
|
|
4927
|
+
const buf = Buffer.alloc(len);
|
|
4928
|
+
fs23.readSync(fd, buf, 0, len, size - len);
|
|
4929
|
+
fs23.closeSync(fd);
|
|
4930
|
+
text = buf.toString("utf8");
|
|
4931
|
+
} catch {
|
|
4932
|
+
return [];
|
|
4933
|
+
}
|
|
4934
|
+
const lines2 = text.split("\n").filter((l) => /tsnet|sidecar|tailnet/i.test(l));
|
|
4935
|
+
const distinct = lines2.filter((l, i) => l !== lines2[i - 1]);
|
|
4936
|
+
return distinct.slice(-max);
|
|
4937
|
+
}
|
|
4938
|
+
function diagnoseTailnet(tsnet) {
|
|
4939
|
+
if (!resolveBinary()) {
|
|
4940
|
+
return {
|
|
4941
|
+
reason: "the sakura-tsnet binary is missing. It is downloaded when sakuraai installs, and that download failed or was skipped (bun/pnpm, or --ignore-scripts).",
|
|
4942
|
+
fix: "npm i -g sakuraai@latest --foreground-scripts && sakuraai daemon restart",
|
|
4943
|
+
log: []
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4946
|
+
if (!tsnet) {
|
|
4947
|
+
return {
|
|
4948
|
+
reason: "the tailnet sidecar has not reported in. It may still be starting, or it failed to start.",
|
|
4949
|
+
fix: "sakuraai daemon restart, wait ~15s, then run sakuraai pair again",
|
|
4950
|
+
log: recentTsnetLog()
|
|
4951
|
+
};
|
|
4952
|
+
}
|
|
4953
|
+
return {
|
|
4954
|
+
reason: `the tailnet sidecar is stuck in state "${tsnet.backendState}" instead of Running.`,
|
|
4955
|
+
fix: "sakuraai daemon restart, wait ~15s, then run sakuraai pair again. If it stays stuck, sakuraai logout && sakuraai login for a fresh key",
|
|
4956
|
+
log: recentTsnetLog()
|
|
4957
|
+
};
|
|
4958
|
+
}
|
|
4888
4959
|
async function buildPairing() {
|
|
4889
4960
|
const token = requireToken();
|
|
4890
4961
|
const cfg = loadConfig();
|
|
4891
4962
|
const port2 = Number(process.env.SAKURA_PORT || cfg.daemon.port);
|
|
4892
4963
|
const tsnet = readTsnetState();
|
|
4893
4964
|
const tsnetUrl = tsnet?.tailscaleIp && tsnet.backendState === "Running" ? `http://${tsnet.tailscaleIp}:${port2}` : null;
|
|
4894
|
-
const
|
|
4965
|
+
const ts = cfg.tailscale;
|
|
4966
|
+
const configured = !!(ts?.controlUrl && ts?.authKey);
|
|
4967
|
+
const cliTailnet = tsnetUrl || configured ? null : await discoverBaseUrl(port2).catch(() => null);
|
|
4895
4968
|
const url = tsnetUrl ?? cliTailnet ?? `http://${localIp()}:${port2}`;
|
|
4896
4969
|
const params = /* @__PURE__ */ new Map([
|
|
4897
4970
|
["url", url],
|
|
4898
4971
|
["token", token]
|
|
4899
4972
|
]);
|
|
4900
|
-
const
|
|
4901
|
-
const embeddedTailscale = !!(ts?.controlUrl && ts?.authKey);
|
|
4973
|
+
const embeddedTailscale = configured && !!tsnetUrl;
|
|
4902
4974
|
if (embeddedTailscale) {
|
|
4903
4975
|
params.set("cn", ts.controlUrl);
|
|
4904
4976
|
params.set("tskey", ts.authKey);
|
|
4905
4977
|
}
|
|
4906
4978
|
const query = [...params].map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
4907
4979
|
const deepLink = `sakura://pair?${query}`;
|
|
4908
|
-
|
|
4980
|
+
const tailnetProblem = configured && !tsnetUrl ? diagnoseTailnet(tsnet) : void 0;
|
|
4981
|
+
return { url, token, deepLink, embeddedTailscale, tailnetProblem };
|
|
4982
|
+
}
|
|
4983
|
+
function tunnelUp() {
|
|
4984
|
+
const ts = readTsnetState();
|
|
4985
|
+
return !!(ts?.tailscaleIp && ts.backendState === "Running");
|
|
4986
|
+
}
|
|
4987
|
+
async function waitForTunnel(timeoutMs) {
|
|
4988
|
+
const deadline = Date.now() + timeoutMs;
|
|
4989
|
+
while (Date.now() < deadline) {
|
|
4990
|
+
if (tunnelUp()) return true;
|
|
4991
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
4992
|
+
}
|
|
4993
|
+
return tunnelUp();
|
|
4909
4994
|
}
|
|
4910
4995
|
function renderQr(deepLink) {
|
|
4911
4996
|
return new Promise((resolve) => {
|
|
@@ -4927,6 +5012,7 @@ var init_pairing = __esm({
|
|
|
4927
5012
|
init_config();
|
|
4928
5013
|
init_auth();
|
|
4929
5014
|
init_tailscale();
|
|
5015
|
+
init_tsnet();
|
|
4930
5016
|
}
|
|
4931
5017
|
});
|
|
4932
5018
|
|
|
@@ -4936,10 +5022,22 @@ __export(pair_exports, {
|
|
|
4936
5022
|
registerPair: () => registerPair,
|
|
4937
5023
|
showPairing: () => showPairing
|
|
4938
5024
|
});
|
|
5025
|
+
import chalk2 from "chalk";
|
|
4939
5026
|
async function showPairing(opts = {}) {
|
|
4940
5027
|
const pairing = await buildPairing();
|
|
4941
5028
|
if (opts.json) return printJson(pairing);
|
|
4942
5029
|
const qr = await renderQr(pairing.deepLink);
|
|
5030
|
+
const problem = pairing.tailnetProblem;
|
|
5031
|
+
if (problem) {
|
|
5032
|
+
info("");
|
|
5033
|
+
warn(chalk2.bold("This Mac is not on Sakura\u2019s private tailnet, so this QR only works on the same Wi\u2011Fi."));
|
|
5034
|
+
info(` Why: ${problem.reason}`);
|
|
5035
|
+
info(` Fix: ${chalk2.cyan(problem.fix)}`);
|
|
5036
|
+
if (problem.log.length) {
|
|
5037
|
+
info(" Recent tailnet log:");
|
|
5038
|
+
problem.log.forEach((l) => info(chalk2.dim(` ${l}`)));
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
4943
5041
|
info("");
|
|
4944
5042
|
info("Scan this with your phone to pair the Sakura app:");
|
|
4945
5043
|
info("");
|
|
@@ -4966,6 +5064,9 @@ function registerPair(program) {
|
|
|
4966
5064
|
}
|
|
4967
5065
|
if (!running()) {
|
|
4968
5066
|
warn("The daemon is not running \u2014 start it with `sakuraai daemon start` so the app can connect.");
|
|
5067
|
+
} else if (loadConfig().tailscale?.authKey && !tunnelUp()) {
|
|
5068
|
+
info("Waiting for Sakura\u2019s private tunnel (up to 90s)\u2026");
|
|
5069
|
+
await waitForTunnel(9e4);
|
|
4969
5070
|
}
|
|
4970
5071
|
await showPairing(opts);
|
|
4971
5072
|
success("Waiting for the app to connect\u2026");
|
|
@@ -4975,6 +5076,7 @@ var init_pair = __esm({
|
|
|
4975
5076
|
"src/commands/pair.ts"() {
|
|
4976
5077
|
"use strict";
|
|
4977
5078
|
init_pairing();
|
|
5079
|
+
init_config();
|
|
4978
5080
|
init_auth();
|
|
4979
5081
|
init_manager();
|
|
4980
5082
|
init_output();
|
|
@@ -4985,7 +5087,7 @@ var init_pair = __esm({
|
|
|
4985
5087
|
import { Command } from "commander";
|
|
4986
5088
|
|
|
4987
5089
|
// src/version.ts
|
|
4988
|
-
var VERSION = "0.0.
|
|
5090
|
+
var VERSION = "0.0.16";
|
|
4989
5091
|
|
|
4990
5092
|
// src/index.ts
|
|
4991
5093
|
init_config();
|
|
@@ -5048,23 +5150,12 @@ init_tailscale();
|
|
|
5048
5150
|
init_auth();
|
|
5049
5151
|
init_config();
|
|
5050
5152
|
init_output();
|
|
5051
|
-
|
|
5153
|
+
init_pairing();
|
|
5052
5154
|
async function tunnelUrl(port2) {
|
|
5053
5155
|
const ts = readTsnetState();
|
|
5054
5156
|
if (ts?.tailscaleIp && ts.backendState === "Running") return `http://${ts.tailscaleIp}:${port2}`;
|
|
5055
5157
|
return await discoverBaseUrl(port2).catch(() => null);
|
|
5056
5158
|
}
|
|
5057
|
-
async function waitForTunnel(timeoutMs) {
|
|
5058
|
-
if (!getServerToken()) return false;
|
|
5059
|
-
const deadline = Date.now() + timeoutMs;
|
|
5060
|
-
while (Date.now() < deadline) {
|
|
5061
|
-
const cfg = loadConfig();
|
|
5062
|
-
const ts = readTsnetState();
|
|
5063
|
-
if (cfg.tailscale?.controlUrl && ts?.tailscaleIp && ts.backendState === "Running") return true;
|
|
5064
|
-
await sleep2(500);
|
|
5065
|
-
}
|
|
5066
|
-
return false;
|
|
5067
|
-
}
|
|
5068
5159
|
function registerRuntime(program, version) {
|
|
5069
5160
|
program.command("start").description("Run the Sakura runtime in this terminal (foreground)").option("--auth <token>", "sign in with a CLI token before starting").action(async (opts) => {
|
|
5070
5161
|
if (opts.auth) {
|
|
@@ -5081,11 +5172,11 @@ function registerRuntime(program, version) {
|
|
|
5081
5172
|
if (!isLoggedIn()) die("Not signed in. Run `sakuraai login` first.");
|
|
5082
5173
|
const info0 = start();
|
|
5083
5174
|
success(`Daemon running (pid ${info0.pid}) on ${info0.url}`);
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5175
|
+
if (getServerToken() && loadConfig().tailscale?.controlUrl) {
|
|
5176
|
+
info("Waiting for Sakura\u2019s private tunnel (up to 90s)\u2026");
|
|
5177
|
+
if (await waitForTunnel(9e4)) {
|
|
5178
|
+
info("Embedded Tailscale tunnel up \u2014 the phone can connect from any network, no Tailscale app needed.");
|
|
5179
|
+
}
|
|
5089
5180
|
}
|
|
5090
5181
|
const { showPairing: showPairing2 } = await Promise.resolve().then(() => (init_pair(), pair_exports));
|
|
5091
5182
|
await showPairing2();
|
|
@@ -5114,7 +5205,7 @@ function registerRuntime(program, version) {
|
|
|
5114
5205
|
// src/commands/session.ts
|
|
5115
5206
|
init_sessions();
|
|
5116
5207
|
init_output();
|
|
5117
|
-
import
|
|
5208
|
+
import fs24 from "fs";
|
|
5118
5209
|
function resolveSessionId(arg) {
|
|
5119
5210
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
5120
5211
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -5123,7 +5214,7 @@ function resolveSessionId(arg) {
|
|
|
5123
5214
|
async function resolvePrompt(positional, opts) {
|
|
5124
5215
|
if (positional) return positional;
|
|
5125
5216
|
if (opts.prompt) return opts.prompt;
|
|
5126
|
-
if (opts.promptFile) return
|
|
5217
|
+
if (opts.promptFile) return fs24.readFileSync(opts.promptFile, "utf8");
|
|
5127
5218
|
if (!process.stdin.isTTY) {
|
|
5128
5219
|
const chunks = [];
|
|
5129
5220
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -5438,14 +5529,14 @@ init_pair();
|
|
|
5438
5529
|
|
|
5439
5530
|
// src/commands/welcome.ts
|
|
5440
5531
|
init_config();
|
|
5441
|
-
import
|
|
5532
|
+
import fs25 from "fs";
|
|
5442
5533
|
import path20 from "path";
|
|
5443
5534
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5444
5535
|
import { spawn as spawn13 } from "child_process";
|
|
5445
5536
|
var WELCOMED_MARKER = path20.join(SAKURA_DIR, ".welcomed");
|
|
5446
5537
|
function resolveBinary2() {
|
|
5447
5538
|
const envBin = process.env.SAKURA_ONBOARD_BIN;
|
|
5448
|
-
if (envBin &&
|
|
5539
|
+
if (envBin && fs25.existsSync(envBin)) return envBin;
|
|
5449
5540
|
const here = path20.dirname(fileURLToPath2(import.meta.url));
|
|
5450
5541
|
const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
|
|
5451
5542
|
const candidates = [
|
|
@@ -5456,7 +5547,7 @@ function resolveBinary2() {
|
|
|
5456
5547
|
path20.join(SAKURA_DIR, name)
|
|
5457
5548
|
// user-dropped
|
|
5458
5549
|
];
|
|
5459
|
-
return candidates.find((p) =>
|
|
5550
|
+
return candidates.find((p) => fs25.existsSync(p)) ?? null;
|
|
5460
5551
|
}
|
|
5461
5552
|
function showWelcome() {
|
|
5462
5553
|
return new Promise((resolve) => {
|
|
@@ -5474,10 +5565,10 @@ function showWelcome() {
|
|
|
5474
5565
|
});
|
|
5475
5566
|
}
|
|
5476
5567
|
async function maybeFirstRunWelcome() {
|
|
5477
|
-
if (
|
|
5568
|
+
if (fs25.existsSync(WELCOMED_MARKER)) return;
|
|
5478
5569
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
5479
5570
|
try {
|
|
5480
|
-
|
|
5571
|
+
fs25.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
5481
5572
|
} catch {
|
|
5482
5573
|
}
|
|
5483
5574
|
await showWelcome();
|
|
@@ -5515,7 +5606,7 @@ function registerWelcome(program) {
|
|
|
5515
5606
|
}
|
|
5516
5607
|
|
|
5517
5608
|
// src/commands/app.ts
|
|
5518
|
-
import
|
|
5609
|
+
import fs26 from "fs";
|
|
5519
5610
|
import os11 from "os";
|
|
5520
5611
|
import path21 from "path";
|
|
5521
5612
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
@@ -5528,11 +5619,11 @@ function dmgUrl() {
|
|
|
5528
5619
|
async function download(url, dest) {
|
|
5529
5620
|
const res = await fetch(url, { redirect: "follow" });
|
|
5530
5621
|
if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
5531
|
-
|
|
5622
|
+
fs26.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
5532
5623
|
}
|
|
5533
5624
|
async function install() {
|
|
5534
5625
|
if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
|
|
5535
|
-
const tmp =
|
|
5626
|
+
const tmp = fs26.mkdtempSync(path21.join(os11.tmpdir(), "sakura-app-"));
|
|
5536
5627
|
const dmg = path21.join(tmp, "Sakura.dmg");
|
|
5537
5628
|
const mount = path21.join(tmp, "mnt");
|
|
5538
5629
|
info(`Downloading Sakura.app ${VERSION}\u2026`);
|
|
@@ -5542,18 +5633,18 @@ async function install() {
|
|
|
5542
5633
|
die(`${e.message}
|
|
5543
5634
|
Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
|
|
5544
5635
|
}
|
|
5545
|
-
|
|
5636
|
+
fs26.mkdirSync(mount);
|
|
5546
5637
|
execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
|
|
5547
5638
|
try {
|
|
5548
5639
|
try {
|
|
5549
5640
|
execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
|
|
5550
5641
|
} catch {
|
|
5551
5642
|
}
|
|
5552
|
-
|
|
5643
|
+
fs26.rmSync(APP_PATH, { recursive: true, force: true });
|
|
5553
5644
|
execFileSync4("ditto", [path21.join(mount, "Sakura.app"), APP_PATH]);
|
|
5554
5645
|
} finally {
|
|
5555
5646
|
execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
|
|
5556
|
-
|
|
5647
|
+
fs26.rmSync(tmp, { recursive: true, force: true });
|
|
5557
5648
|
}
|
|
5558
5649
|
execFileSync4("open", [APP_PATH]);
|
|
5559
5650
|
success("Sakura is in your menu bar (look for the blossom).");
|
|
@@ -5562,7 +5653,7 @@ function registerApp(program) {
|
|
|
5562
5653
|
const app = program.command("app").description("The optional Sakura menu bar app for macOS");
|
|
5563
5654
|
app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
|
|
5564
5655
|
app.command("open").description("Open the Sakura menu bar app").action(() => {
|
|
5565
|
-
if (!
|
|
5656
|
+
if (!fs26.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
|
|
5566
5657
|
execFileSync4("open", [APP_PATH]);
|
|
5567
5658
|
});
|
|
5568
5659
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sakuraai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "Sakura Agent CLI + local runtime for managing AI coding sessions (Claude Code, Codex, OpenCode, ZCode, DeepSeek) and reaching them privately from the Sakura mobile app over Tailscale",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|