@solongate/proxy 0.83.2 → 0.83.4

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.
@@ -405,6 +405,16 @@ function repairQuiet() {
405
405
  }
406
406
  async function runRepair() {
407
407
  const out = (s) => void process.stderr.write(s + "\n");
408
+ if (process.platform !== "win32" && typeof process.getuid === "function" && process.getuid() === 0 && process.env["SOLONGATE_INTERNAL"] !== "1") {
409
+ out("");
410
+ out(" Do not run `solongate repair` with sudo.");
411
+ out(" It installs the guard into your home directory, and as root it would");
412
+ out(` arm root\u2019s home instead${process.env["SUDO_USER"] ? ` of ${process.env["SUDO_USER"]}\u2019s` : ""} \u2014 reporting success while nothing changed for you.`);
413
+ out("");
414
+ out(" Run it as yourself: solongate repair");
415
+ out("");
416
+ return 1;
417
+ }
408
418
  const rep = repairQuiet();
409
419
  out("");
410
420
  out(" SolonGate repair");
package/dist/index.js CHANGED
@@ -465,13 +465,13 @@ ${offset}${err2}${errEnd}`;
465
465
  return next >= src.length || src[next] === "\n" ? str + "\n" : str;
466
466
  }
467
467
  // ^(---|...)
468
- static atDocumentBoundary(src, offset, sep) {
468
+ static atDocumentBoundary(src, offset, sep2) {
469
469
  const ch0 = src[offset];
470
470
  if (!ch0) return true;
471
471
  const prev = src[offset - 1];
472
472
  if (prev && prev !== "\n") return false;
473
- if (sep) {
474
- if (ch0 !== sep) return false;
473
+ if (sep2) {
474
+ if (ch0 !== sep2) return false;
475
475
  } else {
476
476
  if (ch0 !== Char.DIRECTIVES_END && ch0 !== Char.DOCUMENT_END) return false;
477
477
  }
@@ -1647,7 +1647,7 @@ var require_parse_cst = __commonJS({
1647
1647
  const folded = this.type === PlainValue.Type.BLOCK_FOLDED;
1648
1648
  let atStart = true;
1649
1649
  let str = "";
1650
- let sep = "";
1650
+ let sep2 = "";
1651
1651
  let prevMoreIndented = false;
1652
1652
  for (let i = start; i < end; ++i) {
1653
1653
  for (let j = 0; j < bi; ++j) {
@@ -1656,21 +1656,21 @@ var require_parse_cst = __commonJS({
1656
1656
  }
1657
1657
  const ch2 = src[i];
1658
1658
  if (ch2 === "\n") {
1659
- if (sep === "\n") str += "\n";
1660
- else sep = "\n";
1659
+ if (sep2 === "\n") str += "\n";
1660
+ else sep2 = "\n";
1661
1661
  } else {
1662
1662
  const lineEnd = PlainValue.Node.endOfLine(src, i);
1663
1663
  const line = src.slice(i, lineEnd);
1664
1664
  i = lineEnd;
1665
1665
  if (folded && (ch2 === " " || ch2 === " ") && i < keepStart) {
1666
- if (sep === " ") sep = "\n";
1667
- else if (!prevMoreIndented && !atStart && sep === "\n") sep = "\n\n";
1668
- str += sep + line;
1669
- sep = lineEnd < end && src[lineEnd] || "";
1666
+ if (sep2 === " ") sep2 = "\n";
1667
+ else if (!prevMoreIndented && !atStart && sep2 === "\n") sep2 = "\n\n";
1668
+ str += sep2 + line;
1669
+ sep2 = lineEnd < end && src[lineEnd] || "";
1670
1670
  prevMoreIndented = true;
1671
1671
  } else {
1672
- str += sep + line;
1673
- sep = folded && i < keepStart ? " " : "\n";
1672
+ str += sep2 + line;
1673
+ sep2 = folded && i < keepStart ? " " : "\n";
1674
1674
  prevMoreIndented = false;
1675
1675
  }
1676
1676
  if (atStart && line !== "") atStart = false;
@@ -6612,6 +6612,16 @@ function repairQuiet() {
6612
6612
  }
6613
6613
  async function runRepair() {
6614
6614
  const out2 = (s) => void process.stderr.write(s + "\n");
6615
+ if (process.platform !== "win32" && typeof process.getuid === "function" && process.getuid() === 0 && process.env["SOLONGATE_INTERNAL"] !== "1") {
6616
+ out2("");
6617
+ out2(" Do not run `solongate repair` with sudo.");
6618
+ out2(" It installs the guard into your home directory, and as root it would");
6619
+ out2(` arm root\u2019s home instead${process.env["SUDO_USER"] ? ` of ${process.env["SUDO_USER"]}\u2019s` : ""} \u2014 reporting success while nothing changed for you.`);
6620
+ out2("");
6621
+ out2(" Run it as yourself: solongate repair");
6622
+ out2("");
6623
+ return 1;
6624
+ }
6615
6625
  const rep = repairQuiet();
6616
6626
  out2("");
6617
6627
  out2(" SolonGate repair");
@@ -6923,17 +6933,26 @@ var init_global_install = __esm({
6923
6933
  // src/self-update.ts
6924
6934
  var self_update_exports = {};
6925
6935
  __export(self_update_exports, {
6936
+ adminInstallCommand: () => adminInstallCommand,
6937
+ adminUpdateSteps: () => adminUpdateSteps,
6938
+ autoUpdateEnabled: () => autoUpdateEnabled,
6939
+ autoUpdateForcedByEnv: () => autoUpdateForcedByEnv,
6926
6940
  currentVersion: () => currentVersion,
6941
+ globalInstallCheck: () => globalInstallCheck,
6927
6942
  latestVersion: () => latestVersion,
6928
6943
  maybeSelfUpdate: () => maybeSelfUpdate,
6929
6944
  newerThan: () => newerThan,
6945
+ runAutoUpdateCommand: () => runAutoUpdateCommand,
6930
6946
  runUpdateCommand: () => runUpdateCommand,
6931
- tuiUpdateFlow: () => tuiUpdateFlow
6947
+ runningAsRoot: () => runningAsRoot,
6948
+ setAutoUpdate: () => setAutoUpdate,
6949
+ tuiUpdateFlow: () => tuiUpdateFlow,
6950
+ updateNow: () => updateNow
6932
6951
  });
6933
6952
  import { execFile, execFileSync as execFileSync3, spawn } from "child_process";
6934
- import { mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
6953
+ import { access, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4, constants as FS } from "fs";
6935
6954
  import { homedir as homedir3 } from "os";
6936
- import { dirname as dirname2, join as join5 } from "path";
6955
+ import { dirname as dirname2, join as join5, sep } from "path";
6937
6956
  import { fileURLToPath as fileURLToPath2 } from "url";
6938
6957
  function readState() {
6939
6958
  try {
@@ -6950,6 +6969,20 @@ function writeState(s) {
6950
6969
  } catch {
6951
6970
  }
6952
6971
  }
6972
+ function autoUpdateEnabled() {
6973
+ const env = (process.env.SOLONGATE_AUTO_UPDATE ?? "").trim().toLowerCase();
6974
+ if (env === "1" || env === "true" || env === "on" || env === "yes") return true;
6975
+ if (env === "0" || env === "false" || env === "off" || env === "no") return false;
6976
+ return readState().auto === true;
6977
+ }
6978
+ function autoUpdateForcedByEnv() {
6979
+ const env = (process.env.SOLONGATE_AUTO_UPDATE ?? "").trim().toLowerCase();
6980
+ return ["1", "true", "on", "yes", "0", "false", "off", "no"].includes(env);
6981
+ }
6982
+ function setAutoUpdate(on) {
6983
+ const s = readState();
6984
+ writeState(on ? { ...s, auto: true, needsAdmin: void 0 } : { ...s, auto: false });
6985
+ }
6953
6986
  function currentVersion() {
6954
6987
  try {
6955
6988
  const pkg = JSON.parse(readFileSync5(join5(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf-8"));
@@ -7006,6 +7039,17 @@ function spawnGlobalInstall(version) {
7006
7039
  }
7007
7040
  async function runUpdateCommand() {
7008
7041
  const out2 = (s) => void process.stderr.write(s + "\n");
7042
+ if (runningAsRoot()) {
7043
+ out2("");
7044
+ out2(" Do not run `solongate update` with sudo.");
7045
+ out2(" npm would install fine, but the guard hooks would be written into root\u2019s");
7046
+ out2(" home instead of yours, and your agents would stop being guarded.");
7047
+ out2("");
7048
+ out2(" Run these two instead:");
7049
+ for (const s of adminUpdateSteps()) out2(" " + s);
7050
+ out2("");
7051
+ return 1;
7052
+ }
7009
7053
  const cur = currentVersion();
7010
7054
  out2(` current ${cur}`);
7011
7055
  const latest = await latestVersion();
@@ -7014,12 +7058,32 @@ async function runUpdateCommand() {
7014
7058
  return 1;
7015
7059
  }
7016
7060
  if (newerThan(latest, cur)) {
7061
+ const pre = await globalInstallCheck();
7062
+ if (pre.writable === false) {
7063
+ writeState({ ...readState(), needsAdmin: latest });
7064
+ out2(` v${latest} is out, but npm\u2019s global folder needs admin rights on this machine:`);
7065
+ out2(` ${pre.dir}`);
7066
+ out2(" That is the normal macOS setup \u2014 SolonGate cannot install there on its own.");
7067
+ out2("");
7068
+ out2(" Run these two:");
7069
+ for (const s of adminUpdateSteps()) out2(" " + s);
7070
+ out2("");
7071
+ return 1;
7072
+ }
7017
7073
  out2(` updating ${cur} -> ${latest} ...`);
7018
- const ok = await runGlobalInstall2(latest);
7019
- if (!ok) {
7020
- out2(" update failed. Try: npm i -g @solongate/proxy@latest");
7074
+ const r = await runGlobalInstall2(latest);
7075
+ if (!r.ok) {
7076
+ if (r.needsAdmin) {
7077
+ writeState({ ...readState(), needsAdmin: latest });
7078
+ out2(" npm could not write to the global folder (it needs admin rights).");
7079
+ out2(" Run these two:");
7080
+ for (const s of adminUpdateSteps()) out2(" " + s);
7081
+ } else {
7082
+ out2(" update failed. Try: npm i -g @solongate/proxy@latest");
7083
+ }
7021
7084
  return 1;
7022
7085
  }
7086
+ writeState({ ...readState(), needsAdmin: void 0 });
7023
7087
  out2(` \u2713 updated to ${latest} (open a new session to use it)`);
7024
7088
  } else {
7025
7089
  out2(` \u2713 already up to date`);
@@ -7049,20 +7113,99 @@ function runGlobalInstall2(version) {
7049
7113
  ["install", "-g", `${PKG}@${version}`],
7050
7114
  { timeout: 3e5, windowsHide: true, shell: process.platform === "win32" },
7051
7115
  (err2, stdout, stderr) => {
7116
+ const output = `${stdout}
7117
+ ${stderr}`;
7052
7118
  try {
7053
7119
  writeFileSync4(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} install ${version}: ${err2 ? "FAILED" : "ok"}
7054
- ${stdout}
7055
- ${stderr}
7120
+ ${output}
7056
7121
  `, { flag: "a" });
