auq-mcp-server 2.3.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 +122 -0
- package/dist/bin/auq.js +87 -93
- package/dist/bin/tui-app.js +183 -7
- package/dist/package.json +1 -1
- package/dist/src/__tests__/server.abort.test.js +214 -0
- package/dist/src/cli/commands/__tests__/answer.test.js +199 -0
- package/dist/src/cli/commands/__tests__/config.test.js +218 -0
- package/dist/src/cli/commands/__tests__/sessions.test.js +282 -0
- package/dist/src/cli/commands/answer.js +128 -0
- package/dist/src/cli/commands/config.js +263 -0
- package/dist/src/cli/commands/sessions.js +300 -0
- package/dist/src/cli/commands/update.js +124 -0
- package/dist/src/cli/utils.js +95 -0
- package/dist/src/config/__tests__/ConfigLoader.test.js +41 -0
- package/dist/src/config/__tests__/updateCheck.test.js +34 -0
- package/dist/src/config/defaults.js +5 -0
- package/dist/src/config/types.js +6 -0
- package/dist/src/core/ask-user-questions.js +3 -2
- package/dist/src/i18n/locales/en.js +7 -0
- package/dist/src/i18n/locales/ko.js +7 -0
- package/dist/src/server.js +64 -11
- package/dist/src/session/SessionManager.js +69 -4
- package/dist/src/session/__tests__/SessionManager.test.js +65 -0
- package/dist/src/tui/__tests__/session-watcher.test.js +109 -0
- package/dist/src/tui/components/Footer.js +4 -1
- package/dist/src/tui/components/Header.js +3 -1
- package/dist/src/tui/components/SessionDots.js +33 -4
- package/dist/src/tui/components/SessionPicker.js +25 -17
- package/dist/src/tui/components/StepperView.js +68 -5
- package/dist/src/tui/components/UpdateBadge.js +29 -0
- package/dist/src/tui/components/UpdateOverlay.js +199 -0
- package/dist/src/tui/components/__tests__/SessionDots.test.js +160 -1
- package/dist/src/tui/components/__tests__/SessionPicker.test.js +43 -1
- package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +160 -0
- package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -0
- package/dist/src/tui/constants/keybindings.js +3 -0
- package/dist/src/tui/session-watcher.js +50 -0
- package/dist/src/tui/themes/catppuccin-latte.js +7 -0
- package/dist/src/tui/themes/catppuccin-mocha.js +7 -0
- package/dist/src/tui/themes/dark.js +7 -0
- package/dist/src/tui/themes/dracula.js +7 -0
- package/dist/src/tui/themes/github-dark.js +7 -0
- package/dist/src/tui/themes/github-light.js +7 -0
- package/dist/src/tui/themes/gruvbox-dark.js +7 -0
- package/dist/src/tui/themes/gruvbox-light.js +7 -0
- package/dist/src/tui/themes/light.js +7 -0
- package/dist/src/tui/themes/monokai.js +7 -0
- package/dist/src/tui/themes/nord.js +7 -0
- package/dist/src/tui/themes/one-dark.js +7 -0
- package/dist/src/tui/themes/rose-pine.js +7 -0
- package/dist/src/tui/themes/solarized-dark.js +7 -0
- package/dist/src/tui/themes/solarized-light.js +7 -0
- package/dist/src/tui/themes/tokyo-night.js +7 -0
- package/dist/src/tui/utils/__tests__/staleDetection.test.js +118 -0
- package/dist/src/tui/utils/staleDetection.js +51 -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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Sessions Command — `auq sessions list`, `auq sessions show`, and `auq sessions dismiss`
|
|
3
|
+
* Manages listing, viewing, and dismissing/archiving sessions.
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { SessionManager } from "../../session/SessionManager.js";
|
|
8
|
+
import { getSessionDirectory } from "../../session/utils.js";
|
|
9
|
+
import { loadConfig } from "../../config/ConfigLoader.js";
|
|
10
|
+
import { formatAge, outputResult, parseFlags, resolveArchiveDirectory, } from "../utils.js";
|
|
11
|
+
async function sessionsList(args) {
|
|
12
|
+
const { flags } = parseFlags(args);
|
|
13
|
+
const jsonMode = flags.json === true;
|
|
14
|
+
const filterStale = flags.stale === true;
|
|
15
|
+
const filterAll = flags.all === true;
|
|
16
|
+
// --pending is same as default
|
|
17
|
+
// Initialise SessionManager
|
|
18
|
+
const sessionManager = new SessionManager({
|
|
19
|
+
baseDir: getSessionDirectory(),
|
|
20
|
+
});
|
|
21
|
+
await sessionManager.initialize();
|
|
22
|
+
// Load staleThreshold from config
|
|
23
|
+
const config = loadConfig();
|
|
24
|
+
const staleThreshold = config.staleThreshold ?? 7200000;
|
|
25
|
+
// Get all session IDs
|
|
26
|
+
const sessionIds = await sessionManager.getAllSessionIds();
|
|
27
|
+
// Build entries
|
|
28
|
+
const entries = [];
|
|
29
|
+
for (const sessionId of sessionIds) {
|
|
30
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
31
|
+
if (!status)
|
|
32
|
+
continue;
|
|
33
|
+
const createdAt = status.createdAt;
|
|
34
|
+
const ageMs = Date.now() - new Date(createdAt).getTime();
|
|
35
|
+
const stale = ageMs > staleThreshold;
|
|
36
|
+
const age = formatAge(createdAt);
|
|
37
|
+
const isPending = status.status === "pending" || status.status === "in-progress";
|
|
38
|
+
// Apply filter
|
|
39
|
+
if (filterAll) {
|
|
40
|
+
// Show all statuses
|
|
41
|
+
}
|
|
42
|
+
else if (filterStale) {
|
|
43
|
+
// Only stale sessions that are pending/in-progress
|
|
44
|
+
if (!isPending || !stale)
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Default / --pending: only pending/in-progress
|
|
49
|
+
if (!isPending)
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const request = await sessionManager.getSessionRequest(sessionId);
|
|
53
|
+
const questionCount = request?.questions?.length ?? status.totalQuestions ?? 0;
|
|
54
|
+
entries.push({
|
|
55
|
+
sessionId,
|
|
56
|
+
status: status.status,
|
|
57
|
+
createdAt,
|
|
58
|
+
age,
|
|
59
|
+
stale,
|
|
60
|
+
questionCount,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Sort by createdAt descending (newest first)
|
|
64
|
+
entries.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
65
|
+
// Output
|
|
66
|
+
if (jsonMode) {
|
|
67
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (entries.length === 0) {
|
|
71
|
+
console.log("No sessions found.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const staleIndicator = entry.stale ? " ⚠" : "";
|
|
76
|
+
console.log(`${entry.sessionId} ${entry.status} ${entry.age} questions: ${entry.questionCount}${staleIndicator}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ── Sessions Dismiss ───────────────────────────────────────────────
|
|
80
|
+
async function sessionsDismiss(args) {
|
|
81
|
+
const { flags, positionals } = parseFlags(args);
|
|
82
|
+
const jsonMode = flags.json === true;
|
|
83
|
+
const force = flags.force === true;
|
|
84
|
+
const sessionId = positionals[0];
|
|
85
|
+
// ── Validate sessionId ──────────────────────────────────────────
|
|
86
|
+
if (!sessionId) {
|
|
87
|
+
outputResult({
|
|
88
|
+
success: false,
|
|
89
|
+
error: "Missing session ID. Usage: auq sessions dismiss <sessionId> [--force] [--json]",
|
|
90
|
+
}, jsonMode);
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// ── Initialise SessionManager ───────────────────────────────────
|
|
95
|
+
const sessionManager = new SessionManager({
|
|
96
|
+
baseDir: getSessionDirectory(),
|
|
97
|
+
});
|
|
98
|
+
await sessionManager.initialize();
|
|
99
|
+
// ── Verify session exists ──────────────────────────────────────
|
|
100
|
+
const exists = await sessionManager.sessionExists(sessionId);
|
|
101
|
+
if (!exists) {
|
|
102
|
+
outputResult({ success: false, error: `Session not found: ${sessionId}` }, jsonMode);
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// ── Check stale status ─────────────────────────────────────────
|
|
107
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
108
|
+
const config = loadConfig();
|
|
109
|
+
const staleThreshold = config.staleThreshold ?? 7200000;
|
|
110
|
+
const ageMs = status
|
|
111
|
+
? Date.now() - new Date(status.createdAt).getTime()
|
|
112
|
+
: 0;
|
|
113
|
+
const isStale = ageMs > staleThreshold;
|
|
114
|
+
if (!isStale && !force) {
|
|
115
|
+
outputResult({
|
|
116
|
+
success: false,
|
|
117
|
+
error: "Session is not stale. Use --force to dismiss anyway.",
|
|
118
|
+
}, jsonMode);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// ── Archive session ────────────────────────────────────────────
|
|
123
|
+
const archiveBase = resolveArchiveDirectory();
|
|
124
|
+
const archiveDir = join(archiveBase, sessionId);
|
|
125
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
126
|
+
// Copy all files from session directory to archive
|
|
127
|
+
const sessionDir = join(getSessionDirectory(), sessionId);
|
|
128
|
+
const files = await fs.readdir(sessionDir);
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
await fs.copyFile(join(sessionDir, file), join(archiveDir, file));
|
|
131
|
+
}
|
|
132
|
+
// ── Remove from active ─────────────────────────────────────────
|
|
133
|
+
await sessionManager.deleteSession(sessionId);
|
|
134
|
+
// ── Output ──────────────────────────────────────────────────────
|
|
135
|
+
outputResult({ success: true, sessionId, archivedTo: archiveDir }, jsonMode);
|
|
136
|
+
if (!jsonMode) {
|
|
137
|
+
console.log(`Session ${sessionId} dismissed and archived to ${archiveDir}.`);
|
|
138
|
+
}
|
|
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
|
+
}
|
|
271
|
+
// ── Sessions Command Dispatcher ────────────────────────────────────
|
|
272
|
+
export async function runSessionsCommand(args) {
|
|
273
|
+
const subcommand = args[0];
|
|
274
|
+
switch (subcommand) {
|
|
275
|
+
case "list":
|
|
276
|
+
return sessionsList(args.slice(1));
|
|
277
|
+
case "show":
|
|
278
|
+
return sessionsShow(args.slice(1));
|
|
279
|
+
case "dismiss":
|
|
280
|
+
return sessionsDismiss(args.slice(1));
|
|
281
|
+
default:
|
|
282
|
+
console.log("Usage: auq sessions <subcommand>", "\n");
|
|
283
|
+
console.log("Subcommands:");
|
|
284
|
+
console.log(" list [--pending|--stale|--all] [--json] List sessions");
|
|
285
|
+
console.log(" show <sessionId> [--json] Show session details");
|
|
286
|
+
console.log(" dismiss <sessionId> [--force] [--json] Dismiss/archive a session");
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log("Examples:");
|
|
289
|
+
console.log(" auq sessions list");
|
|
290
|
+
console.log(" auq sessions list --stale --json");
|
|
291
|
+
console.log(" auq sessions show <sessionId>");
|
|
292
|
+
console.log(" auq sessions show <sessionId> --json");
|
|
293
|
+
console.log(" auq sessions dismiss <sessionId>");
|
|
294
|
+
console.log(" auq sessions dismiss <sessionId> --force");
|
|
295
|
+
if (subcommand !== undefined) {
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -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,95 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Parse CLI flags from an args array.
|
|
5
|
+
* Handles --flag (boolean) and --flag value patterns.
|
|
6
|
+
* Everything not starting with -- goes to positionals.
|
|
7
|
+
*/
|
|
8
|
+
export function parseFlags(args) {
|
|
9
|
+
const flags = {};
|
|
10
|
+
const positionals = [];
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg.startsWith("--")) {
|
|
14
|
+
const key = arg.slice(2);
|
|
15
|
+
const next = args[i + 1];
|
|
16
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
17
|
+
flags[key] = next;
|
|
18
|
+
i++; // skip next arg — it was consumed as a value
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
flags[key] = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
positionals.push(arg);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { flags, positionals };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Standard CLI output helper.
|
|
32
|
+
* - jsonMode: console.log(JSON.stringify(result, null, 2))
|
|
33
|
+
* - else: human-readable formatted output
|
|
34
|
+
*/
|
|
35
|
+
export function outputResult(result, jsonMode) {
|
|
36
|
+
if (jsonMode) {
|
|
37
|
+
console.log(JSON.stringify(result, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
const message = typeof result.error === "string"
|
|
42
|
+
? result.error
|
|
43
|
+
: typeof result.message === "string"
|
|
44
|
+
? result.message
|
|
45
|
+
: "Unknown error";
|
|
46
|
+
console.error(`Error: ${message}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Human-readable: print each key=value pair (skip success)
|
|
50
|
+
for (const [key, value] of Object.entries(result)) {
|
|
51
|
+
if (key === "success")
|
|
52
|
+
continue;
|
|
53
|
+
if (typeof value === "object" && value !== null) {
|
|
54
|
+
console.log(`${key}:`);
|
|
55
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
56
|
+
console.log(` ${subKey} = ${String(subValue)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(`${key} = ${String(value)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Format age from a timestamp to a human-readable string.
|
|
66
|
+
* Returns "Xm ago", "Xh ago", "Xd ago", etc.
|
|
67
|
+
*/
|
|
68
|
+
export function formatAge(createdAt) {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const then = createdAt instanceof Date
|
|
71
|
+
? createdAt.getTime()
|
|
72
|
+
: new Date(createdAt).getTime();
|
|
73
|
+
const diffMs = now - then;
|
|
74
|
+
if (diffMs < 0)
|
|
75
|
+
return "just now";
|
|
76
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
77
|
+
const minutes = Math.floor(seconds / 60);
|
|
78
|
+
const hours = Math.floor(minutes / 60);
|
|
79
|
+
const days = Math.floor(hours / 24);
|
|
80
|
+
if (days > 0)
|
|
81
|
+
return `${days}d ago`;
|
|
82
|
+
if (hours > 0)
|
|
83
|
+
return `${hours}h ago`;
|
|
84
|
+
if (minutes > 0)
|
|
85
|
+
return `${minutes}m ago`;
|
|
86
|
+
return `${seconds}s ago`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the archive directory for dismissed sessions.
|
|
90
|
+
* Uses the XDG data home standard: ~/.local/share/auq/archive
|
|
91
|
+
*/
|
|
92
|
+
export function resolveArchiveDirectory() {
|
|
93
|
+
const xdgDataHome = process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
|
|
94
|
+
return join(xdgDataHome, "auq", "archive");
|
|
95
|
+
}
|
|
@@ -91,4 +91,45 @@ describe("ConfigLoader", () => {
|
|
|
91
91
|
expect(paths.globalExists).toBe(true);
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
|
+
describe("stale/orphan session config options", () => {
|
|
95
|
+
it("should include stale config defaults when no config files exist", () => {
|
|
96
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
97
|
+
const config = loadConfig();
|
|
98
|
+
expect(config.staleThreshold).toBe(7200000);
|
|
99
|
+
expect(config.notifyOnStale).toBe(true);
|
|
100
|
+
expect(config.staleAction).toBe("warn");
|
|
101
|
+
});
|
|
102
|
+
it("should load staleThreshold from config file", () => {
|
|
103
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
104
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ staleThreshold: 3600000 }));
|
|
105
|
+
const config = loadConfig();
|
|
106
|
+
expect(config.staleThreshold).toBe(3600000);
|
|
107
|
+
});
|
|
108
|
+
it("should load staleAction from config file", () => {
|
|
109
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
110
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ staleAction: "archive" }));
|
|
111
|
+
const config = loadConfig();
|
|
112
|
+
expect(config.staleAction).toBe("archive");
|
|
113
|
+
});
|
|
114
|
+
it("should load notifyOnStale from config file", () => {
|
|
115
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
116
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ notifyOnStale: false }));
|
|
117
|
+
const config = loadConfig();
|
|
118
|
+
expect(config.notifyOnStale).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
it("should fall back to default for invalid staleAction value", () => {
|
|
121
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
122
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ staleAction: "invalid_action" }));
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
// Invalid enum value should be ignored, default used
|
|
125
|
+
expect(config.staleAction).toBe(DEFAULT_CONFIG.staleAction);
|
|
126
|
+
});
|
|
127
|
+
it("should reject negative staleThreshold value", () => {
|
|
128
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
129
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ staleThreshold: -1000 }));
|
|
130
|
+
const config = loadConfig();
|
|
131
|
+
// Negative value should be rejected by min(0), default used
|
|
132
|
+
expect(config.staleThreshold).toBe(DEFAULT_CONFIG.staleThreshold);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
94
135
|
});
|
|
@@ -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
|
+
});
|
|
@@ -8,8 +8,13 @@ export const DEFAULT_CONFIG = {
|
|
|
8
8
|
language: "auto",
|
|
9
9
|
theme: "system",
|
|
10
10
|
autoSelectRecommended: true,
|
|
11
|
+
staleThreshold: 7200000, // 2 hours in ms
|
|
12
|
+
notifyOnStale: true,
|
|
13
|
+
staleAction: "warn",
|
|
11
14
|
notifications: {
|
|
12
15
|
enabled: true,
|
|
13
16
|
sound: true,
|
|
14
17
|
},
|
|
18
|
+
// Update
|
|
19
|
+
updateCheck: true,
|
|
15
20
|
};
|
package/dist/src/config/types.js
CHANGED
|
@@ -21,9 +21,15 @@ export const AUQConfigSchema = z.object({
|
|
|
21
21
|
language: z.string().default("auto"),
|
|
22
22
|
theme: z.string().default("system"),
|
|
23
23
|
autoSelectRecommended: z.boolean().default(true),
|
|
24
|
+
// Stale/Orphan Session Detection
|
|
25
|
+
staleThreshold: z.number().min(0).default(7200000), // 2 hours in ms
|
|
26
|
+
notifyOnStale: z.boolean().default(true),
|
|
27
|
+
staleAction: z.enum(["warn", "remove", "archive"]).default("warn"),
|
|
24
28
|
// Notifications (OSC 9/99)
|
|
25
29
|
notifications: NotificationConfigSchema.default({
|
|
26
30
|
enabled: true,
|
|
27
31
|
sound: true,
|
|
28
32
|
}),
|
|
33
|
+
// Update
|
|
34
|
+
updateCheck: z.boolean().default(true),
|
|
29
35
|
});
|
|
@@ -22,14 +22,15 @@ export const createAskUserQuestionsCore = (options = {}) => {
|
|
|
22
22
|
title: question.title,
|
|
23
23
|
multiSelect: question.multiSelect,
|
|
24
24
|
}));
|
|
25
|
-
const ask = async (questions, callId, workingDirectory) => {
|
|
25
|
+
const ask = async (questions, callId, workingDirectory, signal) => {
|
|
26
26
|
await ensureInitialized();
|
|
27
27
|
const parsedQuestions = QuestionsSchema.parse(questions);
|
|
28
|
-
return sessionManager.startSession(normalizeQuestions(parsedQuestions), callId, workingDirectory);
|
|
28
|
+
return sessionManager.startSession(normalizeQuestions(parsedQuestions), callId, workingDirectory, signal);
|
|
29
29
|
};
|
|
30
30
|
return {
|
|
31
31
|
ask,
|
|
32
32
|
cleanupExpiredSessions: () => sessionManager.cleanupExpiredSessions(),
|
|
33
33
|
ensureInitialized,
|
|
34
|
+
markAbandoned: (sessionId) => sessionManager.updateSessionStatus(sessionId, "abandoned"),
|
|
34
35
|
};
|
|
35
36
|
};
|
|
@@ -39,6 +39,7 @@ export const en = {
|
|
|
39
39
|
copied: "Copied to clipboard",
|
|
40
40
|
saved: "Saved",
|
|
41
41
|
error: "Error",
|
|
42
|
+
staleSession: "Session \"{title}\" may be orphaned (created {hours}h ago)",
|
|
42
43
|
},
|
|
43
44
|
stepper: {
|
|
44
45
|
submitting: "Submitting answers...",
|
|
@@ -72,4 +73,10 @@ export const en = {
|
|
|
72
73
|
ui: {
|
|
73
74
|
themeLabel: "theme:",
|
|
74
75
|
},
|
|
76
|
+
abandoned: {
|
|
77
|
+
title: "AI Disconnected",
|
|
78
|
+
message: "The AI has disconnected. Do you still want to answer?",
|
|
79
|
+
continue: "Answer anyway",
|
|
80
|
+
cancel: "Cancel",
|
|
81
|
+
},
|
|
75
82
|
};
|