auq-mcp-server 2.4.0 → 2.5.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.
Files changed (32) hide show
  1. package/README.md +40 -0
  2. package/dist/bin/auq.js +40 -0
  3. package/dist/bin/tui-app.js +114 -1
  4. package/dist/package.json +1 -1
  5. package/dist/src/cli/commands/sessions.js +138 -2
  6. package/dist/src/cli/commands/update.js +124 -0
  7. package/dist/src/config/__tests__/updateCheck.test.js +34 -0
  8. package/dist/src/config/defaults.js +2 -0
  9. package/dist/src/config/types.js +2 -0
  10. package/dist/src/tui/components/Footer.js +4 -1
  11. package/dist/src/tui/components/Header.js +3 -1
  12. package/dist/src/tui/components/UpdateBadge.js +29 -0
  13. package/dist/src/tui/components/UpdateOverlay.js +199 -0
  14. package/dist/src/tui/constants/keybindings.js +3 -0
  15. package/dist/src/update/__tests__/cache.test.js +136 -0
  16. package/dist/src/update/__tests__/changelog.test.js +86 -0
  17. package/dist/src/update/__tests__/checker.test.js +148 -0
  18. package/dist/src/update/__tests__/index.test.js +37 -0
  19. package/dist/src/update/__tests__/installer.test.js +117 -0
  20. package/dist/src/update/__tests__/package-manager.test.js +73 -0
  21. package/dist/src/update/__tests__/version.test.js +74 -0
  22. package/dist/src/update/cache.js +74 -0
  23. package/dist/src/update/changelog.js +63 -0
  24. package/dist/src/update/checker.js +121 -0
  25. package/dist/src/update/index.js +15 -0
  26. package/dist/src/update/installer.js +51 -0
  27. package/dist/src/update/package-manager.js +49 -0
  28. package/dist/src/update/types.js +7 -0
  29. package/dist/src/update/version.js +114 -0
  30. package/package.json +1 -1
  31. package/dist/src/tui/components/Spinner.js +0 -19
  32. package/dist/src/tui/utils/__tests__/detectTheme.test.js +0 -78