7057
7122
  } catch {
7058
7123
  }
7059
- resolve6(!err2);
7124
+ resolve6({ ok: !err2, needsAdmin: !!err2 && NEEDS_ADMIN_RE.test(output) });
7060
7125
  }
7061
7126
  );
7062
7127
  } catch {
7063
- resolve6(false);
7128
+ resolve6({ ok: false, needsAdmin: false });
7129
+ }
7130
+ });
7131
+ }
7132
+ function adminInstallCommand() {
7133
+ return process.platform === "win32" ? `npm i -g ${PKG}@latest (in an Administrator terminal)` : `sudo npm i -g ${PKG}@latest`;
7134
+ }
7135
+ function runningAsRoot() {
7136
+ return process.platform !== "win32" && typeof process.getuid === "function" && process.getuid() === 0;
7137
+ }
7138
+ function ownNodeModules() {
7139
+ let dir = dirname2(fileURLToPath2(import.meta.url));
7140
+ for (let i = 0; i < 12; i++) {
7141
+ const parent = dirname2(dir);
7142
+ if (parent === dir) break;
7143
+ if (dir.endsWith(`${sep}node_modules`)) return dir;
7144
+ dir = parent;
7145
+ }
7146
+ return null;
7147
+ }
7148
+ function globalInstallCheck() {
7149
+ prefixCheck ??= new Promise((resolve6) => {
7150
+ try {
7151
+ const dir = ownNodeModules();
7152
+ if (!dir) return resolve6({ writable: null, dir: null });
7153
+ access(dir, FS.W_OK, (e) => resolve6({ writable: !e, dir }));
7154
+ } catch {
7155
+ resolve6({ writable: null, dir: null });
7064
7156
  }
7065
7157
  });
7158
+ return prefixCheck;
7159
+ }
7160
+ function adminUpdateSteps() {
7161
+ return [` ${adminInstallCommand()}`, ` solongate repair${process.platform === "win32" ? "" : " (this one WITHOUT sudo)"}`];
7162
+ }
7163
+ async function updateNow() {
7164
+ const cur = currentVersion();
7165
+ const latest = await fetchLatest();
7166
+ if (!latest) return { status: "unreachable", version: cur };
7167
+ if (!newerThan(latest, cur)) return { status: "current", version: cur };
7168
+ if ((await globalInstallCheck()).writable === false) {
7169
+ writeState({ ...readState(), needsAdmin: latest });
7170
+ return { status: "needs-admin", version: latest };
7171
+ }
7172
+ const r = await runGlobalInstall2(latest);
7173
+ if (r.ok) {
7174
+ writeState({ ...readState(), installed: latest, needsAdmin: void 0 });
7175
+ return { status: "updated", version: latest };
7176
+ }
7177
+ if (r.needsAdmin) {
7178
+ writeState({ ...readState(), needsAdmin: latest });
7179
+ return { status: "needs-admin", version: latest };
7180
+ }
7181
+ return { status: "failed", version: latest };
7182
+ }
7183
+ async function runAutoUpdateCommand(arg) {
7184
+ const out2 = (s) => void process.stderr.write(s + "\n");
7185
+ const envForced = autoUpdateForcedByEnv();
7186
+ if (arg === void 0) {
7187
+ out2(` auto-update is ${autoUpdateEnabled() ? "on" : "off"}${envForced ? " (forced by SOLONGATE_AUTO_UPDATE)" : ""}`);
7188
+ out2(" change it with: solongate update auto on|off");
7189
+ return 0;
7190
+ }
7191
+ const v = arg.trim().toLowerCase();
7192
+ const on = ["on", "1", "true", "yes", "enable", "enabled"].includes(v);
7193
+ const off = ["off", "0", "false", "no", "disable", "disabled"].includes(v);
7194
+ if (!on && !off) {
7195
+ out2(` unknown value "${arg}" \u2014 use: solongate update auto on|off`);
7196
+ return 1;
7197
+ }
7198
+ setAutoUpdate(on);
7199
+ out2(on ? " \u2713 auto-update on \u2014 new versions install in the background" : " \u2713 auto-update off \u2014 update with: solongate update");
7200
+ if (on && (await globalInstallCheck()).writable === false) {
7201
+ out2("");
7202
+ out2(" Heads up: npm\u2019s global folder needs admin rights on this machine, so a");
7203
+ out2(" background install cannot succeed here. Updates will still be announced,");
7204
+ out2(" and installing them takes:");
7205
+ for (const s of adminUpdateSteps()) out2(" " + s);
7206
+ }
7207
+ if (envForced) out2(` note: SOLONGATE_AUTO_UPDATE is set and overrides this while it stays set`);
7208
+ return 0;
7066
7209
  }
