Introduction

This is the documentation for the @freecodecamp/freecodecamp-os package.

freecodecamp-os is a tool for creating and running interactive courses within Visual Studio Code.

The freecodecamp-os package is to be used in conjunction with the freeCodeCamp - Courses extension, for the optimal experience.

Getting Started

Welcome to freeCodeCampOS - a platform for creating and hosting interactive coding curricula.

System Requirements

Installation

Option 1: From Source

# Clone the repository
git clone https://github.com/freeCodeCamp/freeCodeCampOS.git
cd freeCodeCampOS

# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install Bun (if not already installed)
curl -fsSL https://bun.sh/install | bash

# Build everything
bun run build

# Run the server
./target/release/freecodecamp-server

Option 2: Using Docker

docker build -f Dockerfile.migration -t freecodecamp-os:latest .
docker run -p 8080:8080 freecodecamp-os:latest

Quick Start

1. Start the Development Server

# Terminal 1: Start the Rust backend
cargo run --bin freecodecamp-server

# Server will listen on http://localhost:8080

2. Start the Client

# Terminal 2: Start the React development server
cd client && bun run dev

# Client will be available at http://localhost:5173

3. Access the Application

Open your browser and navigate to http://localhost:5173 to see the freeCodeCampOS interface.

Creating Your First Curriculum

Method 1: Using the CLI

# Create a new curriculum project
./target/release/create-freecodecamp-os-app

# Follow the interactive prompts to configure your course

Method 2: Manual Setup

Create a directory structure:

my-course/
├── freecodecamp.conf.json
└── curriculum/
    └── locales/
        └── english/
            └── my-course.md

Example freecodecamp.conf.json:

{
  "version": "4.0.0",
  "port": 8080,
  "client": {
    "assets": {
      "header": "./client/assets/logo.svg",
      "favicon": "./client/assets/favicon.svg"
    },
    "landing": {
      "english": {
        "title": "My Course",
        "description": "Learn amazing things",
        "faq_link": "https://example.com",
        "faq_text": "Frequently Asked Questions"
      }
    }
  },
  "curriculum": {
    "locales": {
      "english": "./curriculum/locales/english"
    }
  }
}

Example curriculum file (my-course.md):

# Learn Amazing Things

Welcome to this course!

## 0

### --description--

The first lesson introduces basic concepts.

### --tests--

```js,runner=node
console.log("Testing");
assert(1 + 1 === 2);
```

### --seed--

#### --"add.js"--

```js
function add(a, b) {
  return a + b;
}
```

## 1

### --description--

The second lesson builds on the first.

### --tests--

```js,runner=node
assert(typeof add === 'function');
```

Project Layout

freeCodeCampOS is organized into several components:

cli/          # Command-line tool
client/       # React frontend
config/       # Shared types and configuration
docs/         # User documentation
example/      # Example curriculum
parser/       # Curriculum markdown parser
runner/       # Test execution engine
server/       # HTTP API and server

Common Tasks

Run Tests

cargo test --all

Lint and Format Code

# Check formatting
cargo fmt --all -- --check

# Fix formatting
cargo fmt --all

# Run linter
cargo clippy --all -- -D warnings

Build for Production

# Build optimized binaries
cargo build --release --all

# Build client assets
cd client && bun run build

View Documentation

# Build and serve mdbook documentation
cd docs && mdbook serve

Architecture Overview

Backend (Rust)

  • config - Type definitions for app configuration
  • parser - Parses curriculum markdown files into structured data
  • runner - Executes tests using various language runtimes
  • server - Axum web server with REST API and WebSocket support

Frontend (React + TypeScript)

  • Modern React 19 + TypeScript
  • Vite 7 for fast builds
  • TanStack Query for data fetching
  • Marked 17 for markdown rendering
  • Prism.js for syntax highlighting

Environment Variables

When running the server, you can configure via environment variables:

RUST_LOG=info          # Set log level (debug, info, warn, error)
PORT=8080              # Server port (default: 8080)
CONFIG_PATH=./conf.json # Path to configuration file

Next Steps

  • Read the Project Syntax guide to learn how to write curriculum files
  • Explore the example/ directory for a complete example course
  • Check out the Testing Guide to learn about test structure
  • Review Contributing guidelines to contribute to the project

Getting Help

Report issues on GitHub Issues

CLI

Installation

Releases

Locate your platform in the releases section and download the latest version.

cargo

Requires Rust to be installed: https://www.rust-lang.org/tools/install

cargo install create-freecodecamp-os-app

Usage

To create a new course with some boilerplate:

create-freecodecamp-os-app create

To add a project to an existing course:

create-freecodecamp-os-app add-project

To rename a project in an existing course:

create-freecodecamp-os-app rename-project

To validate the course configuration files:

create-freecodecamp-os-app validate

The version of the CLI is tied to the version of freecodecamp-os. Some options may not be available if the version of the CLI is not compatible with the version of freecodecamp-os that is installed.

Examples

If you create a course using @freecodecamp/freecodecamp-os, open a pull request to add it to this list.

freeCodeCamp.org

Web3

Rust

Configuration

freecodecamp.conf.json

Required Configuration

{
  "version": "4.0.0",
  "config": {
    "projects.json": "<PROJECTS_JSON>",
    "state.json": "<STATE_JSON>"
  },
  "curriculum": {
    "locales": {
      "<LOCALE>": "<LOCALE_DIR>"
    }
  }
}

Minimum Usable Example

{
  "version": "4.0.0",
  "config": {
    "projects.json": "./config/projects.json",
    "state.json": "./config/state.json"
  },
  "curriculum": {
    "locales": {
      "english": "./curriculum/locales/english"
    }
  }
}

Optional Configuration (Features)

port