package/README.md CHANGED
@@ -248,6 +248,7 @@ It is recommended to **disable** the built-in questioning tool in your harness (
248
248
  # you won't likely need these at all
249
249
  auq server # Start MCP server
250
250
  auq --version # Show version
251
+ auq update # Check for and install updates
251
252
  auq --help # Show help
252
253
  ```
253
254
 
@@ -334,6 +335,43 @@ When an AI client disconnects, associated sessions are marked as "abandoned". Th
334
335
 
335
336
  ---
336
337
 
338
+ ### Auto-Update
339
+
340
+ AUQ automatically checks for updates and keeps itself up to date.
341
+
342
+ #### How it works
343
+
344
+ - **Patch updates** (e.g., 2.4.0 → 2.4.1): Automatically installed when the TUI starts. These are bug fixes and minor improvements.
345
+ - **Minor/Major updates** (e.g., 2.4.0 → 2.5.0 or 3.0.0): A fullscreen prompt is shown with changelog and options to update, skip, or defer.
346
+ - **CLI notification**: When running non-TUI commands, a one-line update notification is shown if a newer version is available.
347
+
348
+ #### Manual update
349
+
350
+ Run `auq update` to manually check for and install updates:
351
+
352
+ ```bash
353
+ auq update # Interactive update check
354
+ auq update -y # Skip confirmation prompt
355
+ ```
356
+
357
+ #### Disabling update checks
358
+
359
+ Disable automatic update checks via config:
360
+
361
+ ```bash
362
+ auq config set updateCheck false
363
+ ```
364
+
365
+ Or set the environment variable:
366
+
367
+ ```bash
368
+ NO_UPDATE_NOTIFIER=1 auq ask "question"
369
+ ```
370
+
371
+ Update checks are automatically disabled in CI environments (`CI=true`).
372
+
373
+ The `auq update` command always works regardless of these settings.
374
+
337
375
  ### 🎨 Themes
338
376
 
339
377
  AUQ supports **16 built-in color themes** with automatic persistence. Press `Ctrl+T` to cycle through themes.
@@ -518,6 +556,7 @@ _Settings from local config override global config, which overrides defaults._
518
556
  "language": "auto",
519
557
  "theme": "system",
520
558
  "autoSelectRecommended": true,
559
+ "updateCheck": true,
521
560
  "notifications": {
522
561
  "enabled": true,
523
562
  "sound": true
@@ -543,6 +582,7 @@ _Settings from local config override global config, which overrides defaults._
543
582
  | `staleThreshold` | number | 7200000 | 0+ (milliseconds) | Time before a session is considered stale (2 hours) |
544
583
  | `notifyOnStale` | boolean | true | true/false | Show toast notification when sessions become stale |
545
584
  | `staleAction` | string | "warn" | "warn", "remove", "archive" | Action for stale sessions |
585
+ | `updateCheck` | boolean | true | true/false | Enable automatic update checks on startup |
546
586
 
547
587
  </details>
548
588
 
package/dist/bin/auq.js CHANGED
@@ -20,6 +20,7 @@ Commands:
20
20
  answer <id> [flags] Answer or reject a session
21
21
  sessions <sub> [flags] List/dismiss sessions
22
22
  config <sub> [flags] Get/set configuration
23
+ update Check for and install updates
23
24
 
24
25
  Answer:
25
26
  auq answer <id> --answers '<json>' Submit answers
@@ -28,6 +29,7 @@ Answer:
28
29
 
29
30
  Sessions:
30
31
  auq sessions list [--pending|--stale|--all] [--json]
32
+ auq sessions show <id> [--json]
31
33
  auq sessions dismiss <id> [--force] [--json]
32
34
 
33
35
  Config:
@@ -84,6 +86,40 @@ if (command === "server") {
84
86
  // Keep process alive
85
87
  await new Promise(() => { });
86
88
  }
89
+ // Handle 'update' command
90
+ if (command === "update") {
91
+ const { runUpdateCommand } = await import("../src/cli/commands/update.js");
92
+ await runUpdateCommand(args.slice(1));
93
+ process.exit(0);
94
+ }
95
+ // ── Fire-and-forget update notification ────────────────────────────
96
+ // Start a non-blocking update check for non-TUI CLI commands.
97
+ // The result is awaited briefly after the main command finishes.
98
+ let updateNotification = null;
99
+ if (command &&
100
+ !["server", "--help", "-h", "--version", "-v", "update"].includes(command)) {
101
+ updateNotification = (async () => {
102
+ try {
103
+ if (process.env.NO_UPDATE_NOTIFIER === "1" ||
104
+ process.env.CI === "true" ||
105
+ process.env.CI === "1" ||
106
+ process.env.NODE_ENV === "test")
107
+ return;
108
+ const { UpdateChecker } = await import("../src/update/index.js");
109
+ const checker = new UpdateChecker();
110
+ const result = await Promise.race([
111
+ checker.check(),
112
+ new Promise((r) => setTimeout(() => r(null), 5000)),
113
+ ]);
114
+ if (result) {
115
+ process.stderr.write(`Update available: ${result.currentVersion} \u2192 ${result.latestVersion}. Run \`auq update\` to upgrade.\n`);
116
+ }
117
+ }
118
+ catch {
119
+ // Silently ignore — update checks must never break the main command
120
+ }
121
+ })();
122
+ }
87
123
  // Handle 'ask' command
