@bivy/bivy 0.21.2-staging.7 → 0.21.2-staging.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/archive-install.mjs +150 -0
- package/bin/bivy.mjs +63 -6
- package/dist/node-update.js +26 -1
- package/dist/server.js +9 -11
- package/dist/worktree.js +17 -0
- package/package.json +1 -1
- package/web/assets/{AutomationsView-D5oiv_zB.js → AutomationsView-BKdWg31u.js} +1 -1
- package/web/assets/{ChangesCard-DabSGw7V.js → ChangesCard-BXTFioad.js} +1 -1
- package/web/assets/{LibraryView-QQ-39ZBr.js → LibraryView-DkytoYzU.js} +1 -1
- package/web/assets/{ProviderConnect-DmHCrGcX.js → ProviderConnect-Dar07HSg.js} +1 -1
- package/web/assets/{SessionList-D6uD90Xk.js → SessionList-DSwBLrLK.js} +1 -1
- package/web/assets/{Settings-g0nSrYtx.js → Settings-Cttrq6Hu.js} +3 -3
- package/web/assets/{Sheet-Bzqux6G5.js → Sheet-BWjIuEvT.js} +1 -1
- package/web/assets/{Terminal-Bn1DPRXq.js → Terminal-CLTlfYfL.js} +1 -1
- package/web/assets/{VoiceSettings-I1NQ3Uio.js → VoiceSettings-5wRzbaip.js} +1 -1
- package/web/assets/{abnfDiagram-VCTEODGH-C_Uz8pXL.js → abnfDiagram-VCTEODGH-CqEKXBpr.js} +1 -1
- package/web/assets/architecture-7GRP2DOG-csjxIzux.js +1 -0
- package/web/assets/{architectureDiagram-5GKGNRK7-kMpeUYHR.js → architectureDiagram-5GKGNRK7-65ApDutk.js} +1 -1
- package/web/assets/{blockDiagram-I7D4REHJ-DGQDJBSG.js → blockDiagram-I7D4REHJ-C_hHirkL.js} +1 -1
- package/web/assets/{c4Diagram-7LVT6UL2-mvfHHPoa.js → c4Diagram-7LVT6UL2-B5W_rZdc.js} +1 -1
- package/web/assets/channel-BiEVxt04.js +1 -0
- package/web/assets/{chunk-4HAMMTFA-CQW2e_vu.js → chunk-4HAMMTFA-DQ9e55tV.js} +1 -1
- package/web/assets/{chunk-75Z2AOVW-DR_4-9bt.js → chunk-75Z2AOVW-CnVrxdF0.js} +1 -1
- package/web/assets/{chunk-DU6HZSFF-Dk1Ls8Yb.js → chunk-DU6HZSFF-CBXacbPq.js} +1 -1
- package/web/assets/{chunk-F27PBJKO-DAqIwp1s.js → chunk-F27PBJKO-BuQoorsp.js} +1 -1
- package/web/assets/{chunk-GMAD6QVW-BwMyBYvu.js → chunk-GMAD6QVW-BdokXHOz.js} +1 -1
- package/web/assets/{chunk-GVQU2GXP-d37M-C1O.js → chunk-GVQU2GXP-DF2U9QdV.js} +1 -1
- package/web/assets/{chunk-IMKFNOWR-xYrxp1vI.js → chunk-IMKFNOWR-3k4GQuZc.js} +1 -1
- package/web/assets/{chunk-L3NEJ4N5-_GlbXP9Y.js → chunk-L3NEJ4N5-Bo9LSvX2.js} +1 -1
- package/web/assets/{chunk-OSK3NFVY-C3lev0Mm.js → chunk-OSK3NFVY-CiHrpprI.js} +1 -1
- package/web/assets/{chunk-P2QGCYS3-BP5-Xvp1.js → chunk-P2QGCYS3-DRblecLw.js} +1 -1
- package/web/assets/{chunk-POPQ4Y6H-BpBIRKGq.js → chunk-POPQ4Y6H-DeS-pBCd.js} +1 -1
- package/web/assets/{chunk-PWAF6VOD-BI_Y9bQ6.js → chunk-PWAF6VOD-qX7Vudvg.js} +1 -1
- package/web/assets/{chunk-SHT3W25Y-C8w3nrKt.js → chunk-SHT3W25Y-Dwaucw-l.js} +1 -1
- package/web/assets/{chunk-SVP7TREG-BLQmtYGH.js → chunk-SVP7TREG-B_T5y6kq.js} +1 -1
- package/web/assets/{chunk-TICWLB2K-BrAxw1ww.js → chunk-TICWLB2K-CT9mkxqr.js} +1 -1
- package/web/assets/classDiagram-ZZMXUADV-Bd98Z0Bi.js +1 -0
- package/web/assets/classDiagram-v2-VYDZK3BY-Bd98Z0Bi.js +1 -0
- package/web/assets/{cynefin-OW5HDTMX-OEAX4DSt.js → cynefin-OW5HDTMX-CLIX1FQD.js} +1 -1
- package/web/assets/{cynefinDiagram-5FMLGOSQ-Bz5slpEg.js → cynefinDiagram-5FMLGOSQ-CMsv6adS.js} +1 -1
- package/web/assets/{dagre-GXQ25YYZ-Bj_5wzJD.js → dagre-GXQ25YYZ-DAGYJdFd.js} +1 -1
- package/web/assets/{diagram-S7CK7UJ4-CmgqaEEz.js → diagram-S7CK7UJ4-CsisyJcS.js} +1 -1
- package/web/assets/{diagram-UQ7AKVKN-pv-tCefK.js → diagram-UQ7AKVKN-BwRe_HTu.js} +1 -1
- package/web/assets/{diagram-VSXAHHWV-DI11nLPw.js → diagram-VSXAHHWV-CcUMlRc8.js} +1 -1
- package/web/assets/{diagram-VX7I27RA-C3L472Xr.js → diagram-VX7I27RA-DwfrFboX.js} +1 -1
- package/web/assets/{diagram-Z3DM3KII-CbtI75Z-.js → diagram-Z3DM3KII-BcQ73Mpu.js} +1 -1
- package/web/assets/{ebnfDiagram-PWID7BFC-DqW7RC6o.js → ebnfDiagram-PWID7BFC-CC-NQnEb.js} +1 -1
- package/web/assets/{erDiagram-RLTQ6QDP-BFDGi1kN.js → erDiagram-RLTQ6QDP-amg2k4kY.js} +1 -1
- package/web/assets/eventmodeling-NTZA5JFV-DjC9XhMC.js +1 -0
- package/web/assets/flowDiagram-HODETNUW-4yojXHxH.js +1 -0
- package/web/assets/{ganttDiagram-EL5Y4UJY-BhKTbOyf.js → ganttDiagram-EL5Y4UJY-Btlsv3mU.js} +1 -1
- package/web/assets/{gitGraph-4MIJSDKK-DQHQnEVv.js → gitGraph-4MIJSDKK-hsg4HYS9.js} +1 -1
- package/web/assets/{gitGraphDiagram-WWUBYQGX-3FCKGj6c.js → gitGraphDiagram-WWUBYQGX-U7dHHcXK.js} +1 -1
- package/web/assets/{index-MxqbxzPN.js → index-BNyAZ52N.js} +2 -2
- package/web/assets/{info-A6RAGUB7-WiYQe8d7.js → info-A6RAGUB7-BtNsvOob.js} +1 -1
- package/web/assets/{infoDiagram-27XIBGKW-CjadWRzD.js → infoDiagram-27XIBGKW-Bp4J2HyM.js} +1 -1
- package/web/assets/{ishikawaDiagram-5VMMS53U-cYbsHScv.js → ishikawaDiagram-5VMMS53U-ctIX0r6X.js} +1 -1
- package/web/assets/{journeyDiagram-3NMN7TZE-DDVQyL3s.js → journeyDiagram-3NMN7TZE-FXtnTpcI.js} +1 -1
- package/web/assets/{kanban-definition-UXKFOSKX-AXYMSOn6.js → kanban-definition-UXKFOSKX-BaJFiS7T.js} +1 -1
- package/web/assets/{mermaid-parser.core-rKwSC-zy.js → mermaid-parser.core-bHQmSZkw.js} +3 -3
- package/web/assets/{mermaid.core-Cc0ako0z.js → mermaid.core-jwZj_MwN.js} +4 -4
- package/web/assets/{mindmap-definition-YA3MSWOX-CVvTSlad.js → mindmap-definition-YA3MSWOX-CVANQJvM.js} +1 -1
- package/web/assets/{mount-CqQr8iom.js → mount-p_C2u8PG.js} +5 -5
- package/web/assets/{packet-AYTQ26CC-DWkdOO14.js → packet-AYTQ26CC-DPrM2ZD7.js} +1 -1
- package/web/assets/{pegDiagram-XKGWAZYB-DL0tnp5I.js → pegDiagram-XKGWAZYB-VmazBajj.js} +1 -1
- package/web/assets/{pie-WAS4IAKB-uwzU4l6C.js → pie-WAS4IAKB-260IRQiz.js} +1 -1
- package/web/assets/{pieDiagram-E7YTZNPT-DR8_3t8D.js → pieDiagram-E7YTZNPT-DbPpzs2H.js} +1 -1
- package/web/assets/{quadrantDiagram-AXDQQJYC-sCJ9WcLl.js → quadrantDiagram-AXDQQJYC-CNrmxA_-.js} +1 -1
- package/web/assets/{radar-RG4KPBEZ-BxWFXTaN.js → radar-RG4KPBEZ-D3Ldnx09.js} +1 -1
- package/web/assets/{railroad-74A4TZTK-CfB64wAV.js → railroad-74A4TZTK-CG-7nL_E.js} +1 -1
- package/web/assets/railroad-abnf-HS5TGJTU-lAb9-7Xp.js +1 -0
- package/web/assets/railroad-ebnf-LZEXJU2U-Cm_HkRcz.js +1 -0
- package/web/assets/railroad-peg-WCYAUIDC-BcOdVWM6.js +1 -0
- package/web/assets/{railroadDiagram-O6MQD6OU-CR4x_PZw.js → railroadDiagram-O6MQD6OU-_kJGLUPE.js} +1 -1
- package/web/assets/{requirementDiagram-BXWQKSXE-CMTLDoqb.js → requirementDiagram-BXWQKSXE-nfScnZcQ.js} +1 -1
- package/web/assets/{sankeyDiagram-P5KCCOFB-Dd8Ovsue.js → sankeyDiagram-P5KCCOFB-ASes9Uc0.js} +1 -1
- package/web/assets/{sequenceDiagram-WJ2MYXX4-DYks3202.js → sequenceDiagram-WJ2MYXX4-D3Wq2IBM.js} +1 -1
- package/web/assets/{settingsRoute-CQOWiRDI.js → settingsRoute-DX7j6eFs.js} +1 -1
- package/web/assets/{stateDiagram-D77RDMKH-Bhymc8no.js → stateDiagram-D77RDMKH-BNgsWpiq.js} +1 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-BvMxq8rN.js +1 -0
- package/web/assets/{swimlanes-42K2YHIH-g9jH0r9P.js → swimlanes-42K2YHIH-CtNrMpgE.js} +1 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-B-fVqAnK.js +8 -0
- package/web/assets/{timeline-definition-24CTP7MA-D6iYETPo.js → timeline-definition-24CTP7MA-BAm0LrKX.js} +1 -1
- package/web/assets/{treeView-Q6P3EWNA-BIsVoFWi.js → treeView-Q6P3EWNA-CsOctNbs.js} +1 -1
- package/web/assets/{treemap-WGGIJYW6-DUF3BcJu.js → treemap-WGGIJYW6-CJJBpHZv.js} +1 -1
- package/web/assets/{useStore-B2R-Kqzq.js → useStore-DMCaltTO.js} +1 -1
- package/web/assets/{vennDiagram-4TSXK5OY-DD4Ghlzw.js → vennDiagram-4TSXK5OY-CcWN3ED_.js} +1 -1
- package/web/assets/{wardley-WFR3VGLG-BhZLlop0.js → wardley-WFR3VGLG-CfyXmogb.js} +1 -1
- package/web/assets/{wardleyDiagram-VM6X3IG4-AizFOebB.js → wardleyDiagram-VM6X3IG4-JpKCXIvF.js} +1 -1
- package/web/assets/{xychartDiagram-S5SC5T6Z-BHFrTrgm.js → xychartDiagram-S5SC5T6Z-BMAMhnxx.js} +1 -1
- package/web/index.html +1 -1
- package/web/sw.js +1 -1
- package/web/assets/architecture-7GRP2DOG-Cyd4HgdV.js +0 -1
- package/web/assets/channel-BMzazBiu.js +0 -1
- package/web/assets/classDiagram-ZZMXUADV-BNQjv_CR.js +0 -1
- package/web/assets/classDiagram-v2-VYDZK3BY-BNQjv_CR.js +0 -1
- package/web/assets/eventmodeling-NTZA5JFV-CwwIkGzC.js +0 -1
- package/web/assets/flowDiagram-HODETNUW-CUMuDO8P.js +0 -1
- package/web/assets/railroad-abnf-HS5TGJTU-DQZ5aPmK.js +0 -1
- package/web/assets/railroad-ebnf-LZEXJU2U-DsqtaMU_.js +0 -1
- package/web/assets/railroad-peg-WCYAUIDC-Dg7WpC12.js +0 -1
- package/web/assets/stateDiagram-v2-MP3YSRHH-BjaVva0X.js +0 -1
- package/web/assets/swimlanesDiagram-VR7AAH4N-Btx9J-9Q.js +0 -8
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
/**
|
|
4
|
+
* Install and update from the self-contained GitHub release archives
|
|
5
|
+
* (scripts/release-archives.mjs). Layout under the install root
|
|
6
|
+
* (default ~/.local/share/bivy):
|
|
7
|
+
*
|
|
8
|
+
* versions/<version>-<target>/ one extracted archive each
|
|
9
|
+
* current -> versions/… the active one, swapped atomically
|
|
10
|
+
*
|
|
11
|
+
* The launcher (current/bin/bivy) passes its unresolved `current` path as
|
|
12
|
+
* BIVY_ARCHIVE_ROOT, so a service unit or shim written through it keeps
|
|
13
|
+
* working after `current` moves to a new version.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import crypto from "node:crypto";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
export const RELEASES_URL = "https://github.com/bivysh/bivy/releases";
|
|
21
|
+
export const ARCHIVE_INFO = "bivy-archive.json";
|
|
22
|
+
/** Versions kept on disk: the active one and the one before it, for rollback. */
|
|
23
|
+
const KEEP = 2;
|
|
24
|
+
|
|
25
|
+
/** "linux-x64" etc.: the archive target for this machine, or undefined. */
|
|
26
|
+
export function archiveTarget(platform = process.platform, arch = process.arch) {
|
|
27
|
+
return ["linux", "darwin"].includes(platform) && ["x64", "arm64"].includes(arch) ? `${platform}-${arch}` : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Where a channel's manifest and archives live. `latest` is the newest stable release. */
|
|
31
|
+
export function channelUrls(channel, base = process.env.BIVY_RELEASES_URL || RELEASES_URL) {
|
|
32
|
+
const dir = channel === "latest" ? `${base}/latest/download` : `${base}/download/${encodeURIComponent(channel)}`;
|
|
33
|
+
return {
|
|
34
|
+
manifest: `${dir}/manifest.json`,
|
|
35
|
+
// Stable archives are fetched from their own tag, so a release published
|
|
36
|
+
// mid-update can't swap the file under a manifest read a moment earlier.
|
|
37
|
+
archive: (manifest, file) => (channel === "latest" ? `${base}/download/v${manifest.version}/${file}` : `${dir}/${file}`),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The archive install this CLI runs from, or null. `archiveRoot` is the
|
|
43
|
+
* launcher's BIVY_ARCHIVE_ROOT; it only counts when it really is the install
|
|
44
|
+
* holding `appRoot`, so a value inherited into some other checkout is ignored.
|
|
45
|
+
*/
|
|
46
|
+
export function archiveInstall(archiveRoot, appRoot) {
|
|
47
|
+
if (!archiveRoot) return null;
|
|
48
|
+
try {
|
|
49
|
+
if (fs.realpathSync(path.join(archiveRoot, "app")) !== fs.realpathSync(appRoot)) return null;
|
|
50
|
+
const info = JSON.parse(fs.readFileSync(path.join(archiveRoot, ARCHIVE_INFO), "utf8"));
|
|
51
|
+
// Run as current/bin/bivy or as versions/<v>/bin/bivy, the install root is
|
|
52
|
+
// the same, and `current` is the path worth writing down.
|
|
53
|
+
const parent = path.dirname(archiveRoot);
|
|
54
|
+
const installRoot = path.basename(archiveRoot) === "current" ? parent : path.basename(parent) === "versions" ? path.dirname(parent) : undefined;
|
|
55
|
+
// An archive unpacked anywhere else runs, but has no install to update.
|
|
56
|
+
const current = path.join(installRoot ?? "", "current");
|
|
57
|
+
if (!installRoot || !fs.existsSync(current)) return null;
|
|
58
|
+
// Run through `current`, write `current`; a specific version (say, a new one
|
|
59
|
+
// being checked before the swap) stays itself.
|
|
60
|
+
const root = fs.realpathSync(archiveRoot) === fs.realpathSync(current) ? current : archiveRoot;
|
|
61
|
+
return { root, installRoot, version: info.version, target: info.target };
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fetchManifest(url, fetchImpl = fetch) {
|
|
68
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(15_000) });
|
|
69
|
+
if (!res.ok) throw new Error(`Could not read the release manifest (${res.status} from ${url})`);
|
|
70
|
+
const manifest = await res.json();
|
|
71
|
+
if (typeof manifest?.version !== "string" || !manifest.archives) throw new Error(`${url} is not a Bivy release manifest`);
|
|
72
|
+
return manifest;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Fetch a large file with curl. Node's fetch can abort the process on an
|
|
77
|
+
* assertion when a server closes the connection while the body is paused
|
|
78
|
+
* for backpressure; curl is on every machine install.sh runs on.
|
|
79
|
+
*/
|
|
80
|
+
export function curlDownload(url, dest) {
|
|
81
|
+
const res = spawnSync("curl", ["-fsSL", "--retry", "2", "-o", dest, url], { encoding: "utf8" });
|
|
82
|
+
if (res.status !== 0) throw new Error(`Could not download ${url}: ${(res.stderr || "").trim() || `curl exit ${res.status}`}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Download, verify and extract one archive into versions/. Returns its
|
|
87
|
+
* directory. Nothing outside versions/ changes, so a failure here leaves the
|
|
88
|
+
* running install untouched.
|
|
89
|
+
*/
|
|
90
|
+
export async function stageArchive({ installRoot, manifest, target, archiveUrl, download = curlDownload }) {
|
|
91
|
+
const entry = manifest.archives[target];
|
|
92
|
+
if (!entry) throw new Error(`Bivy ${manifest.version} has no archive for ${target}`);
|
|
93
|
+
const versions = path.join(installRoot, "versions");
|
|
94
|
+
fs.mkdirSync(versions, { recursive: true });
|
|
95
|
+
const work = fs.mkdtempSync(path.join(versions, ".stage-"));
|
|
96
|
+
try {
|
|
97
|
+
const tarball = path.join(work, entry.file);
|
|
98
|
+
await download(archiveUrl(manifest, entry.file), tarball);
|
|
99
|
+
const sha256 = crypto.createHash("sha256").update(fs.readFileSync(tarball)).digest("hex");
|
|
100
|
+
if (sha256 !== entry.sha256) throw new Error(`${entry.file} failed verification: sha256 ${sha256}, the manifest says ${entry.sha256}`);
|
|
101
|
+
|
|
102
|
+
const tar = spawnSync("tar", ["-xzf", tarball, "-C", work], { encoding: "utf8" });
|
|
103
|
+
if (tar.status !== 0) throw new Error(`Could not extract ${entry.file}: ${tar.stderr}`);
|
|
104
|
+
const extracted = path.join(work, entry.file.replace(/\.tar\.gz$/, ""));
|
|
105
|
+
const info = JSON.parse(fs.readFileSync(path.join(extracted, ARCHIVE_INFO), "utf8"));
|
|
106
|
+
if (info.version !== manifest.version || info.target !== target) throw new Error(`${entry.file} holds ${info.version} ${info.target}, not ${manifest.version} ${target}`);
|
|
107
|
+
|
|
108
|
+
const dest = path.join(versions, `${manifest.version}-${target}`);
|
|
109
|
+
// Re-staging a version (a reinstall) must not replace the directory `current`
|
|
110
|
+
// points at while it runs; move it aside first and drop it afterwards.
|
|
111
|
+
const aside = fs.existsSync(dest) ? `${dest}.old-${process.pid}` : undefined;
|
|
112
|
+
if (aside) fs.renameSync(dest, aside);
|
|
113
|
+
fs.renameSync(extracted, dest);
|
|
114
|
+
if (aside) fs.rmSync(aside, { recursive: true, force: true });
|
|
115
|
+
return dest;
|
|
116
|
+
} finally {
|
|
117
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The version directory `current` points at, or undefined. */
|
|
122
|
+
export function activeVersion(installRoot) {
|
|
123
|
+
try {
|
|
124
|
+
return path.resolve(installRoot, fs.readlinkSync(path.join(installRoot, "current")));
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Point `current` at a version directory atomically. Returns what it pointed at before. */
|
|
131
|
+
export function activate(installRoot, versionDir) {
|
|
132
|
+
const previous = activeVersion(installRoot);
|
|
133
|
+
const link = path.join(installRoot, "current");
|
|
134
|
+
const next = `${link}.next-${process.pid}`;
|
|
135
|
+
fs.rmSync(next, { force: true });
|
|
136
|
+
fs.symlinkSync(path.relative(installRoot, versionDir), next);
|
|
137
|
+
fs.renameSync(next, link);
|
|
138
|
+
return previous;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Remove version directories other than the newest KEEP, never the active one. */
|
|
142
|
+
export function prune(installRoot, keep = []) {
|
|
143
|
+
const versions = path.join(installRoot, "versions");
|
|
144
|
+
const active = activeVersion(installRoot);
|
|
145
|
+
const kept = new Set([...new Set([active, ...keep].filter(Boolean).map((p) => path.resolve(p)))].slice(0, KEEP));
|
|
146
|
+
for (const name of fs.readdirSync(versions)) {
|
|
147
|
+
const dir = path.join(versions, name);
|
|
148
|
+
if (!kept.has(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
package/bin/bivy.mjs
CHANGED
|
@@ -45,11 +45,16 @@ import { parseTailscaleStatus, serveConflict, serveFailureHint, serveOffArgs, se
|
|
|
45
45
|
import { resolveAttachSessionId } from "./attach-session-id.mjs";
|
|
46
46
|
import { detectInstallKind as classifyInstallKind, managedInstall, npmGlobalPrefix } from "./install-kind.mjs";
|
|
47
47
|
import { hasConfiguredService as configuredServiceExists } from "./service-state.mjs";
|
|
48
|
+
import { activate, activeVersion, archiveInstall, archiveTarget, channelUrls, fetchManifest, prune, stageArchive } from "./archive-install.mjs";
|
|
48
49
|
import { COMMANDS, EXIT, cliError, completionWords, describeCli, describeCommand, describeTools, renderHelp, resolveCommand, subcommandTable, suggestCommands, toolArgv, wantsJson } from "./cli-commands.mjs";
|
|
49
50
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
52
|
+
// A GitHub release archive install runs through its launcher, which names the
|
|
53
|
+
// install's `current` path. Paths written to disk (service unit, shims) use it,
|
|
54
|
+
// so they stay valid when an update points `current` at a new version.
|
|
55
|
+
const archive = archiveInstall(process.env.BIVY_ARCHIVE_ROOT, path.resolve(__dirname, ".."));
|
|
56
|
+
const repoRoot = archive ? path.join(archive.root, "app") : path.resolve(__dirname, "..");
|
|
57
|
+
const selfScript = path.join(repoRoot, "bin", "bivy.mjs");
|
|
53
58
|
// Mutable node state (config, relay E2E keys, sessions, logs) must live in a
|
|
54
59
|
// stable, user-owned directory that survives reinstalls. Pick it by install type:
|
|
55
60
|
// - BIVY_DATA_DIR set -> honor it (explicit override)
|
|
@@ -77,10 +82,11 @@ process.env.BIVY_DATA_DIR = appDir;
|
|
|
77
82
|
// - "git" dev checkout (has .git)
|
|
78
83
|
// - "npx" ephemeral `npx bivy` run (repoRoot under an npm _npx cache)
|
|
79
84
|
// - "npm-global" `npm i -g @bivy/bivy` (repoRoot is below node_modules/@bivy)
|
|
85
|
+
// - "archive" a GitHub release archive under ~/.local/share/bivy (install.sh)
|
|
80
86
|
// - "packaged" install.sh tarball tree (user-owned, self-preserving)
|
|
81
87
|
// - "managed" a system package (the AUR's `bivy`), updated by its manager
|
|
82
88
|
function detectInstallKind() {
|
|
83
|
-
return classifyInstallKind(repoRoot);
|
|
89
|
+
return archive ? "archive" : classifyInstallKind(repoRoot);
|
|
84
90
|
}
|
|
85
91
|
const cliConfigPath = path.join(appDir, "cli.json");
|
|
86
92
|
const canonicalConfigPath = path.join(appDir, "config.yaml");
|
|
@@ -185,7 +191,7 @@ const mcpProxyEntry = path.join(repoRoot, packaged ? "dist/harness/mcp-proxy-cli
|
|
|
185
191
|
const mcpServeEntry = path.join(repoRoot, packaged ? "dist/harness/mcp-serve-cli.js" : "src/harness/mcp-serve-cli.ts");
|
|
186
192
|
const qrEntry = path.join(repoRoot, "public", "qr.js");
|
|
187
193
|
const tsxCli = packaged ? "" : path.join(repoRoot, "node_modules", "tsx", "dist", "cli.mjs");
|
|
188
|
-
const nodeBin = process.execPath;
|
|
194
|
+
const nodeBin = archive ? path.join(archive.root, "runtime", "bin", "node") : process.execPath;
|
|
189
195
|
const nodeScriptArgs = (entry) => (tsxCli ? [tsxCli, entry] : [entry]);
|
|
190
196
|
|
|
191
197
|
// Resolve the baked-in hosted endpoints (app/relay/client URLs). Imported lazily
|
|
@@ -230,7 +236,7 @@ function commandPath(extraPath = "") {
|
|
|
230
236
|
process.env.PATH || "",
|
|
231
237
|
// npm global executables normally live beside the Node binary (including
|
|
232
238
|
// nvm/fnm installs). Services need this even without shell initialization.
|
|
233
|
-
path.dirname(
|
|
239
|
+
path.dirname(nodeBin),
|
|
234
240
|
].filter(Boolean);
|
|
235
241
|
return [...new Set(parts.flatMap((part) => String(part).split(path.delimiter)).filter(Boolean))].join(path.delimiter);
|
|
236
242
|
}
|
|
@@ -4065,6 +4071,7 @@ function plistContent(config) {
|
|
|
4065
4071
|
PORT: String(config.port),
|
|
4066
4072
|
BIVY_WORKSPACE: config.workspace,
|
|
4067
4073
|
BIVY_DATA_DIR: appDir,
|
|
4074
|
+
...(archive ? { BIVY_ARCHIVE_ROOT: archive.root } : {}),
|
|
4068
4075
|
...config.env,
|
|
4069
4076
|
PATH: commandPath(config.env?.PATH),
|
|
4070
4077
|
})
|
|
@@ -4109,6 +4116,7 @@ function systemdContent(config) {
|
|
|
4109
4116
|
PORT: String(config.port),
|
|
4110
4117
|
BIVY_WORKSPACE: config.workspace,
|
|
4111
4118
|
BIVY_DATA_DIR: appDir,
|
|
4119
|
+
...(archive ? { BIVY_ARCHIVE_ROOT: archive.root } : {}),
|
|
4112
4120
|
...config.env,
|
|
4113
4121
|
PATH: commandPath(config.env?.PATH),
|
|
4114
4122
|
})
|
|
@@ -5515,6 +5523,49 @@ async function cmdUpdate(args = []) {
|
|
|
5515
5523
|
await runUpdate(args);
|
|
5516
5524
|
}
|
|
5517
5525
|
|
|
5526
|
+
/**
|
|
5527
|
+
* Update a GitHub release archive install: stage the channel's archive beside
|
|
5528
|
+
* the running one, point `current` at it, restart, and point `current` back if
|
|
5529
|
+
* the node doesn't come up. The version before stays on disk for that.
|
|
5530
|
+
*/
|
|
5531
|
+
async function updateArchiveInstall(channel, { skipWait, force }) {
|
|
5532
|
+
const urls = channelUrls(channel);
|
|
5533
|
+
const target = archiveTarget();
|
|
5534
|
+
console.log(c.dim(`Checking ${urls.manifest} (channel: ${channel})…`));
|
|
5535
|
+
const manifest = await fetchManifest(urls.manifest);
|
|
5536
|
+
if (manifest.version === archive.version && !force) {
|
|
5537
|
+
console.log(c.green(`Bivy ${archive.version} is the newest on ${channel}.`));
|
|
5538
|
+
return;
|
|
5539
|
+
}
|
|
5540
|
+
console.log(c.dim(`Downloading Bivy ${manifest.version} for ${target}…`));
|
|
5541
|
+
const staged = await stageArchive({ installRoot: archive.installRoot, manifest, target, archiveUrl: urls.archive });
|
|
5542
|
+
const probe = runQuiet(path.join(staged, "bin", "bivy"), ["--version"]);
|
|
5543
|
+
if (probe.code !== 0 || probe.stdout.trim() !== manifest.version) {
|
|
5544
|
+
throw new Error(`The downloaded Bivy ${manifest.version} does not run here (${(probe.stderr || probe.stdout).trim() || `exit ${probe.code}`}); kept ${archive.version}.`);
|
|
5545
|
+
}
|
|
5546
|
+
const config = loadConfig();
|
|
5547
|
+
await waitForIdleSessions(config, { skip: skipWait });
|
|
5548
|
+
const previous = activate(archive.installRoot, staged);
|
|
5549
|
+
if (!hasConfiguredService(config)) {
|
|
5550
|
+
prune(archive.installRoot, [staged, previous]);
|
|
5551
|
+
console.log(c.green(`Updated to ${manifest.version}. Run 'bivy start' to use it.`));
|
|
5552
|
+
return;
|
|
5553
|
+
}
|
|
5554
|
+
if ((await restartServiceReconciled(config)) && (await verifyNodeCameUp(config))) {
|
|
5555
|
+
prune(archive.installRoot, [staged, previous]);
|
|
5556
|
+
console.log(c.green(`Updated ${archive.version} → ${manifest.version} and restarted the background service.`));
|
|
5557
|
+
return;
|
|
5558
|
+
}
|
|
5559
|
+
if (previous && previous !== activeVersion(archive.installRoot)) {
|
|
5560
|
+
activate(archive.installRoot, previous);
|
|
5561
|
+
await restartServiceReconciled(config);
|
|
5562
|
+
console.log(c.red(`⚠ Bivy ${manifest.version} did not come up, so ${archive.version} is back in place.`));
|
|
5563
|
+
} else {
|
|
5564
|
+
reportNodeDidNotStart();
|
|
5565
|
+
}
|
|
5566
|
+
process.exitCode = 1;
|
|
5567
|
+
}
|
|
5568
|
+
|
|
5518
5569
|
async function runUpdate(args = []) {
|
|
5519
5570
|
const skipWait = args.includes("--force") || args.includes("--no-wait");
|
|
5520
5571
|
const kind = detectInstallKind();
|
|
@@ -5564,6 +5615,12 @@ async function runUpdate(args = []) {
|
|
|
5564
5615
|
return;
|
|
5565
5616
|
}
|
|
5566
5617
|
|
|
5618
|
+
if (kind === "archive") {
|
|
5619
|
+
await updateArchiveInstall(channel, { skipWait, force: args.includes("--force") });
|
|
5620
|
+
console.log(c.dim(`=== bivy update finished ${new Date().toISOString()} ===`));
|
|
5621
|
+
return;
|
|
5622
|
+
}
|
|
5623
|
+
|
|
5567
5624
|
if (kind === "packaged") {
|
|
5568
5625
|
console.log(c.dim(`Updating packaged Bivy install (channel: ${channel})…`));
|
|
5569
5626
|
// The actual restart happens inside install.sh, which shells out to
|
package/dist/node-update.js
CHANGED
|
@@ -18,7 +18,28 @@ export function managedInstall(packageRoot) {
|
|
|
18
18
|
}
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* True when this daemon runs from a GitHub release archive install: the
|
|
23
|
+
* launcher's BIVY_ARCHIVE_ROOT names the install holding `packageRoot` (the
|
|
24
|
+
* same check bin/archive-install.mjs makes for the CLI).
|
|
25
|
+
*/
|
|
26
|
+
export function isArchiveInstall(packageRoot, archiveRoot = process.env.BIVY_ARCHIVE_ROOT) {
|
|
27
|
+
if (!archiveRoot)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
return fs.realpathSync(path.join(archiveRoot, "app")) === fs.realpathSync(packageRoot)
|
|
31
|
+
&& fs.existsSync(path.join(archiveRoot, "bivy-archive.json"));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Where to look for a newer version on the install's recorded channel: the npm
|
|
39
|
+
* registry, or for an archive install the channel's GitHub release manifest
|
|
40
|
+
* (URLs as in bin/archive-install.mjs). Both answer with `{ version }`.
|
|
41
|
+
*/
|
|
42
|
+
export function updateRegistryUrl(appDir, override, archive = false) {
|
|
22
43
|
if (override)
|
|
23
44
|
return override;
|
|
24
45
|
let channel = "latest";
|
|
@@ -28,6 +49,10 @@ export function updateRegistryUrl(appDir, override) {
|
|
|
28
49
|
channel = recorded;
|
|
29
50
|
}
|
|
30
51
|
catch { /* Existing installs track stable. */ }
|
|
52
|
+
if (archive) {
|
|
53
|
+
const releases = process.env.BIVY_RELEASES_URL || "https://github.com/bivysh/bivy/releases";
|
|
54
|
+
return channel === "latest" ? `${releases}/latest/download/manifest.json` : `${releases}/download/${encodeURIComponent(channel)}/manifest.json`;
|
|
55
|
+
}
|
|
31
56
|
return `https://registry.npmjs.org/%40bivy%2Fbivy/${encodeURIComponent(channel)}`;
|
|
32
57
|
}
|
|
33
58
|
/** Running version is fixed for this process, never re-read after an install. */
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// First: drop agent-session markers the node inherited before anything reads them.
|
|
4
4
|
import "./node-env.js";
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
-
import { createNodeUpdateChecker, managedInstall, updateRegistryUrl } from "./node-update.js";
|
|
6
|
+
import { createNodeUpdateChecker, isArchiveInstall, managedInstall, updateRegistryUrl } from "./node-update.js";
|
|
7
7
|
import { createRemoteSessionAdmission, RemoteSessionAdmissionError } from "./session/remote-session-admission.js";
|
|
8
8
|
import { requiresExistingSession, resolveTargetSession } from "./session/target-session.js";
|
|
9
9
|
import path from "node:path";
|
|
@@ -114,7 +114,7 @@ import { SessionAllowRules } from "./policy/session-allow.js";
|
|
|
114
114
|
import { TerminalManager } from "./terminal.js";
|
|
115
115
|
import { launchCommand } from "./command-launch.js";
|
|
116
116
|
import { listMultiplexerSessions } from "./multiplexer.js";
|
|
117
|
-
import { createWorktree, removeWorktree, gitRepoRoot } from "./worktree.js";
|
|
117
|
+
import { createWorktree, removeWorktree, gitRepoRoot, isManagedWorktreePath, reapedWorktreeBranch } from "./worktree.js";
|
|
118
118
|
import { HarnessManager } from "./harness/manager.js";
|
|
119
119
|
import { startEgressProxyIfEnabled, applySessionSandboxEgress, stopSessionEgress } from "./harness/egress.js";
|
|
120
120
|
import { initSharedDepCache, sharedDepCacheRoot } from "./harness/dep-cache.js";
|
|
@@ -824,9 +824,10 @@ const sessionRunPolicy = {
|
|
|
824
824
|
if (process.env.BIVY_SESSION_MODEL_FALLBACK) {
|
|
825
825
|
console.log(`[policy] in-session model reroute enabled: ${process.env.BIVY_SESSION_MODEL_FALLBACK}`);
|
|
826
826
|
}
|
|
827
|
+
const archiveInstalled = isArchiveInstall(repoRoot);
|
|
827
828
|
const nodeUpdates = createNodeUpdateChecker({
|
|
828
829
|
current: currentVersion() ?? "",
|
|
829
|
-
registryUrl: () => updateRegistryUrl(appDir, process.env.BIVY_UPDATE_REGISTRY_URL),
|
|
830
|
+
registryUrl: () => updateRegistryUrl(appDir, process.env.BIVY_UPDATE_REGISTRY_URL, archiveInstalled),
|
|
830
831
|
publish: (state) => broadcast(state),
|
|
831
832
|
});
|
|
832
833
|
function runtimeSummary(rt) {
|
|
@@ -7012,10 +7013,6 @@ function closeIdleSessions() {
|
|
|
7012
7013
|
closeSessionRecord(record, "idle-timeout");
|
|
7013
7014
|
}
|
|
7014
7015
|
}
|
|
7015
|
-
function isManagedWorktreePath(wtPath) {
|
|
7016
|
-
const parts = path.resolve(wtPath).split(path.sep);
|
|
7017
|
-
return parts.length >= 3 && parts[parts.length - 3] === ".bivy" && parts[parts.length - 2] === "worktrees";
|
|
7018
|
-
}
|
|
7019
7016
|
function sessionTimestampMs(value) {
|
|
7020
7017
|
const ms = value ? new Date(value).getTime() : NaN;
|
|
7021
7018
|
return Number.isFinite(ms) ? ms : 0;
|
|
@@ -8629,20 +8626,21 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
|
|
|
8629
8626
|
// or branch is gone, we leave it to the invariant to fail safe rather than
|
|
8630
8627
|
// corrupt a neighbour. The clone root is reconstructed from `source`
|
|
8631
8628
|
// (`repo:owner/repo`) because stored `workspace` is the old worktree path.
|
|
8632
|
-
|
|
8633
|
-
|
|
8629
|
+
const reapedBranch = requestedSessionFile && !worktree ? reapedWorktreeBranch(storedMeta) : undefined;
|
|
8630
|
+
if (reapedBranch) {
|
|
8631
|
+
const parsedSource = parseRepoSource(storedMeta?.source);
|
|
8634
8632
|
if (parsedSource) {
|
|
8635
8633
|
const repoDir = path.join(reposRoot, `${parsedSource.owner}__${parsedSource.repo}`);
|
|
8636
8634
|
try {
|
|
8637
8635
|
// Clear any stale registration left by a dir that was rm'd out from under
|
|
8638
8636
|
// git, so re-adding the branch's worktree doesn't hit "already checked out".
|
|
8639
8637
|
runGit(["worktree", "prune"], repoDir);
|
|
8640
|
-
const reprovisioned = await createWorktree({ repoDir, id:
|
|
8638
|
+
const reprovisioned = await createWorktree({ repoDir, id: reapedBranch, branch: reapedBranch });
|
|
8641
8639
|
worktree = reprovisioned;
|
|
8642
8640
|
runtimeWorkspace = reprovisioned.path;
|
|
8643
8641
|
}
|
|
8644
8642
|
catch (error) {
|
|
8645
|
-
console.warn(`Could not re-provision worktree for resumed session on ${
|
|
8643
|
+
console.warn(`Could not re-provision worktree for resumed session on ${reapedBranch}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8646
8644
|
}
|
|
8647
8645
|
}
|
|
8648
8646
|
}
|
package/dist/worktree.js
CHANGED
|
@@ -19,6 +19,23 @@ export function branchSlug(input) {
|
|
|
19
19
|
.replace(/^[-.]+|[-.]+$/g, "")
|
|
20
20
|
.slice(0, 60) || "task");
|
|
21
21
|
}
|
|
22
|
+
/** A Bivy-managed worktree lives at `<repo>/.bivy/worktrees/<name>`. */
|
|
23
|
+
export function isManagedWorktreePath(wtPath) {
|
|
24
|
+
const parts = path.resolve(wtPath).split(path.sep);
|
|
25
|
+
return parts.length >= 3 && parts[parts.length - 3] === ".bivy" && parts[parts.length - 2] === "worktrees";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The branch to re-provision when a worktree session is reopened after its
|
|
29
|
+
* directory was removed, else undefined. Worktree cleanup clears `worktree` but
|
|
30
|
+
* leaves `workspace` on the old managed path, so either one marks the session
|
|
31
|
+
* as having run in a worktree.
|
|
32
|
+
*/
|
|
33
|
+
export function reapedWorktreeBranch(meta) {
|
|
34
|
+
if (!meta?.branch)
|
|
35
|
+
return undefined;
|
|
36
|
+
const wtPath = meta.worktree ?? (meta.workspace && isManagedWorktreePath(meta.workspace) ? meta.workspace : undefined);
|
|
37
|
+
return wtPath && !fs.existsSync(wtPath) ? meta.branch : undefined;
|
|
38
|
+
}
|
|
22
39
|
/** The toplevel of the git repo containing `dir`, or undefined if not a repo. */
|
|
23
40
|
export async function gitRepoRoot(dir) {
|
|
24
41
|
try {
|
package/package.json
CHANGED