7067
7210
  async function tuiUpdateFlow(onStatus) {
7068
7211
  try {
@@ -7075,10 +7218,22 @@ async function tuiUpdateFlow(onStatus) {
7075
7218
  onStatus({ kind: "updated", version: latest });
7076
7219
  return;
7077
7220
  }
7221
+ if (!autoUpdateEnabled()) {
7222
+ onStatus({ kind: "available", version: latest });
7223
+ return;
7224
+ }
7225
+ if (readState().needsAdmin === latest) {
7226
+ onStatus({ kind: "needs-admin", version: latest });
7227
+ return;
7228
+ }
7078
7229
  onStatus({ kind: "updating", version: latest });
7079
- if (await runGlobalInstall2(latest)) {
7080
- writeState({ ...readState(), installed: latest });
7230
+ const r = await runGlobalInstall2(latest);
7231
+ if (r.ok) {
7232
+ writeState({ ...readState(), installed: latest, needsAdmin: void 0 });
7081
7233
  onStatus({ kind: "updated", version: latest });
7234
+ } else if (r.needsAdmin) {
7235
+ writeState({ ...readState(), needsAdmin: latest });
7236
+ onStatus({ kind: "needs-admin", version: latest });
7082
7237
  }
7083
7238
  } catch {
7084
7239
  }
@@ -7097,17 +7252,27 @@ function maybeSelfUpdate(notify = (l) => process.stderr.write(l + "\n")) {
7097
7252
  }
7098
7253
  if (!latest || !newerThan(latest, current)) return;
7099
7254
  const attempts = readState().attempts ?? {};
7100
- if (now - (attempts[latest] ?? 0) >= ATTEMPT_EVERY_MS) {
7101
- writeState({ ...readState(), attempts: { [latest]: now } });
7102
- if (spawnGlobalInstall(latest)) {
7103
- notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 updating in the background, next run uses it${c.reset}`);
7104
- }
7255
+ if (now - (attempts[latest] ?? 0) < ATTEMPT_EVERY_MS) return;
7256
+ writeState({ ...readState(), attempts: { [latest]: now } });
7257
+ const needsAdmin = (await globalInstallCheck()).writable === false;
7258
+ const head = `${c.dim}\u2191 solongate v${latest} available (running v${current})`;
7259
+ if (needsAdmin) {
7260
+ notify(`${head} \u2014 needs admin rights on this machine:${c.reset}`);
7261
+ for (const s of adminUpdateSteps()) notify(`${c.cyan} ${s.trim()}${c.reset}`);
7262
+ return;
7263
+ }
7264
+ if (!autoUpdateEnabled()) {
7265
+ notify(`${head} \u2014 run ${c.reset}${c.cyan}solongate update${c.reset}`);
7266
+ return;
7267
+ }
7268
+ if (spawnGlobalInstall(latest)) {
7269
+ notify(`${head} \u2014 updating in the background, next run uses it${c.reset}`);
7105
7270
  }
7106
7271
  } catch {
7107
7272
  }
7108
7273
  })();
7109
7274
  }
7110
- var PKG, CHECK_EVERY_MS, ATTEMPT_EVERY_MS, STATE_FILE, LOG_FILE;
7275
+ var PKG, CHECK_EVERY_MS, ATTEMPT_EVERY_MS, STATE_FILE, LOG_FILE, NEEDS_ADMIN_RE, prefixCheck;
7111
7276
  var init_self_update = __esm({
7112
7277
  "src/self-update.ts"() {
7113
7278
  "use strict";
@@ -7117,6 +7282,8 @@ var init_self_update = __esm({
7117
7282
  ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
7118
7283
  STATE_FILE = join5(homedir3(), ".solongate", ".self-update.json");
7119
7284
  LOG_FILE = join5(homedir3(), ".solongate", "self-update.log");
7285
+ NEEDS_ADMIN_RE = /\bEACCES\b|\bEPERM\b|permission denied|operation not permitted/i;
7286
+ prefixCheck = null;
7120
7287
  }
7121
7288
  });
7122
7289
 
@@ -11458,6 +11625,19 @@ function SettingsPanel({
11458
11625
  const [editor, setEditor] = useState8(null);
11459
11626
  const [latest, setLatest] = useState8(null);
11460
11627
  const [diagBusy, setDiagBusy] = useState8(null);
11628
+ const [autoUp, setAutoUp] = useState8(() => autoUpdateEnabled());
11629
+ const [updBusy, setUpdBusy] = useState8(false);
11630
+ const [canInstall, setCanInstall] = useState8(null);
11631
+ useEffect7(() => {
11632
+ let live2 = true;
11633
+ globalInstallCheck().then((r) => {
11634
+ if (live2) setCanInstall(r.writable);
11635
+ }).catch(() => {
11636
+ });
11637
+ return () => {
11638
+ live2 = false;
11639
+ };
11640
+ }, []);
11461
11641
  const [diag, setDiag] = useState8(null);
11462
11642
  const [diagStep, setDiagStep] = useState8(0);
11463
11643
  const DIAG_STEPS = {
@@ -11515,10 +11695,10 @@ function SettingsPanel({
11515
11695
  }, []);
11516
11696
  useEffect7(() => {
11517
11697
  const loggingIn = login && login.phase !== "done" && login.phase !== "error";
11518
- if (!loggingIn && !diagBusy) return;
11698
+ if (!loggingIn && !diagBusy && !updBusy) return;
11519
11699
  const t = setInterval(() => setTick((n) => n + 1), 120);
11520
11700
  return () => clearInterval(t);
11521
- }, [login, diagBusy]);
11701
+ }, [login, diagBusy, updBusy]);
11522
11702
  const localQ = useLoader(() => listAccounts().length ? api.settings.getLocalLogs() : Promise.resolve(null));
11523
11703
  const whQ = useLoader(() => listAccounts().length ? api.settings.getWebhooks() : Promise.resolve(null));
11524
11704
  const alertQ = useLoader(() => listAccounts().length ? api.settings.getAlerts() : Promise.resolve(null));
@@ -11552,6 +11732,8 @@ function SettingsPanel({
11552
11732
  { kind: "self" },
11553
11733
  { kind: "doctor" },
11554
11734
  { kind: "repair" },
11735
+ { kind: "cli-update" },
11736
+ { kind: "auto-update" },
11555
11737
  { kind: "ll-enabled" },
11556
11738
  { kind: "ll-path" },
11557
11739
  { kind: "ll-server" },
@@ -11731,6 +11913,46 @@ function SettingsPanel({
11731
11913
  } else if (r.kind === "self") {
11732
11914
  if (!selfProt) return;
11733
11915
  run10(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
11916
+ } else if (r.kind === "cli-update") {
11917
+ if (updBusy) return;
11918
+ setUpdBusy(true);
11919
+ setMsg({ text: "updating\u2026", level: "ok" });
11920
+ setDiag(null);
11921
+ void (async () => {
11922
+ try {
11923
+ const res = await updateNow();
11924
+ if (res.status === "updated") setMsg({ text: `\u2713 v${res.version} installed \xB7 restart (q, then solongate) to apply`, level: "ok" });
11925
+ else if (res.status === "current") setMsg({ text: `\u2713 already on the latest version (v${res.version})`, level: "ok" });
11926
+ else if (res.status === "needs-admin") {
11927
+ setMsg({ text: `\u2717 v${res.version} needs admin rights \u2014 run the two commands below`, level: "bad" });
11928
+ setDiag({
11929
+ of: "cli-update",
11930
+ lines: [
11931
+ { text: "npm\u2019s global folder belongs to root on this machine (the usual macOS", level: "warn" },
11932
+ { text: "setup), so SolonGate cannot install there by itself. In a terminal:", level: "warn" },
11933
+ ...adminUpdateSteps().map((s) => ({ text: s.trim(), level: "ok" })),
11934
+ { text: "run repair WITHOUT sudo, or the guard hooks land in root\u2019s home", level: "dim" }
11935
+ ]
11936
+ });
11937
+ } else if (res.status === "unreachable") setMsg({ text: "\u2717 could not reach the npm registry", level: "bad" });
11938
+ else setMsg({ text: `\u2717 update to v${res.version} failed \u2014 see ~/.solongate/self-update.log`, level: "bad" });
11939
+ setLatest(await latestVersion());
11940
+ setCanInstall((await globalInstallCheck()).writable);
11941
+ } finally {
11942
+ setUpdBusy(false);
11943
+ }
11944
+ })();
11945
+ } else if (r.kind === "auto-update") {
11946
+ if (autoUpdateForcedByEnv()) {
11947
+ setMsg({ text: "SOLONGATE_AUTO_UPDATE is set \u2014 unset it to change this here", level: "bad" });
11948
+ return;
11949
+ }
11950
+ const next = !autoUp;
11951
+ setAutoUpdate(next);
11952
+ setAutoUp(next);
11953
+ setMsg(
11954
+ next ? canInstall === false ? { text: "\u2713 auto-update on \u2014 but npm needs sudo here, so it still cannot install on its own", level: "bad" } : { text: "\u2713 auto-update on \u2014 new versions install in the background", level: "ok" } : { text: "\u2713 auto-update off \u2014 update from this row or with: solongate update", level: "ok" }
11955
+ );
11734
11956
  } else if (r.kind === "ll-enabled") {
11735
11957
  if (!local) return;
11736
11958
  if (!local.enabled && !local.path.trim()) {
@@ -11829,7 +12051,7 @@ function SettingsPanel({
11829
12051
  if (ok) clearLocalLog();
11830
12052
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
11831
12053
  refreshAccounts();
11832
- } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
12054
+ } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "auto-update" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
11833
12055
  if (cur.kind === "alert") run10(cur.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(cur.rule.id, !cur.rule.enabled), alertQ.reload);
11834
12056
  else activate(cur);
11835
12057
  }
@@ -12041,6 +12263,25 @@ function SettingsPanel({
12041
12263
  /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "repair".padEnd(11) }),
12042
12264
  diagBusy === "repair" ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: `${spin} restoring protection\u2026` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "restore every protection file after tampering or deletion \xB7 enter runs it" })
12043
12265
  ] });
12266
+ case "cli-update": {
12267
+ const behind = latest ? newerThan(latest, ver) : false;
12268
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
12269
+ cursor(r),
12270
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "version".padEnd(11) }),
12271
+ /* @__PURE__ */ jsx8(Text8, { color: behind ? theme.warn : theme.ok, children: `v${ver}` }),
12272
+ updBusy ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: ` ${spin} updating\u2026` }) : behind && canInstall === false ? (
12273
+ // Said before they press enter, not after a minute of npm noise.
12274
+ /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` v${latest} on npm \xB7 needs sudo here \xB7 enter shows how` })
12275
+ ) : behind ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` v${latest} on npm \xB7 enter updates now` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: latest ? " latest \xB7 enter checks again" : " enter checks npm and updates" })
12276
+ ] });
12277
+ }
12278
+ case "auto-update":
12279
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
12280
+ cursor(r),
12281
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "auto".padEnd(11) }),
12282
+ onOff(autoUp),
12283
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: autoUpdateForcedByEnv() ? " set by SOLONGATE_AUTO_UPDATE" : canInstall === false ? " cannot work here: npm needs sudo, so updates stay manual \xB7 enter toggles" : autoUp ? " installs new versions in the background \xB7 enter toggles" : " off: nothing installs on its own (npm -g may need sudo) \xB7 enter toggles" })
12284
+ ] });
12044
12285
  case "ll-enabled":
12045
12286
  return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
12046
12287
  cursor(r),
@@ -12093,10 +12334,11 @@ function SettingsPanel({
12093
12334
  ] });
12094
12335
  }
12095
12336
  };
12096
- const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
12337
+ const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "cli-update" || r.kind === "auto-update" ? "UPDATES" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
12097
12338
  const SECTION_DESC = {
12098
12339
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
12099
12340
  PROTECTION: "guard hook: enter install/update \xB7 d remove \xB7 self-protection \xB7 doctor + repair",
12341
+ UPDATES: "the solongate CLI itself \xB7 background auto-update is off unless you turn it on",
12100
12342
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
12101
12343
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
12102
12344
  ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 m on/off`
@@ -12137,7 +12379,7 @@ function SettingsPanel({
12137
12379
  lineKey.push("");
12138
12380
  });
12139
12381
  }
12140
- if (diag && (r.kind === "doctor" && diag.of === "doctor" || r.kind === "repair" && diag.of === "repair")) {
12382
+ if (diag && (r.kind === "doctor" && diag.of === "doctor" || r.kind === "repair" && diag.of === "repair" || r.kind === "cli-update" && diag.of === "cli-update")) {
12141
12383
  diag.lines.forEach((l, i) => {
12142
12384
  lineEls.push(
12143
12385
  /* @__PURE__ */ jsx8(
@@ -12165,7 +12407,7 @@ function SettingsPanel({
12165
12407
  lineEls.push(
12166
12408
  /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", color: theme.dim, children: [
12167
12409
  `solongate v${ver}`,
12168
- latest ? newerThan(latest, ver) ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \xB7 v${latest} on npm \u2014 auto-updating` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.ok, children: " \xB7 latest" }) : null
12410
+ latest ? newerThan(latest, ver) ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \xB7 v${latest} on npm \u2014 ${autoUp ? "auto-updating" : "UPDATES above"}` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.ok, children: " \xB7 latest" }) : null
12169
12411
  ] }, "ver")
12170
12412
  );
12171
12413
  lineKey.push("");
@@ -12394,7 +12636,7 @@ function App() {
12394
12636
  // a row off whichever panel is open.
12395
12637
  /* @__PURE__ */ jsx9(Text9, { color: theme.bad, bold: true, children: " \xB7 allowance used up \xB7 every call denied \xB7 create an account to carry on" })
12396
12638
  ) : accounts.length > 1 ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 Settings to add another" }),
12397
- update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \xB7 restart (q, then solongate) to apply` }) : null
12639
+ update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \xB7 restart (q, then solongate) to apply` }) : update2.kind === "available" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} out \xB7 Settings \u2192 UPDATES` }) : update2.kind === "needs-admin" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} needs admin rights \xB7 Settings \u2192 UPDATES` }) : null
12398
12640
  ] }),
