@iletai/nzb 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -45,6 +45,8 @@ Commands:
45
45
  tui Connect to the daemon via terminal UI
46
46
  setup Interactive first-run configuration
47
47
  update Check for updates and install the latest version
48
+ update check Check for updates without installing
49
+ update --force Force reinstall the latest version
48
50
  cron Manage scheduled cron jobs
49
51
  help Show this help message
50
52
 
@@ -77,21 +79,48 @@ switch (command) {
77
79
  await import("./setup.js");
78
80
  break;
79
81
  case "update": {
80
- const { checkForUpdate, performUpdate } = await import("./update.js");
82
+ const { checkForUpdate, performUpdate, performForceUpdate } = await import("./update.js");
83
+ const updateArgs = args.slice(1);
84
+ const subCmd = updateArgs[0];
85
+ const force = updateArgs.includes("--force");
86
+ // `nzb update check` — check only, don't install
87
+ if (subCmd === "check") {
88
+ const check = await checkForUpdate();
89
+ if (!check.checkSucceeded) {
90
+ console.error("Warning: Could not reach the npm registry. Check your network and try again.");
91
+ process.exit(1);
92
+ }
93
+ if (check.updateAvailable) {
94
+ console.log(`Update available: v${check.current} → v${check.latest}`);
95
+ if (check.publishedAt) {
96
+ console.log(`Published: ${new Date(check.publishedAt).toLocaleDateString()}`);
97
+ }
98
+ }
99
+ else {
100
+ console.log(`nzb v${check.current} is already the latest version.`);
101
+ }
102
+ break;
103
+ }
104
+ // `nzb update` or `nzb update --force` — check and install
81
105
  const check = await checkForUpdate();
82
106
  if (!check.checkSucceeded) {
83
107
  console.error("Warning: Could not reach the npm registry. Check your network and try again.");
84
108
  process.exit(1);
85
109
  }
86
- if (!check.updateAvailable) {
110
+ if (!check.updateAvailable && !force) {
87
111
  console.log(`nzb v${check.current} is already the latest version.`);
88
112
  break;
89
113
  }
90
- console.log(`Update available: v${check.current} → v${check.latest}`);
114
+ if (check.updateAvailable) {
115
+ console.log(`Update available: v${check.current} → v${check.latest}`);
116
+ }
117
+ else if (force) {
118
+ console.log(`Force reinstalling nzb v${check.current}...`);
119
+ }
91
120
  console.log("Installing...");
92
- const result = await performUpdate();
121
+ const result = force ? await performForceUpdate() : await performUpdate();
93
122
  if (result.ok) {
94
- console.log(`Updated to v${check.latest}`);
123
+ console.log(check.updateAvailable ? `Updated to v${check.latest}` : `Reinstalled v${check.current}`);
95
124
  }
96
125
  else {
97
126
  console.error(`Update failed: ${result.output}`);
@@ -855,6 +855,56 @@ export function createTools(deps) {
855
855
  return `Restarting NZB${reason}. I'll be back in a few seconds.`;
856
856
  },
857
857
  }),
858
+ defineTool("check_update", {
859
+ description: "Check for NZB updates and optionally apply them. " +
860
+ "Use 'check' to see if updates are available. " +
861
+ "Use 'update' to install the latest version and restart.",
862
+ parameters: z.object({
863
+ action: z.enum(["check", "update"]).describe("'check' to check for updates, 'update' to install and restart"),
864
+ }),
865
+ handler: async (args) => {
866
+ try {
867
+ const { checkForUpdate, performUpdate, getChangelog } = await import("../update.js");
868
+ if (args.action === "check") {
869
+ const result = await checkForUpdate();
870
+ if (!result.checkSucceeded) {
871
+ return "Could not reach the npm registry. Network may be unavailable.";
872
+ }
873
+ if (!result.updateAvailable) {
874
+ return `NZB v${result.current} is already the latest version.`;
875
+ }
876
+ const changelog = await getChangelog(5);
877
+ const changelogText = changelog.length > 0
878
+ ? "\n\nRecent versions:\n" + changelog.map((e) => `• v${e.version} (${e.date})`).join("\n")
879
+ : "";
880
+ return `Update available: v${result.current} → v${result.latest}${changelogText}`;
881
+ }
882
+ // action === "update"
883
+ const result = await checkForUpdate();
884
+ if (!result.checkSucceeded) {
885
+ return "Could not reach the npm registry. Network may be unavailable.";
886
+ }
887
+ if (!result.updateAvailable) {
888
+ return `NZB v${result.current} is already the latest version. No update needed.`;
889
+ }
890
+ const updateResult = await performUpdate();
891
+ if (!updateResult.ok) {
892
+ return `Update failed: ${updateResult.output}`;
893
+ }
894
+ // Schedule restart after returning the response
895
+ const { restartDaemon } = await import("../daemon.js");
896
+ setTimeout(() => {
897
+ restartDaemon().catch((err) => {
898
+ console.error("[nzb] Post-update restart failed:", err);
899
+ });
900
+ }, 1000);
901
+ return `Updated NZB to v${result.latest}. Restarting now...`;
902
+ }
903
+ catch (err) {
904
+ return `Update error: ${err instanceof Error ? err.message : String(err)}`;
905
+ }
906
+ },
907
+ }),
858
908
  ];
859
909
  }