By default, the server and client communicate over port 8080. To change this, add a port key to the configuration file:

Example

{
  "port": 8080
}

client

  • assets.header: path relative to the root of the course - string
  • assets.favicon: path relative to the root of the course - string
  • landing.<locale>.description: description of the course shown on the landing page - string
  • landing.<locale>.title: title of the course shown on the landing page - string
  • landing.<locale>.faq_link: link to the FAQ page - string
  • landing.<locale>.faq_text: text to display for the FAQ link - string
  • static_paths: static resources to serve - Record<string, string>

Example

{
  "client": {
    "assets": {
      "header": "./client/assets/header.png",
      "favicon": "./client/assets/favicon.ico"
    },
    "static_paths": {
      "/images": "./curriculum/images",
      "/script/injectable.js": "./client/injectable.js"
    }
  }
}

config

  • projects.json: path relative to the root of the course - string
  • state.json: path relative to the root of the course - string

Example

{
  "config": {
    "projects.json": "./config/projects.json",
    "state.json": "./config/state.json"
  }
}

curriculum

  • locales: an object of locale names and their corresponding paths relative to the root of the course - Record<string, string>
  • assertions: an object of locale names and their corresponding paths to a JSON file containing custom assertions - Record<string, string>

Example

{
  "curriculum": {
    "locales": {
      "english": "./curriculum/locales/english"
    },
    "assertions": {
      "afrikaans": "./curriculum/assertions/afrikaans.json"
    }
  }
}

Attention

Currently, english is a required locale, and is used as the default.

hot_reload

  • ignore: a list of paths to ignore when hot reloading - string[]

Example

{
  "hot_reload": {
    "ignore": [".logs/.temp.log", "config/", "/node_modules/", ".git"]
  }
}

tooling

  • helpers: path relative to the root of the course - string
  • plugins: path relative to the root of the course - string

Example

{
  "tooling": {
    "helpers": "./tooling/helpers.js",
    "plugins": "./tooling/plugins.js"
  }
}

projects.json

The projects.json file is where you define the project(s) metadata.

Definitions

  • id: A unique UUID - string
  • title: The title of the project - string
  • dashed_name: The name of the project corresponding to the curriculum/locales/<PROJECT_DASHED_NAME>.md file - string
  • order: The order in which the project should be displayed - number
  • is_integrated: Whether or not to treat the project as a single-lesson project - boolean (default: false)
  • is_public: Whether or not to enable the project for public viewing. Note: the project will still be visible on the landing page, but will be disabled - boolean (default: false)
  • run_tests_on_watch: Whether or not to run tests on file change - boolean (default: false)
  • is_reset_enabled: Whether or not to enable the reset button - boolean (default: false)
  • number_of_lessons: The number of lessons in the project - number1
  • seed_every_lesson: Whether or not to run the seed on lesson load - boolean (default: false)
  • blocking_tests: Run tests synchronously - boolean (default: false)
  • break_on_failure: Stop running tests on the first failure - boolean (default: false)

Required Configuration

[
  {
    "id": "e5f6a1b2-c3d4-4e5f-1a2b-3c4d5e6f7a8b",
    "title": "Course Title",
    "dashed_name": "<PROJECT_DASHED_NAME>",
    "order": 0
  }
]

Optional Configuration

Example

[
  {
    "id": "e5f6a1b2-c3d4-4e5f-1a2b-3c4d5e6f7a8b",
    "title": "Learn X by Building Y",
    "dashed_name": "learn-x-by-building-y",
    "order": 0,
    "is_integrated": false,
    "is_public": false,
    "current_lesson": 0,
    "run_tests_on_watch": false,
    "is_reset_enabled": false,
    "number_of_lessons": 10,
    "seed_every_lesson": false,
    "blocking_tests": false,
    "break_on_failure": false
  }
]

.gitignore

Retaining Files When a Step is Reset

Warning

Resetting a step removes all untracked files from the project directory. To prevent this for specific files, add them to a boilerplate .gitignore file, or the one in root.


  1. This is automagically calculated when the app is launched.

Project Syntax

This is the Markdown syntax used to create projects in the curriculum using the default parser.

Markers

# <TITLE>

# <TITLE>

The first paragraph is used as the description of the project. Optionally, a json code block can be used for extra metadata:

Example

# Learn X by Building Y

```json
{
  "id": "e5f6a1b2-c3d4-4e5f-1a2b-3c4d5e6f7a8b",
  "dashed_name": "learn-x-by-building-y",
  "order": 0,
  "is_integrated": false,
  "tags": ["Coming Soon!"]
}
```

This is a description.

## <N>

## <LESSON_NUMBER>

Zero-based numbering, because of course

Example

## 0

meta

## <N>

```json
{
  "watch": ["path/relative/to/root"],
  "ignore": ["path/relative/to/root"]
}
```

The meta.watch field is used to specify specific files to watch during a lesson. The meta.ignore field is used to specify specific files to ignore during a lesson. The watcher is affected once on lesson load.

The watch and ignore fields are optional. It does not make sense to provide both at the same time.

### --description--

### --description--

<DESCRIPTION_CONTENT>

Example

### --description--

This is the description content.

### --tests--

### --tests--

<TEST_TEXT>

```<LANGUAGE>,runner=<RUNNER>
<TEST_CODE>
```

Available runners: node (for JavaScript/TypeScript) and bash (for shell scripts).

Example

### --tests--

You should ...

