Skip to content

Instantly share code, notes, and snippets.

@bradygaster
Created June 22, 2026 07:45
Show Gist options
  • Select an option

  • Save bradygaster/a2a46b1f8259f442b82269e17ab614ec to your computer and use it in GitHub Desktop.

Select an option

Save bradygaster/a2a46b1f8259f442b82269e17ab614ec to your computer and use it in GitHub Desktop.
GitHub Copilot extension — ski-game

⛷️ Downhill Ski

A retro-style downhill skiing game built as a GitHub Copilot canvas extension. Dodge trees, rocks, and other skiers while hitting ramps for massive air time bonuses and threading slalom gates for extra points.

Downhill Ski gameplay

How to Play

Control Action
← → / A D Steer left/right
↑ / W Snowplow brake
↓ / S Tuck for speed
Space / Enter Start / Restart

Features

  • 3 lives per game — score accumulates across all lives
  • Ramps — hit them to launch into the air; faster = higher & longer jumps. Landing a jump gives a 5% speed boost
  • Slalom trails — snake through blue-flagged gates for increasing bonus points. Complete an entire trail for +200 bonus
  • Other skiers & snowboarders — moving obstacles that drift across the slope
  • Speed control — brake or tuck to manage your speed; no speed changes while airborne
  • Gradual difficulty — starts slow and builds up over time

Install

install_extension({ url: "https://gist.github.com/bradygaster/03f7159944b70ad2d1b848fe93033f76" })

License

MIT

