@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 +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +166 -0
- package/package.json +31 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|