{"id":51186600,"url":"https://github.com/rockops/rockdemo","last_synced_at":"2026-06-27T11:00:54.367Z","repository":{"id":366200378,"uuid":"1275427169","full_name":"rockops/rockdemo","owner":"rockops","description":"Killercoda like scnarios right in your IDE","archived":false,"fork":false,"pushed_at":"2026-06-20T17:14:36.000Z","size":27,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-20T19:09:28.870Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rockops.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-20T17:12:39.000Z","updated_at":"2026-06-20T17:14:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rockops/rockdemo","commit_stats":null,"previous_names":["rockops/rockdemo"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/rockops/rockdemo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockops%2Frockdemo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockops%2Frockdemo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockops%2Frockdemo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockops%2Frockdemo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rockops","download_url":"https://codeload.github.com/rockops/rockdemo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rockops%2Frockdemo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34850575,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-27T11:00:53.641Z","updated_at":"2026-06-27T11:00:54.357Z","avatar_url":"https://github.com/rockops.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rockDemo\n\nA VS Code extension that turns [Killercoda](https://killercoda.com)-style\nmarkdown scenarios into live, clickable demos. It renders **CodeLens buttons**\nabove actionable code blocks in any markdown file, and can run full multi-step,\n**Docker-backed** scenarios from an `index.json` — so you can drive a live demo\nwithout copy-pasting commands by hand.\n\n## What it does\n\nrockDemo parses fenced code blocks whose **closing** fence carries an\nannotation — `{{exec}}`, `{{copy}}`, or `{{open}}` — and adds buttons above the\nblock:\n\n| Annotation | Buttons shown | Behaviour |\n| --- | --- | --- |\n| `{{exec}}` | **▶ Run in terminal** + **📋 Copy** | Sends the command to the active terminal and presses Enter |\n| `{{exec interrupt}}` | **▶ Run** + **📋 Copy** | Sends **Ctrl+C** to the terminal first, then runs the command |\n| `{{copy}}` | **📋 Copy** | Copies the command to the clipboard (does *not* run it) |\n| `{{open}}` | **📂 Open file** | Opens the referenced file, path resolved relative to the scenario |\n\nAs in Killercoda, **`bash` / `sh` / `shell` blocks are runnable by default** —\nthey get the `exec` buttons even without an explicit `{{exec}}` annotation.\n\n### Inline code (single backticks)\n\nInside the demo/scenario webview, **inline `` `code` `` spans are copyable by\ndefault** (Killercoda-style). A trailing `{{…}}` annotation overrides this:\n\n| Markdown | Result |\n| --- | --- |\n| `` `cmd` `` | 📋 copy icon (default) |\n| `` `cmd`{{}} `` | plain text, no icon (copy disabled) |\n| `` `cmd`{{exec}} `` | ▶ run + 📋 copy icons |\n| `` `cmd`{{exec interrupt}} `` | ▶ run (sends Ctrl+C first) + 📋 copy |\n| `` `cmd`{{copy}} `` | 📋 copy icon |\n\n### Markdown rendering\n\nThe webview uses a small zero-dependency markdown renderer that also supports:\n\n- **HTML passthrough** — an allow-list of inline/block tags (`\u003cbr\u003e`, `\u003ckbd\u003e`,\n  `\u003cimg\u003e`, `\u003cdiv\u003e`, `\u003ctable\u003e`, `\u003cdetails\u003e`, headings, lists, etc.) is emitted\n  verbatim instead of being escaped, so author HTML renders as intended.\n- **Blockquotes** — lines starting with `\u003e` render as `\u003cblockquote\u003e`.\n- **Syntax highlighting** — fenced code blocks are syntax-coloured with a\n  **vendored** copy of [highlight.js](https://highlightjs.org/) (see\n  [Third-party notices](#third-party-notices)). The theme follows the editor's\n  light/dark preference. A highlight spec on the info string\n  (e.g. ` ```js {2,5-6} `) shades those lines.\n\n### Two modes\n\n- **Edit mode** — work on the raw markdown; CodeLens buttons sit above each\n  actionable block (great while authoring a scenario).\n- **Demo / preview mode** — click the **▶ Run demo** button in the editor\n  title bar (next to VS Code's own preview button, shown for any markdown\n  file). This opens a dedicated **Webview** that renders the markdown like a\n  preview, **hides the meta fences** (` ```bash ` etc.), and replaces each\n  actionable block with clickable buttons. The panel auto-refreshes as you edit\n  or save the source file.\n\n  \u003e rockDemo uses its own webview rather than VS Code's built-in markdown\n  \u003e preview on purpose: the built-in preview renders content as *untrusted*,\n  \u003e which disables `command:` links and offers no message channel back to the\n  \u003e extension — so it can't run terminal commands. A self-owned webview can.\n\n- **▶ Run in terminal** reuses the active terminal if one exists, otherwise\n  creates a terminal named `rockDemo`. The command is typed *and* executed\n  (a trailing newline is sent). In scenario mode it targets the active node's\n  container shell instead.\n- **📋 Copy** writes the command to the system clipboard and shows a\n  confirmation notification.\n- **📂 Open file** opens the referenced file in the editor. In a scenario, if\n  the path is **container-absolute** (e.g. `/var/killercoda/solution/first.txt`)\n  and falls under a mounted asset, rockDemo opens the **host copy** bind-mounted\n  there — so what you edit is live inside the container. Otherwise the path is\n  treated as relative to the scenario/step file's folder.\n\n## Scenario mode (JSON-driven, Docker-backed)\n\nA full scenario is described by an `index.json` (Killercoda-style). Open it and\nclick **▶ Run demo** in the title bar to launch the **scenario player**:\n\n1. An **intro screen** shows the scenario `title` and `description` with a\n   **START** button. If `details.intro.text` is set, that markdown file is\n   rendered into the intro (with working `{{exec}}`/`{{copy}}`/`{{open}}`\n   buttons).\n2. On open, rockDemo starts an interactive shell in a Docker container for each\n   node. **Docker is a prerequisite.** `{{exec}}` commands run *inside* the\n   active node's container.\n   - With a single `backend.imageid`, the image id is looked up in the bundled\n     default profiles (see [Backends](#backends)).\n   - With `backendExtended.nodes`, **one terminal per node** is opened, each\n     named after its node key. `backendExtended` takes precedence over\n     `backend` when present.\n3. Clicking **START** walks through `details.steps` in order. Each step's\n   markdown is rendered with the demo player and gets navigation at the bottom\n   (**PREV** / **NEXT**, or **FINISH** on the last step). NEXT/FINISH may be\n   **gated** — see [Gating](#step-gating-verify--foreground).\n4. If `details.finish.text` is present, the end screen shows that markdown;\n   otherwise it reports completion.\n5. The **end screen** always has two buttons:\n   - **⟲ RESTART** (green) — tears down **all** node containers, relaunches\n     every one from scratch, and rebuilds the player back at the **intro\n     screen** (a fully clean start, with all gates reset).\n   - **✖ CLOSE** (red) — ends the scenario and tears down all containers (like\n     the **STOP** title-bar button).\n\nThe player auto-rebuilds when you save the `index.json` or any step markdown.\n\n### `index.json` shape\n\n```json\n{\n  \"title\": \"Découverte de kubectl\",\n  \"description\": \"Mes premiers pas avec kubectl\",\n  \"details\": {\n    \"intro\": {\n      \"text\": \"intro.md\",\n      \"background\": \"background.sh\",\n      \"foreground\": \"foreground.sh\",\n      \"host\": \"host2\"\n    },\n    \"steps\": [\n      {\n        \"title\": \"Premieres commandes\",\n        \"text\": \"step1/step1.md\",\n        \"background\": \"sh background.sh\",\n        \"foreground\": \"sh foreground.sh\",\n        \"verify\": \"step1/verify.sh\",\n        \"host\": \"host1\"\n      },\n      { \"title\": \"Création d'une ressource\", \"text\": \"step2/step2.md\", \"host\": \"host2\" }\n    ],\n    \"assets\": {\n      \"host1\": [\n        { \"file\": \"solution/**\", \"target\": \"/var/killercoda/solution\", \"chmod\": \"+w\" }\n      ]\n    },\n    \"finish\": { \"text\": \"finish.md\" }\n  },\n  \"backend\": { \"imageid\": \"ubuntu\" },\n  \"backendExtended\": {\n    \"nodes\": {\n      \"host1\": { \"imageid\": \"alpine\", \"cmd\": \"sh\", \"ip\": \"172.30.1.2\" },\n      \"host2\": { \"imageid\": \"ghcr.io/rockops/rockdemo/ubuntu:24.04\", \"cmd\": \"bash\", \"ip\": \"172.30.2.2\", \"docker\": true }\n    }\n  }\n}\n```\n\n#### Top-level\n\n- `title` / `description` — shown on the intro screen.\n- `backend.imageid` — a **key** into the bundled default profiles\n  (see [Backends](#backends)).\n- `backendExtended.nodes` — explicit multi-container map; **takes precedence**\n  over `backend` when present. Each key is a node name (used as the terminal\n  name, container hostname, and `host:` selector).\n\n#### Per-node fields (both `backends.json` profiles and `backendExtended`)\n\n| Field | Meaning |\n| --- | --- |\n| `imageid` | Docker image to run for this node. |\n| `cmd` | Shell/command to run in the container (e.g. `sh` for alpine, `bash` for ubuntu). Defaults to `sh`. |\n| `ip` | Static IP on the `172.30.0.0/16` subnet. When any node sets one, all nodes join the shared `rockdemo` Docker network. |\n| `docker` | `true` → run the container `--privileged` and start an in-container Docker daemon (Docker-in-Docker). |\n| `background` | Optional. A **script file** (path relative to the extension's `config/` folder, e.g. `ubuntu/background.sh`) run **detached and hidden** in this node's container when the env starts. Output is captured to `/var/log/rockdemo/\u003cscenario\u003e/\u003cnode\u003e_backend_background.log`. |\n| `foreground` | Optional. A **script file** (path relative to `config/`, e.g. `ubuntu/startup.sh`) run **visibly** in this node's terminal when the env starts. It **blocks** the player: the intro **START** button stays disabled until every node's backend foreground finishes. |\n\nThe node name becomes the container **hostname** (visible in the shell prompt).\n\n\u003e **Backend scripts live under `config/\u003cbackend\u003e/`** (e.g.\n\u003e [config/ubuntu/startup.sh](config/ubuntu/startup.sh)) and the `background` /\n\u003e `foreground` value is the file's path relative to `config/`. When a node\n\u003e references one, rockDemo mounts the bundled `config/` folder read-only into the\n\u003e container and runs the script **by path** — so there's nothing to copy and the\n\u003e scripts are version-controlled with the extension.\n\u003e\n\u003e Backend `background`/`foreground` run **once per launch** (and again on\n\u003e **RESTART**), on the intro screen — the moment the env comes up — so they're\n\u003e ideal for readiness waits (e.g. blocking START until the in-container Docker\n\u003e daemon is up). They compose with an intro `foreground`: START waits for both.\n\n#### `details.steps[]` / `details.intro`\n\n- `text` — markdown file (path relative to `index.json`) rendered as the body.\n- `background` — optional. A shell command, or a script file (e.g.\n  `background.sh`), run **detached and hidden** inside a node's container (via\n  `docker exec`) when the screen is entered (once per run). stdout/stderr are\n  captured to `/var/log/rockdemo/\u003cscenario\u003e/\u003cstep\u003e_background.log` inside the\n  container.\n- `foreground` — optional. A **single-line command** sent verbatim to the\n  node's terminal (Killercoda-style — *not* read as a file). It runs from\n  `/scenario`, **in the terminal** (output visible), and **blocks** the terminal\n  until it finishes. While it runs, **START/NEXT is disabled** for that screen and\n  re-enabled once it completes. Reference scripts relative to the scenario folder,\n  e.g. `./foreground.sh` or `sh foreground.sh`.\n- `verify` — optional (steps only). A command (resolved like `foreground`) that\n  checks the step was completed. The step shows a **✓ VERIFY** button and\n  **hides NEXT/FINISH until the command exits 0**. It runs hidden; output is\n  captured to `/var/log/rockdemo/\u003cscenario\u003e/\u003cstep\u003e_verify.log`. On failure the\n  VERIFY button flashes red and a notification points to the log.\n- `host` — selects the target node by name for `background`/`foreground`/\n  `verify`; otherwise the first node is used. If the named host doesn't exist,\n  rockDemo warns naming the missing host.\n\n\u003e When a step has **both** `verify` and `foreground`, NEXT is hidden+disabled\n\u003e until verify passes **and** the foreground command finishes.\n\nThe **scenario folder is bind-mounted read-only at `/scenario`** in every\ncontainer, so scenario scripts are available to run (and `foreground`/`verify`\nrun from there with `.` on `PATH`). Read-only keeps your host files safe.\n\n#### `details.assets`\n\nEach key is a **node name** (must match a node / `backend` host) and maps to a\nlist of asset rules:\n\n- `file` — glob of host **files** to stage, resolved **relative to the\n  scenario's `assets/` folder** (`\u003cscenario\u003e/assets/`). See globbing below.\n- `target` — destination **directory** inside the container (a leading `~`\n  expands to `/root`). A **wildcard** pattern places each file preserving its\n  **full path relative to `assets/`** — so `app1/**` → `target/app1/...` (the\n  matched prefix is kept, not stripped), nested folders recreated. A **literal\n  single file** (no `*`) is placed by **basename** — `app1/readme.md` →\n  `target/readme.md`.\n- `chmod` — `\"+w\"` (read-write), `\"+r\"` (read-only mount), or `\"+x\"`\n  (executable).\n\n**Globbing.** A pattern always resolves to a set of **files** (never folders),\nmatched against the `assets/` tree — mirroring Killercoda:\n\n- `*` matches any run of characters **within a single path segment** (never\n  crosses `/`). As the **last** segment it selects the **files** in a folder\n  (not the sub-folders); as an earlier segment it selects folders to descend\n  into (e.g. `app*/…`).\n- `**` matches **any number of path segments** (recursive, including zero) — the\n  globstar. Use it to pull a folder's whole subtree (e.g. `app1/**`).\n- Wildcards may appear in **any** segment, not just the last.\n\nEach match keeps its **full path relative to `assets/`** under `target` — the\nmatched prefix is never stripped. Examples, against\n[scenario-examples/upload-assets](scenario-examples/upload-assets) (root `assets/`):\n\n| `file` pattern   | matches                                  | lands under `target` as            |\n| ---------------- | ---------------------------------------- | ---------------------------------- |\n| `conf.yaml`      | `assets/conf.yaml`                       | `conf.yaml` (basename)             |\n| `app1/readme.md` | that one file                            | `readme.md` (basename, literal)    |\n| `*`              | top-level **files** only (not folders)   | `conf.yaml`, `run.sh`              |\n| `**`             | every file, recursively                  | `app1/config/app.json`, …          |\n| `app1/**`        | every file under `app1/`                 | `app1/config/app.json`, …          |\n| `**/*.json`      | every `.json` at any depth               | `app1/config/app.json`, …          |\n| `app1/**/*.json` | every `.json` under `app1/`              | `app1/config/app.json`, …          |\n| `app1/*/*.json`  | `.json` exactly one folder under `app1/` | `app1/config/app.json`, …          |\n| `app*/**/*.*`    | files with an extension under any `app*` | `app1/readme.md`, `app2/cnf/cnf.json`, … |\n\nAssets are **live-editable**. Rather than a one-shot `docker cp`, rockDemo\ncopies the matched files into a per-run scratch dir\n(`\u003cscenario\u003e/.rockdemo-run/\u003cnode\u003e/…`) and **bind-mounts that copy** into the\ncontainer. So:\n\n- your **original files are never touched** (only the scratch copy is),\n- editing the staged files in VS Code is reflected live inside the container\n  (and vice-versa for `+w`), and\n- `+r` is enforced as a **read-only mount** (`:ro`) — still editable from the\n  host, just not writable by the container.\n\nThe scratch dir is re-created fresh on every open/RESTART and **deleted when\nthe demo ends**. It's gitignored (`.rockdemo-run/`).\n\nA working example lives in [scenarios/simple/index.json](https://github.com/rockops/rockdemo/blob/main/scenarios/simple/index.json).\n\n## Backends\n\nWhen a scenario uses `backend.imageid` (no `backendExtended`), the value is\ntreated as a **key** into the bundled default profiles in\n[config/backends.json](config/backends.json). These profiles mimic Killercoda's\nnamed environments so the same scenario JSON runs unchanged. Each profile has\nthe same shape as a `backendExtended` block:\n\n```json\n{\n  \"ubuntu\": { \"nodes\": { \"node1\": { \"imageid\": \"ghcr.io/rockops/rockdemo/ubuntu:24.04\", \"ip\": \"172.30.1.2\", \"cmd\": \"bash\", \"docker\": true,\n    \"background\": \"ubuntu/background.sh\", \"foreground\": \"ubuntu/startup.sh\" } } },\n  \"alpine\": { \"nodes\": { \"node1\": { \"imageid\": \"alpine\", \"ip\": \"172.30.1.2\", \"cmd\": \"sh\" } } }\n}\n```\n\nA profile node may also carry `background`/`foreground` **script files** that run\nautomatically when the env starts (see the per-node fields table above). The\nvalue is a path under `config/` (here\n[config/ubuntu/startup.sh](config/ubuntu/startup.sh) blocks **START** until the\nin-container Docker daemon is ready).\n\n- An **unknown key** warns and launches nothing — for anything not covered by a\n  default profile, use `backendExtended`.\n- `config/backends.json` is bundled in the extension; it is the *default*\n  configuration. To customise, use `backendExtended` in your scenario.\n\n### Networking \u0026 `/etc/hosts`\n\nKillercoda gives nodes static IPs. When any node declares an `ip`, rockDemo:\n\n1. creates (idempotently) a user-defined Docker network `rockdemo` on subnet\n   `172.30.0.0/16`,\n2. attaches every node to it with its pinned `--ip`, and\n3. appends `\u003cip\u003e \u003chostname\u003e` lines for all nodes to each container's\n   `/etc/hosts`, so nodes can resolve one another by name.\n\n### Custom images\n\nKillercoda's environments come with tooling pre-installed. rockDemo ships\nDockerfiles under [docker/](docker/) (one subfolder per image). The `ubuntu`\nimage ([docker/ubuntu/Dockerfile](docker/ubuntu/Dockerfile)) is `ubuntu:24.04`\nplus `curl`, `wget`, `telnet`, `docker.io`, and `podman`, with `WORKDIR /root`.\n\nThe image is published to the **GitHub Container Registry** by\n[.github/workflows/docker-image.yml](.github/workflows/docker-image.yml) as\n`ghcr.io/rockops/rockdemo/ubuntu:24.04` (and `:latest`). The workflow runs on pushes to\n`main` that touch `docker/ubuntu/**`, and can also be triggered manually from the\nActions tab. It authenticates with the built-in `GITHUB_TOKEN`, so there are no\nsecrets to configure. Docker pulls the public image automatically the first time\na scenario references it — teammates don't need to build anything.\n\nTo build it locally instead (tag must match the `imageid` in\n`config/backends.json`):\n\n```bash\ndocker build -t ghcr.io/rockops/rockdemo/ubuntu:24.04 docker/ubuntu\n```\n\n\u003e The GHCR package must be **public** for an unauthenticated `docker pull` to\n\u003e work. After the first publish, set the package's visibility to public under the\n\u003e repo/org **Packages** settings (a one-time step).\n\n### Docker-in-Docker\n\nA node with `\"docker\": true` runs `--privileged` (with `--cgroupns=host` and\ndedicated volumes for `/var/lib/docker` and `/var/lib/containers`) and rockDemo\nstarts an in-container `dockerd` for it, so the scenario can run `docker`/\n`podman` *inside* the node. The daemon takes a few seconds to come up.\n\n### Safe cleanup\n\nEvery container, volume, and network rockDemo creates is stamped with the label\n`rockdemo=1`. On activation it sweeps **only** labelled stale resources (e.g.\nfrom a VS Code window that was force-closed mid-scenario), so an unclean exit\nnever leaves orphans — and unrelated Docker objects are never touched. rockDemo\nnever runs `docker volume prune` or any unscoped delete.\n\n## Step gating (verify / foreground)\n\nThe end-of-step navigation reacts to the step's scripts:\n\n- **`verify`** → NEXT/FINISH is **hidden** behind a **VERIFY** button until the\n  verify command exits 0.\n- **`foreground`** → NEXT/START is **disabled** while the foreground command runs\n  and re-enabled when it finishes. Completion is detected via a marker file the\n  command touches when done.\n- Both compose, as noted above.\n\nOn **RESTART** the webview HTML is rebuilt from scratch so all of these gates\nreset to their initial state.\n\n## Project layout\n\n```\nrockdemo/\n├── package.json           # Extension manifest (commands, activation events)\n├── src/extension.js       # All the logic — parser, CodeLens, webview, Docker\n├── config/backends.json   # Bundled default backend profiles (image-id keys)\n├── config/\u003cbackend\u003e/*.sh  # Backend startup scripts (background/foreground)\n├── docker/\u003cimage\u003e/Dockerfile  # Custom images (e.g. docker/ubuntu)\n├── media/                 # Vendored highlight.js + light/dark themes\n├── scenarios/simple/      # A full scenario example (index.json + steps)\n├── example/scenario.md    # A sample single-file scenario\n├── .vscode/launch.json    # \"Run rockDemo Extension\" debug config (F5)\n├── BUILD.md               # Release / publish process\n└── README.md\n```\n\nThe implementation lives entirely in [src/extension.js](https://github.com/rockops/rockdemo/blob/main/src/extension.js).\nNotable pieces:\n\n- `parseScenario` / `parseAnnotation` — line-based parser for actionable fenced\n  blocks and their `{{…}}` annotations (incl. the `interrupt` modifier).\n- `ScenarioCodeLensProvider` — turns blocks into `vscode.CodeLens` buttons.\n- `renderMarkdownToHtml` / `renderInline` / `inlineCodeHtml` / `codeBlockHtml` —\n  the zero-dependency markdown renderer (HTML passthrough, blockquotes, inline\n  code icons, highlight.js integration).\n- `resolveNodes` / `loadBackends` / `nodesFromMap` — resolve a scenario's\n  backend into the list of nodes to launch.\n- `startNodes` / `startNamedContainer` / `startDockerd` / `updateHosts` — launch\n  the per-node containers (hostname, static IP, network, DinD) and wire them up.\n- `runBackground` / `runForeground` / `runVerify` / `pollForegroundDone` — the\n  per-step script execution and gating.\n- `scenarioHtml` / `restartScenario` / `cleanupStaleResources` — the scenario\n  player webview, restart, and safe label-based cleanup.\n\n## Requirements\n\n- VS Code `^1.75.0`.\n- **Docker** on the extension host's PATH (for scenario mode).\n- **No npm dependencies, no build step.** The `vscode` module is provided by the\n  host at runtime, so there is no `npm install` and nothing to compile — the\n  extension runs straight from `src/extension.js`. The only third-party code is a\n  **vendored** copy of [highlight.js](https://highlightjs.org/) in\n  [media/](media/) (a static asset, not an npm dependency).\n\n## Third-party notices\n\nThis extension bundles [highlight.js](https://github.com/highlightjs/highlight.js)\n(the common-languages browser build) under [media/](media/) for syntax\nhighlighting. highlight.js is distributed under the BSD-3-Clause license; its full\nlicense text is kept alongside it at\n[media/LICENSE-highlight.js](media/LICENSE-highlight.js).\n\n## How to test it in VS Code\n\nThe extension is run via VS Code's built-in **Extension Development Host** — a\nsecond VS Code window that loads rockDemo from source.\n\n1. **Open the folder** — `File → Open Folder…` and select the `rockdemo`\n   folder (open the folder itself, not its parent).\n2. **Launch the dev host** — press **F5**, or open the **Run and Debug** panel\n   (`Ctrl/Cmd+Shift+D`) and choose **\"Run rockDemo Extension\"**, then click the\n   green ▶. This config is defined in [.vscode/launch.json](https://github.com/rockops/rockdemo/blob/main/.vscode/launch.json)\n   and starts a new window titled **[Extension Development Host]** with rockDemo\n   active.\n3. **Edit / Demo mode** — open [example/scenario.md](https://github.com/rockops/rockdemo/blob/main/example/scenario.md).\n   CodeLens links appear above each code block (edit mode); click **▶ Run demo**\n   in the title bar to open the demo webview.\n4. **Scenario mode** — open [scenarios/simple/index.json](https://github.com/rockops/rockdemo/blob/main/scenarios/simple/index.json)\n   and click **▶ Run demo**. You'll see the intro (title + description +\n   **START**), and a terminal per node opens running its container (needs Docker\n   installed and running). Click **START** to step through with **PREV / NEXT /\n   FINISH**; `{{exec}}` buttons send their commands into the active node's shell.\n\n### Iterating on changes\n\nAfter editing [src/extension.js](https://github.com/rockops/rockdemo/blob/main/src/extension.js), reload the Extension\nDevelopment Host to pick up the change: focus that window and run **Developer:\nReload Window** (`Ctrl/Cmd+R`), or stop and re-launch with F5. If CodeLens\nbuttons don't appear, confirm:\n\n- the file language is **Markdown** (bottom-right status bar),\n- CodeLens is enabled (`\"editor.codeLens\": true` in settings),\n- the block has a recognised annotation or is a `bash`/`sh`/`shell` block, and\n- the block body is non-empty (empty blocks are skipped).\n\n## Building \u0026 releasing\n\nPackaging the `.vsix` and publishing to the VS Code Marketplace (branches,\nalpha/stable channels, tags, the GitHub Actions pipeline) is documented in\n[BUILD.md](BUILD.md).\n\n## Scenario format (single-file)\n\nThe annotation goes on the **closing** fence:\n\n````markdown\n```bash\necho \"runs in the terminal by default\"\n```\n\n```sh\nexplicit exec — gets ▶ Run + 📋 Copy\n```{{exec}}\n\n```bash\ncopy-only command (e.g. destructive/interactive)\n```{{copy}}\n\n```text\n../path/to/file.py\n```{{open}}\n````\n\nNotes:\n\n- The `lang` after the opening fence only matters for the bash/sh/shell\n  default-exec behaviour; any language works with an explicit annotation.\n- `{{open}}` paths are resolved relative to the scenario file's directory.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frockops%2Frockdemo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frockops%2Frockdemo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frockops%2Frockdemo/lists"}