12399
12641
  /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
12400
12642
  /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexShrink: 0, width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
@@ -17025,7 +17267,8 @@ function printHelp() {
17025
17267
  console.log(` ${c.dim}Usage:${c.reset} ${c.cyan}solongate${c.reset} ${c.dim}[command]${c.reset} ${c.dim}(no command opens the dataroom: all of this in a terminal UI)${c.reset}`);
17026
17268
  head("Setup & status");
17027
17269
  cmd("solongate", "open the dataroom UI (login, policies, audit, settings)");
17028
- cmd("update", "update SolonGate to the newest version and refresh the guard");
17270
+ cmd("update", "update SolonGate and refresh the guard (tells you if it needs sudo)");
17271
+ cmd("update auto on|off", "background auto-update (default off \u2014 on macOS npm -g often needs sudo)");
17029
17272
  cmd("repair", "restore the guard + hook + settings files if they were deleted or disarmed");
17030
17273
  cmd("doctor", "health check: login, policy, guard, local logs");
17031
17274
  cmd("doctor --json", "the same health check as machine-readable JSON");
@@ -17173,6 +17416,10 @@ async function main() {
17173
17416
  return;
17174
17417
  }
17175
17418
  if (subcommand === "update") {
17419
+ if (process.argv[3] === "auto") {
17420
+ const { runAutoUpdateCommand: runAutoUpdateCommand2 } = await Promise.resolve().then(() => (init_self_update(), self_update_exports));
17421
+ process.exit(await runAutoUpdateCommand2(process.argv[4]));
17422
+ }
17176
17423
  const { runUpdateCommand: runUpdateCommand2 } = await Promise.resolve().then(() => (init_self_update(), self_update_exports));
17177
17424
  process.exit(await runUpdateCommand2());
17178
17425
  }
@@ -1,30 +1,77 @@
1
+ /**
2
+ * Is the background updater turned on? Default: NO — see the file header.
3
+ * SOLONGATE_AUTO_UPDATE=on|off (1/0, true/false) overrides the stored setting
4
+ * for one run, so a CI image or a managed fleet can force either way without
5
+ * writing to the user's home directory.
6
+ */
7
+ export declare function autoUpdateEnabled(): boolean;
8
+ /** True when the env var is deciding, so the UI can say the row is overridden. */
9
+ export declare function autoUpdateForcedByEnv(): boolean;
10
+ /** Turn the background updater on/off. Clears the "npm refused" memo on enable. */
11
+ export declare function setAutoUpdate(on: boolean): void;
1
12
  export declare function currentVersion(): string;
2
13
  /** true when `b` is a strictly newer semver than `a` (numeric triples only). */
3
14
  export declare function newerThan(b: string, a: string): boolean;
4
15
  /** The latest version published to npm, or null if it can't be reached. */
5
16
  export declare function latestVersion(): Promise<string | null>;
6
- /** Foreground global install — resolves true on success, output → update log. */
7
17
  /**
8
- * `solongate update` — explicit, visible update instead of waiting for the
9
- * background auto-update: installs the newest CLI globally, then refreshes the
10
- * installed guard hooks. Returns a process exit code.
18
+ * `solongate update` — the normal way to update, and the ONLY way when the
19
+ * background updater is off (which is the default): installs the newest CLI
20
+ * globally, then refreshes the installed guard hooks. Returns a process exit
21
+ * code.
11
22
  */
12
23
  export declare function runUpdateCommand(): Promise<number>;
24
+ /** The exact command that fixes a root-owned global prefix. */
25
+ export declare function adminInstallCommand(): string;
26
+ /** Are we root — i.e. did someone run this under sudo? */
27
+ export declare function runningAsRoot(): boolean;
28
+ export declare function globalInstallCheck(): Promise<{
29
+ writable: boolean | null;
30
+ dir: string | null;
31
+ }>;
32
+ /**
33
+ * The two commands that update SolonGate on a machine whose global npm folder
34
+ * belongs to root. `repair` is deliberately NOT under sudo: it writes the guard
35
+ * hooks into the invoking user's home, so as root it would arm root's home and
36
+ * leave the actual user unguarded.
37
+ */
38
+ export declare function adminUpdateSteps(): string[];
39
+ /**
40
+ * Install the newest version NOW, whatever the auto-update setting says — the
41
+ * dataroom's UPDATES row and `solongate update` both run through here.
42
+ */
43
+ export declare function updateNow(): Promise<{
44
+ status: 'updated' | 'current' | 'needs-admin' | 'unreachable' | 'failed';
45
+ version: string;
46
+ }>;
47
+ /**
48
+ * `solongate update auto [on|off]` — show or change the background updater.
49
+ * Kept separate from `runUpdateCommand` so `solongate update` stays "update me
50
+ * now" and never changes a setting as a side effect.
51
+ */
52
+ export declare function runAutoUpdateCommand(arg?: string): Promise<number>;
13
53
  /** What the dataroom shows about the updater. */
14
54
  export type UpdateStatus = {
15
55
  kind: 'idle';
56
+ } | {
57
+ kind: 'available';
58
+ version: string;
16
59
  } | {
17
60
  kind: 'updating';
18
61
  version: string;
19
62
  } | {
20
63
  kind: 'updated';
21
64
  version: string;
65
+ } | {
66
+ kind: 'needs-admin';
67
+ version: string;
22
68
  };
23
69
  /**
24
- * The dataroom's updater (Claude Code-style): check the registry EVERY time the
25
- * TUI opens (and periodically while it stays open), install the new version in
26
- * the background, then tell the user to restart. `onStatus` drives the in-app
27
- * status line; nothing is ever written to the terminal directly.
70
+ * The dataroom's updater: check the registry EVERY time the TUI opens (and
71
+ * periodically while it stays open). With auto-update ON, install in the
72
+ * background and ask for a restart; with it OFF (the default), only report that
73
+ * a version is available. `onStatus` drives the in-app status line; nothing is
74
+ * ever written to the terminal directly.
28
75
  */
29
76
  export declare function tuiUpdateFlow(onStatus: (s: UpdateStatus) => void): Promise<void>;
30
77
  /**
package/dist/tui/index.js CHANGED
@@ -4495,15 +4495,16 @@ function stopLogsServerDaemon() {
4495
4495
 
4496
4496
  // src/self-update.ts
4497
4497
  import { execFile, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
4498
- import { mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
4498
+ import { access, mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7, constants as FS } from "fs";
4499
4499
  import { homedir as homedir9 } from "os";
4500
- import { dirname as dirname3, join as join9 } from "path";
4500
+ import { dirname as dirname3, join as join9, sep } from "path";
4501
4501
  import { fileURLToPath as fileURLToPath3 } from "url";
4502
4502
  var PKG = "@solongate/proxy";
4503
4503
  var CHECK_EVERY_MS = 30 * 60 * 1e3;
4504
4504
  var ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
4505
4505
  var STATE_FILE2 = join9(homedir9(), ".solongate", ".self-update.json");
4506
4506
  var LOG_FILE2 = join9(homedir9(), ".solongate", "self-update.log");
4507
+ var NEEDS_ADMIN_RE = /\bEACCES\b|\bEPERM\b|permission denied|operation not permitted/i;
4507
4508
  function readState2() {
4508
4509
  try {
4509
4510
  const s = JSON.parse(readFileSync7(STATE_FILE2, "utf-8"));
@@ -4519,6 +4520,20 @@ function writeState2(s) {
4519
4520
  } catch {
4520
4521
  }
4521
4522
  }
4523
+ function autoUpdateEnabled() {
4524
+ const env = (process.env.SOLONGATE_AUTO_UPDATE ?? "").trim().toLowerCase();
4525
+ if (env === "1" || env === "true" || env === "on" || env === "yes") return true;
4526
+ if (env === "0" || env === "false" || env === "off" || env === "no") return false;
4527
+ return readState2().auto === true;
4528
+ }
4529
+ function autoUpdateForcedByEnv() {
4530
+ const env = (process.env.SOLONGATE_AUTO_UPDATE ?? "").trim().toLowerCase();
4531
+ return ["1", "true", "on", "yes", "0", "false", "off", "no"].includes(env);
4532
+ }
4533
+ function setAutoUpdate(on) {
4534
+ const s = readState2();
4535
+ writeState2(on ? { ...s, auto: true, needsAdmin: void 0 } : { ...s, auto: false });
4536
+ }
4522
4537
  function currentVersion() {
4523
4538
  try {
4524
4539
  const pkg = JSON.parse(readFileSync7(join9(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf-8"));
@@ -4564,20 +4579,70 @@ function runGlobalInstall(version) {
4564
4579
  ["install", "-g", `${PKG}@${version}`],
4565
4580
  { timeout: 3e5, windowsHide: true, shell: process.platform === "win32" },
4566
4581
  (err2, stdout, stderr) => {
4582
+ const output = `${stdout}
4583
+ ${stderr}`;
4567
4584
  try {
4568
4585
  writeFileSync7(LOG_FILE2, `${(/* @__PURE__ */ new Date()).toISOString()} install ${version}: ${err2 ? "FAILED" : "ok"}
