@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.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/cli/config.js +37 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/session.js +10 -0
- package/dist/generation/dedupe.js +11 -0
- package/dist/generation/schema.js +50 -0
- package/dist/generation/validator.js +134 -0
- package/dist/platform/fs.js +17 -0
- package/dist/platform/paths.js +27 -0
- package/dist/providers/claudeAgent.js +295 -0
- package/dist/sources/claudeSession.js +112 -0
- package/dist/sources/repository.js +43 -0
- package/dist/sources/topic.js +7 -0
- package/dist/storage/index.js +18 -0
- package/dist/storage/sqlite.js +294 -0
- package/dist/types.js +1 -0
- package/dist/ui/App.js +90 -0
- package/dist/ui/components/AppHeader.js +6 -0
- package/dist/ui/components/Clawd.js +37 -0
- package/dist/ui/components/Divider.js +6 -0
- package/dist/ui/components/Feed.js +32 -0
- package/dist/ui/components/FeedColumn.js +9 -0
- package/dist/ui/components/SelectList.js +13 -0
- package/dist/ui/components/StatusBar.js +9 -0
- package/dist/ui/components/TextInput.js +23 -0
- package/dist/ui/components/WelcomeBanner.js +79 -0
- package/dist/ui/formatters.js +66 -0
- package/dist/ui/logoLayout.js +30 -0
- package/dist/ui/renderApp.js +25 -0
- package/dist/ui/screens/HomeScreen.js +57 -0
- package/dist/ui/screens/InfoScreen.js +16 -0
- package/dist/ui/screens/QuizScreen.js +344 -0
- package/dist/ui/screens/SettingsScreen.js +133 -0
- package/dist/ui/screens/SetupScreen.js +73 -0
- package/dist/ui/sound.js +101 -0
- package/dist/ui/terminal.js +37 -0
- package/dist/ui/textUtils.js +54 -0
- package/dist/ui/theme.js +28 -0
- package/dist/version.js +3 -0
- 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
|
+
}
|
package/dist/ui/sound.js
ADDED
|
@@ -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
|
+
}
|
package/dist/ui/theme.js
ADDED
|
@@ -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
|
+
}
|
package/dist/version.js
ADDED
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
|
+
}
|