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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saterminal",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sat": "src/main.ts"
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://practicesat.vercel.app/api";
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
- return (await response.json()) as T;
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
 
@@ -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://practicesat.vercel.app/question?questionId=${encodeURIComponent(question.meta.questionId)}`;
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://practicesat.vercel.app/api/question/${encodeURIComponent(question.meta.external_id)}`;
37
+ return `https://mysatprep.fun/api/question/${encodeURIComponent(question.meta.external_id)}`;
38
38
  }
39
39
 
40
40
  export function openExternalQuestion(question: PracticeQuestion): void {