4569
- ${stdout}
4570
- ${stderr}
4586
+ ${output}
4571
4587
  `, { flag: "a" });
4572
4588
  } catch {
4573
4589
  }
4574
- resolve3(!err2);
4590
+ resolve3({ ok: !err2, needsAdmin: !!err2 && NEEDS_ADMIN_RE.test(output) });
4575
4591
  }
4576
4592
  );
4577
4593
  } catch {
4578
- resolve3(false);
4594
+ resolve3({ ok: false, needsAdmin: false });
4595
+ }
4596
+ });
4597
+ }
4598
+ function adminInstallCommand() {
4599
+ return process.platform === "win32" ? `npm i -g ${PKG}@latest (in an Administrator terminal)` : `sudo npm i -g ${PKG}@latest`;
4600
+ }
4601
+ function ownNodeModules() {
4602
+ let dir = dirname3(fileURLToPath3(import.meta.url));
4603
+ for (let i = 0; i < 12; i++) {
4604
+ const parent = dirname3(dir);
4605
+ if (parent === dir) break;
4606
+ if (dir.endsWith(`${sep}node_modules`)) return dir;
4607
+ dir = parent;
4608
+ }
4609
+ return null;
4610
+ }
4611
+ var prefixCheck = null;
4612
+ function globalInstallCheck() {
4613
+ prefixCheck ??= new Promise((resolve3) => {
4614
+ try {
4615
+ const dir = ownNodeModules();
4616
+ if (!dir) return resolve3({ writable: null, dir: null });
4617
+ access(dir, FS.W_OK, (e) => resolve3({ writable: !e, dir }));
4618
+ } catch {
4619
+ resolve3({ writable: null, dir: null });
4579
4620
  }
4580
4621
  });
4622
+ return prefixCheck;
4623
+ }
4624
+ function adminUpdateSteps() {
4625
+ return [` ${adminInstallCommand()}`, ` solongate repair${process.platform === "win32" ? "" : " (this one WITHOUT sudo)"}`];
4626
+ }
4627
+ async function updateNow() {
4628
+ const cur = currentVersion();
4629
+ const latest = await fetchLatest();
4630
+ if (!latest) return { status: "unreachable", version: cur };
4631
+ if (!newerThan(latest, cur)) return { status: "current", version: cur };
4632
+ if ((await globalInstallCheck()).writable === false) {
4633
+ writeState2({ ...readState2(), needsAdmin: latest });
4634
+ return { status: "needs-admin", version: latest };
4635
+ }
4636
+ const r = await runGlobalInstall(latest);
4637
+ if (r.ok) {
4638
+ writeState2({ ...readState2(), installed: latest, needsAdmin: void 0 });
4639
+ return { status: "updated", version: latest };
4640
+ }
4641
+ if (r.needsAdmin) {
4642
+ writeState2({ ...readState2(), needsAdmin: latest });
4643
+ return { status: "needs-admin", version: latest };
4644
+ }
4645
+ return { status: "failed", version: latest };
4581
4646
  }
4582
4647
  async function tuiUpdateFlow(onStatus) {
4583
4648
  try {
@@ -4590,10 +4655,22 @@ async function tuiUpdateFlow(onStatus) {
4590
4655
  onStatus({ kind: "updated", version: latest });
4591
4656
  return;
4592
4657
  }
4658
+ if (!autoUpdateEnabled()) {
4659
+ onStatus({ kind: "available", version: latest });
4660
+ return;
4661
+ }
4662
+ if (readState2().needsAdmin === latest) {
4663
+ onStatus({ kind: "needs-admin", version: latest });
4664
+ return;
4665
+ }
4593
4666
  onStatus({ kind: "updating", version: latest });
4594
- if (await runGlobalInstall(latest)) {
4595
- writeState2({ ...readState2(), installed: latest });
4667
+ const r = await runGlobalInstall(latest);
4668
+ if (r.ok) {
4669
+ writeState2({ ...readState2(), installed: latest, needsAdmin: void 0 });
4596
4670
  onStatus({ kind: "updated", version: latest });
4671
+ } else if (r.needsAdmin) {
4672
+ writeState2({ ...readState2(), needsAdmin: latest });
4673
+ onStatus({ kind: "needs-admin", version: latest });
4597
4674
  }
4598
4675
  } catch {
4599
4676
  }
@@ -4654,6 +4731,19 @@ function SettingsPanel({
4654
4731
  const [editor, setEditor] = useState8(null);
4655
4732
  const [latest, setLatest] = useState8(null);
4656
4733
  const [diagBusy, setDiagBusy] = useState8(null);
4734
+ const [autoUp, setAutoUp] = useState8(() => autoUpdateEnabled());
4735
+ const [updBusy, setUpdBusy] = useState8(false);
4736
+ const [canInstall, setCanInstall] = useState8(null);
4737
+ useEffect7(() => {
4738
+ let live2 = true;
4739
+ globalInstallCheck().then((r) => {
4740
+ if (live2) setCanInstall(r.writable);
4741
+ }).catch(() => {
4742
+ });
4743
+ return () => {
4744
+ live2 = false;
4745
+ };
4746
+ }, []);
4657
4747
  const [diag, setDiag] = useState8(null);
4658
4748
  const [diagStep, setDiagStep] = useState8(0);
4659
4749
  const DIAG_STEPS = {
@@ -4711,10 +4801,10 @@ function SettingsPanel({
4711
4801
  }, []);
4712
4802
  useEffect7(() => {
4713
4803
  const loggingIn = login && login.phase !== "done" && login.phase !== "error";
4714
- if (!loggingIn && !diagBusy) return;
4804
+ if (!loggingIn && !diagBusy && !updBusy) return;
4715
4805
  const t = setInterval(() => setTick((n) => n + 1), 120);
4716
4806
  return () => clearInterval(t);
4717
- }, [login, diagBusy]);
4807
+ }, [login, diagBusy, updBusy]);
4718
4808
  const localQ = useLoader(() => listAccounts().length ? api.settings.getLocalLogs() : Promise.resolve(null));
4719
4809
  const whQ = useLoader(() => listAccounts().length ? api.settings.getWebhooks() : Promise.resolve(null));
4720
4810
  const alertQ = useLoader(() => listAccounts().length ? api.settings.getAlerts() : Promise.resolve(null));
@@ -4748,6 +4838,8 @@ function SettingsPanel({
4748
4838
  { kind: "self" },
4749
4839
  { kind: "doctor" },
4750
4840
  { kind: "repair" },
4841
+ { kind: "cli-update" },
4842
+ { kind: "auto-update" },
4751
4843
  { kind: "ll-enabled" },
4752
4844
  { kind: "ll-path" },
4753
4845
  { kind: "ll-server" },
@@ -4927,6 +5019,46 @@ function SettingsPanel({
4927
5019
  } else if (r.kind === "self") {
4928
5020
  if (!selfProt) return;
4929
5021
  run(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
5022
+ } else if (r.kind === "cli-update") {
5023
+ if (updBusy) return;
5024
+ setUpdBusy(true);
5025
+ setMsg({ text: "updating\u2026", level: "ok" });
5026
+ setDiag(null);
5027
+ void (async () => {
5028
+ try {
5029
+ const res = await updateNow();
5030
+ if (res.status === "updated") setMsg({ text: `\u2713 v${res.version} installed \xB7 restart (q, then solongate) to apply`, level: "ok" });
5031
+ else if (res.status === "current") setMsg({ text: `\u2713 already on the latest version (v${res.version})`, level: "ok" });
5032
+ else if (res.status === "needs-admin") {
5033
+ setMsg({ text: `\u2717 v${res.version} needs admin rights \u2014 run the two commands below`, level: "bad" });
5034
+ setDiag({
5035
+ of: "cli-update",
5036
+ lines: [
5037
+ { text: "npm\u2019s global folder belongs to root on this machine (the usual macOS", level: "warn" },
5038
+ { text: "setup), so SolonGate cannot install there by itself. In a terminal:", level: "warn" },
5039
+ ...adminUpdateSteps().map((s) => ({ text: s.trim(), level: "ok" })),
5040
+ { text: "run repair WITHOUT sudo, or the guard hooks land in root\u2019s home", level: "dim" }
5041
+ ]
5042
+ });
5043
+ } else if (res.status === "unreachable") setMsg({ text: "\u2717 could not reach the npm registry", level: "bad" });
5044
+ else setMsg({ text: `\u2717 update to v${res.version} failed \u2014 see ~/.solongate/self-update.log`, level: "bad" });
5045
+ setLatest(await latestVersion());
5046
+ setCanInstall((await globalInstallCheck()).writable);
5047
+ } finally {
5048
+ setUpdBusy(false);
5049
+ }
5050
+ })();
5051
+ } else if (r.kind === "auto-update") {
5052
+ if (autoUpdateForcedByEnv()) {
5053
+ setMsg({ text: "SOLONGATE_AUTO_UPDATE is set \u2014 unset it to change this here", level: "bad" });
5054
+ return;
5055
+ }
5056
+ const next = !autoUp;
5057
+ setAutoUpdate(next);
5058
+ setAutoUp(next);
5059
+ setMsg(
5060
+ next ? canInstall === false ? { text: "\u2713 auto-update on \u2014 but npm needs sudo here, so it still cannot install on its own", level: "bad" } : { text: "\u2713 auto-update on \u2014 new versions install in the background", level: "ok" } : { text: "\u2713 auto-update off \u2014 update from this row or with: solongate update", level: "ok" }
5061
+ );
4930
5062
  } else if (r.kind === "ll-enabled") {
4931
5063
  if (!local) return;
4932
5064
  if (!local.enabled && !local.path.trim()) {
@@ -5025,7 +5157,7 @@ function SettingsPanel({
5025
5157
  if (ok) clearLocalLog();
5026
5158
  setMsg(ok ? { text: `\u2713 ${acctLabel(cur.acc)} is now the ACTIVE key (guard + logging)`, level: "ok" } : { text: "\u2717 could not set active", level: "bad" });
5027
5159
  refreshAccounts();
5028
- } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
5160
+ } else if (cur.kind === "wh" || cur.kind === "alert" || cur.kind === "self" || cur.kind === "auto-update" || cur.kind === "ll-enabled" || cur.kind === "ll-server") {
5029
5161
  if (cur.kind === "alert") run(cur.rule.enabled ? "alert disabled" : "alert enabled", () => api.settings.setAlertEnabled(cur.rule.id, !cur.rule.enabled), alertQ.reload);
5030
5162
  else activate(cur);
5031
5163
  }
@@ -5237,6 +5369,25 @@ function SettingsPanel({
5237
5369
  /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "repair".padEnd(11) }),
5238
5370
  diagBusy === "repair" ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: `${spin} restoring protection\u2026` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "restore every protection file after tampering or deletion \xB7 enter runs it" })
5239
5371
  ] });
5372
+ case "cli-update": {
5373
+ const behind = latest ? newerThan(latest, ver) : false;
5374
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5375
+ cursor(r),
5376
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "version".padEnd(11) }),
5377
+ /* @__PURE__ */ jsx8(Text8, { color: behind ? theme.warn : theme.ok, children: `v${ver}` }),
5378
+ updBusy ? /* @__PURE__ */ jsx8(Text8, { color: theme.accentBright, children: ` ${spin} updating\u2026` }) : behind && canInstall === false ? (
5379
+ // Said before they press enter, not after a minute of npm noise.
5380
+ /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` v${latest} on npm \xB7 needs sudo here \xB7 enter shows how` })
5381
+ ) : behind ? /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: ` v${latest} on npm \xB7 enter updates now` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: latest ? " latest \xB7 enter checks again" : " enter checks npm and updates" })
5382
+ ] });
5383
+ }
5384
+ case "auto-update":
5385
+ return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5386
+ cursor(r),
5387
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: "auto".padEnd(11) }),
5388
+ onOff(autoUp),
5389
+ /* @__PURE__ */ jsx8(Text8, { color: theme.dim, children: autoUpdateForcedByEnv() ? " set by SOLONGATE_AUTO_UPDATE" : canInstall === false ? " cannot work here: npm needs sudo, so updates stay manual \xB7 enter toggles" : autoUp ? " installs new versions in the background \xB7 enter toggles" : " off: nothing installs on its own (npm -g may need sudo) \xB7 enter toggles" })
5390
+ ] });
5240
5391
  case "ll-enabled":
5241
5392
  return /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", children: [
5242
5393
  cursor(r),
@@ -5289,10 +5440,11 @@ function SettingsPanel({
5289
5440
  ] });
5290
5441
  }
5291
5442
  };
5292
- const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
5443
+ const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" || r.kind === "allowance" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" || r.kind === "doctor" || r.kind === "repair" ? "PROTECTION" : r.kind === "cli-update" || r.kind === "auto-update" ? "UPDATES" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
5293
5444
  const SECTION_DESC = {
5294
5445
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
5295
5446
  PROTECTION: "guard hook: enter install/update \xB7 d remove \xB7 self-protection \xB7 doctor + repair",
5447
+ UPDATES: "the solongate CLI itself \xB7 background auto-update is off unless you turn it on",
5296
5448
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
5297
5449
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
5298
5450
  ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 m on/off`