860
910
  function formatAge(date) {
package/dist/daemon.js CHANGED
@@ -8,7 +8,7 @@ import { PID_FILE_PATH } from "./paths.js";
8
8
  import { closeDb, getDb } from "./store/db.js";
9
9
  import { createBot, sendProactiveMessage, sendWorkerNotification, startBot, stopBot } from "./telegram/bot.js";
10
10
  import { startCronScheduler, stopCronScheduler } from "./cron/scheduler.js";
11
- import { checkForUpdate } from "./update.js";
11
+ import { checkForUpdate, getDismissedVersion, isAutoUpdateEnabled, scheduleUpdateCheck, shouldCheckUpdate, stopUpdateCheck } from "./update.js";
12
12
  // Log the active CA bundle (injected by cli.ts via re-exec).
13
13
  if (process.env.NODE_EXTRA_CA_CERTS) {
14
14
  console.log(`[nzb] Using system CA bundle: ${process.env.NODE_EXTRA_CA_CERTS}`);
@@ -152,18 +152,60 @@ async function main() {
152
152
  console.log("[nzb] Telegram user ID missing — skipping bot. Run 'nzb setup' and enter your Telegram user ID (get it from @userinfobot).");
153
153
  }
154
154
  console.log("[nzb] NZB is fully operational.");
155
- // Non-blocking update check — notify via console + all active channels
156
- checkForUpdate()
157
- .then(({ updateAvailable, current, latest }) => {
158
- if (updateAvailable) {
159
- const msg = `⬆ Update available: v${current} → v${latest} — run \`nzb update\` to install`;
160
- console.log(`[nzb] ${msg}`);
161
- if (config.telegramEnabled)
155
+ // Non-blocking update check — delayed 30s after startup, respects check interval
156
+ setTimeout(async () => {
157
+ try {
158
+ const autoEnabled = await isAutoUpdateEnabled();
159
+ if (!autoEnabled)
160
+ return;
161
+ const shouldCheck = await shouldCheckUpdate();
162
+ if (!shouldCheck)
163
+ return;
164
+ const result = await checkForUpdate();
165
+ if (result.updateAvailable && result.latest) {
166
+ const dismissed = await getDismissedVersion();
167
+ if (dismissed === result.latest)
168
+ return;
169
+ const msg = `⬆ Update available: v${result.current} → v${result.latest} — run \`nzb update\` to install`;
170
+ console.log(`[nzb] ${msg}`);
171
+ if (config.telegramEnabled) {
172
+ try {
173
+ const { sendUpdateNotification } = await import("./telegram/handlers/update.js");
174
+ const tgBot = (await import("./telegram/bot.js")).getBot();
175
+ if (tgBot && config.authorizedUserId !== undefined) {
176
+ await sendUpdateNotification(tgBot, config.authorizedUserId, result);
177
+ }
178
+ }
179
+ catch {
180
+ // Fallback to plain text
181
+ sendProactiveMessage(msg).catch(() => { });
182
+ }
183
+ }
184
+ broadcastToSSE(msg);
185
+ }
186
+ }
187
+ catch {
188
+ // Silent — network may be unavailable
189
+ }
190
+ }, 30_000);
191
+ // Schedule periodic update checks (every 6 hours)
192
+ scheduleUpdateCheck(async (result) => {
193
+ const msg = `⬆ Update available: v${result.current} → v${result.latest} — run \`nzb update\` to install`;
194
+ console.log(`[nzb] ${msg}`);
195
+ if (config.telegramEnabled) {
196
+ try {
197
+ const { sendUpdateNotification } = await import("./telegram/handlers/update.js");
198
+ const tgBot = (await import("./telegram/bot.js")).getBot();
199
+ if (tgBot && config.authorizedUserId !== undefined) {
200
+ await sendUpdateNotification(tgBot, config.authorizedUserId, result);
201
+ }
202
+ }
203
+ catch {
162
204
  sendProactiveMessage(msg).catch(() => { });
163
- broadcastToSSE(msg);
205
+ }
164
206
  }
165
- })
166
- .catch(() => { }); // silent — network may be unavailable
207
+ broadcastToSSE(msg);
208
+ });
167
209
  // Notify user if this is a restart (not a fresh start)
168
210
  if (config.telegramEnabled && process.env.NZB_RESTARTED === "1") {
169
211
  await sendProactiveMessage("I'm back online.").catch(() => { });
@@ -198,6 +240,7 @@ async function shutdown() {
198
240
  // Stop health check timer first
199
241
  stopHealthCheck();
200
242
  stopCronScheduler();
243
+ stopUpdateCheck();
201
244
  if (config.telegramEnabled) {
202
245
  try {
203
246
  await stopBot();
@@ -225,6 +268,7 @@ export async function restartDaemon() {
225
268
  console.log("[nzb] Restarting...");
226
269
  stopHealthCheck();
227
270
  stopCronScheduler();
271
+ stopUpdateCheck();
228
272
  const activeWorkers = getWorkers();
229
273
  const runningCount = Array.from(activeWorkers.values()).filter((w) => w.status === "running").length;
230
274
  if (runningCount > 0) {
@@ -17,6 +17,7 @@ import { registerMediaHandlers } from "./handlers/media.js";
17
17
  import { registerReactionHandlers } from "./handlers/reactions.js";
18
18
  import { registerMessageHandler } from "./handlers/streaming.js";
19
19
  import { registerSmartSuggestionHandlers } from "./handlers/suggestions.js";
20
+ import { registerUpdateHandlers } from "./handlers/update.js";
20
21
  import { initLogChannel, logError, logInfo } from "./log-channel.js";
21
22
  import { createMenus } from "./menus.js";
22
23
  let bot;
@@ -106,6 +107,7 @@ export function createBot() {
106
107
  // --- Handler registrations ---
107
108
  registerCallbackHandlers(bot);
108
109
  registerCronHandlers(bot);
110
+ registerUpdateHandlers(bot);
109
111
  registerInlineQueryHandler(bot);
110
112
  registerSmartSuggestionHandlers(bot);
111
113
  registerReactionHandlers(bot);
@@ -155,6 +157,7 @@ export async function startBot() {
155
157
  { command: "skills", description: "List installed skills" },
156
158
  { command: "memory", description: "Show stored memories" },
157
159
  { command: "cron", description: "Manage cron jobs" },
160
+ { command: "update", description: "Check for updates" },
158
161
  { command: "settings", description: "Bot settings" },
159
162
  { command: "restart", description: "Restart NZB" },
160
163
  ]);
@@ -32,6 +32,7 @@ export function registerCommandHandlers(bot, deps) {
32
32
  "/skills — Installed skills\n" +
33
33
  "/workers — Active worker sessions\n" +
34
34
  "/cron — Manage cron jobs\n" +
35
+ "/update — Check for updates\n" +
35
36
  "/restart — Restart NZB\n\n" +
36
37
  "⚡ Breakthrough Features:\n" +
37
38
  "• @bot query — Use me inline in any chat!\n" +
@@ -0,0 +1,211 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { checkForUpdate, dismissVersion, getChangelog, getCurrentVersion, isAutoUpdateEnabled, performUpdate, toggleAutoUpdate, } from "../../update.js";
3
+ /** Build the update menu inline keyboard. */
4
+ async function buildUpdateKeyboard() {
5
+ const autoEnabled = await isAutoUpdateEnabled();
6
+ return new InlineKeyboard()
7
+ .text("🔍 Check Update", "update:check")
8
+ .text("⬆️ Update Now", "update:now")
9
+ .row()
10
+ .text("📋 Changelog", "update:changelog")
11
+ .text(`⚙️ Auto-Update: ${autoEnabled ? "ON" : "OFF"}`, "update:auto:toggle");
12
+ }
13
+ /** Build the notification keyboard shown when an update is available. */
14
+ export function buildUpdateNotificationKeyboard() {
15
+ return new InlineKeyboard()
16
+ .text("⬆️ Update Now", "update:now")
17
+ .text("📋 Changelog", "update:changelog")
18
+ .row()
19
+ .text("❌ Dismiss", "update:dismiss");
20
+ }
21
+ /** Format an update check result for display. */
22
+ function formatCheckResult(result) {
23
+ if (!result.checkSucceeded) {
24
+ return "❌ Could not reach npm registry. Check your network and try again.";
25
+ }
26
+ if (result.updateAvailable) {
27
+ const published = result.publishedAt
28
+ ? `\nPublished: ${new Date(result.publishedAt).toLocaleDateString()}`
29
+ : "";
30
+ return `🆕 Update available!\n\nCurrent: v${result.current}\nLatest: v${result.latest}${published}`;
31
+ }
32
+ return `✅ NZB v${result.current} is up to date.`;
33
+ }
34
+ /** Register the /update command and callback handlers. */
35
+ export function registerUpdateHandlers(bot) {
36
+ // /update command — show update menu
37
+ bot.command("update", async (ctx) => {
38
+ const keyboard = await buildUpdateKeyboard();
39
+ const version = getCurrentVersion();
40
+ await ctx.reply(`📦 NZB Update Manager (v${version})`, { reply_markup: keyboard });
41
+ });
42
+ // Check for updates
43
+ bot.callbackQuery("update:check", async (ctx) => {
44
+ try {
45
+ await ctx.answerCallbackQuery({ text: "Checking for updates..." });
46
+ const result = await checkForUpdate();
47
+ const text = formatCheckResult(result);
48
+ if (result.updateAvailable) {
49
+ await ctx.editMessageText(text, { reply_markup: buildUpdateNotificationKeyboard() });
50
+ }
51
+ else {
52
+ const keyboard = await buildUpdateKeyboard();
53
+ await ctx.editMessageText(text, { reply_markup: keyboard });
54
+ }
55
+ }
56
+ catch (err) {
57
+ console.error("[nzb] Update check callback error:", err instanceof Error ? err.message : err);
58
+ await ctx.answerCallbackQuery({
59
+ text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
60
+ show_alert: true,
61
+ }).catch(() => { });
62
+ }
63
+ });
64
+ // Update now — show confirmation dialog
65
+ bot.callbackQuery("update:now", async (ctx) => {
66
+ try {
67
+ await ctx.answerCallbackQuery();
68
+ const result = await checkForUpdate();
69
+ if (!result.checkSucceeded) {
70
+ await ctx.editMessageText("❌ Cannot reach npm registry. Try again later.");
71
+ return;
72
+ }
73
+ if (!result.updateAvailable) {
74
+ const keyboard = await buildUpdateKeyboard();
75
+ await ctx.editMessageText(`✅ NZB v${result.current} is already the latest version.`, {
76
+ reply_markup: keyboard,
77
+ });
78
+ return;
79
+ }
80
+ const confirmKeyboard = new InlineKeyboard()
81
+ .text("✅ Yes, update now", "update:confirm")
82
+ .text("❌ Cancel", "update:cancel");
83
+ await ctx.editMessageText(`⚠️ Update NZB?\n\nv${result.current} → v${result.latest}\n\nThis will install the new version and restart the daemon.`, { reply_markup: confirmKeyboard });
84
+ }
85
+ catch (err) {
86
+ console.error("[nzb] Update now callback error:", err instanceof Error ? err.message : err);
87
+ await ctx.answerCallbackQuery({
88
+ text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
89
+ show_alert: true,
90
+ }).catch(() => { });
91
+ }
92
+ });
93
+ // Confirm update — actually perform the update
94
+ bot.callbackQuery("update:confirm", async (ctx) => {
95
+ try {
96
+ await ctx.answerCallbackQuery({ text: "Updating..." });
97
+ await ctx.editMessageText("⏳ Installing update... This may take a minute.");
98
+ const result = await performUpdate();
99
+ if (!result.ok) {
100
+ const keyboard = await buildUpdateKeyboard();
101
+ await ctx.editMessageText(`❌ Update failed:\n${result.output}`, { reply_markup: keyboard });
102
+ return;
103
+ }
104
+ await ctx.editMessageText("✅ Update installed! Restarting NZB...");
105
+ // Restart daemon after a short delay
106
+ setTimeout(async () => {
107
+ try {
108
+ const { restartDaemon } = await import("../../daemon.js");
109
+ await restartDaemon();
110
+ }
111
+ catch (err) {
112
+ console.error("[nzb] Post-update restart failed:", err);
113
+ }
114
+ }, 1000);
115
+ }
116
+ catch (err) {
117
+ console.error("[nzb] Update confirm callback error:", err instanceof Error ? err.message : err);
118
+ await ctx.answerCallbackQuery({
119
+ text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
120
+ show_alert: true,
121
+ }).catch(() => { });
122
+ }
123
+ });
124
+ // Cancel update
125
+ bot.callbackQuery("update:cancel", async (ctx) => {
126
+ try {
127
+ await ctx.answerCallbackQuery({ text: "Cancelled" });
128
+ const keyboard = await buildUpdateKeyboard();
129
+ const version = getCurrentVersion();
130
+ await ctx.editMessageText(`📦 NZB Update Manager (v${version})`, { reply_markup: keyboard });
131
+ }
132
+ catch (err) {
133
+ console.error("[nzb] Update cancel callback error:", err instanceof Error ? err.message : err);
134
+ await ctx.answerCallbackQuery({
135
+ text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
136
+ show_alert: true,
137
+ }).catch(() => { });
138
+ }
139
+ });
140
+ // Show changelog
141
+ bot.callbackQuery("update:changelog", async (ctx) => {
142
+ try {
143
+ await ctx.answerCallbackQuery({ text: "Fetching changelog..." });
144
+ const entries = await getChangelog(8);
145
+ if (entries.length === 0) {
146
+ await ctx.editMessageText("📋 Could not fetch version history.", {
147
+ reply_markup: await buildUpdateKeyboard(),
148
+ });
149
+ return;
150
+ }
151
+ const current = getCurrentVersion();
152
+ const lines = entries.map((e) => {
153
+ const marker = e.version === current ? " ← current" : "";
154
+ return `• v${e.version} (${e.date})${marker}`;
155
+ });
156
+ const keyboard = await buildUpdateKeyboard();
157
+ await ctx.editMessageText(`📋 Recent versions:\n\n${lines.join("\n")}`, { reply_markup: keyboard });
158
+ }
159
+ catch (err) {
160
+ console.error("[nzb] Changelog callback error:", err instanceof Error ? err.message : err);
161
+ await ctx.answerCallbackQuery({
162
+ text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
163
+ show_alert: true,
164
+ }).catch(() => { });
165
+ }
166
+ });
167
+ // Toggle auto-update
168
+ bot.callbackQuery("update:auto:toggle", async (ctx) => {
169
+ try {
170
+ const newState = await toggleAutoUpdate();
171
+ await ctx.answerCallbackQuery({ text: `Auto-Update ${newState ? "ON" : "OFF"}` });
172
+ const keyboard = await buildUpdateKeyboard();
173
+ const version = getCurrentVersion();
174
+ await ctx.editMessageText(`📦 NZB Update Manager (v${version})`, { reply_markup: keyboard });
175
+ }
176
+ catch (err) {
177
+ console.error("[nzb] Auto-update toggle error:", err instanceof Error ? err.message : err);
178
+ await ctx.answerCallbackQuery({
179
+ text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
180
+ show_alert: true,
181
+ }).catch(() => { });
182
+ }
183
+ });
184
+ // Dismiss update notification
185
+ bot.callbackQuery("update:dismiss", async (ctx) => {
186
+ try {
187
+ const result = await checkForUpdate();
188
+ if (result.latest) {
189
+ await dismissVersion(result.latest);
190
+ }
191
+ await ctx.answerCallbackQuery({ text: "Dismissed" });
192
+ await ctx.editMessageText(`📦 NZB v${result.current} — update notification dismissed.`);
193
+ }
194
+ catch (err) {
195
+ console.error("[nzb] Dismiss callback error:", err instanceof Error ? err.message : err);
196
+ await ctx.answerCallbackQuery({
197
+ text: `Error: ${err instanceof Error ? err.message : "Unknown error"}`,
198
+ show_alert: true,
199
+ }).catch(() => { });
200
+ }
201
+ });
202
+ }
203
+ /** Send a proactive update notification to a chat. */
204
+ export async function sendUpdateNotification(bot, chatId, result) {
205
+ const keyboard = buildUpdateNotificationKeyboard();
206
+ const published = result.publishedAt
207
+ ? ` (${new Date(result.publishedAt).toLocaleDateString()})`
208
+ : "";
209
+ await bot.api.sendMessage(chatId, `🆕 NZB v${result.latest} available!${published}\nCurrent: v${result.current}\n\nRun \`nzb update\` or use the buttons below.`, { reply_markup: keyboard, parse_mode: "Markdown" });
210
+ }
211
+ //# sourceMappingURL=update.js.map
package/dist/update.js CHANGED
@@ -4,6 +4,9 @@ import { dirname, join } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
  const PKG_NAME = "@iletai/nzb";
7
+ const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}`;
8
+ const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
9
+ let updateCheckTimer;
7
10
  function getPackageJson() {
8
11
  try {
9
12
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
@@ -54,11 +57,37 @@ function isNewer(local, remote) {
54
57
  export async function checkForUpdate() {
55
58
  const current = getLocalVersion();
56
59
  const latest = await getLatestVersion();
60
+ let publishedAt;
61
+ // Try to fetch publish date from registry
62
+ if (latest) {
63
+ try {
64
+ const res = await fetch(`${NPM_REGISTRY_URL}/latest`, { signal: AbortSignal.timeout(10_000) });
65
+ if (res.ok) {
66
+ const data = (await res.json());
67
+ const time = data.time;
68
+ if (time && latest in time) {
69
+ publishedAt = time[latest];
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // Expected: registry may be unreachable
75
+ }
76
+ }
77
+ // Record the check timestamp
78
+ try {
79
+ const { setState } = await import("./store/db.js");
80
+ setState("last_update_check", new Date().toISOString());
81
+ }
82
+ catch {
83
+ // Expected: DB may not be initialized during CLI usage
84
+ }
57
85
  return {
58
86
  current,
59
87
  latest,
60
88
  updateAvailable: latest !== null && isNewer(current, latest),
61
89
  checkSucceeded: latest !== null,
90
+ publishedAt,
62
91
  };
63
92
  }
64
93
  /** Run `npm install -g <pkg>@latest` and return success/failure. */
@@ -67,7 +96,23 @@ export async function performUpdate() {
67
96
  const { name } = getPackageJson();
68
97
  const output = execSync(`npm install -g ${name}@latest`, {
69
98
  encoding: "utf-8",
70
- timeout: 60_000,
99
+ timeout: 120_000,
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ });
102
+ return { ok: true, output: output.trim() };
103
+ }
104
+ catch (err) {
105
+ const msg = err.stderr?.trim() || err.message || "Unknown error";
106
+ return { ok: false, output: msg };
107
+ }
108
+ }
109
+ /** Force update even if the same version is installed. */
110
+ export async function performForceUpdate() {
111
+ try {
112
+ const { name } = getPackageJson();
113
+ const output = execSync(`npm install -g ${name}@latest --force`, {
114
+ encoding: "utf-8",
115
+ timeout: 120_000,
71
116
  stdio: ["ignore", "pipe", "pipe"],
72
117
  });
73
118
  return { ok: true, output: output.trim() };
@@ -77,4 +122,128 @@ export async function performUpdate() {
77
122
  return { ok: false, output: msg };
78
123
  }
79
124
  }
125
+ /**
126
+ * Check if enough time has passed since the last update check.
127
+ * Returns true if we should check now (>= 6 hours since last check).
128
+ */
129
+ export async function shouldCheckUpdate() {
130
+ try {
131
+ const { getState } = await import("./store/db.js");
132
+ const last = getState("last_update_check");
133
+ if (!last)
134
+ return true;
135
+ const elapsed = Date.now() - new Date(last).getTime();
136
+ return elapsed >= CHECK_INTERVAL_MS;
137
+ }
138
+ catch {
139
+ // DB not ready — check anyway
140
+ return true;
141
+ }
142
+ }
143
+ /**
144
+ * Check if auto-update notifications are enabled.
145
+ * Defaults to true if not explicitly set.
146
+ */
147
+ export async function isAutoUpdateEnabled() {
148
+ try {
149
+ const { getState } = await import("./store/db.js");
150
+ const val = getState("auto_update_enabled");
151
+ return val !== "false";
152
+ }
153
+ catch {
154
+ return true;
155
+ }
156
+ }
157
+ /** Toggle auto-update notifications on/off. Returns the new state. */
158
+ export async function toggleAutoUpdate() {
159
+ const { getState, setState } = await import("./store/db.js");
160
+ const current = getState("auto_update_enabled");
161
+ const newVal = current === "false" ? "true" : "false";
162
+ setState("auto_update_enabled", newVal);
163
+ return newVal === "true";
164
+ }
165
+ /**
166
+ * Get the version that the user dismissed (won't be notified about again).
167
+ */
168
+ export async function getDismissedVersion() {
169
+ try {
170
+ const { getState } = await import("./store/db.js");
171
+ return getState("dismissed_version");
172
+ }
173
+ catch {
174
+ return undefined;
175
+ }
176
+ }
177
+ /** Dismiss update notifications for a specific version. */
178
+ export async function dismissVersion(version) {
179
+ const { setState } = await import("./store/db.js");
180
+ setState("dismissed_version", version);
181
+ }
182
+ /**
183
+ * Fetch recent version history from npm registry for changelog display.
184
+ * Returns the last N versions with their publish dates.
185
+ */
186
+ export async function getChangelog(limit = 5) {
187
+ try {
188
+ const res = await fetch(NPM_REGISTRY_URL, { signal: AbortSignal.timeout(15_000) });
189
+ if (!res.ok)
190
+ return [];
191
+ const data = (await res.json());
192
+ if (!data.time || !data.versions)
193
+ return [];
194
+ const versions = Object.keys(data.versions)
195
+ .filter((v) => v in data.time && v !== "created" && v !== "modified")
196
+ .sort((a, b) => {
197
+ const dateA = new Date(data.time[a]).getTime();
198
+ const dateB = new Date(data.time[b]).getTime();
199
+ return dateB - dateA;
200
+ })
201
+ .slice(0, limit);
202
+ return versions.map((v) => ({
203
+ version: v,
204
+ date: new Date(data.time[v]).toISOString().split("T")[0],
205
+ }));
206
+ }
207
+ catch {
208
+ return [];
209
+ }
210
+ }
211
+ /** Get the current local version. */
212
+ export function getCurrentVersion() {
213
+ return getLocalVersion();
214
+ }
215
+ /**
216
+ * Schedule periodic update checks (every 6 hours).
217
+ * Calls the provided callback when an update is found.
218
+ */
219
+ export function scheduleUpdateCheck(onUpdateFound) {
220
+ if (updateCheckTimer)
221
+ return; // already scheduled
222
+ updateCheckTimer = setInterval(async () => {
223
+ try {
224
+ const autoEnabled = await isAutoUpdateEnabled();
225
+ if (!autoEnabled)
226
+ return;
227
+ const result = await checkForUpdate();
228
+ if (result.updateAvailable && result.latest) {
229
+ const dismissed = await getDismissedVersion();
230
+ if (dismissed === result.latest)
231
+ return;
232
+ onUpdateFound(result);
233
+ }
234
+ }
235
+ catch {
236
+ // Silent — network may be unavailable
237
+ }
238
+ }, CHECK_INTERVAL_MS);
239
+ updateCheckTimer.unref();
240
+ console.log("[nzb] Update check scheduled (every 6 hours)");
241
+ }
242
+ /** Stop the periodic update check timer. */
243
+ export function stopUpdateCheck() {
244
+ if (updateCheckTimer) {
245
+ clearInterval(updateCheckTimer);
246
+ updateCheckTimer = undefined;
247
+ }
248
+ }
80
249
  //# sourceMappingURL=update.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iletai/nzb",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "NZB — a personal AI assistant for developers, built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "nzb": "dist/cli.js"