```js,runner=node
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

### --seed--

### --seed--

#### --"<FILE_PATH>"--

```<FILE_EXTENSION>
<FILE_CONTENT>
```

#### --cmd--

```bash
<COMMAND>
```

Example

#### --seed--

#### --"index.js"--

```js
// I am an example boilerplate file
```

#### --cmd--

```bash
npm install
```

### --hints--

### --hints--

#### 0

Markdown-valid hint

#### 1

A second Markdown hint with code:

```rust
impl Developer for Camper {
  fn can_code(&self) -> bool {
    true
  }
}
```

Hooks

Hooks can be defined at the lesson level to run code before or after tests.

### --before-all--

Runs once before all tests in the lesson.

### --after-all--

Runs once after all tests in the lesson.

### --before-each--

Runs before each individual test in the lesson.

### --after-each--

Runs after each individual test in the lesson.

Example

### --before-each--

```js,runner=node
const __testVar = 1;

</div>
</details>

### `#### --force--`

Any seed marked with the force flag will overwrite the [`seed_every_lesson` configuration option](configuration.md#definitions-1). Specifically, the force flag causes the seed to run, if it were not going to, and it prevents the seed from running, if it were going to.

```markdown
### --seed--

#### --force--

<!-- Rest of seed -->

Attention

The force flag is ignored when the whole project is reset.

## --fcc-end--

An EOF discriminator.

## --fcc-end--

Example

curriculum/locales/english/learn-x-by-building-y.md

# Learn X by Building Y

In this course, you will learn X by building y.

## 0

### --description--

Declare a variable `a` with value `1`, in `index.js`.

```javascript
const a = 1;
```

### --tests--

You should declare a variable named `a`.

```js
const test = `console.assert(typeof a);`;
const filePath = 'learn-x-by-building-y/index.js';
const cb = (stdout, stderr) => {
  assert.isEmpty(stderr);
  assert.exists(stdout);
};
await __helpers.javascriptTest(filePath, test, cb);
```

You should give `a` a value of `1`.

```js
const test = `console.assert(a === 1, \`expected \${a} to equal 1\`);`;
const filePath = 'learn-x-by-building-y/index.js';
const cb = (stdout, stderr) => {
  assert.isEmpty(stderr);
  assert.exists(stdout);
};
await __helpers.javascriptTest(filePath, test, cb);
```

### --seed--

#### --"index.js"--

```js
// I am an example boilerplate file
```

## 1

```json
{
  "watch": ["learn-x-by-building-y/test/index.js"]
}
```

### --description--

Create a new directory named `test`, and create a file `test/index.ts`.

Then add the following:

```ts
const test: string = 'test';
```

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

### --seed--

#### --"index.js"--

```javascript
// I am an example boilerplate file
const a = 1;
```

## 2

### --description--

Description.

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

### --seed--

#### --cmd--

```bash
echo "I should run first"
```

#### --cmd--

```bash
mkdir test
```

#### --cmd--

```bash
touch test/index.ts
```

#### --"test/index.ts"--

```ts
const test: string = 'test';
```

## 3

### --description--

Description.

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

## 4

### --description--

Description.

### --tests--

You should ...

```js
await new Promise(resolve => setTimeout(resolve, 2000));
assert.equal(true, true);
```

## 5

### --description--

Description.

### --tests--

This test will pass after 5 seconds.

```js
await new Promise(resolve => setTimeout(resolve, 5000));
assert.equal(1, 1);
```

## --fcc-end--

It is also possible to add the seed for a lesson in a separate file named <PROJECT_DASHED_NAME>-seed.md within the locales directory.

curriculum/locales/english/learn-x-by-building-y-seed.md

## 0

### --seed--

#### --"index.js"--

```javascript
// Seed in a separate file
```

## --fcc-end--

Note

If seed for the same lesson is included in both the project file and a seed file, the seed in the project file will be used.

freeCodeCamp - Courses

The freeCodeCamp - Courses VSCode Extension makes working with freecodecamp-os in VSCode feature-rich.

Features

Commands

freeCodeCamp: Develop Course

Runs the develop-course script in the freecodecamp.conf.json of the current workspace. Also, enables debug-level logging in the terminal by setting NODE_ENV=development.

Also, with NODE_ENV=development, your workspace is validated following: https://github.com/freeCodeCamp/freeCodeCampOS/blob/main/.freeCodeCamp/tooling/validate.js

freeCodeCamp: Run Course

Runs the run-course script in the freecodecamp.conf.json of the current workspace.

freeCodeCamp: Shutdown Course

Disposes all terminals, and closes the visible text editors.

freeCodeCamp: Open Course

Within a new directory, this command shows the available courses to clone and then clones the selected course in the current workspace.

Lifecycle

The lifecycle of a lesson follows:

  1. Server parses lesson from curriculum Markdown file
  2. Server sends client lesson data
  3. Client renders lesson as HTML

Lesson

Todo

Lifecycle

The lifecycle of the testing system follows:

  1. Server parses test from curriculum Markdown file
  2. Server evaluates any --before-all-- ops
  • If any --before-all-- ops fail, an error is printed to the console
  • If any --before-all-- ops fail, the tests stop running
  1. Server evaluates all tests in parallel1
    1. Server evaluates any --before-each-- ops
      1. If any --before-each-- ops fail, test code is not run
    2. Server evaluates the test
    3. Server evaluates any --after-each-- ops
  2. Server evaluates any --after-all-- ops
  • If any --after-all-- ops fail, an error is printed to the console

  1. Tests can be configured to run in order, in a blocking fashion with the blockingTests configuration option.

Test Utilities

The following built-in helpers are available directly in the test context. Many are convenience wrappers around Nodejs' fs and child_process modules, which make use of the global ROOT variable to run relative to the root of the workspace.

controlWrapper

Wraps a function in an interval to retry until it does not throw or times out.

function controlWrapper(
  cb: () => any,
  { timeout = 10_000, stepSize = 250 }
): Promise<ReturnType<cb> | null>;

The callback function must throw for the control wrapper to re-try.

const cb = async () => {
  const flakyFetch = await fetch('http://localhost:3123');
  return flakyFetch.json();
};
const result = await controlWrapper(cb);

getBashHistory

Get the .logs/.bash_history.log file contents

Safety

Throws if file does not exist, or if read permission is denied.

function getBashHistory(): Promise<string>;
const bashHistory = await getBashHistory();

getCommandOutput

Returns the output of a command called from the given path relative to the root of the workspace.

Safety

Throws if path is not a valid POSIX/DOS path, and if promisified exec throws.

function getCommandOutput(
  command: string,
  path = ''
): Promise<{ stdout: string; stderr: string } | Error>;
const { stdout, stderr } = await getCommandOutput('ls');

getCWD

Get the .logs/.cwd.log file contents

Safety

Throws if file does not exist, or if read permission is denied.

function getCWD(): Promise<string>;
const cwd = await getCWD();

getLastCommand

Get the \(n^{th}\) latest line from .logs/.bash_history.log.

Safety

Throws if file does not exist, or if read permission is denied.

function getLastCommand(n = 0): Promise<string>;
const lastCommand = await getLastCommand();

getLastCWD

Get the \(n^{th}\) latest line from .logs/.cwd.log.

Safety

Throws if file does not exist, or if read permission is denied.

function getLastCWD(n = 0): Promise<string>;
const lastCWD = await getLastCWD();

getTemp

Get the .logs/.temp.log file contents.

Safety

Throws if file does not exist, or if read permission is denied.

function getTemp(): Promise<string>;
const temp = await getTemp();

Note

Use the output of the .temp.log file at your own risk. This file is raw input from the terminal including ANSI escape codes.

Output varies depending on emulator, terminal size, order text is typed, etc. For more info, see https://github.com/freeCodeCamp/solana-curriculum/issues/159

getTerminalOutput

Get the .logs/.terminal_out.log file contents.

Safety

Throws if file does not exist, or if read permission is denied.

function getTerminalOutput(): Promise<string>;
const terminalOutput = await getTerminalOutput();

importSansCache

Import a module side-stepping Nodejs' cache - cache-busting imports.

Safety

Throws if path is not a valid POSIX/DOS path, and if the import throws.

function importSansCache(path: string): Promise<any>;
const { exportedFile } = await importSansCache(
  'learn-x-by-building-y/index.js'
);

Globals

In the new test environment, several utilities are available directly or via the module scope of the test worker.

chai

The chai library is used for assertions. The following are available:

ROOT

The root of the workspace. This is available as a global variable.

path utilities

The node:path and node:fs/promises modules are available as:

  • join: From node:path.
  • readFile: From node:fs/promises.
  • writeFile: From node:fs/promises.

Built-in Helpers

The following helpers are available directly in the test context:

  • controlWrapper: Retries a function until it succeeds.
  • getBashHistory: Gets the .logs/.bash_history.log contents.
  • getCommandOutput: Returns the output of a command.
  • getCWD: Gets the .logs/.cwd.log contents.
  • getLastCommand: Gets the \(n^{th}\) latest line from the bash history.
  • getLastCWD: Gets the \(n^{th}\) latest line from the CWD history.
  • getTemp: Gets the .logs/.temp.log contents.
  • getTerminalOutput: Gets the .logs/.terminal_out.log contents.
  • importSansCache: Imports a module while bypassing the Node.js cache.

Collisions

As the tests are run in the context of the test worker, variable naming collisions may occur. To avoid this, it is recommended to prefix object names with __ (dunder).

Lifecycle

Resetting can follow one of two lifecycles:

  1. Whole project reset
  2. Lesson reset

Whole Project

A whole project reset is only invoked when the Reset button is clicked in the client.

This will run a git clean on the project directory - removing all files (tracked and untracked), but resetting them to their last committed state.

Then, the seed of each lesson will be run in order.

Lesson

A lesson reset only happens when either seedEveryLesson is set to true in the project config, or the force flag is set on a given lessons seed.

This will only run the seed for the current lesson.

Reset

Todo

Plugin System

Attention

The plugin system described below is currently not implemented in freeCodeCampOS v4.0.0.

The plugin system is a way to hook into events during the runtime of the application.

Plugins are defined within the JS file specified by the tooling.plugins configuration option.

Hooks

onTestsStart

Called when the tests start running, before any --before- hooks.

onTestsEnd

Called when the tests finish running, after all --after- hooks.

onProjectStart

Called when the project first loads, before any tests are run, and only happens once per project.

onProjectFinished

Called when the project is finished, after all tests are run and passed, and only happens once per project.

onLessonPassed

Called when a lesson passes, after all tests are run and passed, and only happens once per lesson.

onLessonFailed

Called when a lesson fails, after all tests are run and any fail.

onLessonLoad

Called once when a lesson is loaded, after the onProjectStart if the first lesson.

Parser

It is possible to define a custom parser for the curriculum files. This is useful when the curriculum files are not in the default format described in the project syntax section.

The first parameter of the parser functions is the project dashed name. This is the same as the dashed_name field in the projects.json file.

It is up to the parser to read, parse, and return the data in the format expected by the application.

getProjectMeta

(projectDashedName: string) =>
  Promise<{
    title: string;
    description: string;
    numberOfLessons: number;
    tags: string[];
  }>;

The title, tags, and description fields are expected to be either plain strings, or HTML strings which are then rendered in the client.

getLesson

Attention

This function can be called multiple times per lesson. Therefore, it is expected to be idempotent.

(projectDashedName: string, lessonNumber: number) =>
  Promise<{
    meta?: { watch?: string[]; ignore?: string[] };
    description: string;
    tests: [[string, string]];
    hints: string[];
    seed: [{ filePath: string; fileSeed: string } | string];
    isForce?: boolean;
    beforeAll?: string;
    afterAll?: string;
    beforeEach?: string;
    afterEach?: string;
  }>;

The meta field is expected to be an object with either a watch or ignore field. The watch field is expected to be an array of strings, and the ignore field is expected to be an array of strings.

The description field is expected to be either a plain string, or an HTML string which is then rendered in the client.

The tests[][0] field is the test text, and the tests[][1] field is the test code. The test text is expected to be either a plain string, or an HTML string.

The hints field is expected to be an array of plain strings, or an array of HTML strings.

The seed[].filePath field is the relative path to the file from the workspace root. The seed[].fileSeed field is the file content to be written to the file.

The seed[] field can also be a plain string, which is then treated as a bash command to be run in the workspace root.

An example of this can be seen in the default parser implementation in the parser crate.

Example

import { pluginEvents } from "@freecodecamp/freecodecamp-os/.freeCodeCamp/plugin/index.js";

pluginEvents.onTestsStart = async (project, testsState) => {
  console.log('onTestsStart');
};

pluginEvents.onTestsEnd = async (project, testsState) => {
  console.log('onTestsEnd');
};

pluginEvents.onProjectStart = async project => {
  console.log('onProjectStart');
};

pluginEvents.onProjectFinished = async project => {
  console.log('onProjectFinished');
};

pluginEvents.onLessonFailed = async project => {
  console.log('onLessonFailed');
};

pluginEvents.onLessonPassed = async project => {
  console.log('onLessonPassed');
};

Client Injection

With the static_paths config option, you can add a /script/injectable.js script to be injected in the head of the client.

Example

{
  "client": {
    "static_paths": {
      "/script/injectable.js": "./client/injectable.js"
    }
  }
}

There is a reserved websocket event (__run-client-code) that can be used to send code from the client to the server to be executed.

Execution Environment

The provided code must start with a valid shebang (e.g., #!/usr/bin/env node or #!/bin/bash). The server writes the code to a temporary file, sets executable permissions (on Unix), and executes it directly.

No globals or automatic imports are provided. You are responsible for importing any necessary modules and defining the environment within your script.

Response Data

The server returns a RESPONSE event with the following data:

  • stdout: The standard output of the process.
  • stderr: The standard error of the process.
  • exit_code: The process exit code.

Example

This enables scripts like the following to be run:

client/injectable.js

function checkForToken() {
  const serverTokenCode = `#!/usr/bin/env node
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
const ROOT = process.cwd();

try {
  const tokenFile = await readFile(join(ROOT, 'config/token.txt'));
  const token = tokenFile.toString();
  console.log(token);
} catch (e) {
  process.exit(1);
}`;
  socket.send(
    JSON.stringify({
      event: '__run-client-code',
      data: serverTokenCode
    })
  );
}

async function askForToken() {
  const modal = document.createElement('dialog');
  const p = document.createElement('p');
  p.innerText = 'Enter your token';
  p.style.color = 'black';
  const input = document.createElement('input');
  input.type = 'text';
  input.id = 'token-input';
  input.style.color = 'black';
  const button = document.createElement('button');
  button.innerText = 'Submit';
  button.style.color = 'black';
  button.onclick = async () => {
    const token = input.value;
    const serverTokenCode = `#!/usr/bin/env node
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
const ROOT = process.cwd();

try {
  await writeFile(join(ROOT, 'config/token.txt'), '${token}');
  process.exit(0);
} catch (e) {
  process.exit(1);
}`;
    socket.send(
      JSON.stringify({
        event: '__run-client-code',
        data: serverTokenCode
      })
    );
    modal.close();
  };

  modal.appendChild(p);
  modal.appendChild(input);
  modal.appendChild(button);
  document.body.appendChild(modal);
  modal.showModal();
}

const socket = new WebSocket(
  `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${
    window.location.host
  }/ws`
);

window.onload = function () {
  socket.onmessage = function (event) {
    const parsedData = JSON.parse(event.data);
    if (
      parsedData.event === 'RESPONSE' &&
      parsedData.data.event === '__run-client-code'
    ) {
      if (parsedData.data.error) {
        console.error(parsedData.data.error);
        return;
      }
      
      const { stdout, exit_code } = parsedData.data;
      if (exit_code !== 0 || !stdout.trim()) {
        askForToken();
        return;
      }
      window.__token = stdout.trim();
    }
  };
  let interval;
  interval = setInterval(() => {
    if (socket.readyState === 1) {
      clearInterval(interval);
      checkForToken();
    }
  }, 1000);
};

Contributing

Local Development

  1. Open freeCodeCampOS/example as a new workspace in VSCode
  2. Install dependencies and build the project:
    bun install
    bun run build
    
  3. Run the development server:
    cargo run --bin server
    
  4. In a separate terminal, run the client in development mode:
    cd client && bun run dev
    

Gitpod

  1. Open the project in Gitpod:

Open in Gitpod

Opening a Pull Request

  1. Fork the repository
  2. Push your changes to your fork
  3. Open a pull request with the recommended style

Commit Message

<type>(<scope>): <description>

Example

feat(docs): add contributing.md

Pull Request Title

<type>(<scope>): <description>

Example

feat(docs): add contributing.md

Pull Request Body

Answer the following questions:

  • What does this pull request do?
  • How should this be manually tested?
  • Any background context you want to provide?
  • What are the relevant issues?
  • Screenshots (if appropriate)

Types

  • fix
  • feat
  • refactor
  • chore

Scopes

Any top-level directory or config file. Changing a package should have a scope of dep or deps.

Documentation

This documention is built using mdBook. Read their documentation to install the latest version.

Also, the documentation uses mdbook-admonish:

cargo install mdbook-admonish

Serve the Documentation

cd docs
mdbook serve

This will spin up a local server at http://localhost:3000. Also, this has hot-reloading, so any changes you make will be reflected in the browser.

Build the Documentation

cd docs
mdbook build

CLI (create-freecodecamp-os-app)

The CLI is written in Rust, and is located in the cli directory.

Development

$ cd cli
cli$ cargo run

Flight Manual

Release

Releases are done manually through the GitHub Actions.

Making a Release

In the Actions tab, select the Publish to npm workflow. Then, select Run workflow.

Changelog

[4.0.0] - 2026-03-09

Major Rewrite (Rust Migration)

The platform has been completely rewritten from Node.js to Rust for improved performance, type safety, and a single-binary distribution.

Added

  • New Rust-based components:
    • server: Axum-based HTTP/WebSocket server.
    • parser: Markdown AST parser using Comrak.
    • runner: Multi-language test runner (Node.js, Bash).
    • cli: Unified CLI for curriculum management.
    • config: Shared configuration system.
  • CLI Commands:
    • create: Scaffold a new course or project.
    • rename-project: Rename an existing project.
    • validate: Validate curriculum and configuration.
  • Configuration Features:
    • hot_reload: Support for live reloading with an ignore list.
    • static_paths: Support for custom static routes in the client.
    • tooling: Configuration for custom helpers and plugins.
  • Parser Improvements:
    • Rich error reporting using miette.
    • Support for project metadata embedded directly in markdown.
    • Support for multiple hooks: before-all, after-all, before-each, after-each.
  • Runner Improvements:
    • Built-in test utilities (e.g., getBashHistory, controlWrapper) available in the test runner.
    • Support for runner specification in code blocks (e.g., js,runner=node).

Changed

  • Configuration Format:
    • Transitioned from camelCase to snake_case in freecodecamp.conf.json.
    • projects.json is no longer required as metadata is now extracted from curriculum files.
    • client.static renamed to client.static_paths.
  • Project Structure:
    • Modularized into a Rust workspace.
    • Client moved to its own directory and now uses Vite 7 with React 19.

Deprecated

  • Node.js-based server and tooling (replaced by Rust binaries).
  • projects.json (use project metadata in markdown instead).

[3.6.0] -

Add

  • Use bash's script command to record terminal input and output
    • Gated behind a feature flag
    • Existing .logs/ files will be deprecated in favour of script command in 4.0

[3.5.1] - 2024-03-19

Fix

  • Remove watcher from context in worker

[3.5.0] - 2024-03-18

Add

  • meta to getLesson
    • meta.watch and meta.ignore to alter watch behaviour when lesson loads

Fix

  • Add / to end of .git in defaultPathsToIgnore to prevent files starting with .git from being ignored
  • Trim description and title fields from parser

[3.4.1] - 2024-03-11

Fix

  • Convert Error class to object in worker before sending to parent

[3.4.0] - 2024-03-05

Add

  • pluginEvents.onLessonLoad
  • Loader showing progress for reset step

[3.3.0] - 2024-02-15

Add

  • tags to getProjectMeta

[3.2.0] - 2024-02-12

Add

  • Parser API for curriculum files
    • pluginEvents.getProjectMeta
    • pluginEvents.getLesson

Fix

  • Refactor original regex Markdown parser to use marked.lexer
    • This allows for more complex Markdown files to be parsed
    • Render Markdown in server
    • Pass HTML string to client to render

[3.1.0] - 2024-02-05

Add

  • client.landing.locale.title field for landing page h1

Fix

  • Give .description element a max-width of 750px
  • Add ws as dependency

[3.0.0] - 2024-02-01

Change

  • Remove freecodecamp.conf.json fields controlled by freecodecamp-courses extension
  • Allow hints for integrated projects
  • Replace use of FCC_OS_PORT with port field in freecodecamp.conf.json
  • Make version field required in freecodecamp.conf.json
  • Move project title and description to curriculum markdown files
  • Rename .terminal-out.log to .terminal_out.log

Migration Guide

  1. Configure the following settings for the freeCodeCamp - Courses extension, and remove them from the freecodecamp.conf.json file:
  • "freecodecamp-courses.autoStart"
  • "freecodecamp-courses.path"
  • "freecodecamp-courses.prepare"
  • "freecodecamp-courses.scripts.develop-course"
  • "freecodecamp-courses.scripts.run-course"
  • "freecodecamp-courses.workspace.files"
  • "freecodecamp-courses.workspace.previews"
  • "freecodecamp-courses.workspace.terminals"
  1. Instead of FCC_OS_PORT environment variable, use port field in freecodecamp.conf.json file
  2. Add a SemVer compliant version field to freecodecamp.conf.json file
  3. Remove the title and description fields in the projects.json, and add the description to each corresponding Markdown file immediately after the title
  4. Rename the .terminal-out.log file to .terminal_out.log

[2.1.0] - 2024-01-23

Add

  • Worker threads to run tests in parallel
  • ### --after-each-- to run code after each test
  • Cancel Tests button that terminates all workers
  • Plugin system for events:
    • onTestsStart
    • onTestsEnd
    • onProjectStart
    • onLessonPassed
    • onLessonFailed
    • onProjectFinished
  • /script/injectable.js static file to inject a JS script into the client
  • __run-client-code websocket event to run code in the server's context
  • Add create-freecodecamp-os-app cli for creating the boilerplate

Update

  • dependency @types/node to v18.18.13
  • dependency @types/react-dom to v18.2.17
  • dependency typescript to v5.3.2
  • dependency ts-loader to v9.5.1
  • dependency marked-highlight to v2.0.7
  • dependency marked to v9.1.6
  • babel monorepo to v7.23.3
  • dependency @types/prismjs to v1.26.3
  • dependency @types/react to v18.2.36
  • actions/setup-node digest to 1a4442c (#380)
  • dependency chai to v4.3.10
  • dependency @types/marked to v5.0.2

[2.0.0 - deprecated] - 2023-09-25

Add

  • make watcher global
  • process.env.FCC_OS_PORT || 8080 for server listen port
  • working hints

Change

  • remove __helpers.makeDirectory
  • remove __helpers.runCommand
  • remove __helpers.writeJsonFile
  • remove __helpers.getDirectory
  • remove __helpers.getFile
  • remove __helpers.getJsonFile
  • remove __helpers.copyDirectory
  • remove __helpers.copyProjectFiles
  • remove __helpers.fileExists
  • update controlWrapper to match documented API
  • start lessons at 0 instead of 1
  • remove landing page topic (h2)
  • config.path is no longer required
  • Remove postinstall script
  • Tests no longer have --before-all-- context

Update

  • dependency babel-loader to v9.1.2
  • dependency marked to v9
    • Markedjs had multiple major releases within 2 months
  • dependency typescript to v5.0.4
  • dependency webpack-cli to v5.1.1

Migration Guide

  1. Refactor tests to use Nodejs API instead of removed __helpers functions.
  2. Change all lesson numbers to be zero-based (start at 0)
  3. Manually build client before running tooling server (npm run build:client)
    1. Suggestion: Add cd ./node_modules/@freecodecamp/freecodecamp-os/ && npm run build:client to freecodecamp.conf.json > prepare
  4. Change --before-all-- into --before-each--
    1. Probably remove --after-all--
    2. No longer use global in tests

[1.10.0] - 2023-08-08

Add

  • config.client.static to serve files (e.g. images) in client

[1.9.2] - 2023-06-17

Fix

  • remove seeding files from watch during seeding

Update

  • react monorepo (#313)
  • github actions (#311)
  • react monorepo
  • dependency @types/node to v18.16.18

[1.9.1] - 2023-05-30

Fix

  • fix 1.9.0 introduced bug of hanging tests

Update

  • dependency webpack-dev-server to v4.15.0
  • dependency @types/react to v18.2.6
  • dependency @types/marked to v4.3.0
  • react monorepo
  • dependency @types/node to v18.16.5
  • dependency @babel/core to v7.21.8
  • babel monorepo to v7.21.5
  • pin dependencies (#241)

[1.9.0] - 2023-05-20

Fix

  • adjust build path
  • set $HOME for Gitpod

Add

  • add blockingTests flag
  • add breakOnFailure flag

Bugs

  • when blockingTests && breakOnFailure, proceeding tests appear to hang in client

[1.8.4] - 2023-04-19

Fix

  • seed files on lesson (#237)

Update

  • dependency webpack-dev-server to v4.13.3
  • dependency html-webpack-plugin to v5.5.1
  • dependency @types/react to v18.0.35
  • dependency @types/node to v18.15.11
  • babel monorepo to v7.21.4

[1.8.3] - 2023-03-30

Fix

  • adjust import pathing (#225)

Update

  • dependency @types/node to v18.15.10
  • dependency marked to v4.3.0
  • dependency nodemon to v2.0.22
  • dependency @types/node to v18.15.9
  • dependency @types/node to v18.15.8
  • dependency @types/react to v18.0.29
  • dependency webpack-dev-server to v4.13.1
  • dependency webpack-dev-server to v4.13.0
  • dependency style-loader to v3.3.2
  • dependency @types/node to v18.15.3
  • dependency @babel/core to v7.21.3
  • dependency @types/node to v18.15.0
  • dependency nodemon to v2.0.21
  • dependency @types/node to v18.14.6

[1.8.2] - 2023-03-03

Fix

  • parse crlf line endings (#210)

[1.8.1] - 2023-03-02

Fix

  • use node_module pathing (#209)

[1.8.0] - 2023-03-02

Change

  • do not copy .freeCodeCamp into root (#208)

Update

  • dependency @types/node to v18.14.2
  • babel monorepo to v7.21.0
  • dependency @types/node to v18.14.1
  • dependency @types/node to v18.14.0
  • dependency @types/react-dom to v18.0.11
  • dependency @types/react to v18.0.28
  • dependency @types/node to v18.13.0

[1.7.3] - 2023-02-08

Fix

  • handle completed projects (#197)
  • set default terminal (#200)

Update

  • dependency @types/node to v18.11.19
  • dependency typescript to v4.9.5

[1.7.2] - 2023-01-31

Fix

  • apply css to all code blocks (#196)

[1.7.1] - 2023-01-31

Add

  • validate curriculum on develop (#182)

Update

  • dependency @types/react to v18.0.27
  • dependency marked to v4.2.12
  • dependency @babel/core to v7.20.12
  • dependency @types/node to v18.11.18
  • dependency marked to v4.2.5
  • dependency @types/react-dom to v18.0.10
  • dependency @babel/core to v7.20.7
  • dependency @types/node to v18.11.17
  • dependency css-loader to v6.7.3
  • dependency @types/node to v18.11.16

[1.7.0] - 2023-01-31

Add

  • create cache-busting helper (#181)

Update

  • dependency @types/node to v18.11.13
  • dependency typescript to v4.9.4
  • dependency marked to v4.2.4
  • dependency @types/node to v18.11.12

[1.6.9] - 2022-12-08

Fix

  • pass WebSocket to reset function (#174)

[1.6.8] - 2022-12-07

Change

  • remove .env file from npm (#173)

[1.6.7] - 2022-12-07

Add

  • create mkdir helper function (#172)

[1.6.6] - 2022-12-06

Fix

  • handle file parser errors (#166)

Update

  • readme docs (#160)
  • dependency ts-loader to v9.4.2
  • dependency @types/react to v18.0.26
  • dependency @types/node to v18.11.10
  • dependency @babel/core to v7.20.5
  • dependency typescript to v4.9.3
  • dependency marked to v4.2.3
  • dependency @types/react-dom to v18.0.9
  • dependency css-loader to v6.7.2
  • dependency chai to v4.3.7
  • dependency marked to v4.2.2
  • babel monorepo
  • dependency @types/react to v18.0.25
  • dependency @types/node to v18.11.9
  • dependency @types/node to v18.11.8
  • dependency @types/node to v18.11.7
  • dependency @babel/plugin-syntax-import-assertions to v7.20.0
  • dependency @types/react-dom to v18.0.8
  • dependency @types/react to v18.0.24
  • dependency @babel/core to v7.19.6
  • dependency @babel/preset-env to v7.19.4
  • dependency express to v4.18.2
  • dependency typescript to v4.8.4
  • dependency marked to v4.1.1
  • babel monorepo to v7.19.3

[1.6.5] - 2022-09-28

Fix

  • reset project, prevent lesson under/over -flow (#139)
  • patch and enable reset button (#133)
  • step skipping buttons (#138)
  • docker settings (#137)

Add

  • feat: add script for camper info (#132)

Update

  • dependency ts-loader to v9.4.1
  • dependency webpack-dev-server to v4.11.1
  • dependency @types/react to v18.0.21

Change

  • updated styles for error page (#131)

[1.6.4] - 2022-09-19

Add

  • progress to projects (#121)

Change

  • disable reset button (#128)

[1.6.3] - 2022-09-19

Add

  • create test-util to get .temp.log file (#127)

Update

  • dependency nodemon to v2.0.20
  • dependency @types/react to v18.0.20
  • babel monorepo to v7.19.1
  • pin dependency logover to 2.0.0

[1.6.2] - 2022-09-16

Fix

  • fetch project on hot-reload

[1.6.1] - 2022-09-15

Fix

  • optional chaining to hot-reload in server (#119)

[1.6.0] - 2022-09-14

Add

  • versioning
  • hot-ignore (#118)

[1.5.5] - 2022-09-12

Change

  • replace spark with fade in (#116)

Update

  • dependency webpack-dev-server to v4.11.0
  • dependency @types/react to v18.0.19
  • babel monorepo to v7.19.0
  • dependency typescript to v4.8.3
  • dependency @types/react to v18.0.18
  • dependency @types/marked to v4.0.7
  • pin dependency @types/node to 18.7.15

[1.5.4] - 2022-09-09

Change

  • UI revamp (#108)

[1.5.3] - 2022-09-07

Fix

  • correctly show/hide files in production mode (#107)

[1.5.2] - 2022-09-07

Fix

  • add controlWrapper function (#106)

[1.5.1] - 2022-09-07

Fix

  • fix npm deps and installation (#105)

[1.5.0] - 2022-09-06

Add

  • before-all and before-each hooks (#82)
  • git build script for multiple projects (#58)
  • loader config for version 1.1.1 (#44)

Change

  • all major changes (#97)
  • with small improvements and updates (#54)
  • Improved client styling (#42)

Fix

  • ignoring of Mac files (#60)
  • pinning Ubuntu to version 20.04 (#53)
  • updating of dependency marked to v4.0.19 (598e2e7)
  • updating of dependency logover to v1.3.5 (1866487)
  • updating of dependency logover to v1.3.4 (513d189)
  • updating of dependency ws to v8.8.1 (ece46b5)
  • updating of dependency marked to v4.0.18 (a13a865)
  • updating of dependency logover to v1.3.2 (e1e6b10)
  • updating of dependency nodemon to v2.0.19 (459d450)
  • updating of dependency marked to v4.0.17 (bce9486)
  • updating of dependency ws to v8.8.0 (fc52646)
  • updating of dependency ws to v8.7.0 (36fc630)
  • updating of dependency ws to v8.6.0 (9ad962a)
  • updating of dependency express to v4.18.1 (0b7e21f)
  • updating of dependency prismjs to v1.28.0 (29734ff)
  • updating of dependency marked to v4.0.14 (17856ed)
  • updating of dependency marked to v4.0.13 (e482c37)
  • pinning of dependency nodemon to v2.0.15 (73e53e5)

Update

  • dependency marked to v4.0.19 (598e2e7)
  • dependency @types/react to v18.0.17 (4ca81e9)
  • dependency @types/react to v17.0.48 (7e68a76)
  • dependency @types/react to v17.0.47 (f419a80)
  • dependency @babel/core to v7.18.5 (f94eec6)
  • dependency @types/react to v17.0.45 (f44ab1c)
  • dependency @types/react-dom to v17.0.17 (014323f)
  • dependency @types/react-dom to v17.0.16 (f496e14)
  • dependency ts-loader to v9.3.1 (a4bb535)
  • dependency nodemon to v2.0.18 (c1540dd)
  • dependency typescript to v4.7.4 (a2ba270)
  • dependency typescript to v4.7.3 (f5cb880)
  • dependency typescript to v4.7.2 (29e5897)
  • dependency ts-loader to v9.3.0 (5081424)
  • dependency typescript to v4.6.4 (b1e8fe5)
  • dependency nodemon to v2.0.16 (68b6da7)
  • dependency @types/react-dom to v17.0.16 (f496e14)
  • dependency webpack-cli to v4.10.0 (85e4326)
  • dependency webpack-dev-server to v4.10.0 (b36bd79)

Roadmap

For the most part, this roadmap outlines todos for freecodecamp-os. If this roadmap is empty, then there are no todos 🎉

Documentation

Features

  • Loader to show progress of "Reset Step"
  • Crowdin translation integration