saterminal 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api.ts +40 -5
- package/src/cli.ts +18 -0
- package/src/tui/question.ts +2 -2
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { defaultFocus, domainsForSkills } from "./focus.ts";
|
|
2
2
|
import type { Focus, PracticeQuestion, QuestionDetail, QuestionMeta } from "./types.ts";
|
|
3
3
|
|
|
4
|
-
const baseUrl = "https://
|
|
4
|
+
const baseUrl = "https://mysatprep.fun/api";
|
|
5
5
|
|
|
6
6
|
type ApiEnvelope<T> = {
|
|
7
7
|
success: boolean;
|
|
8
8
|
data: T;
|
|
9
|
+
error?: string;
|
|
9
10
|
message?: string;
|
|
10
11
|
};
|
|
11
12
|
|
|
@@ -18,7 +19,7 @@ export async function fetchQuestionBank(excludeIds: Iterable<string> = [], focus
|
|
|
18
19
|
|
|
19
20
|
const response = await fetchJson<ApiEnvelope<QuestionMeta[]>>(`${baseUrl}/get-questions?${params}`);
|
|
20
21
|
if (!response.success || !Array.isArray(response.data)) {
|
|
21
|
-
throw new Error(response.message || "Question bank fetch failed.");
|
|
22
|
+
throw new Error(response.error || response.message || "Question bank fetch failed.");
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
return response.data.filter((item) => item.questionId && item.external_id);
|
|
@@ -27,7 +28,7 @@ export async function fetchQuestionBank(excludeIds: Iterable<string> = [], focus
|
|
|
27
28
|
export async function fetchQuestion(id: string): Promise<QuestionDetail> {
|
|
28
29
|
const response = await fetchJson<ApiEnvelope<QuestionDetail>>(`${baseUrl}/question/${id}`);
|
|
29
30
|
if (!response.success || !response.data) {
|
|
30
|
-
throw new Error(response.message || `Question ${id} fetch failed.`);
|
|
31
|
+
throw new Error(response.error || response.message || `Question ${id} fetch failed.`);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
return response.data;
|
|
@@ -69,10 +70,44 @@ async function fetchJson<T>(url: string): Promise<T> {
|
|
|
69
70
|
accept: "application/json",
|
|
70
71
|
},
|
|
71
72
|
});
|
|
73
|
+
const body = await response.text();
|
|
74
|
+
const contentType = response.headers.get("content-type") ?? "unknown content type";
|
|
72
75
|
|
|
73
76
|
if (!response.ok) {
|
|
74
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
77
|
+
throw new Error(`${response.status} ${response.statusText}: ${responseError(body)}`);
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
if (!contentType.toLowerCase().includes("application/json")) {
|
|
81
|
+
throw new Error(`Expected JSON from ${url}, got ${contentType}: ${snippet(body)}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(body) as T;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
throw new Error(`Invalid JSON from ${url}: ${message}. Response: ${snippet(body)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function responseError(body: string): string {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(body) as { error?: unknown; message?: unknown; details?: unknown };
|
|
95
|
+
const message = parsed.error || parsed.message || parsed.details;
|
|
96
|
+
if (typeof message === "string" && message.trim()) {
|
|
97
|
+
return message;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Fall back to the raw body below.
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return snippet(body);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function snippet(value: string): string {
|
|
107
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
108
|
+
if (!normalized) {
|
|
109
|
+
return "empty response";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return normalized.length > 160 ? `${normalized.slice(0, 157)}...` : normalized;
|
|
78
113
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { difficultyLabels, domainLabels, focusSummary, skillLabels } from "./focus.ts";
|
|
2
3
|
import { progressBarText } from "./progress.ts";
|
|
3
4
|
import { buildSummaryRows, loadAttemptEvents, loadAttempts, loadFocus } from "./state.ts";
|
|
@@ -16,6 +17,7 @@ export type HistoryFilters = {
|
|
|
16
17
|
export type ParsedCli =
|
|
17
18
|
| { kind: "tui" }
|
|
18
19
|
| { kind: "review" }
|
|
20
|
+
| { kind: "version" }
|
|
19
21
|
| { kind: "help" }
|
|
20
22
|
| { kind: "error"; message: string }
|
|
21
23
|
| {
|
|
@@ -107,6 +109,10 @@ export function parseArgs(args: string[]): ParsedCli {
|
|
|
107
109
|
return { kind: "help" };
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
if (arg === "-V" || arg === "--version") {
|
|
113
|
+
return { kind: "version" };
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
if (arg === "-p" || arg === "--pretty") {
|
|
111
117
|
pretty = true;
|
|
112
118
|
continue;
|
|
@@ -222,6 +228,11 @@ export async function runCliCommand(
|
|
|
222
228
|
return 0;
|
|
223
229
|
}
|
|
224
230
|
|
|
231
|
+
if (parsed.kind === "version") {
|
|
232
|
+
stdout.write(`sat ${await packageVersion()}\n`);
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
225
236
|
if (parsed.kind === "error") {
|
|
226
237
|
stderr.write(`${parsed.message}\n\n${helpText()}\n`);
|
|
227
238
|
return 1;
|
|
@@ -392,12 +403,19 @@ export function helpText(): string {
|
|
|
392
403
|
" -p, --pretty Use colorized human-readable output",
|
|
393
404
|
" --json Output JSON",
|
|
394
405
|
" --no-color Disable ANSI color in pretty output",
|
|
406
|
+
" -V, --version Show version",
|
|
395
407
|
" -h, --help Show this help",
|
|
396
408
|
"",
|
|
397
409
|
"Run `sat` with no command to open the interactive TUI.",
|
|
398
410
|
].join("\n");
|
|
399
411
|
}
|
|
400
412
|
|
|
413
|
+
async function packageVersion(): Promise<string> {
|
|
414
|
+
const packageJson = await readFile(new URL("../package.json", import.meta.url), "utf8");
|
|
415
|
+
const parsed = JSON.parse(packageJson) as { version?: unknown };
|
|
416
|
+
return typeof parsed.version === "string" && parsed.version ? parsed.version : "unknown";
|
|
417
|
+
}
|
|
418
|
+
|
|
401
419
|
export function filterHistory(attempts: Attempt[], filters: HistoryFilters = {}, now = new Date()): Attempt[] {
|
|
402
420
|
let rows = [...attempts].sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
403
421
|
|
package/src/tui/question.ts
CHANGED
|
@@ -30,11 +30,11 @@ export function questionRows(detail: QuestionDetail, width: number): DisplayRow[
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function practiceQuestionUrl(question: PracticeQuestion): string {
|
|
33
|
-
return `https://
|
|
33
|
+
return `https://mysatprep.fun/question/${encodeURIComponent(question.meta.questionId)}`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function practiceQuestionApiUrl(question: PracticeQuestion): string {
|
|
37
|
-
return `https://
|
|
37
|
+
return `https://mysatprep.fun/api/question/${encodeURIComponent(question.meta.external_id)}`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function openExternalQuestion(question: PracticeQuestion): void {
|