88
124
  if (command === "ask") {
89
125
  const { SessionManager } = await import("../src/session/index.js");
@@ -143,6 +179,7 @@ if (command === "ask") {
143
179
  const callId = randomUUID();
144
180
  const { formattedResponse, sessionId } = await sessionManager.startSession(questions, callId, workingDirectory);
145
181
  console.log(formattedResponse);
182
+ await updateNotification;
146
183
  process.exit(0);
147
184
  }
148
185
  catch (error) {
@@ -160,18 +197,21 @@ if (command === "ask") {
160
197
  if (command === "answer") {
161
198
  const { runAnswerCommand } = await import("../src/cli/commands/answer.js");
162
199
  await runAnswerCommand(args.slice(1));
200
+ await updateNotification;
163
201
  process.exit(0);
164
202
  }
165
203
  // Handle 'sessions' command
166
204
  if (command === "sessions") {
167
205
  const { runSessionsCommand } = await import("../src/cli/commands/sessions.js");
168
206
  await runSessionsCommand(args.slice(1));
207
+ await updateNotification;
169
208
  process.exit(0);
170
209
  }
171
210
  // Handle 'config' command
172
211
  if (command === "config") {
173
212
  const { runConfigCommand } = await import("../src/cli/commands/config.js");
174
213
  await runConfigCommand(args.slice(1));
214
+ await updateNotification;
175
215
  process.exit(0);
176
216
  }
177
217
  // Default: Start TUI
@@ -15,6 +15,8 @@ import { isSessionStale, isSessionAbandoned, formatStaleToastMessage, } from "..
15
15
  import { ThemeProvider } from "../src/tui/ThemeProvider.js";
16
16
  import { ConfigProvider } from "../src/tui/ConfigContext.js";
17
17
  import { getAdjustedIndexAfterRemoval, getDirectJumpIndex, getNextSessionIndex, getPrevSessionIndex, } from "../src/tui/utils/sessionSwitching.js";
18
+ import { UpdateChecker, fetchChangelog, installUpdate, detectPackageManager, readCache, writeCache, } from "../src/update/index.js";
19
+ import { UpdateOverlay } from "../src/tui/components/UpdateOverlay.js";
18
20
  import { KEYS } from "../src/tui/constants/keybindings.js";
19
21
  const App = ({ config }) => {
20
22
  const [state, setState] = useState({ mode: "WAITING" });
@@ -29,6 +31,12 @@ const App = ({ config }) => {
29
31
  const [sessionMeta, setSessionMeta] = useState(new Map());
30
32
  const [lastInteractions, setLastInteractions] = useState(new Map());
31
33
  const [staleToastShown, setStaleToastShown] = useState(new Set());
34
+ const [updateInfo, setUpdateInfo] = useState(null);
35
+ const [showUpdateOverlay, setShowUpdateOverlay] = useState(false);
36
+ const [isInstallingUpdate, setIsInstallingUpdate] = useState(false);
37
+ const [installError, setInstallError] = useState(null);
38
+ const [changelogContent, setChangelogContent] = useState(null);
39
+ const [updateDismissed, setUpdateDismissed] = useState(false);
32
40
  // Get session directory for logging
33
41
  const sessionDir = getSessionDirectory();
34
42
  // Notification configuration from config
@@ -124,6 +132,45 @@ const App = ({ config }) => {
124
132
  clearProgress(notificationConfig);
125
133
  };
126
134
  }, [notificationConfig]);
135
+ // ── Auto-update checker ─────────────────────────────────────
136
+ useEffect(() => {
137
+ // Skip update checks if disabled
138
+ if (config?.updateCheck === false)
139
+ return;
140
+ if (process.env.NO_UPDATE_NOTIFIER === "1")
141
+ return;
142
+ if (process.env.CI === "true" || process.env.CI === "1")
143
+ return;
144
+ if (process.env.NODE_ENV === "test")
145
+ return;
146
+ if (!process.stdout.isTTY)
147
+ return;
148
+ const checker = new UpdateChecker();
149
+ let intervalId = null;
150
+ const runCheck = async () => {
151
+ try {
152
+ const result = await checker.check();
153
+ if (result) {
154
+ setUpdateInfo(result);
155
+ // Fetch changelog for the overlay
156
+ const changelog = await fetchChangelog(result.latestVersion);
157
+ setChangelogContent(changelog.content);
158
+ }
159
+ }
160
+ catch {
161
+ // Silently fail — update checks should never break the TUI
162
+ }
163
+ };
164
+ runCheck();
165
+ intervalId = setInterval(() => {
166
+ checker.clearCache();
167
+ runCheck();
168
+ }, 3600000); // 1 hour
169
+ return () => {
170
+ if (intervalId)
171
+ clearInterval(intervalId);
172
+ };
173
+ }, [config?.updateCheck]);
127
174
  // Auto-transition: WAITING → PROCESSING when queue has items
128
175
  useEffect(() => {
129
176
  if (!isInitialized)
@@ -282,6 +329,51 @@ const App = ({ config }) => {
282
329
  const handleFlowStateChange = useCallback((flowState) => {
283
330
  setIsInReviewOrRejection(flowState.showReview || flowState.showRejectionConfirm);
284
331
  }, []);
332
+ // ── Auto-update handlers ────────────────────────────────────
333
+ const handleUpdateInstall = async () => {
334
+ try {
335
+ setIsInstallingUpdate(true);
336
+ setInstallError(null);
337
+ const pm = detectPackageManager();
338
+ const success = await installUpdate(pm);
339
+ if (success) {
340
+ setShowUpdateOverlay(false);
341
+ setToast({
342
+ message: `Updated to v${updateInfo.latestVersion}. Please restart auq.`,
343
+ type: "success",
344
+ });
345
+ // Exit after short delay so user sees the message
346
+ setTimeout(() => process.exit(0), 2000);
347
+ }
348
+ else {
349
+ setInstallError("Installation failed. Please try manually.");
350
+ }
351
+ setIsInstallingUpdate(false);
352
+ }
353
+ catch (err) {
354
+ setIsInstallingUpdate(false);
355
+ setInstallError(err instanceof Error ? err.message : "Installation failed");
356
+ }
357
+ };
358
+ const handleSkipVersion = async () => {
359
+ if (updateInfo) {
360
+ try {
361
+ const cache = await readCache();
362
+ if (cache) {
363
+ await writeCache({ ...cache, skippedVersion: updateInfo.latestVersion });
364
+ }
365
+ }
366
+ catch {
367
+ // Non-critical — skip-version simply won't persist
368
+ }
369
+ }
370
+ setShowUpdateOverlay(false);
371
+ setUpdateInfo(null);
372
+ };
373
+ const handleRemindLater = () => {
374
+ setShowUpdateOverlay(false);
375
+ setUpdateDismissed(true);
376
+ };
285
377
  const switchToSession = useCallback((targetIndex) => {
286
378
  if (state.mode !== "PROCESSING" || sessionQueue.length <= 1) {
287
379
  return;
@@ -343,8 +435,23 @@ const App = ({ config }) => {
343
435
  isActive: state.mode === "PROCESSING" &&
344
436
  !isInReviewOrRejection &&
345
437
  !showSessionPicker &&
438
+ !showUpdateOverlay &&
346
439
  sessionQueue.length >= 2,
347
440
  });
441
+ // Update overlay keyboard shortcut (independent of session count)
442
+ useInput((input, key) => {
443
+ if (!key.ctrl && !key.meta && input === KEYS.UPDATE) {
444
+ if (updateInfo && !showUpdateOverlay) {
445
+ setShowUpdateOverlay(true);
446
+ }
447
+ }
448
+ }, {
449
+ isActive: state.mode === "PROCESSING" &&
450
+ !isInReviewOrRejection &&
451
+ !showSessionPicker &&
452
+ !showUpdateOverlay &&
453
+ !!updateInfo,
454
+ });
348
455
  // Handle session completion
349
456
  const handleSessionComplete = (wasRejected = false, rejectionReason) => {
350
457
  // Clear progress bar on session completion
@@ -407,7 +514,12 @@ const App = ({ config }) => {
407
514
  React.createElement(Box, { flexDirection: "column", paddingX: 1 },
408
515
  React.createElement(Header, { pendingCount: state.mode === "PROCESSING"
409
516
  ? Math.max(0, sessionQueue.length - 1)
410
- : sessionQueue.length }),
517
+ : sessionQueue.length, updateInfo: !showUpdateOverlay && updateInfo
518
+ ? {
519
+ updateType: updateInfo.updateType,
520
+ latestVersion: updateInfo.latestVersion,
521
+ }
522
+ : null, onUpdateBadgeActivate: () => setShowUpdateOverlay(true) }),
411
523
  mainContent,
412
524
  state.mode === "PROCESSING" && sessionQueue.length >= 2 && (React.createElement(SessionDots, { sessions: sessionQueue.map((s) => ({
413
525
  ...s,
@@ -428,6 +540,7 @@ const App = ({ config }) => {
428
540
  switchToSession(idx);
429
541
  setShowSessionPicker(false);
430
542
  }, onClose: () => setShowSessionPicker(false) })),
543
+ showUpdateOverlay && updateInfo && (React.createElement(UpdateOverlay, { isOpen: showUpdateOverlay, currentVersion: updateInfo.currentVersion, latestVersion: updateInfo.latestVersion, updateType: updateInfo.updateType, changelog: changelogContent, changelogUrl: updateInfo.changelogUrl, isInstalling: isInstallingUpdate, installError: installError, onInstall: handleUpdateInstall, onSkipVersion: handleSkipVersion, onRemindLater: handleRemindLater })),
431
544
  React.createElement(ThemeIndicator, null)))));
432
545
  };
433
546
  export const runTui = (config) => {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"
@@ -1,6 +1,6 @@
1
1
  /**
2
- * CLI Sessions Command — `auq sessions list` and `auq sessions dismiss`
3
- * Manages listing and dismissing/archiving sessions.
2
+ * CLI Sessions Command — `auq sessions list`, `auq sessions show`, and `auq sessions dismiss`
3
+ * Manages listing, viewing, and dismissing/archiving sessions.
4
4
  */
5
5
  import { promises as fs } from "fs";
6
6
  import { join } from "path";
@@ -137,23 +137,159 @@ async function sessionsDismiss(args) {
137
137
  console.log(`Session ${sessionId} dismissed and archived to ${archiveDir}.`);
138
138
  }
139
139
  }
140
+ // ── Sessions Show ─────────────────────────────────────────────────
141
+ async function sessionsShow(args) {
142
+ const { flags, positionals } = parseFlags(args);
143
+ const jsonMode = flags.json === true;
144
+ const sessionId = positionals[0];
145
+ // ── Validate sessionId ──────────────────────────────────────────
146
+ if (!sessionId) {
147
+ outputResult({
148
+ success: false,
149
+ error: "Missing session ID. Usage: auq sessions show <sessionId> [--json]",
150
+ }, jsonMode);
151
+ process.exitCode = 1;
152
+ return;
153
+ }
154
+ // ── Initialise SessionManager ───────────────────────────────────
155
+ const sessionManager = new SessionManager({
156
+ baseDir: getSessionDirectory(),
157
+ });
158
+ await sessionManager.initialize();
159
+ // ── Verify session exists ──────────────────────────────────────
160
+ const exists = await sessionManager.sessionExists(sessionId);
161
+ if (!exists) {
162
+ outputResult({ success: false, error: `Session not found: ${sessionId}` }, jsonMode);
163
+ process.exitCode = 1;
164
+ return;
165
+ }
166
+ // ── Fetch session data ──────────────────────────────────────────
167
+ const status = await sessionManager.getSessionStatus(sessionId);
168
+ const request = await sessionManager.getSessionRequest(sessionId);
169
+ const answersData = await sessionManager.getSessionAnswers(sessionId);
170
+ if (!status || !request) {
171
+ outputResult({ success: false, error: `Could not read session data for: ${sessionId}` }, jsonMode);
172
+ process.exitCode = 1;
173
+ return;
174
+ }
175
+ const questions = request.questions;
176
+ const answers = answersData?.answers ?? null;
177
+ // ── Build answer lookup (questionIndex → UserAnswer) ────────────
178
+ const answerMap = new Map();
179
+ if (answers) {
180
+ for (const a of answers) {
181
+ answerMap.set(a.questionIndex, {
182
+ selectedOption: a.selectedOption,
183
+ selectedOptions: a.selectedOptions,
184
+ customText: a.customText,
185
+ });
186
+ }
187
+ }
188
+ // ── JSON output ─────────────────────────────────────────────────
189
+ if (jsonMode) {
190
+ const result = {
191
+ sessionId,
192
+ status: status.status,
193
+ createdAt: status.createdAt,
194
+ totalQuestions: questions.length,
195
+ questions: questions.map((q, i) => ({
196
+ index: i,
197
+ prompt: q.prompt,
198
+ title: q.title,
199
+ multiSelect: q.multiSelect ?? false,
200
+ options: q.options.map((o) => ({
201
+ label: o.label,
202
+ ...(o.description ? { description: o.description } : {}),
203
+ })),
204
+ })),
205
+ answers: answers
206
+ ? answers.map((a) => ({
207
+ questionIndex: a.questionIndex,
208
+ selectedOption: a.selectedOption ?? null,
209
+ selectedOptions: a.selectedOptions ?? null,
210
+ customText: a.customText ?? null,
211
+ timestamp: a.timestamp,
212
+ }))
213
+ : null,
214
+ };
215
+ console.log(JSON.stringify(result, null, 2));
216
+ return;
217
+ }
218
+ // ── Human-readable output ───────────────────────────────────────
219
+ const age = formatAge(status.createdAt);
220
+ console.log(`Session: ${sessionId}`);
221
+ console.log(`Status: ${status.status} | Created: ${age}`);
222
+ console.log(`Questions: ${questions.length}`);
223
+ console.log("");
224
+ for (let i = 0; i < questions.length; i++) {
225
+ const q = questions[i];
226
+ const selectTag = q.multiSelect ? "[multi-select]" : "[single-select]";
227
+ const answer = answerMap.get(i);
228
+ // Determine which options are selected
229
+ const selectedLabels = new Set();
230
+ if (answer) {
231
+ if (answer.selectedOption)
232
+ selectedLabels.add(answer.selectedOption);
233
+ if (answer.selectedOptions) {
234
+ for (const opt of answer.selectedOptions)
235
+ selectedLabels.add(opt);
236
+ }
237
+ }
238
+ console.log(` ${i + 1}. ${q.prompt} ${selectTag}`);
239
+ for (const opt of q.options) {
240
+ const prefix = selectedLabels.has(opt.label) ? "✓" : "→";
241
+ console.log(` ${prefix} ${opt.label}`);
242
+ if (opt.description) {
243
+ console.log(` ${opt.description}`);
244
+ }
245
+ }
246
+ // Show custom text if provided
247
+ if (answer?.customText) {
248
+ console.log(` ✎ Custom: ${answer.customText}`);
249
+ }
250
+ console.log("");
251
+ }
252
+ // ── Answer summary ──────────────────────────────────────────────
253
+ if (answers && answers.length > 0) {
254
+ const summaryParts = [];
255
+ for (const a of answers) {
256
+ if (a.selectedOption) {
257
+ summaryParts.push(a.selectedOption);
258
+ }
259
+ else if (a.selectedOptions && a.selectedOptions.length > 0) {
260
+ summaryParts.push(a.selectedOptions.join(", "));
261
+ }
262
+ else if (a.customText) {
263
+ summaryParts.push(`"${a.customText}"`);
264
+ }
265
+ }
266
+ if (summaryParts.length > 0) {
267
+ console.log(` (User answered: ${summaryParts.join(", ")})`);
268
+ }
269
+ }
270
+ }
140
271
  // ── Sessions Command Dispatcher ────────────────────────────────────
141
272
  export async function runSessionsCommand(args) {
142
273
  const subcommand = args[0];
143
274
  switch (subcommand) {
144
275
  case "list":
145
276
  return sessionsList(args.slice(1));
277
+ case "show":
278
+ return sessionsShow(args.slice(1));
146
279
  case "dismiss":
147
280
  return sessionsDismiss(args.slice(1));
148
281
  default:
149
282
  console.log("Usage: auq sessions <subcommand>", "\n");
150
283
  console.log("Subcommands:");
151
284
  console.log(" list [--pending|--stale|--all] [--json] List sessions");
285
+ console.log(" show <sessionId> [--json] Show session details");
152
286
  console.log(" dismiss <sessionId> [--force] [--json] Dismiss/archive a session");
153
287
  console.log("");
154
288
  console.log("Examples:");
155
289
  console.log(" auq sessions list");
156
290
  console.log(" auq sessions list --stale --json");
291
+ console.log(" auq sessions show <sessionId>");
292
+ console.log(" auq sessions show <sessionId> --json");
157
293
  console.log(" auq sessions dismiss <sessionId>");
158
294
  console.log(" auq sessions dismiss <sessionId> --force");
159
295
  if (subcommand !== undefined) {
@@ -0,0 +1,124 @@
1
+ /**
2
+ * CLI Update Command — `auq update`
3
+ *
4
+ * Checks for available updates, displays changelog, and installs
5
+ * the latest version using the detected package manager.
6
+ */
7
+ import { createInterface } from "node:readline";
8
+ import { UpdateChecker } from "../../update/checker.js";
9
+ import { fetchChangelog } from "../../update/changelog.js";
10
+ import { detectPackageManager } from "../../update/package-manager.js";
11
+ import { installUpdate, getManualCommand } from "../../update/installer.js";
12
+ import { parseFlags } from "../utils.js";
13
+ /**
14
+ * Prompt the user for input via readline.
15
+ *
16
+ * Uses stderr for the question text to keep stdout clean for piping.
17
+ */
18
+ function prompt(question) {
19
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
20
+ return new Promise((resolve) => {
21
+ rl.question(question, (answer) => {
22
+ rl.close();
23
+ resolve(answer);
24
+ });
25
+ });
26
+ }
27
+ /**
28
+ * Run the `auq update` command.
29
+ *
30
+ * Usage:
31
+ * auq update Check for updates and install interactively
32
+ * auq update -y Check and install without confirmation
33
+ * auq update --yes Same as -y
34
+ * auq update --json Output result as JSON
35
+ */
36
+ export async function runUpdateCommand(args) {
37
+ const { flags } = parseFlags(args);
38
+ const jsonMode = flags.json === true;
39
+ // parseFlags only handles --flag; check raw args for short -y flag
40
+ const skipPrompt = flags.yes === true || args.includes("-y");
41
+ // 1. Check for updates (blocking, with status output)
42
+ process.stderr.write("Checking for updates...\n");
43
+ const checker = new UpdateChecker();
44
+ let result;
45
+ try {
46
+ result = await checker.check();
47
+ }
48
+ catch {
49
+ const msg = "Unable to check for updates. Please check your network connection.";
50
+ if (jsonMode) {
51
+ console.log(JSON.stringify({ success: false, error: msg }, null, 2));
52
+ }
53
+ else {
54
+ process.stderr.write(`\u274c ${msg}\n`);
55
+ }
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ // 2. If no update available
60
+ if (!result) {
61
+ const version = checker["currentVersion"];
62
+ const msg = `Already up to date (v${version})`;
63
+ if (jsonMode) {
64
+ console.log(JSON.stringify({ success: true, upToDate: true, currentVersion: version }, null, 2));
65
+ }
66
+ else {
67
+ process.stderr.write(`\u2714 ${msg}\n`);
68
+ }
69
+ return;
70
+ }
71
+ // 3. Display update info
72
+ process.stderr.write(`\nUpdate available: ${result.currentVersion} \u2192 ${result.latestVersion} (${result.updateType})\n`);
73
+ // 4. Fetch and display changelog
74
+ const changelog = await fetchChangelog(result.latestVersion);
75
+ if (changelog.content) {
76
+ process.stderr.write(`\nChangelog:\n${changelog.content}\n`);
77
+ }
78
+ else {
79
+ process.stderr.write(`\nView changelog: ${changelog.fallbackUrl}\n`);
80
+ }
81
+ // 5. Breaking change warning for major updates
82
+ if (result.updateType === "major") {
83
+ process.stderr.write("\n\u26a0 Breaking changes may be included in this major version update.\n");
84
+ }
85
+ // 6. Confirmation prompt (unless --yes/-y)
86
+ if (!skipPrompt) {
87
+ const answer = await prompt("\nInstall update? (Y/n): ");
88
+ const trimmed = answer.trim().toLowerCase();
89
+ if (trimmed !== "" && trimmed !== "y" && trimmed !== "yes") {
90
+ process.stderr.write("Update cancelled.\n");
91
+ return;
92
+ }
93
+ }
94
+ // 7. Detect package manager and show what will run
95
+ const pm = detectPackageManager();
96
+ const manualCmd = getManualCommand(pm);
97
+ process.stderr.write(`\nInstalling with ${pm.name}: ${manualCmd}\n`);
98
+ // 8. Execute installation
99
+ const success = await installUpdate(pm);
100
+ if (success) {
101
+ const msg = "Update complete! Please restart auq.";
102
+ if (jsonMode) {
103
+ console.log(JSON.stringify({
104
+ success: true,
105
+ upToDate: false,
106
+ previousVersion: result.currentVersion,
107
+ installedVersion: result.latestVersion,
108
+ }, null, 2));
109
+ }
110
+ else {
111
+ process.stderr.write(`\u2705 ${msg}\n`);
112
+ }
113
+ }
114
+ else {
115
+ const msg = `Update failed. Run manually: ${manualCmd}`;
116
+ if (jsonMode) {
117
+ console.log(JSON.stringify({ success: false, error: "Installation failed", manualCommand: manualCmd }, null, 2));
118
+ }
119
+ else {
120
+ process.stderr.write(`\u274c ${msg}\n`);
121
+ }
122
+ process.exitCode = 1;
123
+ }
124
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { AUQConfigSchema } from "../types.js";
3
+ import { DEFAULT_CONFIG } from "../defaults.js";
4
+ describe("updateCheck config", () => {
5
+ it("DEFAULT_CONFIG includes updateCheck: true", () => {
6
+ expect(DEFAULT_CONFIG.updateCheck).toBe(true);
7
+ });
8
+ it("schema accepts updateCheck: true", () => {
9
+ const result = AUQConfigSchema.parse({ updateCheck: true });
10
+ expect(result.updateCheck).toBe(true);
11
+ });
12
+ it("schema accepts updateCheck: false", () => {
13
+ const result = AUQConfigSchema.parse({ updateCheck: false });
14
+ expect(result.updateCheck).toBe(false);
15
+ });
16
+ it("schema defaults updateCheck to true when missing", () => {
17
+ const result = AUQConfigSchema.parse({});
18
+ expect(result.updateCheck).toBe(true);
19
+ });
20
+ it("partial schema retains default for updateCheck when not provided", () => {
21
+ const result = AUQConfigSchema.partial().parse({ maxOptions: 8 });
22
+ // Zod .default(true) still applies even when field is omitted in partial parse
23
+ expect(result.updateCheck).toBe(true);
24
+ });
25
+ it("schema rejects non-boolean updateCheck", () => {
26
+ expect(() => AUQConfigSchema.parse({ updateCheck: "yes" })).toThrow();
27
+ });
28
+ it("updateCheck coexists with other config values", () => {
29
+ const result = AUQConfigSchema.parse({ updateCheck: false });
30
+ // Other defaults should still be set
31
+ expect(result.updateCheck).toBe(false);
32
+ expect(result.maxOptions).toBeDefined();
33
+ });
34
+ });
@@ -15,4 +15,6 @@ export const DEFAULT_CONFIG = {
15
15
  enabled: true,
16
16
  sound: true,
17
17
  },
18
+ // Update
19
+ updateCheck: true,
18
20
  };
@@ -30,4 +30,6 @@ export const AUQConfigSchema = z.object({
30
30
  enabled: true,
31
31
  sound: true,
32
32
  }),
33
+ // Update
34
+ updateCheck: z.boolean().default(true),
33
35
  });