sakuraai 0.0.15 → 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 +116 -43
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4679,13 +4679,14 @@ function startTsnet(port2) {
|
|
|
4679
4679
|
const bin = resolveBinary();
|
|
4680
4680
|
if (!bin) {
|
|
4681
4681
|
log.warn(
|
|
4682
|
-
"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)."
|
|
4683
4683
|
);
|
|
4684
4684
|
return;
|
|
4685
4685
|
}
|
|
4686
4686
|
stopping = false;
|
|
4687
4687
|
resetStateIfKeyChanged(ts.authKey);
|
|
4688
|
-
|
|
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);
|
|
4689
4690
|
}
|
|
4690
4691
|
function resetStateIfKeyChanged(authKey) {
|
|
4691
4692
|
const hash = crypto.createHash("sha256").update(authKey).digest("hex");
|
|
@@ -4701,7 +4702,7 @@ function resetStateIfKeyChanged(authKey) {
|
|
|
4701
4702
|
fs21.mkdirSync(STATE_DIR, { recursive: true });
|
|
4702
4703
|
fs21.writeFileSync(KEY_MARK, hash);
|
|
4703
4704
|
}
|
|
4704
|
-
function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
4705
|
+
function spawnSidecar(bin, controlUrl, authKey, hostname2, port2, forceLogin) {
|
|
4705
4706
|
const env = {
|
|
4706
4707
|
...process.env,
|
|
4707
4708
|
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
@@ -4711,18 +4712,30 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
|
4711
4712
|
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
4712
4713
|
SAKURA_TS_PORT: String(port2)
|
|
4713
4714
|
};
|
|
4714
|
-
|
|
4715
|
+
if (forceLogin) env.TSNET_FORCE_LOGIN = "1";
|
|
4716
|
+
log.info(`starting embedded Tailscale sidecar (${path19.basename(bin)})${forceLogin ? " with a forced login" : ""}`);
|
|
4715
4717
|
const c = spawn11(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
4716
4718
|
child = c;
|
|
4717
|
-
|
|
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
|
+
});
|
|
4718
4730
|
c.stdout?.on("data", pipe);
|
|
4719
4731
|
c.stderr?.on("data", pipe);
|
|
4720
4732
|
c.on("exit", (code) => {
|
|
4721
4733
|
child = null;
|
|
4722
4734
|
if (stopping) return;
|
|
4735
|
+
const force = forceLogin || joinFailed && !fs21.existsSync(JOINED_MARK);
|
|
4723
4736
|
log.warn(`tsnet sidecar exited (code ${code}); restarting in 5s`);
|
|
4724
4737
|
restartTimer = setTimeout(() => {
|
|
4725
|
-
if (!stopping) spawnSidecar(bin, controlUrl, authKey, hostname2, port2);
|
|
4738
|
+
if (!stopping) spawnSidecar(bin, controlUrl, authKey, hostname2, port2, force);
|
|
4726
4739
|
}, 5e3);
|
|
4727
4740
|
});
|
|
4728
4741
|
}
|
|
@@ -4744,7 +4757,7 @@ function stopTsnet() {
|
|
|
4744
4757
|
} catch {
|
|
4745
4758
|
}
|
|
4746
4759
|
}
|
|
4747
|
-
var child, stopping, restartTimer, STATE_DIR, KEY_MARK;
|
|
4760
|
+
var child, stopping, restartTimer, STATE_DIR, KEY_MARK, JOINED_MARK;
|
|
4748
4761
|
var init_tsnet = __esm({
|
|
4749
4762
|
"src/daemon/tsnet.ts"() {
|
|
4750
4763
|
"use strict";
|
|
@@ -4755,6 +4768,7 @@ var init_tsnet = __esm({
|
|
|
4755
4768
|
restartTimer = null;
|
|
4756
4769
|
STATE_DIR = path19.join(SAKURA_DIR, "tsnet-state");
|
|
4757
4770
|
KEY_MARK = path19.join(STATE_DIR, "joined-with");
|
|
4771
|
+
JOINED_MARK = path19.join(STATE_DIR, "joined");
|
|
4758
4772
|
}
|
|
4759
4773
|
});
|
|
4760
4774
|
|
|
@@ -4903,27 +4917,80 @@ var init_manager = __esm({
|
|
|
4903
4917
|
// src/pairing.ts
|
|
4904
4918
|
import os10 from "os";
|
|
4905
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
|
+
}
|
|
4906
4959
|
async function buildPairing() {
|
|
4907
4960
|
const token = requireToken();
|
|
4908
4961
|
const cfg = loadConfig();
|
|
4909
4962
|
const port2 = Number(process.env.SAKURA_PORT || cfg.daemon.port);
|
|
4910
4963
|
const tsnet = readTsnetState();
|
|
4911
4964
|
const tsnetUrl = tsnet?.tailscaleIp && tsnet.backendState === "Running" ? `http://${tsnet.tailscaleIp}:${port2}` : null;
|
|
4912
|
-
const
|
|
4965
|
+
const ts = cfg.tailscale;
|
|
4966
|
+
const configured = !!(ts?.controlUrl && ts?.authKey);
|
|
4967
|
+
const cliTailnet = tsnetUrl || configured ? null : await discoverBaseUrl(port2).catch(() => null);
|
|
4913
4968
|
const url = tsnetUrl ?? cliTailnet ?? `http://${localIp()}:${port2}`;
|
|
4914
4969
|
const params = /* @__PURE__ */ new Map([
|
|
4915
4970
|
["url", url],
|
|
4916
4971
|
["token", token]
|
|
4917
4972
|
]);
|
|
4918
|
-
const
|
|
4919
|
-
const embeddedTailscale = !!(ts?.controlUrl && ts?.authKey);
|
|
4973
|
+
const embeddedTailscale = configured && !!tsnetUrl;
|
|
4920
4974
|
if (embeddedTailscale) {
|
|
4921
4975
|
params.set("cn", ts.controlUrl);
|
|
4922
4976
|
params.set("tskey", ts.authKey);
|
|
4923
4977
|
}
|
|
4924
4978
|
const query = [...params].map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
4925
4979
|
const deepLink = `sakura://pair?${query}`;
|
|
4926
|
-
|
|
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();
|
|
4927
4994
|
}
|
|
4928
4995
|
function renderQr(deepLink) {
|
|
4929
4996
|
return new Promise((resolve) => {
|
|
@@ -4945,6 +5012,7 @@ var init_pairing = __esm({
|
|
|
4945
5012
|
init_config();
|
|
4946
5013
|
init_auth();
|
|
4947
5014
|
init_tailscale();
|
|
5015
|
+
init_tsnet();
|
|
4948
5016
|
}
|
|
4949
5017
|
});
|
|
4950
5018
|
|
|
@@ -4954,10 +5022,22 @@ __export(pair_exports, {
|
|
|
4954
5022
|
registerPair: () => registerPair,
|
|
4955
5023
|
showPairing: () => showPairing
|
|
4956
5024
|
});
|
|
5025
|
+
import chalk2 from "chalk";
|
|
4957
5026
|
async function showPairing(opts = {}) {
|
|
4958
5027
|
const pairing = await buildPairing();
|
|
4959
5028
|
if (opts.json) return printJson(pairing);
|
|
4960
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
|
+
}
|
|
4961
5041
|
info("");
|
|
4962
5042
|
info("Scan this with your phone to pair the Sakura app:");
|
|
4963
5043
|
info("");
|
|
@@ -4984,6 +5064,9 @@ function registerPair(program) {
|
|
|
4984
5064
|
}
|
|
4985
5065
|
if (!running()) {
|
|
4986
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);
|
|
4987
5070
|
}
|
|
4988
5071
|
await showPairing(opts);
|
|
4989
5072
|
success("Waiting for the app to connect\u2026");
|
|
@@ -4993,6 +5076,7 @@ var init_pair = __esm({
|
|
|
4993
5076
|
"src/commands/pair.ts"() {
|
|
4994
5077
|
"use strict";
|
|
4995
5078
|
init_pairing();
|
|
5079
|
+
init_config();
|
|
4996
5080
|
init_auth();
|
|
4997
5081
|
init_manager();
|
|
4998
5082
|
init_output();
|
|
@@ -5003,7 +5087,7 @@ var init_pair = __esm({
|
|
|
5003
5087
|
import { Command } from "commander";
|
|
5004
5088
|
|
|
5005
5089
|
// src/version.ts
|
|
5006
|
-
var VERSION = "0.0.
|
|
5090
|
+
var VERSION = "0.0.16";
|
|
5007
5091
|
|
|
5008
5092
|
// src/index.ts
|
|
5009
5093
|
init_config();
|
|
@@ -5066,23 +5150,12 @@ init_tailscale();
|
|
|
5066
5150
|
init_auth();
|
|
5067
5151
|
init_config();
|
|
5068
5152
|
init_output();
|
|
5069
|
-
|
|
5153
|
+
init_pairing();
|
|
5070
5154
|
async function tunnelUrl(port2) {
|
|
5071
5155
|
const ts = readTsnetState();
|
|
5072
5156
|
if (ts?.tailscaleIp && ts.backendState === "Running") return `http://${ts.tailscaleIp}:${port2}`;
|
|
5073
5157
|
return await discoverBaseUrl(port2).catch(() => null);
|
|
5074
5158
|
}
|
|
5075
|
-
async function waitForTunnel(timeoutMs) {
|
|
5076
|
-
if (!getServerToken()) return false;
|
|
5077
|
-
const deadline = Date.now() + timeoutMs;
|
|
5078
|
-
while (Date.now() < deadline) {
|
|
5079
|
-
const cfg = loadConfig();
|
|
5080
|
-
const ts = readTsnetState();
|
|
5081
|
-
if (cfg.tailscale?.controlUrl && ts?.tailscaleIp && ts.backendState === "Running") return true;
|
|
5082
|
-
await sleep2(500);
|
|
5083
|
-
}
|
|
5084
|
-
return false;
|
|
5085
|
-
}
|
|
5086
5159
|
function registerRuntime(program, version) {
|
|
5087
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) => {
|
|
5088
5161
|
if (opts.auth) {
|
|
@@ -5099,11 +5172,11 @@ function registerRuntime(program, version) {
|
|
|
5099
5172
|
if (!isLoggedIn()) die("Not signed in. Run `sakuraai login` first.");
|
|
5100
5173
|
const info0 = start();
|
|
5101
5174
|
success(`Daemon running (pid ${info0.pid}) on ${info0.url}`);
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
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
|
+
}
|
|
5107
5180
|
}
|
|
5108
5181
|
const { showPairing: showPairing2 } = await Promise.resolve().then(() => (init_pair(), pair_exports));
|
|
5109
5182
|
await showPairing2();
|
|
@@ -5132,7 +5205,7 @@ function registerRuntime(program, version) {
|
|
|
5132
5205
|
// src/commands/session.ts
|
|
5133
5206
|
init_sessions();
|
|
5134
5207
|
init_output();
|
|
5135
|
-
import
|
|
5208
|
+
import fs24 from "fs";
|
|
5136
5209
|
function resolveSessionId(arg) {
|
|
5137
5210
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
5138
5211
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -5141,7 +5214,7 @@ function resolveSessionId(arg) {
|
|
|
5141
5214
|
async function resolvePrompt(positional, opts) {
|
|
5142
5215
|
if (positional) return positional;
|
|
5143
5216
|
if (opts.prompt) return opts.prompt;
|
|
5144
|
-
if (opts.promptFile) return
|
|
5217
|
+
if (opts.promptFile) return fs24.readFileSync(opts.promptFile, "utf8");
|
|
5145
5218
|
if (!process.stdin.isTTY) {
|
|
5146
5219
|
const chunks = [];
|
|
5147
5220
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -5456,14 +5529,14 @@ init_pair();
|
|
|
5456
5529
|
|
|
5457
5530
|
// src/commands/welcome.ts
|
|
5458
5531
|
init_config();
|
|
5459
|
-
import
|
|
5532
|
+
import fs25 from "fs";
|
|
5460
5533
|
import path20 from "path";
|
|
5461
5534
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5462
5535
|
import { spawn as spawn13 } from "child_process";
|
|
5463
5536
|
var WELCOMED_MARKER = path20.join(SAKURA_DIR, ".welcomed");
|
|
5464
5537
|
function resolveBinary2() {
|
|
5465
5538
|
const envBin = process.env.SAKURA_ONBOARD_BIN;
|
|
5466
|
-
if (envBin &&
|
|
5539
|
+
if (envBin && fs25.existsSync(envBin)) return envBin;
|
|
5467
5540
|
const here = path20.dirname(fileURLToPath2(import.meta.url));
|
|
5468
5541
|
const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
|
|
5469
5542
|
const candidates = [
|
|
@@ -5474,7 +5547,7 @@ function resolveBinary2() {
|
|
|
5474
5547
|
path20.join(SAKURA_DIR, name)
|
|
5475
5548
|
// user-dropped
|
|
5476
5549
|
];
|
|
5477
|
-
return candidates.find((p) =>
|
|
5550
|
+
return candidates.find((p) => fs25.existsSync(p)) ?? null;
|
|
5478
5551
|
}
|
|
5479
5552
|
function showWelcome() {
|
|
5480
5553
|
return new Promise((resolve) => {
|
|
@@ -5492,10 +5565,10 @@ function showWelcome() {
|
|
|
5492
5565
|
});
|
|
5493
5566
|
}
|
|
5494
5567
|
async function maybeFirstRunWelcome() {
|
|
5495
|
-
if (
|
|
5568
|
+
if (fs25.existsSync(WELCOMED_MARKER)) return;
|
|
5496
5569
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
5497
5570
|
try {
|
|
5498
|
-
|
|
5571
|
+
fs25.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
5499
5572
|
} catch {
|
|
5500
5573
|
}
|
|
5501
5574
|
await showWelcome();
|
|
@@ -5533,7 +5606,7 @@ function registerWelcome(program) {
|
|
|
5533
5606
|
}
|
|
5534
5607
|
|
|
5535
5608
|
// src/commands/app.ts
|
|
5536
|
-
import
|
|
5609
|
+
import fs26 from "fs";
|
|
5537
5610
|
import os11 from "os";
|
|
5538
5611
|
import path21 from "path";
|
|
5539
5612
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
@@ -5546,11 +5619,11 @@ function dmgUrl() {
|
|
|
5546
5619
|
async function download(url, dest) {
|
|
5547
5620
|
const res = await fetch(url, { redirect: "follow" });
|
|
5548
5621
|
if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
5549
|
-
|
|
5622
|
+
fs26.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
5550
5623
|
}
|
|
5551
5624
|
async function install() {
|
|
5552
5625
|
if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
|
|
5553
|
-
const tmp =
|
|
5626
|
+
const tmp = fs26.mkdtempSync(path21.join(os11.tmpdir(), "sakura-app-"));
|
|
5554
5627
|
const dmg = path21.join(tmp, "Sakura.dmg");
|
|
5555
5628
|
const mount = path21.join(tmp, "mnt");
|
|
5556
5629
|
info(`Downloading Sakura.app ${VERSION}\u2026`);
|
|
@@ -5560,18 +5633,18 @@ async function install() {
|
|
|
5560
5633
|
die(`${e.message}
|
|
5561
5634
|
Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
|
|
5562
5635
|
}
|
|
5563
|
-
|
|
5636
|
+
fs26.mkdirSync(mount);
|
|
5564
5637
|
execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
|
|
5565
5638
|
try {
|
|
5566
5639
|
try {
|
|
5567
5640
|
execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
|
|
5568
5641
|
} catch {
|
|
5569
5642
|
}
|
|
5570
|
-
|
|
5643
|
+
fs26.rmSync(APP_PATH, { recursive: true, force: true });
|
|
5571
5644
|
execFileSync4("ditto", [path21.join(mount, "Sakura.app"), APP_PATH]);
|
|
5572
5645
|
} finally {
|
|
5573
5646
|
execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
|
|
5574
|
-
|
|
5647
|
+
fs26.rmSync(tmp, { recursive: true, force: true });
|
|
5575
5648
|
}
|
|
5576
5649
|
execFileSync4("open", [APP_PATH]);
|
|
5577
5650
|
success("Sakura is in your menu bar (look for the blossom).");
|
|
@@ -5580,7 +5653,7 @@ function registerApp(program) {
|
|
|
5580
5653
|
const app = program.command("app").description("The optional Sakura menu bar app for macOS");
|
|
5581
5654
|
app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
|
|
5582
5655
|
app.command("open").description("Open the Sakura menu bar app").action(() => {
|
|
5583
|
-
if (!
|
|
5656
|
+
if (!fs26.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
|
|
5584
5657
|
execFileSync4("open", [APP_PATH]);
|
|
5585
5658
|
});
|
|
5586
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",
|