{
"name": "ski-game",
"version": 1
}
import { createServer } from "node:http";
import { joinSession, createCanvas } from "@github/copilot-sdk/extension";
const servers = new Map();
const gameStates = new Map();
function renderHtml() {
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Downhill Ski</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; }
body {
background: var(--background-color-default, #f0f8ff);
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
}
canvas { display: block; width: 100%; height: 100%; }
#overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: rgba(0,0,0,0.6); color: white; z-index: 10;
}
#overlay.hidden { display: none; }
#overlay h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
#overlay p { font-size: 1.1rem; margin-bottom: 1rem; opacity: 0.8; }
#overlay button {
padding: 12px 32px; font-size: 1.1rem; border: none; border-radius: 6px;
background: #2ea44f; color: white; cursor: pointer; font-weight: 600;
}
#overlay button:hover { background: #2c974b; }
#hud {
position: absolute; top: 12px; left: 12px; right: 12px;
display: flex; justify-content: space-between;
font-size: 14px; font-weight: 600; color: #1f2328;
pointer-events: none; z-index: 5;
}
#hud span {
background: rgba(255,255,255,0.85); padding: 4px 10px; border-radius: 4px;
}
</style>
</head>
<body>
<div id="hud"><span id="score-display">Score: 0</span><span id="lives-display">Lives: 3</span><span id="speed-display">Speed: 1x</span></div>
<div id="overlay">
<h1>⛷️ Downhill Ski</h1>
<p>Dodge trees and rocks. Hit ramps for air time bonus!<br/>← → or A/D to steer. Space/Enter to start.</p>
<button id="start-btn">Start Game</button>
</div>
<canvas id="game"></canvas>
<script>
const cvs = document.getElementById("game");
const ctx = cvs.getContext("2d");
const overlay = document.getElementById("overlay");
const startBtn = document.getElementById("start-btn");
const scoreDisplay = document.getElementById("score-display");
const livesDisplay = document.getElementById("lives-display");
const speedDisplay = document.getElementById("speed-display");
let W, H;
function resize() { W = cvs.width = window.innerWidth; H = cvs.height = window.innerHeight; }
resize();
window.addEventListener("resize", resize);
// Game state
let running = false;
let score = 0;
let lives = 3;
let highScore = 0;
let speed = 0.5;
let speedBonus = 0;
let skier = { x: 0, y: 0, angle: 0, crashed: false, airborne: false, airTime: 0, airHeight: 0, shadow: 0 };
let obstacles = [];
let ramps = [];
let otherSkiers = [];
let slalomGates = [];
let particles = [];
let trails = [];
let frameCount = 0;
let airBonusFloaters = []; // floating "+N AIR!" text
const keys = {};
window.addEventListener("keydown", e => {
keys[e.key] = true;
// Restart/continue with Space or Enter when not running
if ((e.key === " " || e.key === "Enter") && !running) {
e.preventDefault();
if (lives > 0 && skier.crashed) {
startRun(); // continue with remaining lives
} else {
startGame(); // new game
}
}
});
window.addEventListener("keyup", e => { keys[e.key] = false; });
function startGame() {
// Fresh game: reset lives and score
lives = 3;
score = 0;
startRun();
}
function startRun() {
overlay.classList.add("hidden");
running = true;
speed = 0.5;
speedBonus = 0;
frameCount = 0;
skier = { x: W / 2, y: H * 0.25, angle: 0, crashed: false, airborne: false, airTime: 0, airHeight: 0, shadow: 0 };
obstacles = [];
ramps = [];
otherSkiers = [];
slalomGates = [];
particles = [];
trails = [];
airBonusFloaters = [];
livesDisplay.textContent = "Lives: " + lives;
for (let i = 0; i < 15; i++) {
spawnObstacle(Math.random() * H);
}
for (let i = 0; i < 3; i++) {
spawnRamp(H * 0.5 + Math.random() * H);
}
}
startBtn.addEventListener("click", startGame);
function spawnObstacle(y) {
const type = Math.random() < 0.65 ? "tree" : "rock";
obstacles.push({
x: Math.random() * (W - 40) + 20,
y: y !== undefined ? y : H + 20 + Math.random() * 100,
type,
size: type === "tree" ? 14 + Math.random() * 10 : 10 + Math.random() * 8,
});
}
function spawnRamp(y) {
ramps.push({
x: Math.random() * (W - 100) + 50,
y: y !== undefined ? y : H + 50 + Math.random() * 300,
width: 50 + Math.random() * 30,
hit: false,
});
}
function spawnOtherSkier(y) {
const isSnowboarder = Math.random() < 0.4;
otherSkiers.push({
x: Math.random() * (W - 60) + 30,
y: y !== undefined ? y : H + 30 + Math.random() * 200,
vx: (Math.random() - 0.5) * 2, // slight lateral drift
type: isSnowboarder ? "snowboarder" : "skier",
color: isSnowboarder ? "#e91e63" : ["#ff5722", "#9c27b0", "#00bcd4"][Math.floor(Math.random() * 3)],
speedOffset: (Math.random() - 0.5) * 1.5, // some go slightly faster/slower than you
});
}
function spawnSlalomTrail(y) {
// A slalom trail is a sequence of gates snaking left-right
const startY = y !== undefined ? y : H + 100;
const startX = W * 0.3 + Math.random() * W * 0.4;
const gateCount = 4 + Math.floor(Math.random() * 3); // 4-6 gates
const trailId = Date.now() + Math.random();
const gateSpacing = 70;
let x = startX;
for (let i = 0; i < gateCount; i++) {
const direction = (i % 2 === 0) ? 1 : -1;
x += direction * (40 + Math.random() * 30);
x = Math.max(60, Math.min(W - 60, x));
slalomGates.push({
x: x,
y: startY + i * gateSpacing,
trailId: trailId,
index: i,
total: gateCount,
passed: false,
missed: false,
width: 40,
});
}
}
function update() {
if (!running || skier.crashed) return;
frameCount++;
// Start slow and gradually increase speed
// Begins at 0.5, accelerates smoothly toward a cap of 14
const baseSpeed = 0.5 + frameCount * 0.003;
// Snowplow brake (up arrow / W) and tuck boost (down arrow / S) — only on ground
if (!skier.airborne) {
if (keys["ArrowUp"] || keys["w"] || keys["W"]) {
speedBonus -= 0.04;
if (speedBonus < -baseSpeed * 0.6) speedBonus = -baseSpeed * 0.6;
} else if (keys["ArrowDown"] || keys["s"] || keys["S"]) {
speedBonus += 0.02;
}
}
speed = Math.max(0.3, Math.min(baseSpeed, 14) + speedBonus);
// Steering (reduced control while airborne)
const turnSpeed = skier.airborne ? 1.5 : 4;
if (keys["ArrowLeft"] || keys["a"] || keys["A"]) {
skier.x -= turnSpeed;
skier.angle = -0.3;
} else if (keys["ArrowRight"] || keys["d"] || keys["D"]) {
skier.x += turnSpeed;
skier.angle = 0.3;
} else {
skier.angle *= 0.8;
}
// Keep skier in bounds
if (skier.x < 20) skier.x = 20;
if (skier.x > W - 20) skier.x = W - 20;
// Airborne physics — faster = higher and longer air
if (skier.airborne) {
skier.airTime++;
const totalFlight = 50 + speed * 8;
const progress = skier.airTime / totalFlight;
skier.airHeight = Math.sin(progress * Math.PI) * (20 + speed * 5);
skier.shadow = skier.airHeight * 0.4;
if (skier.airTime >= totalFlight) {
// Land! Bonus scales with air time and speed
const bonus = Math.floor(skier.airTime * speed * 0.5);
score += bonus;
airBonusFloaters.push({ x: skier.x, y: skier.y - 40, text: "+" + bonus + " AIR!", age: 0 });
skier.airborne = false;
skier.airTime = 0;
skier.airHeight = 0;
skier.shadow = 0;
// Boost speed by 5% after landing a jump
speedBonus += speed * 0.05;
}
}
// Move obstacles up (skier moves down the hill)
for (let i = obstacles.length - 1; i >= 0; i--) {
obstacles[i].y -= speed;
if (obstacles[i].y < -50) {
obstacles.splice(i, 1);
score += 10;
}
}
// Move ramps up
for (let i = ramps.length - 1; i >= 0; i--) {
ramps[i].y -= speed;
if (ramps[i].y < -80) {
ramps.splice(i, 1);
}
}
// Spawn new obstacles and ramps
while (obstacles.length < 12 + Math.floor(speed)) {
spawnObstacle(H + 20 + Math.random() * 200);
}
if (ramps.length < 2 && Math.random() < 0.02) {
spawnRamp();
}
// Spawn other skiers occasionally
if (otherSkiers.length < 3 && Math.random() < 0.008) {
spawnOtherSkier();
}
// Spawn slalom trails occasionally
const activeTrails = new Set(slalomGates.map(g => g.trailId));
if (activeTrails.size < 1 && Math.random() < 0.005) {
spawnSlalomTrail();
}
// Move other skiers
for (let i = otherSkiers.length - 1; i >= 0; i--) {
const other = otherSkiers[i];
other.y -= (speed + other.speedOffset);
other.x += other.vx;
// Bounce off edges
if (other.x < 30 || other.x > W - 30) other.vx *= -1;
if (other.y < -60) {
otherSkiers.splice(i, 1);
}
}
// Move slalom gates
for (let i = slalomGates.length - 1; i >= 0; i--) {
slalomGates[i].y -= speed;
if (slalomGates[i].y < -60) {
slalomGates.splice(i, 1);
}
}
// Slalom gate passing detection
if (!skier.airborne) {
for (const gate of slalomGates) {
if (!gate.passed && !gate.missed && Math.abs(skier.y - gate.y) < 12) {
if (Math.abs(skier.x - gate.x) < gate.width * 0.6) {
gate.passed = true;
// Check if all gates in this trail are passed
const trailGates = slalomGates.filter(g => g.trailId === gate.trailId);
const allPassed = trailGates.filter(g => g.index <= gate.index).every(g => g.passed);
if (allPassed) {
const bonus = 25 * (gate.index + 1);
score += bonus;
airBonusFloaters.push({ x: skier.x, y: skier.y - 30, text: "+" + bonus + " GATE!", age: 0 });
// Extra bonus for completing entire trail
if (gate.index === gate.total - 1) {
const trailBonus = 200;
score += trailBonus;
airBonusFloaters.push({ x: skier.x, y: skier.y - 55, text: "+" + trailBonus + " TRAIL COMPLETE!", age: 0 });
}
}
} else {
gate.missed = true;
}
}
}
}
// Other skier collision (they're obstacles too!)
if (!skier.airborne) {
for (const other of otherSkiers) {
const dx = skier.x - other.x;
const dy = skier.y - other.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 18) {
crash();
return;
}
}
}
// Ramp collision
if (!skier.airborne) {
for (const ramp of ramps) {
if (!ramp.hit) {
const dx = skier.x - ramp.x;
const dy = skier.y - ramp.y;
if (Math.abs(dx) < ramp.width * 0.5 && Math.abs(dy) < 20) {
ramp.hit = true;
skier.airborne = true;
skier.airTime = 0;
}
}
}
}
// Snow trail (only when on ground)
if (!skier.airborne && frameCount % 3 === 0) {
trails.push({ x: skier.x, y: skier.y + 16, age: 0 });
}
for (let i = trails.length - 1; i >= 0; i--) {
trails[i].y -= speed;
trails[i].age++;
if (trails[i].age > 30) trails.splice(i, 1);
}
// Collision detection (skip if airborne - you fly over obstacles!)
if (!skier.airborne) {
const skierRadius = 10;
for (const obs of obstacles) {
const dx = skier.x - obs.x;
const dy = skier.y - obs.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < skierRadius + obs.size * 0.5) {
crash();
return;
}
}
}
// Bonus floaters
for (let i = airBonusFloaters.length - 1; i >= 0; i--) {
airBonusFloaters[i].age++;
airBonusFloaters[i].y -= 1.5;
if (airBonusFloaters[i].age > 60) airBonusFloaters.splice(i, 1);
}
// Snow particles
if (frameCount % 2 === 0) {
particles.push({
x: Math.random() * W,
y: H + 5,
vx: (Math.random() - 0.5) * 0.5,
size: 1.5 + Math.random() * 2,
age: 0,
});
}
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].y -= speed * 0.7;
particles[i].x += particles[i].vx;
particles[i].age++;
if (particles[i].y < -10) particles.splice(i, 1);
}
scoreDisplay.textContent = "Score: " + score;
speedDisplay.textContent = skier.airborne
? "🚀 AIR TIME! +" + Math.floor(skier.airTime * speed * 0.5)
: "Speed: " + speed.toFixed(1) + "x";
}
function crash() {
skier.crashed = true;
running = false;
lives--;
if (lives > 0) {
// Still have lives — show continue screen
overlay.classList.remove("hidden");
overlay.querySelector("h1").textContent = "💥 Crashed!";
overlay.querySelector("p").textContent = "Score: " + score + " — " + lives + " " + (lives === 1 ? "life" : "lives") + " left. Press Space/Enter to continue.";
startBtn.textContent = "Continue";
startBtn.onclick = () => { startBtn.onclick = startGame; startRun(); };
} else {
// Game over
if (score > highScore) highScore = score;
overlay.classList.remove("hidden");
overlay.querySelector("h1").textContent = "💀 Game Over";
overlay.querySelector("p").textContent = "Final Score: " + score + (highScore > 0 ? " | High Score: " + highScore : "") + "\\nPress Space/Enter to play again.";
startBtn.textContent = "New Game";
startBtn.onclick = startGame;
}
}
function drawSkier() {
ctx.save();
ctx.translate(skier.x, skier.y);
// Shadow when airborne
if (skier.airborne) {
ctx.fillStyle = "rgba(0, 0, 0, 0.15)";
ctx.beginPath();
ctx.ellipse(skier.shadow * 0.3, skier.shadow + 10, 12, 5, 0, 0, Math.PI * 2);
ctx.fill();
}
// Lift skier up when airborne
const lift = skier.airborne ? -skier.airHeight : 0;
ctx.translate(0, lift);
ctx.rotate(skier.angle);
// Body
ctx.fillStyle = "#1a73e8";
ctx.beginPath();
ctx.ellipse(0, 0, 8, 14, 0, 0, Math.PI * 2);
ctx.fill();
// Head
ctx.fillStyle = "#ffcc80";
ctx.beginPath();
ctx.arc(0, -16, 7, 0, Math.PI * 2);
ctx.fill();
// Skis (angled when airborne for style)
ctx.strokeStyle = "#333";
ctx.lineWidth = 3;
ctx.lineCap = "round";
if (skier.airborne) {
ctx.beginPath();
ctx.moveTo(-8, 10);
ctx.lineTo(-4, 24);
ctx.moveTo(8, 10);
ctx.lineTo(4, 24);
ctx.stroke();
} else {
ctx.beginPath();
ctx.moveTo(-6, 12);
ctx.lineTo(-6, 22);
ctx.moveTo(6, 12);
ctx.lineTo(6, 22);
ctx.stroke();
}
ctx.restore();
}
function drawTree(x, y, size) {
ctx.save();
ctx.translate(x, y);
// Trunk
ctx.fillStyle = "#5d4037";
ctx.fillRect(-3, size * 0.3, 6, size * 0.5);
// Foliage layers
const greens = ["#2e7d32", "#388e3c", "#43a047"];
for (let i = 0; i < 3; i++) {
ctx.fillStyle = greens[i];
ctx.beginPath();
ctx.moveTo(0, -size + i * size * 0.3);
ctx.lineTo(-size * 0.6 + i * 2, size * 0.1 + i * size * 0.2);
ctx.lineTo(size * 0.6 - i * 2, size * 0.1 + i * size * 0.2);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
function drawRock(x, y, size) {
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = "#78909c";
ctx.beginPath();
ctx.ellipse(0, 0, size * 0.7, size * 0.5, 0.2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#546e7a";
ctx.beginPath();
ctx.ellipse(-size * 0.2, -size * 0.1, size * 0.3, size * 0.25, -0.1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawRamp(ramp) {
ctx.save();
ctx.translate(ramp.x, ramp.y);
const w = ramp.width;
// Ramp base - orange/brown wedge shape
const grad = ctx.createLinearGradient(0, 10, 0, -12);
grad.addColorStop(0, "#e65100");
grad.addColorStop(1, "#ff8f00");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(-w / 2, 10);
ctx.lineTo(-w / 2 + 8, -12);
ctx.lineTo(w / 2 - 8, -12);
ctx.lineTo(w / 2, 10);
ctx.closePath();
ctx.fill();
// Top edge highlight
ctx.strokeStyle = "#fff59d";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-w / 2 + 8, -12);
ctx.lineTo(w / 2 - 8, -12);
ctx.stroke();
// Arrow indicators
ctx.fillStyle = "#fff59d";
ctx.font = "bold 14px sans-serif";
ctx.textAlign = "center";
ctx.fillText("▲", 0, 5);
ctx.restore();
}
function drawOtherSkier(other) {
ctx.save();
ctx.translate(other.x, other.y);
if (other.type === "snowboarder") {
// Snowboarder: wider stance, single board
ctx.fillStyle = other.color;
ctx.beginPath();
ctx.ellipse(0, 0, 7, 12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffcc80";
ctx.beginPath();
ctx.arc(0, -14, 6, 0, Math.PI * 2);
ctx.fill();
// Board
ctx.strokeStyle = "#333";
ctx.lineWidth = 4;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(-10, 14);
ctx.lineTo(10, 14);
ctx.stroke();
} else {
// Other skier
ctx.fillStyle = other.color;
ctx.beginPath();
ctx.ellipse(0, 0, 7, 12, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#ffcc80";
ctx.beginPath();
ctx.arc(0, -14, 6, 0, Math.PI * 2);
ctx.fill();
// Skis
ctx.strokeStyle = "#333";
ctx.lineWidth = 2.5;
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(-5, 10);
ctx.lineTo(-5, 20);
ctx.moveTo(5, 10);
ctx.lineTo(5, 20);
ctx.stroke();
}
ctx.restore();
}
function drawSlalomGate(gate) {
ctx.save();
ctx.translate(gate.x, gate.y);
const w = gate.width;
// Gate poles
const poleColor = gate.passed ? "#4caf50" : gate.missed ? "#9e9e9e" : "#2196f3";
ctx.strokeStyle = poleColor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-w / 2, -15);
ctx.lineTo(-w / 2, 15);
ctx.moveTo(w / 2, -15);
ctx.lineTo(w / 2, 15);
ctx.stroke();
// Banner between poles
const bannerColor = gate.passed ? "rgba(76, 175, 80, 0.3)" : gate.missed ? "rgba(158, 158, 158, 0.2)" : "rgba(33, 150, 243, 0.3)";
ctx.fillStyle = bannerColor;
ctx.fillRect(-w / 2, -4, w, 8);
// Pole flags
ctx.fillStyle = poleColor;
ctx.beginPath();
ctx.moveTo(-w / 2, -15);
ctx.lineTo(-w / 2 + 10, -12);
ctx.lineTo(-w / 2, -9);
ctx.fill();
ctx.beginPath();
ctx.moveTo(w / 2, -15);
ctx.lineTo(w / 2 - 10, -12);
ctx.lineTo(w / 2, -9);
ctx.fill();
ctx.restore();
}
function draw() {
// Snow background
ctx.fillStyle = "#f5f9ff";
ctx.fillRect(0, 0, W, H);
// Ski tracks (trails)
for (const t of trails) {
const alpha = 1 - t.age / 30;
ctx.fillStyle = "rgba(200, 215, 230, " + alpha + ")";
ctx.fillRect(t.x - 3, t.y, 2, 4);
ctx.fillRect(t.x + 2, t.y, 2, 4);
}
// Snow particles
for (const p of particles) {
ctx.fillStyle = "rgba(180, 200, 220, 0.5)";
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
// Ramps
for (const ramp of ramps) {
if (ramp.y > -80 && ramp.y < H + 80) drawRamp(ramp);
}
// Slalom gates
for (const gate of slalomGates) {
if (gate.y > -60 && gate.y < H + 60) drawSlalomGate(gate);
}
// Obstacles
for (const obs of obstacles) {
if (obs.y < -50 || obs.y > H + 50) continue;
if (obs.type === "tree") drawTree(obs.x, obs.y, obs.size);
else drawRock(obs.x, obs.y, obs.size);
}
// Other skiers/snowboarders
for (const other of otherSkiers) {
if (other.y > -60 && other.y < H + 60) drawOtherSkier(other);
}
// Skier
if (!skier.crashed) drawSkier();
// Air bonus floaters
for (const f of airBonusFloaters) {
const alpha = 1 - f.age / 60;
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = "#ff6d00";
ctx.font = "bold 18px sans-serif";
ctx.textAlign = "center";
ctx.fillText(f.text, f.x, f.y);
ctx.restore();
}
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>`;
}
async function startServer(instanceId) {
const server = createServer((req, res) => {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(renderHtml());
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;
return { server, url: `http://127.0.0.1:${port}/` };
}
const session = await joinSession({
canvases: [
createCanvas({
id: "ski-game",
displayName: "Downhill Ski",
description: "A downhill skiing game where you dodge trees and rocks while racing down the mountain.",
actions: [
{
name: "reset_game",
description: "Reset the skiing game to start fresh",
handler: async (ctx) => {
return { message: "Reload the canvas to reset the game" };
},
},
],
open: async (ctx) => {
let entry = servers.get(ctx.instanceId);
if (!entry) {
entry = await startServer(ctx.instanceId);
servers.set(ctx.instanceId, entry);
}
return { title: "⛷️ Downhill Ski", url: entry.url };
},
onClose: async (ctx) => {
const entry = servers.get(ctx.instanceId);
if (entry) {
servers.delete(ctx.instanceId);
await new Promise((resolve) => entry.server.close(() => resolve()));
}
},
}),
],
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment