@datajaddah/course 0.0.2
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 +31 -0
- package/bin/datajaddah-course.js +238 -0
- package/lib/init.js +375 -0
- package/lib/preview.js +1617 -0
- package/lib/repo-contract.js +1291 -0
- package/lib/slide-markdown.js +505 -0
- package/package.json +41 -0
- package/preview-dist/assets/index-CJUgarn8.css +1 -0
- package/preview-dist/assets/index-CavDNP3d.js +49 -0
- package/preview-dist/index.html +13 -0
- package/scaffold/.agents/rules/general.md +91 -0
- package/scaffold/.agents/skills/course-coding-exercises/SKILL.md +22 -0
- package/scaffold/.agents/skills/course-coding-exercises/references/coding-exercises.md +111 -0
- package/scaffold/.agents/skills/course-platform-overview/SKILL.md +36 -0
- package/scaffold/.agents/skills/course-platform-overview/references/platform-overview.md +105 -0
- package/scaffold/.agents/skills/course-quizzes/SKILL.md +23 -0
- package/scaffold/.agents/skills/course-quizzes/references/quizzes.md +121 -0
- package/scaffold/.agents/skills/course-repo-contract/SKILL.md +24 -0
- package/scaffold/.agents/skills/course-repo-contract/references/repo-contract.md +169 -0
- package/scaffold/.agents/skills/course-slides-v2/SKILL.md +28 -0
- package/scaffold/.agents/skills/course-slides-v2/references/fit-guidance.md +31 -0
- package/scaffold/.agents/skills/course-slides-v2/references/slides-v2.md +138 -0
- package/scaffold/.agents/skills/course-spreadsheet-labs/SKILL.md +23 -0
- package/scaffold/.agents/skills/course-spreadsheet-labs/references/spreadsheet-labs.md +239 -0
- package/scaffold/.agents/skills/course-video-lessons/SKILL.md +22 -0
- package/scaffold/.agents/skills/course-video-lessons/references/video-lessons.md +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @datajaddah/course
|
|
2
|
+
|
|
3
|
+
CLI for authoring, validating, and previewing Datajaddah courses from GitHub repositories.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @datajaddah/course
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
### `datajaddah-course init [path] [--title "Title"] [--force]`
|
|
14
|
+
|
|
15
|
+
Scaffold a new course repository with example lessons (slides, quiz, coding exercise, spreadsheet lab).
|
|
16
|
+
|
|
17
|
+
### `datajaddah-course validate [path] [--json]`
|
|
18
|
+
|
|
19
|
+
Validate a course repository against the `repo_tree_v1` contract. Reports structural errors, missing files, and invalid frontmatter.
|
|
20
|
+
|
|
21
|
+
### `datajaddah-course preview [path] [--port 4310] [--host 127.0.0.1]`
|
|
22
|
+
|
|
23
|
+
Start a local preview server that renders all course content in a browser. Refreshes from disk on each page load.
|
|
24
|
+
|
|
25
|
+
## Supported Lesson Kinds
|
|
26
|
+
|
|
27
|
+
- **slides** — Markdown slides with narration scripts, layouts, and image slots
|
|
28
|
+
- **video** — External video URL with notes
|
|
29
|
+
- **quiz** — Multiple-choice questions with feedback
|
|
30
|
+
- **coding_exercise** — Python or R exercises with starter and solution code
|
|
31
|
+
- **spreadsheet_lab** — Univerjs-powered spreadsheet exercises with checks
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { initCourseRepository } from "../lib/init.js";
|
|
9
|
+
import { startPreviewServer } from "../lib/preview.js";
|
|
10
|
+
import {
|
|
11
|
+
RepoContractError,
|
|
12
|
+
buildValidationSummary,
|
|
13
|
+
parseCourseRepository,
|
|
14
|
+
} from "../lib/repo-contract.js";
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
function getVersion() {
|
|
19
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, "../package.json"), "utf8"));
|
|
20
|
+
return pkg.version;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printUsage() {
|
|
24
|
+
console.error("Usage:");
|
|
25
|
+
console.error(" datajaddah-course validate [path] [--json]");
|
|
26
|
+
console.error(" datajaddah-course init [path] [--title \"Course Title\"] [--force]");
|
|
27
|
+
console.error(" datajaddah-course preview [path] [--host 127.0.0.1] [--port 4310]");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printValidateHelp() {
|
|
31
|
+
console.error("Usage: datajaddah-course validate [path] [--json]");
|
|
32
|
+
console.error("");
|
|
33
|
+
console.error("Validate a course repository against the repo_tree_v1 contract.");
|
|
34
|
+
console.error("");
|
|
35
|
+
console.error("Options:");
|
|
36
|
+
console.error(" [path] Path to the course repository (default: current directory)");
|
|
37
|
+
console.error(" --json Output validation result as JSON");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printInitHelp() {
|
|
41
|
+
console.error("Usage: datajaddah-course init [path] [--title \"Course Title\"] [--force]");
|
|
42
|
+
console.error("");
|
|
43
|
+
console.error("Initialize a new course scaffold.");
|
|
44
|
+
console.error("");
|
|
45
|
+
console.error("Options:");
|
|
46
|
+
console.error(" [path] Target directory (default: current directory)");
|
|
47
|
+
console.error(" --title <title> Course title (default: derived from directory name)");
|
|
48
|
+
console.error(" --force Overwrite existing files");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printPreviewHelp() {
|
|
52
|
+
console.error("Usage: datajaddah-course preview [path] [--host 127.0.0.1] [--port 4310] [--dev]");
|
|
53
|
+
console.error("");
|
|
54
|
+
console.error("Start a local preview server for the course.");
|
|
55
|
+
console.error("");
|
|
56
|
+
console.error("Options:");
|
|
57
|
+
console.error(" [path] Path to the course repository (default: current directory)");
|
|
58
|
+
console.error(" --host <host> Bind address (default: 127.0.0.1)");
|
|
59
|
+
console.error(" --port <port> Port number (default: 4310)");
|
|
60
|
+
console.error(" --dev Enable Vite HMR for preview UI development");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseValidateArgs(args) {
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
const normalizedArgs = [];
|
|
66
|
+
let positionalOnly = false;
|
|
67
|
+
|
|
68
|
+
for (const arg of args) {
|
|
69
|
+
if (!positionalOnly && arg === "--") {
|
|
70
|
+
positionalOnly = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
normalizedArgs.push(arg);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let targetArg = ".";
|
|
77
|
+
let json = false;
|
|
78
|
+
let seenPath = false;
|
|
79
|
+
|
|
80
|
+
for (const arg of normalizedArgs) {
|
|
81
|
+
if (arg === "--json") {
|
|
82
|
+
json = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (arg.startsWith("-")) {
|
|
86
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
87
|
+
}
|
|
88
|
+
if (!seenPath) {
|
|
89
|
+
targetArg = arg;
|
|
90
|
+
seenPath = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
rootPath: path.resolve(cwd, targetArg),
|
|
98
|
+
json,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function printValidationSummary(summary) {
|
|
103
|
+
console.log("Repository contract valid.");
|
|
104
|
+
console.log(`Path: ${summary.path}`);
|
|
105
|
+
console.log(`Contract: ${summary.contract}`);
|
|
106
|
+
if (summary.courseTitle) {
|
|
107
|
+
console.log(`Course: ${summary.courseTitle}`);
|
|
108
|
+
}
|
|
109
|
+
console.log(`Chapters: ${summary.chapterCount}`);
|
|
110
|
+
console.log(`Lessons: ${summary.lessonCount}`);
|
|
111
|
+
console.log(
|
|
112
|
+
"Resources: " +
|
|
113
|
+
`slides=${summary.resourceCounts.slides}, ` +
|
|
114
|
+
`video=${summary.resourceCounts.video}, ` +
|
|
115
|
+
`quiz=${summary.resourceCounts.quiz}, ` +
|
|
116
|
+
`coding_exercise=${summary.resourceCounts.coding_exercise}, ` +
|
|
117
|
+
`spreadsheet_lab=${summary.resourceCounts.spreadsheet_lab}`
|
|
118
|
+
);
|
|
119
|
+
if (summary.warnings.length > 0) {
|
|
120
|
+
console.log("Warnings:");
|
|
121
|
+
for (const warning of summary.warnings) {
|
|
122
|
+
console.log(`- ${warning}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runValidation(args) {
|
|
128
|
+
let options;
|
|
129
|
+
try {
|
|
130
|
+
options = parseValidateArgs(args);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const parsedRepo = parseCourseRepository(options.rootPath);
|
|
138
|
+
const summary = buildValidationSummary(options.rootPath, parsedRepo);
|
|
139
|
+
if (options.json) {
|
|
140
|
+
console.log(JSON.stringify(summary));
|
|
141
|
+
} else {
|
|
142
|
+
printValidationSummary(summary);
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof RepoContractError) {
|
|
147
|
+
if (options.json) {
|
|
148
|
+
console.log(
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
ok: false,
|
|
151
|
+
path: options.rootPath,
|
|
152
|
+
error: {
|
|
153
|
+
path: error.path,
|
|
154
|
+
line: error.line,
|
|
155
|
+
message: error.message,
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
console.error("Repository contract invalid.");
|
|
161
|
+
console.error(`${error.path}:${error.line}: ${error.message}`);
|
|
162
|
+
}
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function runValidate(args) {
|
|
170
|
+
process.exit(runValidation(args));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function runInit(args) {
|
|
174
|
+
let created;
|
|
175
|
+
try {
|
|
176
|
+
created = initCourseRepository(args, process.cwd());
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.error(`Initialized Datajaddah course scaffold at ${created.targetPath}`);
|
|
183
|
+
console.error(`Course title: ${created.title}`);
|
|
184
|
+
console.error(`Files written: ${created.writtenFiles.length}`);
|
|
185
|
+
|
|
186
|
+
const validationStatus = runValidation([created.targetPath]);
|
|
187
|
+
process.exit(validationStatus);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function runPreview(args) {
|
|
191
|
+
try {
|
|
192
|
+
await startPreviewServer(args, process.cwd());
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (error instanceof RepoContractError) {
|
|
195
|
+
console.error("Repository contract invalid.");
|
|
196
|
+
console.error(`${error.path}:${error.line}: ${error.message}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const [command, ...args] = process.argv.slice(2);
|
|
205
|
+
|
|
206
|
+
if (command === "--version" || command === "-v") {
|
|
207
|
+
console.log(getVersion());
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!command || command === "--help" || command === "-h") {
|
|
212
|
+
printUsage();
|
|
213
|
+
process.exit(command ? 0 : 1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (command === "validate") {
|
|
217
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
218
|
+
printValidateHelp();
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
runValidate(args);
|
|
222
|
+
} else if (command === "init") {
|
|
223
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
224
|
+
printInitHelp();
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
runInit(args);
|
|
228
|
+
} else if (command === "preview") {
|
|
229
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
230
|
+
printPreviewHelp();
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
await runPreview(args);
|
|
234
|
+
} else {
|
|
235
|
+
console.error(`Unknown command: ${command}`);
|
|
236
|
+
printUsage();
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const SCAFFOLD_ROOT = fileURLToPath(new URL("../scaffold", import.meta.url));
|
|
6
|
+
|
|
7
|
+
function toTitleCase(value) {
|
|
8
|
+
return value
|
|
9
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
10
|
+
.replace(/\s+/g, " ")
|
|
11
|
+
.trim()
|
|
12
|
+
.split(" ")
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
15
|
+
.join(" ");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function yamlString(value) {
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadDirectoryTemplates(directoryPath, relativePrefix = "") {
|
|
23
|
+
const entries = readdirSync(directoryPath, { withFileTypes: true }).sort((left, right) =>
|
|
24
|
+
left.name.localeCompare(right.name)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return entries.flatMap((entry) => {
|
|
28
|
+
const absolutePath = path.join(directoryPath, entry.name);
|
|
29
|
+
const relativePath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name;
|
|
30
|
+
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
return loadDirectoryTemplates(absolutePath, relativePath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
path: relativePath,
|
|
38
|
+
content: readFileSync(absolutePath, "utf8"),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildTemplates(courseTitle) {
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
path: "course.md",
|
|
48
|
+
content: `---
|
|
49
|
+
schema_version: 1
|
|
50
|
+
title: ${yamlString(courseTitle)}
|
|
51
|
+
duration: 45
|
|
52
|
+
tags:
|
|
53
|
+
- sample
|
|
54
|
+
- draft
|
|
55
|
+
---
|
|
56
|
+
Replace this description with the course overview.
|
|
57
|
+
`,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
path: "assets/.gitkeep",
|
|
61
|
+
content: "",
|
|
62
|
+
},
|
|
63
|
+
...loadDirectoryTemplates(path.join(SCAFFOLD_ROOT, ".agents"), ".agents"),
|
|
64
|
+
{
|
|
65
|
+
path: "chapters/01-intro/chapter.md",
|
|
66
|
+
content: `---
|
|
67
|
+
title: "Introduction"
|
|
68
|
+
---
|
|
69
|
+
Use this chapter to orient learners, define the first learning goal, and establish the course workflow.
|
|
70
|
+
`,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
path: "chapters/01-intro/lessons/01-welcome/lesson.md",
|
|
74
|
+
content: `---
|
|
75
|
+
kind: slides
|
|
76
|
+
title: "Welcome"
|
|
77
|
+
---
|
|
78
|
+
Introduce the course and explain what the learner will build.
|
|
79
|
+
`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
path: "chapters/01-intro/lessons/01-welcome/slides/01-cover.md",
|
|
83
|
+
content: `---
|
|
84
|
+
title: "Welcome"
|
|
85
|
+
---
|
|
86
|
+
# Welcome to ${courseTitle}
|
|
87
|
+
|
|
88
|
+
Use this first slide to introduce the topic and set expectations.
|
|
89
|
+
|
|
90
|
+
@script
|
|
91
|
+
Welcome to the course. Use this slide to set context, explain the value of the material, and tell the learner what comes next.
|
|
92
|
+
@endscript
|
|
93
|
+
`,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
path: "chapters/01-intro/lessons/01-welcome/slides/02-outline.md",
|
|
97
|
+
content: `---
|
|
98
|
+
title: "Lesson Outline"
|
|
99
|
+
---
|
|
100
|
+
## Today
|
|
101
|
+
|
|
102
|
+
- Why this topic matters
|
|
103
|
+
- What the learner will build
|
|
104
|
+
- How the lesson is structured
|
|
105
|
+
|
|
106
|
+
@script
|
|
107
|
+
Walk through the lesson outline and explain the sequence of concepts, practice, and assessment.
|
|
108
|
+
@endscript
|
|
109
|
+
`,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
path: "chapters/01-intro/lessons/02-checkpoint-quiz/lesson.md",
|
|
113
|
+
content: `---
|
|
114
|
+
kind: quiz
|
|
115
|
+
title: "Checkpoint Quiz"
|
|
116
|
+
---
|
|
117
|
+
Check whether the learner understands the main goal of this opening lesson.
|
|
118
|
+
`,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
path: "chapters/01-intro/lessons/02-checkpoint-quiz/quiz/quiz.md",
|
|
122
|
+
content: `---
|
|
123
|
+
title: "Checkpoint Quiz"
|
|
124
|
+
---
|
|
125
|
+
Use this quiz to reinforce the core idea from the chapter. Replace the sample question with one that matches your course topic before publishing.
|
|
126
|
+
`,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
path: "chapters/01-intro/lessons/02-checkpoint-quiz/quiz/questions/01-course-goal.md",
|
|
130
|
+
content: `---
|
|
131
|
+
title: "Course Goal Check"
|
|
132
|
+
correct_feedback: "Correct. The first chapter should build a practical foundation for the course topic."
|
|
133
|
+
randomize_choices: false
|
|
134
|
+
---
|
|
135
|
+
What should this first chapter help a learner do in ${courseTitle}?
|
|
136
|
+
|
|
137
|
+
@choice
|
|
138
|
+
title: Build a practical foundation
|
|
139
|
+
correct: true
|
|
140
|
+
|
|
141
|
+
Build a practical foundation
|
|
142
|
+
@endchoice
|
|
143
|
+
|
|
144
|
+
@choice
|
|
145
|
+
title: Memorize repository files
|
|
146
|
+
correct: false
|
|
147
|
+
feedback: Repository filenames and authoring rules are internal details, not the learner objective for a topic course.
|
|
148
|
+
|
|
149
|
+
Memorize repository files
|
|
150
|
+
@endchoice
|
|
151
|
+
|
|
152
|
+
@choice
|
|
153
|
+
title: Skip practice entirely
|
|
154
|
+
correct: false
|
|
155
|
+
feedback: An introduction should prepare learners for practice, not replace it.
|
|
156
|
+
|
|
157
|
+
Skip practice entirely
|
|
158
|
+
@endchoice
|
|
159
|
+
`,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
path: "chapters/01-intro/lessons/03-first-exercise/lesson.md",
|
|
163
|
+
content: `---
|
|
164
|
+
kind: coding_exercise
|
|
165
|
+
title: "First Exercise"
|
|
166
|
+
---
|
|
167
|
+
Give the learner a small task that matches the concepts introduced in the chapter.
|
|
168
|
+
`,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
path: "chapters/01-intro/lessons/03-first-exercise/exercise/exercise.md",
|
|
172
|
+
content: `---
|
|
173
|
+
title: "First Exercise"
|
|
174
|
+
language: "python"
|
|
175
|
+
---
|
|
176
|
+
## Context
|
|
177
|
+
|
|
178
|
+
You are given a helper function that should add all values in a list.
|
|
179
|
+
|
|
180
|
+
## Instructions
|
|
181
|
+
|
|
182
|
+
Implement \`sum_values\` in \`starter.py\`.
|
|
183
|
+
|
|
184
|
+
- Keep the function signature unchanged.
|
|
185
|
+
- Return \`0\` for an empty list.
|
|
186
|
+
- Compare your answer with \`solution.py\` after you finish.
|
|
187
|
+
`,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
path: "chapters/01-intro/lessons/03-first-exercise/exercise/starter.py",
|
|
191
|
+
content: `def sum_values(values: list[int]) -> int:
|
|
192
|
+
return 0
|
|
193
|
+
`,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
path: "chapters/01-intro/lessons/03-first-exercise/exercise/solution.py",
|
|
197
|
+
content: `def sum_values(values: list[int]) -> int:
|
|
198
|
+
return sum(values)
|
|
199
|
+
`,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
path: "chapters/01-intro/lessons/04-first-spreadsheet/lesson.md",
|
|
203
|
+
content: `---
|
|
204
|
+
kind: spreadsheet_lab
|
|
205
|
+
title: "First Spreadsheet"
|
|
206
|
+
---
|
|
207
|
+
Give the learner a small spreadsheet task that reinforces the concepts from the chapter.
|
|
208
|
+
`,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
path: "chapters/01-intro/lessons/04-first-spreadsheet/spreadsheet/exercise.md",
|
|
212
|
+
content: `---
|
|
213
|
+
title: "First Spreadsheet"
|
|
214
|
+
---
|
|
215
|
+
## Context
|
|
216
|
+
|
|
217
|
+
You have a small table with two values that need to be summed.
|
|
218
|
+
|
|
219
|
+
## Instructions
|
|
220
|
+
|
|
221
|
+
- In cell \`B4\`, enter a SUM formula to calculate the total of cells \`B2\` and \`B3\`.
|
|
222
|
+
- The result should equal \`300\`.
|
|
223
|
+
`,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
path: "chapters/01-intro/lessons/04-first-spreadsheet/spreadsheet/workbook.json",
|
|
227
|
+
content: JSON.stringify(
|
|
228
|
+
{
|
|
229
|
+
sheets: {
|
|
230
|
+
Sheet1: {
|
|
231
|
+
name: "Sheet1",
|
|
232
|
+
cellData: {
|
|
233
|
+
0: { 0: { v: "Item" }, 1: { v: "Amount" } },
|
|
234
|
+
1: { 0: { v: "Item A" }, 1: { v: 100 } },
|
|
235
|
+
2: { 0: { v: "Item B" }, 1: { v: 200 } },
|
|
236
|
+
3: { 0: { v: "Total" } },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
null,
|
|
242
|
+
2
|
|
243
|
+
) + "\n",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
path: "chapters/01-intro/lessons/04-first-spreadsheet/spreadsheet/solution.json",
|
|
247
|
+
content: JSON.stringify(
|
|
248
|
+
{
|
|
249
|
+
sheets: {
|
|
250
|
+
Sheet1: {
|
|
251
|
+
name: "Sheet1",
|
|
252
|
+
cellData: {
|
|
253
|
+
0: { 0: { v: "Item" }, 1: { v: "Amount" } },
|
|
254
|
+
1: { 0: { v: "Item A" }, 1: { v: 100 } },
|
|
255
|
+
2: { 0: { v: "Item B" }, 1: { v: 200 } },
|
|
256
|
+
3: { 0: { v: "Total" }, 1: { v: 300, f: "=SUM(B2:B3)" } },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
null,
|
|
262
|
+
2
|
|
263
|
+
) + "\n",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
path: "chapters/01-intro/lessons/04-first-spreadsheet/spreadsheet/checks.json",
|
|
267
|
+
content: JSON.stringify(
|
|
268
|
+
[
|
|
269
|
+
{
|
|
270
|
+
id: "total-value",
|
|
271
|
+
label: "Total equals 300",
|
|
272
|
+
type: "value_equals",
|
|
273
|
+
cell: "B4",
|
|
274
|
+
value: 300,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
null,
|
|
278
|
+
2
|
|
279
|
+
) + "\n",
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function parseInitArgs(args, cwd) {
|
|
285
|
+
let targetArg = ".";
|
|
286
|
+
let title;
|
|
287
|
+
let force = false;
|
|
288
|
+
let seenPath = false;
|
|
289
|
+
let positionalOnly = false;
|
|
290
|
+
|
|
291
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
292
|
+
const arg = args[index];
|
|
293
|
+
|
|
294
|
+
if (!positionalOnly && arg === "--") {
|
|
295
|
+
positionalOnly = true;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!positionalOnly && arg === "--force") {
|
|
300
|
+
force = true;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!positionalOnly && arg === "--title") {
|
|
305
|
+
index += 1;
|
|
306
|
+
if (index >= args.length) {
|
|
307
|
+
throw new Error("Missing value for --title");
|
|
308
|
+
}
|
|
309
|
+
title = args[index];
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!positionalOnly && arg.startsWith("--title=")) {
|
|
314
|
+
title = arg.slice("--title=".length);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!seenPath) {
|
|
319
|
+
targetArg = arg;
|
|
320
|
+
seenPath = true;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const targetPath = path.resolve(cwd, targetArg);
|
|
328
|
+
const fallbackTitle = toTitleCase(path.basename(targetPath)) || "New Course";
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
targetPath,
|
|
332
|
+
title: title?.trim() || fallbackTitle,
|
|
333
|
+
force,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function initCourseRepository(args, cwd = process.cwd()) {
|
|
338
|
+
const { targetPath, title, force } = parseInitArgs(args, cwd);
|
|
339
|
+
|
|
340
|
+
if (existsSync(targetPath)) {
|
|
341
|
+
const stats = statSync(targetPath);
|
|
342
|
+
if (!stats.isDirectory()) {
|
|
343
|
+
throw new Error(`Target path must be a directory: ${targetPath}`);
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
mkdirSync(targetPath, { recursive: true });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const templates = buildTemplates(title);
|
|
350
|
+
const conflicts = templates
|
|
351
|
+
.map((template) => path.join(targetPath, template.path))
|
|
352
|
+
.filter((filePath) => existsSync(filePath));
|
|
353
|
+
|
|
354
|
+
if (conflicts.length > 0 && !force) {
|
|
355
|
+
const conflictList = conflicts.map((filePath) => `- ${path.relative(targetPath, filePath)}`).join("\n");
|
|
356
|
+
throw new Error(
|
|
357
|
+
`Refusing to overwrite existing scaffold files in ${targetPath}.\n${conflictList}\nRe-run with --force to overwrite these files.`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const writtenFiles = [];
|
|
362
|
+
for (const template of templates) {
|
|
363
|
+
const absolutePath = path.join(targetPath, template.path);
|
|
364
|
+
mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
365
|
+
writeFileSync(absolutePath, template.content, "utf8");
|
|
366
|
+
writtenFiles.push(template.path);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
targetPath,
|
|
371
|
+
title,
|
|
372
|
+
force,
|
|
373
|
+
writtenFiles,
|
|
374
|
+
};
|
|
375
|
+
}
|