routstrd 0.3.10 → 0.3.11
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/README.md +4 -0
- package/bun.lock +2 -2
- package/dist/index.js +153 -29
- package/package.json +2 -2
- package/src/cli.ts +48 -25
- package/src/tui/usage/app.ts +41 -2
- package/src/tui/usage/render.ts +10 -3
- package/src/tui/usage/types.ts +6 -0
- package/src/utils/update-checker.ts +114 -0
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ Routstr daemon - A CLI tool for managing routstr processes, similar to `cocod` (
|
|
|
6
6
|
|
|
7
7
|
routstrd is a Bun-based CLI tool that provides a background daemon for the Routstr protocol. It integrates with `cocod` for wallet management and uses the Routstr SDK to handle provider routing and model discovery.
|
|
8
8
|
|
|
9
|
+
## Routstr for Teams
|
|
10
|
+
|
|
11
|
+
For team-based routing, see [routstrd-auth](https://github.com/Routstr/routstrd-auth).
|
|
12
|
+
|
|
9
13
|
## Features
|
|
10
14
|
|
|
11
15
|
- **Daemon Mode**: Run routstrd as a background HTTP server
|
package/bun.lock
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "routstrd",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@cashu/cashu-ts": "^4.3.0",
|
|
8
|
-
"@routstr/sdk": "^0.3.
|
|
8
|
+
"@routstr/sdk": "^0.3.18",
|
|
9
9
|
"applesauce-core": "^5.1.0",
|
|
10
10
|
"applesauce-relay": "^5.1.0",
|
|
11
11
|
"applesauce-wallet-connect": "^6.0.0",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
|
|
89
89
|
"@panva/hpke-noble": ["@panva/hpke-noble@1.1.2", "", { "dependencies": { "@noble/ciphers": "^2.2.0", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", "@noble/post-quantum": "^0.6.1" }, "peerDependencies": { "hpke": "^1.0.0" } }, "sha512-Gr/jKr/ErpRow8aHK+SAp0sAaWy7iJGHlFsXsdTjD8ULcN4vvsVVWR3fiD6Ogf+VoSB28XY/xWRDUEIGXw+9aA=="],
|
|
90
90
|
|
|
91
|
-
"@routstr/sdk": ["@routstr/sdk@0.3.
|
|
91
|
+
"@routstr/sdk": ["@routstr/sdk@0.3.18", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "ehbp": "^0.2.3", "rxjs": "^7.8.1", "tinfoil": "^1.1.6", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-OtBZp1iURl4lQOcmKOFw0sGlgEIVJTR+nTg2MCms/fx74j3YxOJrAyQXxsy+vqoiD1KwJ6a3g7TEowluW9Yv4A=="],
|
|
92
92
|
|
|
93
93
|
"@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="],
|
|
94
94
|
|
package/dist/index.js
CHANGED
|
@@ -20769,6 +20769,85 @@ var require_browser = __commonJS((exports) => {
|
|
|
20769
20769
|
});
|
|
20770
20770
|
});
|
|
20771
20771
|
|
|
20772
|
+
// src/utils/update-checker.ts
|
|
20773
|
+
async function getLatestNpmVersion(packageName) {
|
|
20774
|
+
try {
|
|
20775
|
+
const response = await fetch(`${NPM_REGISTRY}/${encodeURIComponent(packageName)}/latest`);
|
|
20776
|
+
if (!response.ok)
|
|
20777
|
+
return null;
|
|
20778
|
+
const data = await response.json();
|
|
20779
|
+
return data.version ?? null;
|
|
20780
|
+
} catch {
|
|
20781
|
+
return null;
|
|
20782
|
+
}
|
|
20783
|
+
}
|
|
20784
|
+
async function getGlobalPackageVersion(packageName) {
|
|
20785
|
+
try {
|
|
20786
|
+
const proc = Bun.spawn(["bun", "pm", "ls", "-g"], {
|
|
20787
|
+
stdout: "pipe",
|
|
20788
|
+
stderr: "ignore"
|
|
20789
|
+
});
|
|
20790
|
+
const output = await new Response(proc.stdout).text();
|
|
20791
|
+
await proc.exited;
|
|
20792
|
+
const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20793
|
+
const match = output.match(new RegExp(`${escaped}@([^\\s]+)`));
|
|
20794
|
+
if (!match)
|
|
20795
|
+
return null;
|
|
20796
|
+
const version = match[1];
|
|
20797
|
+
if (!version || !/^\d+\.\d+\.\d+/.test(version))
|
|
20798
|
+
return null;
|
|
20799
|
+
return version;
|
|
20800
|
+
} catch {
|
|
20801
|
+
return null;
|
|
20802
|
+
}
|
|
20803
|
+
}
|
|
20804
|
+
function compareVersions(a, b) {
|
|
20805
|
+
const parse5 = (v) => {
|
|
20806
|
+
const match = v.replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
20807
|
+
if (!match?.[1] || !match[2] || !match[3])
|
|
20808
|
+
return null;
|
|
20809
|
+
return [
|
|
20810
|
+
parseInt(match[1], 10),
|
|
20811
|
+
parseInt(match[2], 10),
|
|
20812
|
+
parseInt(match[3], 10)
|
|
20813
|
+
];
|
|
20814
|
+
};
|
|
20815
|
+
const va = parse5(a);
|
|
20816
|
+
const vb = parse5(b);
|
|
20817
|
+
if (!va || !vb)
|
|
20818
|
+
return null;
|
|
20819
|
+
const [a1, a2, a3] = va;
|
|
20820
|
+
const [b1, b2, b3] = vb;
|
|
20821
|
+
if (a1 !== b1)
|
|
20822
|
+
return a1 - b1;
|
|
20823
|
+
if (a2 !== b2)
|
|
20824
|
+
return a2 - b2;
|
|
20825
|
+
if (a3 !== b3)
|
|
20826
|
+
return a3 - b3;
|
|
20827
|
+
return 0;
|
|
20828
|
+
}
|
|
20829
|
+
async function checkForUpdates() {
|
|
20830
|
+
const packages = await Promise.all(UPDATE_PACKAGES.map(async ({ name, label }) => {
|
|
20831
|
+
const [current, latest] = await Promise.all([
|
|
20832
|
+
getGlobalPackageVersion(name),
|
|
20833
|
+
getLatestNpmVersion(name)
|
|
20834
|
+
]);
|
|
20835
|
+
const hasUpdate = !!(current && latest && (compareVersions(current, latest) ?? -1) < 0);
|
|
20836
|
+
return { name, label, current, latest, hasUpdate };
|
|
20837
|
+
}));
|
|
20838
|
+
return {
|
|
20839
|
+
hasUpdate: packages.some((p) => p.hasUpdate),
|
|
20840
|
+
packages
|
|
20841
|
+
};
|
|
20842
|
+
}
|
|
20843
|
+
var NPM_REGISTRY = "https://registry.npmjs.org", UPDATE_PACKAGES;
|
|
20844
|
+
var init_update_checker = __esm(() => {
|
|
20845
|
+
UPDATE_PACKAGES = [
|
|
20846
|
+
{ name: "routstrd", label: "routstrd" },
|
|
20847
|
+
{ name: "@routstr/cocod", label: "cocod" }
|
|
20848
|
+
];
|
|
20849
|
+
});
|
|
20850
|
+
|
|
20772
20851
|
// src/tui/usage/constants.ts
|
|
20773
20852
|
function getVisibleTabs(hasNpubs) {
|
|
20774
20853
|
const tabs = ALL_TABS.filter((t) => t.id !== "npubs" || hasNpubs);
|
|
@@ -21036,13 +21115,20 @@ function formatReqs(value) {
|
|
|
21036
21115
|
return (value / 1000).toFixed(1) + "k";
|
|
21037
21116
|
return value.toString();
|
|
21038
21117
|
}
|
|
21039
|
-
function renderHeader(activeTab, width, visibleTabs) {
|
|
21118
|
+
function renderHeader(activeTab, width, visibleTabs, updateInfo) {
|
|
21040
21119
|
const title = `${COLORS.bold}${COLORS.cyan}ROUTSTRD USAGE MONITOR${COLORS.reset}`;
|
|
21041
21120
|
const vimIndicator = `${COLORS.yellow}[vim]${COLORS.reset}`;
|
|
21042
21121
|
const maxKey = visibleTabs.length;
|
|
21043
21122
|
const help = `${COLORS.dim}[Q] Quit [\u2191\u2193] Scroll [\u2190\u2192] Tabs [1-${maxKey}] Tabs [R] Refresh${COLORS.reset}`;
|
|
21044
21123
|
const fill = width - title.length - help.length - vimIndicator.length - 6;
|
|
21045
|
-
|
|
21124
|
+
const headerLine = `${title}${vimIndicator}${" ".repeat(Math.max(1, fill))}${help}`;
|
|
21125
|
+
if (updateInfo?.hasUpdate) {
|
|
21126
|
+
const banner = `${COLORS.bold}${COLORS.yellow} \u21BB UPDATE AVAILABLE \u2014 run ${COLORS.green}routstrd update${COLORS.yellow} to update${COLORS.reset}`;
|
|
21127
|
+
return `${headerLine}
|
|
21128
|
+
${banner}
|
|
21129
|
+
`;
|
|
21130
|
+
}
|
|
21131
|
+
return `${headerLine}
|
|
21046
21132
|
`;
|
|
21047
21133
|
}
|
|
21048
21134
|
function renderSearchBar() {
|
|
@@ -21550,6 +21636,31 @@ async function runUsageTui() {
|
|
|
21550
21636
|
let autoRefresh = true;
|
|
21551
21637
|
let cleanedUp = false;
|
|
21552
21638
|
let fetching = false;
|
|
21639
|
+
const UPDATE_CHECK_INTERVAL_MS = 210 * 60 * 1000;
|
|
21640
|
+
const UPDATE_CHECK_DELAY_MS = 3000;
|
|
21641
|
+
let updateInfo = null;
|
|
21642
|
+
let lastUpdateCheck = 0;
|
|
21643
|
+
let updateCheckInProgress = false;
|
|
21644
|
+
async function maybeCheckForUpdates() {
|
|
21645
|
+
if (updateCheckInProgress)
|
|
21646
|
+
return;
|
|
21647
|
+
const now2 = Date.now();
|
|
21648
|
+
if (updateInfo && now2 - lastUpdateCheck < UPDATE_CHECK_INTERVAL_MS)
|
|
21649
|
+
return;
|
|
21650
|
+
updateCheckInProgress = true;
|
|
21651
|
+
lastUpdateCheck = now2;
|
|
21652
|
+
try {
|
|
21653
|
+
const result = await checkForUpdates();
|
|
21654
|
+
const outdated = result.packages.filter((p) => p.hasUpdate);
|
|
21655
|
+
updateInfo = {
|
|
21656
|
+
hasUpdate: result.hasUpdate,
|
|
21657
|
+
text: outdated.length > 0 ? `Update available: ${outdated.map((p) => p.label).join(", ")} ${outdated.map((p) => p.latest).join(", ")}` : ""
|
|
21658
|
+
};
|
|
21659
|
+
render2();
|
|
21660
|
+
} catch {} finally {
|
|
21661
|
+
updateCheckInProgress = false;
|
|
21662
|
+
}
|
|
21663
|
+
}
|
|
21553
21664
|
if (isInteractive) {
|
|
21554
21665
|
stdout.write(enterAlternateScreen() + hideCursor());
|
|
21555
21666
|
stdin.setRawMode?.(true);
|
|
@@ -21620,7 +21731,7 @@ Press Q to quit.`);
|
|
|
21620
21731
|
}
|
|
21621
21732
|
const content = renderTabContent(currentTab, stats, balance, status, width, clients);
|
|
21622
21733
|
const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`;
|
|
21623
|
-
const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
21734
|
+
const chrome = renderHeader(currentTab, width, visibleTabs, updateInfo ?? undefined) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
21624
21735
|
const chromeLines = chrome.split(`
|
|
21625
21736
|
`).length - 1;
|
|
21626
21737
|
const footerSeparator = renderSeparator(width);
|
|
@@ -21786,10 +21897,12 @@ Press Q to quit.`);
|
|
|
21786
21897
|
if (isInteractive)
|
|
21787
21898
|
stdin.on("data", handleKey);
|
|
21788
21899
|
await fetchData();
|
|
21900
|
+
setTimeout(() => void maybeCheckForUpdates(), UPDATE_CHECK_DELAY_MS);
|
|
21789
21901
|
refreshInterval = setInterval(() => {
|
|
21790
21902
|
if (autoRefresh) {
|
|
21791
21903
|
fetchData();
|
|
21792
21904
|
}
|
|
21905
|
+
maybeCheckForUpdates();
|
|
21793
21906
|
}, 2000);
|
|
21794
21907
|
}
|
|
21795
21908
|
var init_app = __esm(() => {
|
|
@@ -21798,6 +21911,7 @@ var init_app = __esm(() => {
|
|
|
21798
21911
|
init_state();
|
|
21799
21912
|
init_constants();
|
|
21800
21913
|
init_render();
|
|
21914
|
+
init_update_checker();
|
|
21801
21915
|
});
|
|
21802
21916
|
|
|
21803
21917
|
// src/tui/usage/index.ts
|
|
@@ -22609,7 +22723,7 @@ async function isCocodInstalled(cocodPath) {
|
|
|
22609
22723
|
// package.json
|
|
22610
22724
|
var package_default = {
|
|
22611
22725
|
name: "routstrd",
|
|
22612
|
-
version: "0.3.
|
|
22726
|
+
version: "0.3.11",
|
|
22613
22727
|
module: "src/index.ts",
|
|
22614
22728
|
type: "module",
|
|
22615
22729
|
private: false,
|
|
@@ -22633,7 +22747,7 @@ var package_default = {
|
|
|
22633
22747
|
},
|
|
22634
22748
|
dependencies: {
|
|
22635
22749
|
"@cashu/cashu-ts": "^4.3.0",
|
|
22636
|
-
"@routstr/sdk": "^0.3.
|
|
22750
|
+
"@routstr/sdk": "^0.3.18",
|
|
22637
22751
|
"applesauce-core": "^5.1.0",
|
|
22638
22752
|
"applesauce-relay": "^5.1.0",
|
|
22639
22753
|
"applesauce-wallet-connect": "^6.0.0",
|
|
@@ -22647,6 +22761,7 @@ var package_default = {
|
|
|
22647
22761
|
};
|
|
22648
22762
|
|
|
22649
22763
|
// src/cli.ts
|
|
22764
|
+
init_update_checker();
|
|
22650
22765
|
function parsePositiveIntOrExit(value, fieldName) {
|
|
22651
22766
|
const parsed = Number.parseInt(value, 10);
|
|
22652
22767
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -22878,32 +22993,41 @@ To ensure routstrd persists across system restarts, run: 'routstrd service insta
|
|
|
22878
22993
|
}
|
|
22879
22994
|
program.name("routstrd").description("Routstr daemon - Manage routstr processes").version(package_default.version, "--version", "output the version number");
|
|
22880
22995
|
program.command("update").description("Update routstrd and cocod to the latest versions").action(async () => {
|
|
22881
|
-
|
|
22882
|
-
|
|
22883
|
-
|
|
22884
|
-
|
|
22885
|
-
|
|
22886
|
-
const
|
|
22887
|
-
|
|
22888
|
-
|
|
22889
|
-
|
|
22890
|
-
|
|
22891
|
-
|
|
22996
|
+
const packages = [
|
|
22997
|
+
{ name: "routstrd", label: "routstrd" },
|
|
22998
|
+
{ name: "@routstr/cocod", label: "cocod" }
|
|
22999
|
+
];
|
|
23000
|
+
let updatedAny = false;
|
|
23001
|
+
for (const { name, label } of packages) {
|
|
23002
|
+
const installed = await getGlobalPackageVersion(name);
|
|
23003
|
+
const latest = await getLatestNpmVersion(name);
|
|
23004
|
+
if (installed && latest && (compareVersions(installed, latest) ?? -1) >= 0) {
|
|
23005
|
+
console.log(`${label} is already up to date (v${installed}).`);
|
|
23006
|
+
continue;
|
|
23007
|
+
}
|
|
23008
|
+
const fromPart = installed ? ` from v${installed}` : "";
|
|
23009
|
+
const toPart = latest ? ` to v${latest}` : "";
|
|
23010
|
+
console.log(`Updating ${label}${fromPart}${toPart}...`);
|
|
23011
|
+
const proc = Bun.spawn(["bun", "install", "-g", name], {
|
|
23012
|
+
stdout: "inherit",
|
|
23013
|
+
stderr: "inherit"
|
|
23014
|
+
});
|
|
23015
|
+
const code = await proc.exited;
|
|
23016
|
+
if (code !== 0) {
|
|
23017
|
+
console.error(`Failed to update ${label}.`);
|
|
23018
|
+
process.exit(1);
|
|
23019
|
+
}
|
|
23020
|
+
console.log(`${label} updated successfully.
|
|
22892
23021
|
`);
|
|
22893
|
-
|
|
22894
|
-
|
|
22895
|
-
|
|
22896
|
-
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
22900
|
-
|
|
22901
|
-
process.exit(1);
|
|
23022
|
+
updatedAny = true;
|
|
23023
|
+
}
|
|
23024
|
+
if (updatedAny) {
|
|
23025
|
+
console.log("All requested updates have been applied!");
|
|
23026
|
+
await restartDaemonsAfterUpdate();
|
|
23027
|
+
} else {
|
|
23028
|
+
console.log(`
|
|
23029
|
+
All packages are already up to date \u2014 nothing to do.`);
|
|
22902
23030
|
}
|
|
22903
|
-
console.log(`cocod updated successfully.
|
|
22904
|
-
`);
|
|
22905
|
-
console.log("Both routstrd and cocod have been updated!");
|
|
22906
|
-
await restartDaemonsAfterUpdate();
|
|
22907
23031
|
});
|
|
22908
23032
|
program.command("refund").description("Refund pending tokens and API keys to a specified mint").option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to first mint in wallet)").option("-y, --yes", "Skip confirmation prompt", false).option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false).action(async (options) => {
|
|
22909
23033
|
await ensureDaemonRunning();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@cashu/cashu-ts": "^4.3.0",
|
|
27
|
-
"@routstr/sdk": "^0.3.
|
|
27
|
+
"@routstr/sdk": "^0.3.18",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
30
|
"applesauce-wallet-connect": "^6.0.0",
|
package/src/cli.ts
CHANGED
|
@@ -36,6 +36,11 @@ import {
|
|
|
36
36
|
resolveCocodExecutable,
|
|
37
37
|
} from "./daemon/wallet/cocod-client";
|
|
38
38
|
import packageJson from "../package.json" with { type: "json" };
|
|
39
|
+
import {
|
|
40
|
+
compareVersions,
|
|
41
|
+
getGlobalPackageVersion,
|
|
42
|
+
getLatestNpmVersion,
|
|
43
|
+
} from "./utils/update-checker.ts";
|
|
39
44
|
|
|
40
45
|
type RoutstrModel = {
|
|
41
46
|
id: string;
|
|
@@ -363,34 +368,52 @@ program
|
|
|
363
368
|
.command("update")
|
|
364
369
|
.description("Update routstrd and cocod to the latest versions")
|
|
365
370
|
.action(async () => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
});
|
|
371
|
-
const routstrdCode = await routstrdProc.exited;
|
|
372
|
-
if (routstrdCode !== 0) {
|
|
373
|
-
console.error("Failed to update routstrd.");
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|
|
376
|
-
console.log("routstrd updated successfully.\n");
|
|
371
|
+
const packages = [
|
|
372
|
+
{ name: "routstrd", label: "routstrd" },
|
|
373
|
+
{ name: "@routstr/cocod", label: "cocod" },
|
|
374
|
+
];
|
|
377
375
|
|
|
378
|
-
|
|
379
|
-
const cocodProc = Bun.spawn(["bun", "install", "-g", "@routstr/cocod"], {
|
|
380
|
-
stdout: "inherit",
|
|
381
|
-
stderr: "inherit",
|
|
382
|
-
});
|
|
383
|
-
const cocodCode = await cocodProc.exited;
|
|
384
|
-
if (cocodCode !== 0) {
|
|
385
|
-
console.error("Failed to update cocod.");
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
console.log("cocod updated successfully.\n");
|
|
376
|
+
let updatedAny = false;
|
|
389
377
|
|
|
390
|
-
|
|
378
|
+
for (const { name, label } of packages) {
|
|
379
|
+
const installed = await getGlobalPackageVersion(name);
|
|
380
|
+
const latest = await getLatestNpmVersion(name);
|
|
391
381
|
|
|
392
|
-
|
|
393
|
-
|
|
382
|
+
// Only skip when we're confident the installed version is current.
|
|
383
|
+
// If we can't determine either version we fall through to installing.
|
|
384
|
+
if (
|
|
385
|
+
installed &&
|
|
386
|
+
latest &&
|
|
387
|
+
(compareVersions(installed, latest) ?? -1) >= 0
|
|
388
|
+
) {
|
|
389
|
+
console.log(`${label} is already up to date (v${installed}).`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const fromPart = installed ? ` from v${installed}` : "";
|
|
394
|
+
const toPart = latest ? ` to v${latest}` : "";
|
|
395
|
+
console.log(`Updating ${label}${fromPart}${toPart}...`);
|
|
396
|
+
|
|
397
|
+
const proc = Bun.spawn(["bun", "install", "-g", name], {
|
|
398
|
+
stdout: "inherit",
|
|
399
|
+
stderr: "inherit",
|
|
400
|
+
});
|
|
401
|
+
const code = await proc.exited;
|
|
402
|
+
if (code !== 0) {
|
|
403
|
+
console.error(`Failed to update ${label}.`);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
console.log(`${label} updated successfully.\n`);
|
|
407
|
+
updatedAny = true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (updatedAny) {
|
|
411
|
+
console.log("All requested updates have been applied!");
|
|
412
|
+
// Restart daemons so the new binaries take effect immediately.
|
|
413
|
+
await restartDaemonsAfterUpdate();
|
|
414
|
+
} else {
|
|
415
|
+
console.log("\nAll packages are already up to date — nothing to do.");
|
|
416
|
+
}
|
|
394
417
|
});
|
|
395
418
|
|
|
396
419
|
program
|
package/src/tui/usage/app.ts
CHANGED
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
} from "./terminal.ts";
|
|
29
29
|
import { COLORS } from "./constants.ts";
|
|
30
30
|
import { renderHeader, renderSearchBar, renderSeparator, renderTabContent, renderTabs } from "./render.ts";
|
|
31
|
-
import type { TabId, UsageStats } from "./types.ts";
|
|
31
|
+
import type { TabId, UpdateInfo, UsageStats } from "./types.ts";
|
|
32
|
+
import { checkForUpdates } from "../../utils/update-checker.ts";
|
|
32
33
|
|
|
33
34
|
export async function runUsageTui(): Promise<void> {
|
|
34
35
|
const running = await isDaemonRunning();
|
|
@@ -53,6 +54,39 @@ export async function runUsageTui(): Promise<void> {
|
|
|
53
54
|
let cleanedUp = false;
|
|
54
55
|
let fetching = false;
|
|
55
56
|
|
|
57
|
+
// Update-check state — re-checked at most every 210 minutes to avoid
|
|
58
|
+
// spamming the npm registry. The first check happens shortly after
|
|
59
|
+
// startup so the TUI doesn't block on a network call.
|
|
60
|
+
const UPDATE_CHECK_INTERVAL_MS = 210 * 60 * 1000;
|
|
61
|
+
const UPDATE_CHECK_DELAY_MS = 3000;
|
|
62
|
+
let updateInfo: UpdateInfo | null = null;
|
|
63
|
+
let lastUpdateCheck = 0;
|
|
64
|
+
let updateCheckInProgress = false;
|
|
65
|
+
|
|
66
|
+
async function maybeCheckForUpdates(): Promise<void> {
|
|
67
|
+
if (updateCheckInProgress) return;
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (updateInfo && now - lastUpdateCheck < UPDATE_CHECK_INTERVAL_MS) return;
|
|
70
|
+
|
|
71
|
+
updateCheckInProgress = true;
|
|
72
|
+
lastUpdateCheck = now;
|
|
73
|
+
try {
|
|
74
|
+
const result = await checkForUpdates();
|
|
75
|
+
const outdated = result.packages.filter((p) => p.hasUpdate);
|
|
76
|
+
updateInfo = {
|
|
77
|
+
hasUpdate: result.hasUpdate,
|
|
78
|
+
text: outdated.length > 0
|
|
79
|
+
? `Update available: ${outdated.map((p) => p.label).join(", ")} ${outdated.map((p) => p.latest).join(", ")}`
|
|
80
|
+
: "",
|
|
81
|
+
};
|
|
82
|
+
render();
|
|
83
|
+
} catch {
|
|
84
|
+
// Silently ignore — update checks are best-effort
|
|
85
|
+
} finally {
|
|
86
|
+
updateCheckInProgress = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
if (isInteractive) {
|
|
57
91
|
stdout.write(enterAlternateScreen() + hideCursor());
|
|
58
92
|
stdin.setRawMode?.(true);
|
|
@@ -138,7 +172,7 @@ export async function runUsageTui(): Promise<void> {
|
|
|
138
172
|
|
|
139
173
|
const content = renderTabContent(currentTab, stats, balance, status, width, clients);
|
|
140
174
|
const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`;
|
|
141
|
-
const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
175
|
+
const chrome = renderHeader(currentTab, width, visibleTabs, updateInfo ?? undefined) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
142
176
|
const chromeLines = chrome.split("\n").length - 1;
|
|
143
177
|
const footerSeparator = renderSeparator(width);
|
|
144
178
|
const footerLines = footerSeparator.split("\n").length - 1;
|
|
@@ -265,9 +299,14 @@ export async function runUsageTui(): Promise<void> {
|
|
|
265
299
|
|
|
266
300
|
await fetchData();
|
|
267
301
|
|
|
302
|
+
// Kick off the first update check after a short delay so the initial
|
|
303
|
+
// data fetch isn't blocked by a network round-trip to npm.
|
|
304
|
+
setTimeout(() => void maybeCheckForUpdates(), UPDATE_CHECK_DELAY_MS);
|
|
305
|
+
|
|
268
306
|
refreshInterval = setInterval(() => {
|
|
269
307
|
if (autoRefresh) {
|
|
270
308
|
void fetchData();
|
|
271
309
|
}
|
|
310
|
+
void maybeCheckForUpdates();
|
|
272
311
|
}, 2000);
|
|
273
312
|
}
|
package/src/tui/usage/render.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { vimState } from "./state.ts";
|
|
9
9
|
import { stripAnsi } from "./terminal.ts";
|
|
10
10
|
import type { BalanceInfo, StatusInfo } from "./data.ts";
|
|
11
|
-
import type { TabId, UsageStats } from "./types.ts";
|
|
11
|
+
import type { TabId, UpdateInfo, UsageStats } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
/** Format a cost value: 0.12, 1.23, 12.34, 123.45, 1.23k, 1.23m */
|
|
14
14
|
function formatCost(value: number): string {
|
|
@@ -24,13 +24,20 @@ function formatReqs(value: number): string {
|
|
|
24
24
|
return value.toString();
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function renderHeader(activeTab: TabId, width: number, visibleTabs: Tab[]): string {
|
|
27
|
+
export function renderHeader(activeTab: TabId, width: number, visibleTabs: Tab[], updateInfo?: UpdateInfo): string {
|
|
28
28
|
const title = `${COLORS.bold}${COLORS.cyan}ROUTSTRD USAGE MONITOR${COLORS.reset}`;
|
|
29
29
|
const vimIndicator = `${COLORS.yellow}[vim]${COLORS.reset}`;
|
|
30
30
|
const maxKey = visibleTabs.length;
|
|
31
31
|
const help = `${COLORS.dim}[Q] Quit [↑↓] Scroll [←→] Tabs [1-${maxKey}] Tabs [R] Refresh${COLORS.reset}`;
|
|
32
32
|
const fill = width - title.length - help.length - vimIndicator.length - 6;
|
|
33
|
-
|
|
33
|
+
const headerLine = `${title}${vimIndicator}${" ".repeat(Math.max(1, fill))}${help}`;
|
|
34
|
+
|
|
35
|
+
if (updateInfo?.hasUpdate) {
|
|
36
|
+
const banner = `${COLORS.bold}${COLORS.yellow} ↻ UPDATE AVAILABLE — run ${COLORS.green}routstrd update${COLORS.yellow} to update${COLORS.reset}`;
|
|
37
|
+
return `${headerLine}\n${banner}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return `${headerLine}\n`;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export function renderSearchBar(): string {
|
package/src/tui/usage/types.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { UsageTrackingEntry } from "../../daemon/types.ts";
|
|
2
2
|
import type { UsageSummary } from "../../daemon/http/usage-summary.ts";
|
|
3
|
+
import type { UpdateCheckResult } from "../../utils/update-checker.ts";
|
|
3
4
|
|
|
4
5
|
export type { UsageSummary };
|
|
5
6
|
|
|
7
|
+
export interface UpdateInfo {
|
|
8
|
+
hasUpdate: boolean;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export interface UsageStats {
|
|
7
13
|
entries: UsageTrackingEntry[];
|
|
8
14
|
totalEntries: number;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2
|
+
|
|
3
|
+
/** Packages that `routstrd update` manages. */
|
|
4
|
+
export const UPDATE_PACKAGES = [
|
|
5
|
+
{ name: "routstrd", label: "routstrd" },
|
|
6
|
+
{ name: "@routstr/cocod", label: "cocod" },
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch the latest published version of a package from the npm registry.
|
|
11
|
+
* Returns null if the version cannot be determined (e.g. offline, not found).
|
|
12
|
+
*/
|
|
13
|
+
export async function getLatestNpmVersion(
|
|
14
|
+
packageName: string,
|
|
15
|
+
): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(
|
|
18
|
+
`${NPM_REGISTRY}/${encodeURIComponent(packageName)}/latest`,
|
|
19
|
+
);
|
|
20
|
+
if (!response.ok) return null;
|
|
21
|
+
const data = (await response.json()) as { version?: string };
|
|
22
|
+
return data.version ?? null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the version of a globally-installed bun package.
|
|
30
|
+
* Returns null when the package is not installed globally or the version
|
|
31
|
+
* cannot be parsed as semver (e.g. installed from a git URL).
|
|
32
|
+
*/
|
|
33
|
+
export async function getGlobalPackageVersion(
|
|
34
|
+
packageName: string,
|
|
35
|
+
): Promise<string | null> {
|
|
36
|
+
try {
|
|
37
|
+
const proc = Bun.spawn(["bun", "pm", "ls", "-g"], {
|
|
38
|
+
stdout: "pipe",
|
|
39
|
+
stderr: "ignore",
|
|
40
|
+
});
|
|
41
|
+
const output = await new Response(proc.stdout).text();
|
|
42
|
+
await proc.exited;
|
|
43
|
+
// Lines look like: ├── routstrd@0.3.10 or └── @routstr/cocod@0.0.24
|
|
44
|
+
const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45
|
+
const match = output.match(new RegExp(`${escaped}@([^\\s]+)`));
|
|
46
|
+
if (!match) return null;
|
|
47
|
+
const version = match[1];
|
|
48
|
+
// Reject non-semver versions (e.g. github:routstr/cocod#3f6ac14)
|
|
49
|
+
if (!version || !/^\d+\.\d+\.\d+/.test(version)) return null;
|
|
50
|
+
return version;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compare two semver version strings.
|
|
58
|
+
* Returns a positive number if `a` is newer, negative if `b` is newer,
|
|
59
|
+
* 0 if equal, or null if either value is not parseable semver.
|
|
60
|
+
*/
|
|
61
|
+
export function compareVersions(a: string, b: string): number | null {
|
|
62
|
+
const parse = (v: string): [number, number, number] | null => {
|
|
63
|
+
const match = v.replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
64
|
+
if (!match?.[1] || !match[2] || !match[3]) return null;
|
|
65
|
+
return [
|
|
66
|
+
parseInt(match[1], 10),
|
|
67
|
+
parseInt(match[2], 10),
|
|
68
|
+
parseInt(match[3], 10),
|
|
69
|
+
];
|
|
70
|
+
};
|
|
71
|
+
const va = parse(a);
|
|
72
|
+
const vb = parse(b);
|
|
73
|
+
if (!va || !vb) return null;
|
|
74
|
+
const [a1, a2, a3] = va;
|
|
75
|
+
const [b1, b2, b3] = vb;
|
|
76
|
+
if (a1 !== b1) return a1 - b1;
|
|
77
|
+
if (a2 !== b2) return a2 - b2;
|
|
78
|
+
if (a3 !== b3) return a3 - b3;
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PackageUpdate {
|
|
83
|
+
name: string;
|
|
84
|
+
label: string;
|
|
85
|
+
current: string | null;
|
|
86
|
+
latest: string | null;
|
|
87
|
+
hasUpdate: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface UpdateCheckResult {
|
|
91
|
+
hasUpdate: boolean;
|
|
92
|
+
packages: PackageUpdate[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check both routstrd and cocod for available updates.
|
|
97
|
+
* Returns a result with per-package details and an overall `hasUpdate` flag.
|
|
98
|
+
*/
|
|
99
|
+
export async function checkForUpdates(): Promise<UpdateCheckResult> {
|
|
100
|
+
const packages = await Promise.all(
|
|
101
|
+
UPDATE_PACKAGES.map(async ({ name, label }) => {
|
|
102
|
+
const [current, latest] = await Promise.all([
|
|
103
|
+
getGlobalPackageVersion(name),
|
|
104
|
+
getLatestNpmVersion(name),
|
|
105
|
+
]);
|
|
106
|
+
const hasUpdate = !!(current && latest && (compareVersions(current, latest) ?? -1) < 0);
|
|
107
|
+
return { name, label, current, latest, hasUpdate } satisfies PackageUpdate;
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
hasUpdate: packages.some((p) => p.hasUpdate),
|
|
112
|
+
packages,
|
|
113
|
+
};
|
|
114
|
+
}
|