@jiy/quizme 0.1.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/dist/cli/config.js +37 -0
  4. package/dist/cli/index.js +64 -0
  5. package/dist/cli/session.js +10 -0
  6. package/dist/generation/dedupe.js +11 -0
  7. package/dist/generation/schema.js +50 -0
  8. package/dist/generation/validator.js +134 -0
  9. package/dist/platform/fs.js +17 -0
  10. package/dist/platform/paths.js +27 -0
  11. package/dist/providers/claudeAgent.js +295 -0
  12. package/dist/sources/claudeSession.js +112 -0
  13. package/dist/sources/repository.js +43 -0
  14. package/dist/sources/topic.js +7 -0
  15. package/dist/storage/index.js +18 -0
  16. package/dist/storage/sqlite.js +294 -0
  17. package/dist/types.js +1 -0
  18. package/dist/ui/App.js +90 -0
  19. package/dist/ui/components/AppHeader.js +6 -0
  20. package/dist/ui/components/Clawd.js +37 -0
  21. package/dist/ui/components/Divider.js +6 -0
  22. package/dist/ui/components/Feed.js +32 -0
  23. package/dist/ui/components/FeedColumn.js +9 -0
  24. package/dist/ui/components/SelectList.js +13 -0
  25. package/dist/ui/components/StatusBar.js +9 -0
  26. package/dist/ui/components/TextInput.js +23 -0
  27. package/dist/ui/components/WelcomeBanner.js +79 -0
  28. package/dist/ui/formatters.js +66 -0
  29. package/dist/ui/logoLayout.js +30 -0
  30. package/dist/ui/renderApp.js +25 -0
  31. package/dist/ui/screens/HomeScreen.js +57 -0
  32. package/dist/ui/screens/InfoScreen.js +16 -0
  33. package/dist/ui/screens/QuizScreen.js +344 -0
  34. package/dist/ui/screens/SettingsScreen.js +133 -0
  35. package/dist/ui/screens/SetupScreen.js +73 -0
  36. package/dist/ui/sound.js +101 -0
  37. package/dist/ui/terminal.js +37 -0
  38. package/dist/ui/textUtils.js +54 -0
  39. package/dist/ui/theme.js +28 -0
  40. package/dist/version.js +3 -0
  41. package/package.json +55 -0
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { AppHeader } from "../components/AppHeader.js";
5
+ import { SelectList } from "../components/SelectList.js";
6
+ import { StatusBar } from "../components/StatusBar.js";
7
+ import { hintLine, theme } from "../theme.js";
8
+ const LEVELS = [
9
+ { id: "junior", label: "Junior" },
10
+ { id: "mid", label: "Mid-level" },
11
+ { id: "senior", label: "Senior" },
12
+ { id: "staff", label: "Staff+" }
13
+ ];
14
+ export function SetupScreen({ onComplete }) {
15
+ const [step, setStep] = useState("language");
16
+ const [language, setLanguage] = useState("en");
17
+ const [levelIndex, setLevelIndex] = useState(1);
18
+ useInput((input, key) => {
19
+ if (step === "language") {
20
+ if (input === "1") {
21
+ setLanguage("zh-CN");
22
+ setStep("level");
23
+ return;
24
+ }
25
+ if (input === "2") {
26
+ setLanguage("en");
27
+ setStep("level");
28
+ return;
29
+ }
30
+ if (key.return) {
31
+ setStep("level");
32
+ }
33
+ return;
34
+ }
35
+ if (step === "level") {
36
+ if (key.upArrow) {
37
+ setLevelIndex((i) => Math.max(0, i - 1));
38
+ return;
39
+ }
40
+ if (key.downArrow) {
41
+ setLevelIndex((i) => Math.min(LEVELS.length - 1, i + 1));
42
+ return;
43
+ }
44
+ const num = Number(input);
45
+ if (num >= 1 && num <= LEVELS.length) {
46
+ setLevelIndex(num - 1);
47
+ return;
48
+ }
49
+ if (key.return) {
50
+ onComplete({
51
+ language,
52
+ level: LEVELS[levelIndex].id,
53
+ dailyGoal: 5,
54
+ soundEnabled: false,
55
+ createdAt: new Date().toISOString()
56
+ });
57
+ }
58
+ }
59
+ });
60
+ const isZh = language === "zh-CN";
61
+ if (step === "language") {
62
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(AppHeader, { title: "QuizMe", subtitle: isZh ? "首次设置 · 选择语言" : "First run · Choose language" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.text, children: "1. \u4E2D\u6587" }), _jsx(Text, { color: theme.text, children: "2. English" })] }), _jsx(StatusBar, { status: isZh ? "语言" : "Language", hints: hintLine([
63
+ isZh ? "输入 1 或 2" : "type 1 or 2",
64
+ isZh ? "Enter 默认 English" : "enter for English"
65
+ ]) })] }));
66
+ }
67
+ const levelItems = LEVELS.map((l) => ({ id: l.id, label: l.label }));
68
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(AppHeader, { title: "QuizMe", subtitle: isZh ? "首次设置 · 选择等级" : "First run · Choose level" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(SelectList, { items: levelItems, selectedIndex: levelIndex, showIndex: true }) }), _jsx(StatusBar, { status: isZh ? "等级" : "Level", hints: hintLine([
69
+ isZh ? "↑↓ 选择" : "↑↓ select",
70
+ isZh ? "Enter 确认" : "enter confirm",
71
+ isZh ? "1-4 快捷" : "1-4 shortcut"
72
+ ]) })] }));
73
+ }
@@ -0,0 +1,101 @@
1
+ import { exec } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ const os = platform();
4
+ const currentOs = os === "darwin" || os === "linux" || os === "win32" ? os : "linux";
5
+ const SOUND_MAP = {
6
+ navigate: {
7
+ darwin: "/System/Library/Sounds/Pop.aiff",
8
+ linux: "button-pressed",
9
+ win32: null
10
+ },
11
+ select: {
12
+ darwin: "/System/Library/Sounds/Tink.aiff",
13
+ linux: "button-toggle-on",
14
+ win32: null
15
+ },
16
+ correct: {
17
+ darwin: "/System/Library/Sounds/Glass.aiff",
18
+ linux: "complete",
19
+ win32: null
20
+ },
21
+ incorrect: {
22
+ darwin: "/System/Library/Sounds/Basso.aiff",
23
+ linux: "dialog-error",
24
+ win32: null
25
+ },
26
+ start: {
27
+ darwin: "/System/Library/Sounds/Hero.aiff",
28
+ linux: "service-login",
29
+ win32: null
30
+ },
31
+ complete: {
32
+ darwin: "/System/Library/Sounds/Ping.aiff",
33
+ linux: "positive",
34
+ win32: null
35
+ },
36
+ toggleOn: {
37
+ darwin: "/System/Library/Sounds/Pop.aiff",
38
+ linux: "button-toggle-on",
39
+ win32: null
40
+ },
41
+ toggleOff: {
42
+ darwin: "/System/Library/Sounds/Pop.aiff",
43
+ linux: "button-toggle-off",
44
+ win32: null
45
+ }
46
+ };
47
+ function playDarwin(path) {
48
+ if (!path)
49
+ return;
50
+ exec(`afplay "${path}"`, { timeout: 2000 }, () => { });
51
+ }
52
+ function playLinux(name) {
53
+ if (!name)
54
+ return;
55
+ exec(`canberra-gtk-play -i ${name}`, { timeout: 2000 }, () => { });
56
+ }
57
+ function playWin32() {
58
+ process.stdout.write("\x07");
59
+ }
60
+ const players = {
61
+ darwin: playDarwin,
62
+ linux: playLinux,
63
+ win32: playWin32
64
+ };
65
+ function playSound(name) {
66
+ const entry = SOUND_MAP[name];
67
+ if (!entry)
68
+ return;
69
+ const arg = entry[currentOs];
70
+ if (!arg && currentOs !== "win32")
71
+ return;
72
+ try {
73
+ players[currentOs](arg ?? undefined);
74
+ }
75
+ catch {
76
+ // sound is a non-critical enhancement; swallow errors
77
+ }
78
+ }
79
+ /**
80
+ * Create a sound player bound to the current config.
81
+ * Returns an object with play methods for each sound event.
82
+ * All methods are no-ops when config.soundEnabled is false.
83
+ */
84
+ export function createSoundPlayer(config) {
85
+ const enabled = config?.soundEnabled === true;
86
+ function fire(name) {
87
+ if (!enabled)
88
+ return;
89
+ playSound(name);
90
+ }
91
+ return {
92
+ playNavigate: () => fire("navigate"),
93
+ playSelect: () => fire("select"),
94
+ playCorrect: () => fire("correct"),
95
+ playIncorrect: () => fire("incorrect"),
96
+ playStart: () => fire("start"),
97
+ playComplete: () => fire("complete"),
98
+ playToggleOn: () => fire("toggleOn"),
99
+ playToggleOff: () => fire("toggleOff")
100
+ };
101
+ }
@@ -0,0 +1,37 @@
1
+ export function renderHome(stats, lang) {
2
+ const isZh = lang === "zh-CN";
3
+ const streak = stats ? stats.currentStreak : 0;
4
+ const today = stats ? stats.todayCount : 0;
5
+ const accuracy = stats ? `${(stats.accuracy * 100).toFixed(0)}%` : "—";
6
+ return [
7
+ "",
8
+ "=== QuizMe ===",
9
+ isZh
10
+ ? `连续: ${streak} 天 今日: ${today} 题 准确率: ${accuracy}`
11
+ : `Streak: ${streak} days Today: ${today} Accuracy: ${accuracy}`,
12
+ ""
13
+ ].join("\n");
14
+ }
15
+ export function renderQuestion(question, index, total) {
16
+ const lines = [
17
+ "",
18
+ `Q${index + 1}/${total} · ${question.topic} · Difficulty ${question.difficulty}`,
19
+ "",
20
+ question.question,
21
+ ""
22
+ ];
23
+ for (const choice of question.choices) {
24
+ lines.push(` ${choice.id}. ${choice.text}`);
25
+ }
26
+ return lines.join("\n");
27
+ }
28
+ export function renderResult(question, selected) {
29
+ const correct = selected === question.answer;
30
+ const wrongReason = correct ? "" : `\n${selected}: ${question.whyWrong[selected] || "Not the best option in this context."}`;
31
+ return [
32
+ "",
33
+ correct ? "Correct." : `Incorrect. Correct answer: ${question.answer}.`,
34
+ question.explanation,
35
+ wrongReason
36
+ ].join("\n");
37
+ }
@@ -0,0 +1,54 @@
1
+ export function truncate(text, width) {
2
+ if (width <= 0)
3
+ return "";
4
+ if (text.length <= width)
5
+ return text;
6
+ if (width === 1)
7
+ return text.slice(0, 1);
8
+ return `${text.slice(0, width - 1)}…`;
9
+ }
10
+ export function wrapText(text, width) {
11
+ if (width <= 0)
12
+ return [""];
13
+ if (text.length <= width)
14
+ return [text];
15
+ const lines = [];
16
+ const tokens = text.match(/\S+\s*/g) ?? [text];
17
+ let current = "";
18
+ for (const token of tokens) {
19
+ const trimmed = token.trimEnd();
20
+ const next = current ? `${current}${token}` : token;
21
+ if (next.trimEnd().length <= width) {
22
+ current = next;
23
+ continue;
24
+ }
25
+ if (current.trim()) {
26
+ lines.push(current.trimEnd());
27
+ current = "";
28
+ }
29
+ if (trimmed.length <= width) {
30
+ current = token;
31
+ continue;
32
+ }
33
+ for (let i = 0; i < trimmed.length; i += width) {
34
+ const part = trimmed.slice(i, i + width);
35
+ if (i + width >= trimmed.length) {
36
+ current = part;
37
+ }
38
+ else {
39
+ lines.push(part);
40
+ }
41
+ }
42
+ }
43
+ if (current.trim()) {
44
+ lines.push(current.trimEnd());
45
+ }
46
+ return lines.length ? lines : [""];
47
+ }
48
+ export function shortenPath(path, max = 42) {
49
+ const home = process.env.HOME;
50
+ const normalized = home && path.startsWith(home)
51
+ ? `~${path.slice(home.length)}`
52
+ : path;
53
+ return truncate(normalized, max);
54
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Claude Code dark theme tokens (fs5 preset).
3
+ * Values extracted from Claude Code's built-in theme definitions.
4
+ */
5
+ export const theme = {
6
+ claude: "#D77757",
7
+ clawdBody: "#D77757",
8
+ clawdBackground: "#000000",
9
+ text: "#FFFFFF",
10
+ inverseText: "#000000",
11
+ inactive: "#999999",
12
+ subtle: "#505050",
13
+ suggestion: "#B1B9F9",
14
+ permission: "#B1B9F9",
15
+ success: "#4EBA65",
16
+ error: "#FF6B80",
17
+ warning: "#FFC107",
18
+ promptBorder: "#888888",
19
+ selectionBg: "#264F78",
20
+ userMessageBg: "#373737"
21
+ };
22
+ export const symbols = {
23
+ pointer: "›",
24
+ pointerIdle: " "
25
+ };
26
+ export function hintLine(parts) {
27
+ return parts.filter(Boolean).join(" · ");
28
+ }
@@ -0,0 +1,3 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ export const QUIZME_VERSION = require("../package.json").version;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@jiy/quizme",
3
+ "version": "0.1.0",
4
+ "description": "Interactive CLI quiz practice for developers using Claude Code context.",
5
+ "type": "module",
6
+ "bin": {
7
+ "quizme": "./dist/cli/index.js"
8
+ },
9
+ "main": "./dist/cli/index.js",
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "start": "node --import tsx ./src/cli/index.ts",
13
+ "build": "tsc -p tsconfig.build.json",
14
+ "test": "node --import tsx --test test/paths.test.ts test/validator.test.ts test/store.test.ts",
15
+ "typecheck": "tsc --noEmit",
16
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
17
+ },
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "license": "MIT",
22
+ "author": "jingyuan <zjingyuan@ctrip.com>",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/kian-zh/quizme.git"
26
+ },
27
+ "homepage": "https://github.com/kian-zh/quizme#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/kian-zh/quizme/issues"
30
+ },
31
+ "keywords": [
32
+ "cli",
33
+ "quiz",
34
+ "claude",
35
+ "interview",
36
+ "developer-tools",
37
+ "ink"
38
+ ],
39
+ "dependencies": {
40
+ "better-sqlite3": "^12.11.1",
41
+ "ink": "^5.2.1",
42
+ "react": "^18.3.1"
43
+ },
44
+ "devDependencies": {
45
+ "@types/better-sqlite3": "^7.6.13",
46
+ "@types/node": "^26.0.1",
47
+ "@types/react": "^19.2.17",
48
+ "tsx": "^4.22.4",
49
+ "typescript": "^6.0.3"
50
+ },
51
+ "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
52
+ "pnpm": {
53
+ "onlyBuiltDependencies": ["better-sqlite3"]
54
+ }
55
+ }