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.
- package/README.md +40 -0
- package/dist/bin/auq.js +40 -0
- package/dist/bin/tui-app.js +114 -1
- package/dist/package.json +1 -1
- package/dist/src/cli/commands/sessions.js +138 -2
- package/dist/src/cli/commands/update.js +124 -0
- package/dist/src/config/__tests__/updateCheck.test.js +34 -0
- package/dist/src/config/defaults.js +2 -0
- package/dist/src/config/types.js +2 -0
- package/dist/src/tui/components/Footer.js +4 -1
- package/dist/src/tui/components/Header.js +3 -1
- package/dist/src/tui/components/UpdateBadge.js +29 -0
- package/dist/src/tui/components/UpdateOverlay.js +199 -0
- package/dist/src/tui/constants/keybindings.js +3 -0
- package/dist/src/update/__tests__/cache.test.js +136 -0
- package/dist/src/update/__tests__/changelog.test.js +86 -0
- package/dist/src/update/__tests__/checker.test.js +148 -0
- package/dist/src/update/__tests__/index.test.js +37 -0
- package/dist/src/update/__tests__/installer.test.js +117 -0
- package/dist/src/update/__tests__/package-manager.test.js +73 -0
- package/dist/src/update/__tests__/version.test.js +74 -0
- package/dist/src/update/cache.js +74 -0
- package/dist/src/update/changelog.js +63 -0
- package/dist/src/update/checker.js +121 -0
- package/dist/src/update/index.js +15 -0
- package/dist/src/update/installer.js +51 -0
- package/dist/src/update/package-manager.js +49 -0
- package/dist/src/update/types.js +7 -0
- package/dist/src/update/version.js +114 -0
- package/package.json +1 -1
- package/dist/src/tui/components/Spinner.js +0 -19
- 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
|
package/dist/bin/tui-app.js
CHANGED
|
@@ -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
|
-
* 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
|
+
});
|