@@ -5333,7 +5485,7 @@ function SettingsPanel({
5333
5485
  lineKey.push("");
5334
5486
  });
5335
5487
  }
5336
- if (diag && (r.kind === "doctor" && diag.of === "doctor" || r.kind === "repair" && diag.of === "repair")) {
5488
+ if (diag && (r.kind === "doctor" && diag.of === "doctor" || r.kind === "repair" && diag.of === "repair" || r.kind === "cli-update" && diag.of === "cli-update")) {
5337
5489
  diag.lines.forEach((l, i) => {
5338
5490
  lineEls.push(
5339
5491
  /* @__PURE__ */ jsx8(
@@ -5361,7 +5513,7 @@ function SettingsPanel({
5361
5513
  lineEls.push(
5362
5514
  /* @__PURE__ */ jsxs8(Text8, { wrap: "truncate", color: theme.dim, children: [
5363
5515
  `solongate v${ver}`,
5364
- latest ? newerThan(latest, ver) ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \xB7 v${latest} on npm \u2014 auto-updating` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.ok, children: " \xB7 latest" }) : null
5516
+ latest ? newerThan(latest, ver) ? /* @__PURE__ */ jsx8(Text8, { color: theme.warn, children: ` \xB7 v${latest} on npm \u2014 ${autoUp ? "auto-updating" : "UPDATES above"}` }) : /* @__PURE__ */ jsx8(Text8, { color: theme.ok, children: " \xB7 latest" }) : null
5365
5517
  ] }, "ver")
5366
5518
  );
5367
5519
  lineKey.push("");
@@ -5566,7 +5718,7 @@ function App() {
5566
5718
  // a row off whichever panel is open.
5567
5719
  /* @__PURE__ */ jsx9(Text9, { color: theme.bad, bold: true, children: " \xB7 allowance used up \xB7 every call denied \xB7 create an account to carry on" })
5568
5720
  ) : accounts.length > 1 ? /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: ` (${acctIdx + 1}/${accounts.length} \xB7 a switch \xB7 Settings to manage)` }) : /* @__PURE__ */ jsx9(Text9, { color: theme.dim, children: " \xB7 Settings to add another" }),
5569
- update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \xB7 restart (q, then solongate) to apply` }) : null
5721
+ update2.kind === "updating" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 updating to v${update2.version}\u2026` }) : update2.kind === "updated" ? /* @__PURE__ */ jsx9(Text9, { color: theme.ok, bold: true, children: ` \u2191 v${update2.version} installed \xB7 restart (q, then solongate) to apply` }) : update2.kind === "available" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} out \xB7 Settings \u2192 UPDATES` }) : update2.kind === "needs-admin" ? /* @__PURE__ */ jsx9(Text9, { color: theme.warn, children: ` \u2191 v${update2.version} needs admin rights \xB7 Settings \u2192 UPDATES` }) : null
5570
5722
  ] }),
5571
5723
  /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexGrow: 1, children: [
5572
5724
  /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", flexShrink: 0, width: 16, borderStyle: "round", borderColor: focus === "nav" ? theme.accent : "gray", paddingX: 1, children: SECTIONS.map((s, i) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.2",
3
+ "version": "0.83.4",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {