quizmill 0.1.3

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 (3) hide show
  1. package/README.md +33 -0
  2. package/bin/quizmill.js +195 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # quizmill
2
+
3
+ **The mill that grinds questions into knowledge.**
4
+
5
+ Turn a folder of JSON — a *learning pack* — into a fast, installable,
6
+ offline-first practice app. Engine docs at
7
+ [github.com/quizmill/quizmill](https://github.com/quizmill/quizmill);
8
+ the story at [quizmill.dev](https://quizmill.dev).
9
+
10
+ ```
11
+ npx quizmill new my-topic # scaffold a pack (or have your AI agent fill it)
12
+ npx quizmill run my-topic # practice at localhost:3000
13
+ npx quizmill run quizmill/pack-claude-cert # or any GitHub pack repo
14
+ npx quizmill build my-topic # static app in my-topic-app/ — deploy anywhere
15
+ ```
16
+
17
+ | Command | What |
18
+ |---|---|
19
+ | `new [dir]` | scaffold a learning pack with an agent-ready README |
20
+ | `validate <dir>` | schema + cross-reference checks (agents loop on this) |
21
+ | `run [dir\|owner/repo]` | activate a pack and start the app |
22
+ | `build [dir\|owner/repo]` | emit a deployable static app in `<pack-id>-app/` |
23
+ | `list` | published packs you can install |
24
+ | `upgrade` | update the cached engine in `~/.quizmill` |
25
+
26
+ The CLI is a zero-dependency wrapper: it keeps an engine checkout in
27
+ `~/.quizmill/engine` and shells out to its npm scripts. Requires Node
28
+ ≥ 18, git and npm.
29
+
30
+ Packs are private by default — they live wherever you put them. Push
31
+ one to a GitHub repo and `quizmill run owner/repo` works for anyone
32
+ with access; see the engine README to get listed in the public
33
+ registry. MIT.
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * quizmill CLI — run the practice-app engine without cloning it.
4
+ *
5
+ * Keeps an engine checkout in ~/.quizmill/engine (override with
6
+ * QUIZMILL_ENGINE for development) and shells out to its npm scripts,
7
+ * so the CLI stays a thin, dependency-free wrapper and the engine
8
+ * remains the single source of truth.
9
+ *
10
+ * npx quizmill new my-topic scaffold a pack to edit (or hand to your agent)
11
+ * npx quizmill validate my-topic schema + cross-reference checks
12
+ * npx quizmill run my-topic activate a pack and start the app
13
+ * npx quizmill run owner/repo ...straight from a GitHub pack repo
14
+ * npx quizmill build my-topic static site in <pack-id>-app/
15
+ * npx quizmill list published packs
16
+ * npx quizmill upgrade update the cached engine
17
+ */
18
+ const { execSync, execFileSync } = require('node:child_process');
19
+ const fs = require('node:fs');
20
+ const os = require('node:os');
21
+ const path = require('node:path');
22
+
23
+ const ENGINE_REPO = 'https://github.com/quizmill/quizmill.git';
24
+ const HOME = process.env.QUIZMILL_HOME || path.join(os.homedir(), '.quizmill');
25
+ const ENGINE = process.env.QUIZMILL_ENGINE || path.join(HOME, 'engine');
26
+
27
+ const log = (s) => console.log(s);
28
+ const die = (s) => {
29
+ console.error(`✗ ${s}`);
30
+ process.exit(1);
31
+ };
32
+
33
+ function run(cmd, args, opts = {}) {
34
+ execFileSync(cmd, args, { stdio: 'inherit', ...opts });
35
+ }
36
+
37
+ function ensureEngine() {
38
+ if (!fs.existsSync(path.join(ENGINE, 'package.json'))) {
39
+ log(`… first run: fetching the quizmill engine into ${ENGINE}`);
40
+ fs.mkdirSync(path.dirname(ENGINE), { recursive: true });
41
+ run('git', ['clone', '--depth', '1', ENGINE_REPO, ENGINE]);
42
+ }
43
+ if (!fs.existsSync(path.join(ENGINE, 'node_modules'))) {
44
+ log('… installing engine dependencies (first run only)');
45
+ run('npm', ['install', '--no-fund', '--no-audit'], { cwd: ENGINE });
46
+ }
47
+ }
48
+
49
+ function engineNpm(args) {
50
+ ensureEngine();
51
+ run('npm', args, { cwd: ENGINE });
52
+ }
53
+
54
+ /** Resolve a pack argument: absolute path for dirs, verbatim otherwise
55
+ * (the engine handles owner/repo and URLs itself). */
56
+ function packSource(arg) {
57
+ if (!arg) return null;
58
+ return fs.existsSync(arg) ? path.resolve(arg) : arg;
59
+ }
60
+
61
+ const TEMPLATE_MANIFEST = (id) => ({
62
+ schemaVersion: 1,
63
+ id,
64
+ title: 'My Topic Practice',
65
+ description: 'Multiple-choice practice on my topic. Edit me.',
66
+ homeSubtitle: 'Keep the wheel turning.',
67
+ themeColor: '#0f766e',
68
+ categories: [{ key: 'basics', label: 'Basics' }],
69
+ });
70
+
71
+ const TEMPLATE_QUESTION = (id) => ({
72
+ id: `${id}-basics-001`,
73
+ categoryKey: 'basics',
74
+ difficulty: 2,
75
+ prompt: 'Replace me: what makes a good first question?',
76
+ options: [
77
+ { key: 'A', text: 'A vague one' },
78
+ { key: 'B', text: 'One with exactly one defensibly correct answer' },
79
+ { key: 'C', text: 'A trick question' },
80
+ { key: 'D', text: 'One with joke options' },
81
+ ],
82
+ correctKey: 'B',
83
+ explanation:
84
+ 'Every question needs exactly one defensibly correct answer, plausible distractors, and an explanation that teaches why the answer is right and the tempting distractor is wrong.',
85
+ source: 'original',
86
+ reviewStatus: 'draft',
87
+ });
88
+
89
+ const AGENT_NOTE = (id) => `# ${id} — a quizmill learning pack
90
+
91
+ Edit pack.json and questions.json by hand, or point your AI agent at
92
+ this directory. The format is specified by the engine's Zod schema —
93
+ agents should loop on the validator until it exits clean:
94
+
95
+ npx quizmill validate ${id}
96
+
97
+ Then run it:
98
+
99
+ npx quizmill run ${id}
100
+
101
+ Quality bar: exactly one defensibly correct answer per question,
102
+ plausible distractors (no jokes), explanations that teach, difficulty
103
+ spread 1–5, and shuffle which letter is correct.
104
+ `;
105
+
106
+ function cmdNew(dirArg) {
107
+ const dir = dirArg || 'my-pack';
108
+ const id = path
109
+ .basename(dir)
110
+ .toLowerCase()
111
+ .replace(/[^a-z0-9-]+/g, '-')
112
+ .replace(/^-+|-+$/g, '');
113
+ if (!id) die(`cannot derive a pack id from "${dir}"`);
114
+ if (fs.existsSync(dir)) die(`${dir} already exists`);
115
+ fs.mkdirSync(dir, { recursive: true });
116
+ const write = (name, data) =>
117
+ fs.writeFileSync(path.join(dir, name), JSON.stringify(data, null, 2) + '\n');
118
+ write('pack.json', { ...TEMPLATE_MANIFEST(id), id });
119
+ write('questions.json', [TEMPLATE_QUESTION(id)]);
120
+ write('scenarios.json', []);
121
+ fs.writeFileSync(path.join(dir, 'README.md'), AGENT_NOTE(id));
122
+ log(`✓ scaffolded ${dir}/`);
123
+ log(` Edit it (or tell your agent: "fill ${dir} with questions about …"), then:`);
124
+ log(` npx quizmill run ${dir}`);
125
+ }
126
+
127
+ function cmdRun(arg) {
128
+ const src = packSource(arg);
129
+ if (src) engineNpm(['run', 'pack:use', src]);
130
+ engineNpm(['run', 'dev']);
131
+ }
132
+
133
+ function cmdBuild(arg) {
134
+ const src = packSource(arg);
135
+ if (src) engineNpm(['run', 'pack:use', src]);
136
+ engineNpm(['run', 'build']);
137
+ const manifest = JSON.parse(
138
+ fs.readFileSync(path.join(ENGINE, 'content', 'pack', 'pack.json'), 'utf8'),
139
+ );
140
+ const dest = path.resolve(`${manifest.id}-app`);
141
+ fs.rmSync(dest, { recursive: true, force: true });
142
+ fs.cpSync(path.join(ENGINE, 'out'), dest, { recursive: true });
143
+ log(`✓ static app for "${manifest.title}" in ${dest}/ — deploy it anywhere`);
144
+ }
145
+
146
+ function cmdValidate(arg) {
147
+ if (!arg) die('usage: quizmill validate <pack-dir>');
148
+ const src = packSource(arg);
149
+ if (!fs.existsSync(src)) die(`not a directory: ${arg}`);
150
+ engineNpm(['run', 'pack:validate', src]);
151
+ }
152
+
153
+ function cmdUpgrade() {
154
+ ensureEngine();
155
+ if (!process.env.QUIZMILL_ENGINE) {
156
+ run('git', ['pull', '--ff-only'], { cwd: ENGINE });
157
+ }
158
+ run('npm', ['install', '--no-fund', '--no-audit'], { cwd: ENGINE });
159
+ log('✓ engine up to date');
160
+ }
161
+
162
+ const HELP = `quizmill — the mill that grinds questions into knowledge
163
+ https://quizmill.dev
164
+
165
+ usage:
166
+ quizmill new [dir] scaffold a learning pack
167
+ quizmill validate <dir> schema + cross-reference checks
168
+ quizmill run [dir|owner/repo] activate a pack (default: current/demo) and start the app
169
+ quizmill build [dir|owner/repo] build a static app into <pack-id>-app/
170
+ quizmill list published packs you can install
171
+ quizmill upgrade update the cached engine (~/.quizmill)
172
+
173
+ A learning pack is three JSON files. You can write them, but the
174
+ intended author is your AI agent — see the create-learning-pack skill
175
+ in the engine repo.`;
176
+
177
+ const [cmd, arg] = process.argv.slice(2);
178
+ try {
179
+ switch (cmd) {
180
+ case 'new': cmdNew(arg); break;
181
+ case 'validate': cmdValidate(arg); break;
182
+ case 'run': cmdRun(arg); break;
183
+ case 'build': cmdBuild(arg); break;
184
+ case 'list': engineNpm(['run', 'pack:list']); break;
185
+ case 'upgrade': cmdUpgrade(); break;
186
+ case 'help':
187
+ case '--help':
188
+ case undefined: log(HELP); break;
189
+ default:
190
+ die(`unknown command "${cmd}" — try: quizmill help`);
191
+ }
192
+ } catch (err) {
193
+ // Child process failures already printed their output via stdio:inherit.
194
+ process.exit(typeof err.status === 'number' ? err.status : 1);
195
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "quizmill",
3
+ "version": "0.1.3",
4
+ "description": "Turn a folder of JSON into an installable, offline-first practice app. The mill that grinds questions into knowledge.",
5
+ "bin": {
6
+ "quizmill": "bin/quizmill.js"
7
+ },
8
+ "files": [
9
+ "bin"
10
+ ],
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "license": "MIT",
15
+ "homepage": "https://quizmill.dev",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/quizmill/quizmill.git",
19
+ "directory": "cli"
20
+ },
21
+ "keywords": [
22
+ "quiz",
23
+ "practice",
24
+ "learning",
25
+ "flashcards",
26
+ "mcq",
27
+ "pwa",
28
+ "ai-agent"
29
+ ]
30
+ }