@mdsrs/cli 0.0.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/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @mdsrs/cli
2
+
3
+ Command-line tools for Markdown-native spaced repetition collections.
4
+
5
+ ## Commands
6
+
7
+ ```sh
8
+ mdsrs init ./cards
9
+ mdsrs check ./cards
10
+ mdsrs export ./cards
11
+ mdsrs export ./cards --pretty
12
+ ```
13
+
14
+ `init` creates a starter collection with basic, cloze, math, and nested deck
15
+ examples. `check` loads a collection and prints a short summary. `export` emits
16
+ the parsed collection as JSON for other tools.
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ export interface CliIo {
3
+ stdout: Pick<NodeJS.WriteStream, 'write'>;
4
+ stderr: Pick<NodeJS.WriteStream, 'write'>;
5
+ }
6
+ export declare const runCli: (argv: string[], io?: CliIo) => Promise<1 | 0>;
7
+ interface InitCollectionOptions {
8
+ force?: boolean;
9
+ }
10
+ interface InitCollectionResult {
11
+ rootPath: string;
12
+ writtenFiles: string[];
13
+ }
14
+ export declare const initCollection: (root: string, options?: InitCollectionOptions) => Promise<InitCollectionResult>;
15
+ export {};
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAMA,MAAM,WAAW,KAAK;IACrB,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC1C,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CAC1C;AAcD,eAAO,MAAM,MAAM,GAClB,MAAM,MAAM,EAAE,EACd,KAAI,KAA0D,mBAwC9D,CAAC;AAgCF,UAAU,qBAAqB;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,oBAAoB;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;CACvB;AAoDD,eAAO,MAAM,cAAc,GAC1B,MAAM,MAAM,EACZ,UAAS,qBAA0B,KACjC,OAAO,CAAC,oBAAoB,CAqB9B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadCollection } from '@mdsrs/fs';
6
+ const usage = `Usage:
7
+ mdsrs init <root> [--force]
8
+ mdsrs check <root>
9
+ mdsrs export <root> [--pretty]
10
+ mdsrs help
11
+ `;
12
+ export const runCli = async (argv, io = { stdout: process.stdout, stderr: process.stderr }) => {
13
+ const [command, ...args] = argv;
14
+ try {
15
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
16
+ io.stdout.write(usage);
17
+ return 0;
18
+ }
19
+ if (command === 'init') {
20
+ const { root, options } = parseRootCommand(args);
21
+ rejectUnusedOptions(options, ['pretty']);
22
+ const result = await initCollection(root, { force: options.force });
23
+ io.stdout.write(formatInitSummary(result));
24
+ return 0;
25
+ }
26
+ if (command === 'check') {
27
+ const { root, options } = parseRootCommand(args);
28
+ rejectUnusedOptions(options, ['force', 'pretty']);
29
+ const collection = await loadCollection(root);
30
+ io.stdout.write(formatSummary(collection));
31
+ return 0;
32
+ }
33
+ if (command === 'export') {
34
+ const { root, options } = parseRootCommand(args);
35
+ rejectUnusedOptions(options, ['force']);
36
+ const collection = await loadCollection(root);
37
+ io.stdout.write(`${JSON.stringify(collection, null, options.pretty ? 2 : 0)}\n`);
38
+ return 0;
39
+ }
40
+ io.stderr.write(`Unknown command: ${command}\n\n${usage}`);
41
+ return 1;
42
+ }
43
+ catch (error) {
44
+ io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
45
+ return 1;
46
+ }
47
+ };
48
+ const parseRootCommand = (args) => {
49
+ const options = { force: false, pretty: false };
50
+ const positionals = [];
51
+ for (const arg of args) {
52
+ if (arg === '--force')
53
+ options.force = true;
54
+ else if (arg === '--pretty')
55
+ options.pretty = true;
56
+ else if (arg.startsWith('-')) {
57
+ throw new Error(`Unknown option: ${arg}`);
58
+ }
59
+ else {
60
+ positionals.push(arg);
61
+ }
62
+ }
63
+ const [root, extra] = positionals;
64
+ if (!root)
65
+ throw new Error(`Missing collection root.\n\n${usage}`);
66
+ if (extra)
67
+ throw new Error(`Unexpected argument: ${extra}`);
68
+ return {
69
+ root,
70
+ options
71
+ };
72
+ };
73
+ const rejectUnusedOptions = (options, unusedOptions) => {
74
+ for (const option of unusedOptions) {
75
+ if (options[option])
76
+ throw new Error(`Option is not supported for this command: --${option}`);
77
+ }
78
+ };
79
+ const starterFiles = {
80
+ 'README.md': `# mdsrs cards
81
+
82
+ This folder is a Markdown-native spaced repetition collection.
83
+
84
+ Run:
85
+
86
+ \`\`\`sh
87
+ mdsrs check .
88
+ mdsrs export . --pretty
89
+ \`\`\`
90
+
91
+ Cards live in Markdown files. Use \`Q:\` and \`A:\` for basic cards, or \`C:\`
92
+ with bracketed text for cloze cards.
93
+ `,
94
+ 'cards/index.md': `---
95
+ name = "Cards"
96
+ ---
97
+
98
+ Q: What is the source of truth in mdsrs?
99
+ A: Markdown files.
100
+
101
+ ---
102
+
103
+ C: Editing card [content] changes its deterministic hash.
104
+ `,
105
+ 'cards/example/math.md': `---
106
+ name = "Math"
107
+ ---
108
+
109
+ Q: What is the derivative of $x^2$?
110
+ A: $2x$.
111
+
112
+ ---
113
+
114
+ C: Euler's identity is $e^{i\\pi} + [1] = 0$.
115
+ `,
116
+ 'cards/example/concepts.md': `---
117
+ name = "Concepts"
118
+ ---
119
+
120
+ Q: What does a card hash identify?
121
+ A: The normalized card content, not a database row.
122
+
123
+ ---
124
+
125
+ C: Nested folders become nested [decks].
126
+ `
127
+ };
128
+ export const initCollection = async (root, options = {}) => {
129
+ const rootPath = path.resolve(root);
130
+ await mkdir(rootPath, { recursive: true });
131
+ const entries = await readdir(rootPath);
132
+ if (!options.force && entries.length > 0) {
133
+ throw new Error(`Refusing to initialize non-empty directory without --force: ${rootPath}`);
134
+ }
135
+ const writtenFiles = [];
136
+ for (const [relativePath, content] of Object.entries(starterFiles)) {
137
+ const absolutePath = path.join(rootPath, ...relativePath.split('/'));
138
+ await mkdir(path.dirname(absolutePath), { recursive: true });
139
+ await writeFile(absolutePath, content, 'utf8');
140
+ writtenFiles.push(relativePath);
141
+ }
142
+ return {
143
+ rootPath,
144
+ writtenFiles
145
+ };
146
+ };
147
+ const countDeckNodes = (nodes) => nodes.reduce((total, node) => total + 1 + countDeckNodes(node.children), 0);
148
+ const formatSummary = (collection) => [
149
+ `root: ${collection.rootPath}`,
150
+ `sources: ${collection.sources.length}`,
151
+ `cards: ${collection.cards.length}`,
152
+ `decks: ${countDeckNodes(collection.deckTree)}`,
153
+ `assets: ${collection.assets.length}`
154
+ ].join('\n') + '\n';
155
+ const formatInitSummary = (result) => [
156
+ `initialized: ${result.rootPath}`,
157
+ `files: ${result.writtenFiles.length}`,
158
+ ...result.writtenFiles.map((filePath) => `created: ${filePath}`)
159
+ ].join('\n') + '\n';
160
+ const isMain = () => {
161
+ const entrypoint = process.argv[1];
162
+ return entrypoint ? path.resolve(entrypoint) === fileURLToPath(import.meta.url) : false;
163
+ };
164
+ if (isMain()) {
165
+ process.exitCode = await runCli(process.argv.slice(2));
166
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@mdsrs/cli",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "bin": {
7
+ "mdsrs": "./dist/index.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@mdsrs/fs": "0.0.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^24.0.0",
23
+ "typescript": "^6.0.3",
24
+ "vitest": "^4.0.0"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "test": "vitest run",
29
+ "typecheck": "tsc -p tsconfig.json --noEmit"
30
+ }
31
+ }