pdf-brain 1.2.0 → 1.3.0
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/package.json +1 -1
- package/src/agent/manifest.ts +1 -0
- package/src/cli.ts +11 -1
- package/src/updater.ts +184 -0
package/package.json
CHANGED
package/src/agent/manifest.ts
CHANGED
|
@@ -51,6 +51,7 @@ ${docCount} documents indexed. Every command returns contextual next-action hint
|
|
|
51
51
|
pdf-brain ingest <dir> [--enrich] [--auto-tag] [--recursive]
|
|
52
52
|
|
|
53
53
|
### Maintenance
|
|
54
|
+
pdf-brain update Self-update to latest release
|
|
54
55
|
pdf-brain doctor [--fix] Health check (WAL, orphans, connectivity)
|
|
55
56
|
pdf-brain config show|get|set View/modify configuration
|
|
56
57
|
pdf-brain reindex [--clean] Re-embed all documents
|
package/src/cli.ts
CHANGED
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
import { type CommandResult, generateHints } from "./agent/hints.js";
|
|
64
64
|
import { formatHintBlock } from "./agent/format.js";
|
|
65
65
|
import { renderHelp } from "./agent/manifest.js";
|
|
66
|
+
import { backgroundUpdateCheck, runUpdate } from "./updater.js";
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
69
|
* Check if a string is a URL
|
|
@@ -1926,7 +1927,16 @@ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
|
1926
1927
|
// Handle taxonomy command separately (don't need full PDFLibrary)
|
|
1927
1928
|
const args = process.argv.slice(2);
|
|
1928
1929
|
|
|
1929
|
-
|
|
1930
|
+
// Background update check (fire-and-forget, once per day)
|
|
1931
|
+
backgroundUpdateCheck(VERSION);
|
|
1932
|
+
|
|
1933
|
+
// Handle update command before Effect machinery
|
|
1934
|
+
if (args[0] === "update") {
|
|
1935
|
+
runUpdate(VERSION).catch((err) => {
|
|
1936
|
+
console.error(`Update failed: ${err}`);
|
|
1937
|
+
process.exit(1);
|
|
1938
|
+
});
|
|
1939
|
+
} else if (args[0] === "taxonomy") {
|
|
1930
1940
|
const quiet = args.includes("--quiet") || args.includes("--no-hints");
|
|
1931
1941
|
const taxonomyProgram = Effect.gen(function* () {
|
|
1932
1942
|
const subcommand = args[1];
|
package/src/updater.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-updater for pdf-brain.
|
|
3
|
+
*
|
|
4
|
+
* - Background: silently downloads + replaces binary when a new version drops
|
|
5
|
+
* - `pdf-brain update` — force check + install now
|
|
6
|
+
*
|
|
7
|
+
* State file: ~/.pdf-brain/update-check.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, chmodSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
|
|
13
|
+
const REPO = "joelhooks/pdf-library";
|
|
14
|
+
const STATE_DIR = join(process.env.HOME || "~", ".pdf-brain");
|
|
15
|
+
const STATE_FILE = join(STATE_DIR, "update-check.json");
|
|
16
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
17
|
+
|
|
18
|
+
interface UpdateState {
|
|
19
|
+
lastCheck: number;
|
|
20
|
+
latestVersion: string | null;
|
|
21
|
+
lastAutoUpdate: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readState(): UpdateState {
|
|
25
|
+
try {
|
|
26
|
+
if (existsSync(STATE_FILE)) {
|
|
27
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
return { lastCheck: 0, latestVersion: null, lastAutoUpdate: 0 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeState(state: UpdateState): void {
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
|
|
36
|
+
writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fetch latest release tag from GitHub.
|
|
42
|
+
*/
|
|
43
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
44
|
+
try {
|
|
45
|
+
const resp = await fetch(
|
|
46
|
+
`https://api.github.com/repos/${REPO}/releases/latest`,
|
|
47
|
+
{
|
|
48
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
49
|
+
signal: AbortSignal.timeout(5000),
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
if (!resp.ok) return null;
|
|
53
|
+
const data = (await resp.json()) as { tag_name?: string };
|
|
54
|
+
return data.tag_name?.replace(/^v/, "") ?? null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compare semver strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
62
|
+
*/
|
|
63
|
+
function compareSemver(a: string, b: string): number {
|
|
64
|
+
const pa = a.split(".").map(Number);
|
|
65
|
+
const pb = b.split(".").map(Number);
|
|
66
|
+
for (let i = 0; i < 3; i++) {
|
|
67
|
+
const va = pa[i] || 0;
|
|
68
|
+
const vb = pb[i] || 0;
|
|
69
|
+
if (va > vb) return 1;
|
|
70
|
+
if (va < vb) return -1;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the download URL for the current platform.
|
|
77
|
+
*/
|
|
78
|
+
function getAssetUrl(version: string): string {
|
|
79
|
+
const os = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux";
|
|
80
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
81
|
+
const ext = os === "windows" ? ".exe" : "";
|
|
82
|
+
return `https://github.com/${REPO}/releases/download/v${version}/pdf-brain-${os}-${arch}${ext}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Download binary and atomically replace the current one.
|
|
87
|
+
* Returns true on success, false on failure. Never throws.
|
|
88
|
+
*/
|
|
89
|
+
async function downloadAndReplace(version: string): Promise<boolean> {
|
|
90
|
+
const url = getAssetUrl(version);
|
|
91
|
+
const binaryPath = process.execPath;
|
|
92
|
+
const tmpPath = `${binaryPath}.update-${Date.now()}`;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
96
|
+
if (!resp.ok) return false;
|
|
97
|
+
|
|
98
|
+
const buffer = await resp.arrayBuffer();
|
|
99
|
+
await Bun.write(tmpPath, buffer);
|
|
100
|
+
chmodSync(tmpPath, 0o755);
|
|
101
|
+
renameSync(tmpPath, binaryPath);
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
try { unlinkSync(tmpPath); } catch {}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Background auto-update. Runs silently on every invocation (once/day).
|
|
111
|
+
* If newer version exists, downloads and replaces the binary.
|
|
112
|
+
* Current invocation keeps running the old code — new version takes effect next run.
|
|
113
|
+
*/
|
|
114
|
+
export function backgroundUpdateCheck(currentVersion: string): void {
|
|
115
|
+
// Don't auto-update in dev mode
|
|
116
|
+
if (currentVersion.includes("compiled") || currentVersion === "0.0.0") return;
|
|
117
|
+
|
|
118
|
+
const state = readState();
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
|
|
121
|
+
// Checked recently, skip
|
|
122
|
+
if (now - state.lastCheck < CHECK_INTERVAL_MS) return;
|
|
123
|
+
|
|
124
|
+
// Fire-and-forget: check + update in background
|
|
125
|
+
(async () => {
|
|
126
|
+
const latest = await fetchLatestVersion();
|
|
127
|
+
const newState: UpdateState = {
|
|
128
|
+
lastCheck: now,
|
|
129
|
+
latestVersion: latest,
|
|
130
|
+
lastAutoUpdate: state.lastAutoUpdate,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (latest && compareSemver(latest, currentVersion) > 0) {
|
|
134
|
+
const ok = await downloadAndReplace(latest);
|
|
135
|
+
if (ok) {
|
|
136
|
+
newState.lastAutoUpdate = now;
|
|
137
|
+
newState.latestVersion = latest;
|
|
138
|
+
// Brief note so they know why behavior might change
|
|
139
|
+
console.error(`\x1b[2mUpdated pdf-brain v${currentVersion} → v${latest}\x1b[0m`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
writeState(newState);
|
|
144
|
+
})().catch(() => {});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Explicit `pdf-brain update` command. Checks, downloads, replaces, verifies.
|
|
149
|
+
*/
|
|
150
|
+
export async function runUpdate(currentVersion: string): Promise<void> {
|
|
151
|
+
console.log("Checking for updates...\n");
|
|
152
|
+
|
|
153
|
+
const latest = await fetchLatestVersion();
|
|
154
|
+
if (!latest) {
|
|
155
|
+
console.log("Could not reach GitHub. Check your connection.");
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
writeState({ lastCheck: Date.now(), latestVersion: latest, lastAutoUpdate: readState().lastAutoUpdate });
|
|
160
|
+
|
|
161
|
+
if (compareSemver(latest, currentVersion) <= 0) {
|
|
162
|
+
console.log(`Already on latest (v${currentVersion}).`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(`v${currentVersion} → v${latest}`);
|
|
167
|
+
console.log(`Downloading...`);
|
|
168
|
+
|
|
169
|
+
const ok = await downloadAndReplace(latest);
|
|
170
|
+
if (!ok) {
|
|
171
|
+
console.error(`Download failed.`);
|
|
172
|
+
console.error(
|
|
173
|
+
`\nManual install:\n curl -fsSL https://raw.githubusercontent.com/${REPO}/main/scripts/install.sh | bash`
|
|
174
|
+
);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`Updated to v${latest}.`);
|
|
179
|
+
|
|
180
|
+
// Verify
|
|
181
|
+
const result = Bun.spawnSync([process.execPath, "--version"], { stdout: "pipe" });
|
|
182
|
+
const output = result.stdout.toString().trim();
|
|
183
|
+
if (output) console.log(output);
|
|
184
|
+
}
|