star-sdk-cli 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.mjs +1840 -7
  2. package/package.json +2 -1
package/dist/cli.mjs CHANGED
@@ -3,6 +3,1633 @@
3
3
  // src/cli.ts
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
+ import * as os from "os";
7
+
8
+ // src/skills.ts
9
+ var SKILL_CONTENT = `---
10
+ name: star-sdk
11
+ description: Use when you need a game that works on mobile without audio issues, has leaderboards without building a backend, or needs multiplayer without WebSocket complexity. Handles iOS Safari audio unlocking, DPR scaling, and coordinate conversion automatically.
12
+ ---
13
+
14
+ # Star SDK
15
+
16
+ Browser game development SDK with audio, canvas, leaderboards, and multiplayer.
17
+
18
+ ## Import
19
+
20
+ \`\`\`javascript
21
+ import Star from 'star-sdk';
22
+ \`\`\`
23
+
24
+ **CRITICAL:** Always use \`import Star from 'star-sdk'\` - not destructured imports.
25
+
26
+ ## API Overview
27
+
28
+ | API | Use When | Docs |
29
+ |-----|----------|------|
30
+ | \`Star.game()\` | Game loop, canvas, UI, input | [canvas.md](./canvas.md) |
31
+ | \`Star.audio\` | Sound effects, music | [audio.md](./audio.md) |
32
+ | \`Star.leaderboard\` | Scores, rankings | [leaderboard.md](./leaderboard.md) |
33
+ | \`Star.multiplayer\` | Real-time multiplayer | [multiplayer.md](./multiplayer.md) |
34
+
35
+ ## Quick Start
36
+
37
+ \`\`\`javascript
38
+ import Star from 'star-sdk';
39
+
40
+ Star.game(ctx => {
41
+ const { canvas, width, height, ctx: c } = ctx;
42
+ let score = 0;
43
+
44
+ // Preload audio
45
+ Star.audio.preload({ coin: 'coin', jump: 'jump' });
46
+
47
+ // Game loop
48
+ ctx.loop((dt) => {
49
+ c.fillStyle = '#1a1a2e';
50
+ c.fillRect(0, 0, width, height);
51
+ c.fillStyle = '#fff';
52
+ c.font = '24px sans-serif';
53
+ c.fillText(\`Score: \${score}\`, 20, 40);
54
+ });
55
+
56
+ // Input
57
+ canvas.onclick = () => {
58
+ score += 10;
59
+ Star.audio.play('coin');
60
+ };
61
+ });
62
+ \`\`\`
63
+
64
+ ## Common Patterns
65
+
66
+ ### Game Over -> Submit Score -> Show Leaderboard
67
+
68
+ \`\`\`javascript
69
+ function gameOver(finalScore) {
70
+ Star.leaderboard.submit(finalScore);
71
+ Star.leaderboard.show();
72
+ }
73
+ \`\`\`
74
+
75
+ ### Audio (It Just Works)
76
+
77
+ Star.audio handles mobile audio unlocking automatically. Just call \`play()\` - no special handling needed.
78
+
79
+ \`\`\`javascript
80
+ Star.audio.preload({ coin: 'coin', jump: 'jump' });
81
+ Star.audio.play('coin'); // Works on mobile, desktop, everywhere
82
+ \`\`\`
83
+
84
+ ### Coordinate Handling
85
+
86
+ \`\`\`javascript
87
+ canvas.onclick = (e) => {
88
+ const point = ctx.toStagePoint(e); // Correct coordinates
89
+ console.log(point.x, point.y);
90
+ };
91
+ \`\`\`
92
+
93
+ ## Don't Do This
94
+
95
+ - **Don't** create canvas manually - use \`Star.game()\`
96
+ - **Don't** use \`setInterval\` for game loops - use \`ctx.loop()\`
97
+ - **Don't** destructure Star - use \`Star.audio\`, \`Star.leaderboard\`, etc.
98
+ - **Don't** invent audio preset names - only 17 exist (see audio.md)
99
+
100
+ ## Audio Presets (Full List)
101
+
102
+ Only these 17 presets exist:
103
+ - UI: \`beep\`, \`click\`, \`select\`, \`error\`, \`success\`
104
+ - Actions: \`jump\`, \`swoosh\`, \`shoot\`, \`laser\`, \`explosion\`
105
+ - Combat: \`hit\`, \`hurt\`
106
+ - Collection: \`coin\`, \`pickup\`, \`bonus\`, \`unlock\`, \`powerup\`
107
+
108
+ ## Full Game Example
109
+
110
+ \`\`\`javascript
111
+ import Star from 'star-sdk';
112
+
113
+ Star.game(ctx => {
114
+ const { canvas, width, height, ctx: c } = ctx;
115
+ let score = 0;
116
+ let gameOver = false;
117
+ let playerY = height / 2;
118
+ let obstacles = [];
119
+
120
+ Star.audio.preload({
121
+ jump: 'jump',
122
+ coin: 'coin',
123
+ hurt: 'hurt'
124
+ });
125
+
126
+ // Spawn obstacles
127
+ setInterval(() => {
128
+ if (!gameOver) {
129
+ obstacles.push({ x: width, y: Math.random() * height, passed: false });
130
+ }
131
+ }, 2000);
132
+
133
+ ctx.loop((dt) => {
134
+ if (gameOver) return;
135
+
136
+ // Clear
137
+ c.fillStyle = '#1a1a2e';
138
+ c.fillRect(0, 0, width, height);
139
+
140
+ // Update obstacles
141
+ obstacles.forEach(obs => {
142
+ obs.x -= 200 * dt;
143
+
144
+ // Score when passed
145
+ if (!obs.passed && obs.x < 50) {
146
+ obs.passed = true;
147
+ score += 10;
148
+ Star.audio.play('coin');
149
+ }
150
+
151
+ // Collision
152
+ if (Math.abs(obs.x - 50) < 20 && Math.abs(obs.y - playerY) < 30) {
153
+ gameOver = true;
154
+ Star.audio.play('hurt');
155
+ Star.leaderboard.submit(score);
156
+ Star.leaderboard.show();
157
+ }
158
+ });
159
+
160
+ // Remove off-screen
161
+ obstacles = obstacles.filter(o => o.x > -20);
162
+
163
+ // Draw player
164
+ c.fillStyle = '#e94560';
165
+ c.beginPath();
166
+ c.arc(50, playerY, 15, 0, Math.PI * 2);
167
+ c.fill();
168
+
169
+ // Draw obstacles
170
+ c.fillStyle = '#0f4c75';
171
+ obstacles.forEach(obs => {
172
+ c.fillRect(obs.x - 10, obs.y - 25, 20, 50);
173
+ });
174
+
175
+ // Draw score
176
+ c.fillStyle = '#fff';
177
+ c.font = '24px sans-serif';
178
+ c.fillText(\`Score: \${score}\`, 20, 40);
179
+ });
180
+
181
+ // Jump on click/tap
182
+ canvas.onclick = () => {
183
+ if (!gameOver) {
184
+ playerY -= 50;
185
+ Star.audio.play('jump');
186
+ }
187
+ };
188
+ });
189
+ \`\`\`
190
+
191
+ For detailed API documentation, see the linked files above.
192
+ `;
193
+ var AUDIO_DOCS = `**Installation**
194
+
195
+ First, add the package to your project:
196
+ \` \` \`bash
197
+ yarn add star-audio
198
+ \` \` \`
199
+
200
+ ### Star Audio SDK
201
+
202
+ **Mobile-first, bulletproof audio for web games.** Works on iOS/Android out of the box. Missing files won't crash your game.
203
+
204
+ **Import:**
205
+ \` \` \`javascript
206
+ import createAudio from 'star-audio';
207
+ const audio = createAudio();
208
+ \` \` \`
209
+
210
+ **CRITICAL:** Import in JavaScript - don't add \`<script src="/star-sdk/audio.js">\` tags.
211
+
212
+ **Why Star Audio?**
213
+ - \u2705 **Mobile-first** - Works on iOS/Android, handles audio unlock automatically, plays even in silent mode
214
+ - \u2705 **Never throws** - Missing audio files? Game keeps running with clear warnings
215
+ - \u2705 **Zero-config** - Works on first play, no setup needed
216
+ - \u2705 **No try/catch needed** - Fire-and-forget API, perfect for AI-generated games
217
+
218
+ ---
219
+
220
+ **Quick Start:**
221
+
222
+ \` \` \`javascript
223
+ const audio = createAudio();
224
+
225
+ // CRITICAL: Preload presets before playing them
226
+ // You can set per-sound volumes in preload
227
+ audio.preload({
228
+ jump: { synth: 'jump', volume: 0.5 }, // Quieter jump
229
+ shoot: { synth: 'shoot', volume: 0.8 }, // Loud shoot
230
+ coin: { synth: 'coin', volume: 0.6 },
231
+ explosion: { synth: 'explosion', volume: 1.0 }
232
+ });
233
+
234
+ // Now play them - volumes are already set
235
+ audio.play('jump');
236
+ audio.play('shoot');
237
+ \` \` \`
238
+
239
+ **ONLY THESE 17 PRESETS EXIST - DO NOT INVENT NAMES:**
240
+ - UI: \`beep\`, \`click\`, \`select\`, \`error\`, \`success\`
241
+ - Actions: \`jump\`, \`swoosh\`, \`shoot\`, \`laser\`, \`explosion\`
242
+ - Combat: \`hit\`, \`hurt\`
243
+ - Collection: \`coin\`, \`pickup\`, \`bonus\`, \`unlock\`, \`powerup\`
244
+
245
+ **If you need a sound that's not in this list, use custom synth or generate audio.**
246
+
247
+ **CRITICAL:** Preload all presets before use. Set volumes in preload:
248
+ \` \` \`javascript
249
+ audio.preload({
250
+ jump: { synth: 'jump', volume: 0.5 }, // With custom volume
251
+ coin: 'coin', // Default volume
252
+ explosion: 'explosion' // For crashes/impacts
253
+ });
254
+ \` \` \`
255
+
256
+ ---
257
+
258
+ **Custom synth (advanced):**
259
+
260
+ \` \` \`javascript
261
+ audio.preload({
262
+ 'sfx.charge': {
263
+ waveform: 'triangle',
264
+ frequency: [200, 300, 450, 650, 900], // Rising charge-up
265
+ duration: 0.40,
266
+ volume: 0.38
267
+ }
268
+ });
269
+ \` \` \`
270
+
271
+ **Make sounds feel good:**
272
+ - **Use frequency arrays** - Sweeps/arpeggios are more satisfying than single tones
273
+ - **Rising = positive** - Ascending pitches for rewards (coin, jump, powerup)
274
+ - **Descending = impact** - Falling pitches for actions (shoot, hurt, explosion)
275
+ - **More notes = richer** - 3-6 frequencies sound fuller than 1-2
276
+ - **Musical intervals** - Use harmonious ratios (octaves, fifths, major chords)
277
+
278
+ **Waveform choice:**
279
+ - \`sine\` - Pure, pleasant (UI, bells, rewards)
280
+ - \`triangle\` - Warm, full (jumps, explosions, success)
281
+ - \`square\` - Retro, characterful (powerups, beeps, chiptune)
282
+ - \`sawtooth\` - Harsh, aggressive (lasers, damage, errors)
283
+
284
+ **Frequency guide:**
285
+ - High (800-2000 Hz): Bright, attention-grabbing (UI, coins)
286
+ - Mid (200-800 Hz): Game actions (jumps, shoots)
287
+ - Low (30-200 Hz): Impacts, bass (explosions, rumbles)
288
+ - Arrays: 3-4 notes for melodies, 6+ for noise-like effects
289
+
290
+ ---
291
+
292
+ **Audio files:**
293
+
294
+ \` \` \`javascript
295
+ audio.preload({
296
+ 'sfx.boom': 'assets/boom.mp3',
297
+ 'bgm.theme': 'assets/music.mp3'
298
+ });
299
+ audio.play('sfx.boom');
300
+ audio.music.crossfadeTo('bgm.theme', { duration: 1.5 });
301
+ \` \` \`
302
+
303
+ ---
304
+
305
+ **Controls:**
306
+
307
+ \` \` \`javascript
308
+ audio.setMusicVolume(0.5);
309
+ audio.setSfxVolume(0.8);
310
+ audio.toggleMute();
311
+ \` \` \``;
312
+ var CANVAS_DOCS = `**Installation**
313
+
314
+ First, add the package to your project:
315
+
316
+ \`\`\`bash
317
+ yarn add star-dom
318
+ \`\`\`
319
+
320
+ ### Star DOM SDK
321
+
322
+ Use the **Star DOM SDK** to initialize games reliably.
323
+ It prevents the most common bugs:
324
+
325
+ - \u2705 No "cannot read addEventListener of null"
326
+ - \u2705 No canvas sizing/DPR/blur issues
327
+ - \u2705 No accidentally wiping the canvas with \`innerHTML\`
328
+ - \u2705 Games work identically on ALL devices (fixed 16:9 with letterboxing)
329
+
330
+ -----
331
+
332
+ ### Fixed 16:9 Resolution
333
+
334
+ **Default: 640\xD7360 (landscape) or 360\xD7640 (portrait).** Games work identically on every device.
335
+
336
+ The SDK uses letterboxing to maintain the exact game area. This means:
337
+ - Positions like \`x: 320, y: 180\` always mean the exact center
338
+ - Two objects at \`x: 100\` and \`x: 540\` are always the same distance apart
339
+ - No "works on my screen, breaks on mobile" bugs
340
+
341
+ \`\`\`ts
342
+ // These values work identically on ALL devices:
343
+ const player = { x: 320, y: 300 }; // Center-bottom area
344
+ const enemy = { x: 600, y: 50 }; // Top-right area
345
+ const playerSize = 32; // Always 32px
346
+ const speed = 200; // Always 200px/sec
347
+ \`\`\`
348
+
349
+ -----
350
+
351
+ ### Golden Path (How to Use)
352
+
353
+ Import \`game\` and wrap your code in it. The \`game\` function handles DOM readiness, creates a canvas and a UI overlay, and gives you a safe context to build.
354
+
355
+ \`\`\`ts
356
+ import { game } from 'star-canvas';
357
+
358
+ game(({ ctx, width, height, on, loop, ui, canvas }) => {
359
+ // ctx: The 2D canvas context
360
+ // width, height: The logical size (CSS pixels) - READ-ONLY
361
+ // on: Safe, delegated event listener
362
+ // loop: Stable game loop (with dt)
363
+ // ui: Safe overlay for HTML
364
+ // canvas: The <canvas> element
365
+
366
+ // 1. Draw on the canvas
367
+ loop((dt) => {
368
+ ctx.clearRect(0, 0, width, height);
369
+ ctx.fillStyle = '#22d3ee'; // cyan-400
370
+ ctx.fillRect(width / 2 - 25, height / 2 - 25, 50, 50);
371
+ });
372
+
373
+ // 2. Render HTML to the safe UI overlay
374
+ // UI is interactive by default (scroll, buttons work)
375
+ // Adding canvas.addEventListener makes UI click-through automatically
376
+ ui.render(\`
377
+ <div class="absolute top-4 left-4 text-white">
378
+ <button id="start-btn" class="px-4 py-2 bg-blue-500 rounded pointer-events-auto">
379
+ Click Me
380
+ </button>
381
+ </div>
382
+ \`);
383
+
384
+ // 3. Listen for button clicks
385
+ on('click', '#start-btn', () => {
386
+ console.log('Button clicked!');
387
+ });
388
+
389
+ // 4. For canvas games: listen for taps on canvas
390
+ // This automatically makes UI click-through (taps pass through to canvas)
391
+ // Buttons with pointer-events-auto still work
392
+ canvas.addEventListener('pointerdown', (e) => {
393
+ console.log('Canvas/screen tapped!', e);
394
+ });
395
+ });
396
+ \`\`\`
397
+
398
+ > **CRITICAL:** Always import the SDK in your JavaScript/TypeScript.
399
+ > **Do not** add a \`<script src="/star-sdk/dom.js">\` tag in HTML.
400
+ >
401
+ > **Recommended Import:**
402
+ >
403
+ > \`\`\`ts
404
+ > import { game } from 'star-canvas';
405
+ > \`\`\`
406
+
407
+ -----
408
+
409
+ ## Core API: \`game(setup, options?)\`
410
+
411
+ The \`setup\` function receives one argument: a \`GameContext\` object with the following properties:
412
+
413
+ ### \`ctx: CanvasRenderingContext2D\`
414
+
415
+ The 2D drawing context. Its transform is already scaled for DPR. You **always draw in logical CSS pixels**.
416
+
417
+ ### \`canvas: HTMLCanvasElement\`
418
+
419
+ The \`<canvas>\` element itself.
420
+
421
+ - **Use this for gameplay input listeners** (e.g., \`pointerdown\`, \`pointermove\`).
422
+
423
+ ### \`width: number\` (getter)
424
+
425
+ ### \`height: number\` (getter)
426
+
427
+ The logical CSS pixel width and height of the stage. **Use these for all game logic and drawing.** They are getters, so they are always up-to-date.
428
+
429
+ ### \`on(type, selector, handler, options?)\`
430
+
431
+ Attaches a **delegated event listener** to the document.
432
+
433
+ - \u2705 **Use this for UI elements** (buttons, menus) inside your \`ui.render()\` HTML.
434
+ - \u2705 Survives \`ui.render()\` calls.
435
+ - Returns an \`off()\` function to unsubscribe.
436
+
437
+ ### \`loop(tick)\`
438
+
439
+ Starts a \`requestAnimationFrame\` loop.
440
+
441
+ - \`tick\` function receives \`(dt, now)\`, where \`dt\` is **delta time in seconds**.
442
+ - **ALWAYS** multiply movement by \`dt\` (e.g., \`player.x += speed * dt\`).
443
+ - Returns \`{ start(), stop(), running }\`. The loop starts automatically.
444
+
445
+ ### \`ui: GameUI\`
446
+
447
+ A safe manager for your HTML overlay, stacked on top of the canvas.
448
+
449
+ - \`ui.root\`: The \`<div>\` element for your UI. It is **interactive by default** (standard HTML behavior - scroll, buttons work).
450
+ - \`ui.render(html: string)\`: **Use this** to set your UI. It's safe and won't destroy the canvas.
451
+ - Automatically skips updates if HTML is unchanged (safe to call in loop for static content)
452
+ - For best performance with dynamic content (score), only call when values actually change
453
+ - \`ui.el(selector)\`: Scoped \`querySelector\` for the UI root.
454
+ - \`ui.all(selector)\`: Scoped \`querySelectorAll\` for the UI root.
455
+
456
+ **Auto-detection:** When you add \`canvas.addEventListener('pointerdown', ...)\`, the SDK automatically makes UI click-through so taps reach the canvas. Buttons with \`pointer-events-auto\` still work.
457
+
458
+ ### Cursor Management
459
+
460
+ **CRITICAL:** Choose cursor based on how players interact. Update cursor when state changes (e.g., menu \u2192 playing \u2192 gameover).
461
+
462
+ \`\`\`ts
463
+ // MOUSE-BASED GAMES (click/point-and-click/puzzle/clicker/strategy/constellation)
464
+ if (state === 'playing') canvas.style.cursor = 'pointer'; // Show where to click
465
+ if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'pointer'; // Keep visible
466
+
467
+ // PRECISION AIMING (shooter/drawing/building)
468
+ if (state === 'playing') canvas.style.cursor = 'crosshair';
469
+ if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'auto';
470
+
471
+ // KEYBOARD/TOUCH ONLY (platformer/WASD/rhythm/endless runner)
472
+ if (state === 'playing') canvas.style.cursor = 'none'; // Hide (doesn't matter)
473
+ if (state === 'menu' || state === 'gameover') canvas.style.cursor = 'auto'; // Show for menus!
474
+ \`\`\`
475
+
476
+ **Decision:** Does player click on game objects? \u2192 \`'pointer'\` | Aim precisely? \u2192 \`'crosshair'\` | WASD/touch only? \u2192 \`'none'\` during play, \`'auto'\` for menus
477
+
478
+ ### \`toStagePoint(event)\`
479
+
480
+ Converts \`MouseEvent\` or \`PointerEvent\` client coordinates to the stage's logical coordinates.
481
+
482
+ - **USE THIS** for all canvas pointer input.
483
+
484
+ ### \`createDrag()\`
485
+
486
+ Creates a drag state helper that handles coordinate conversion and offset tracking automatically.
487
+
488
+ \`\`\`ts
489
+ const drag = createDrag();
490
+
491
+ canvas.addEventListener('pointerdown', (e) => {
492
+ canvas.setPointerCapture(e.pointerId); // IMPORTANT: Capture for reliable drags
493
+ const { x, y } = drag.point(e); // Convert coordinates
494
+ const hit = pieces.find(p => /* hit test */);
495
+ if (hit) drag.grab(e, hit); // Start drag with offset
496
+ });
497
+
498
+ canvas.addEventListener('pointermove', (e) => drag.move(e)); // Updates position
499
+ canvas.addEventListener('pointerup', () => {
500
+ const dropped = drag.release(); // Returns dropped object (or null)
501
+ });
502
+ \`\`\`
503
+
504
+ **API:**
505
+ - \`point(e)\` - Pure coordinate conversion, no side effects
506
+ - \`grab(e, obj)\` - Start dragging an object, computing offset from cursor
507
+ - \`move(e)\` - Update dragged object's position
508
+ - \`release()\` - End drag, returns dropped object or null
509
+ - \`dragging\` - The currently dragged object (or null)
510
+
511
+ ### \`GameOptions\` (optional)
512
+
513
+ Pass an options object as the second argument to \`game()\`:
514
+
515
+ - \`preset?: 'landscape' | 'portrait' | 'responsive'\`: Game orientation preset.
516
+ - \`'landscape'\` (default): 640\xD7360 - for platformers, shooters, racing
517
+ - \`'portrait'\`: 360\xD7640 - for puzzle, cards, match-3, mobile-style
518
+ - \`'responsive'\`: Fills container, no fixed dimensions (legacy - gameplay varies by device)
519
+ - \`width?: number\`: Override width (default: 640 for landscape, 360 for portrait)
520
+ - \`height?: number\`: Override height (default: 360 for landscape, 640 for portrait)
521
+ - \`fit?: 'contain' | 'cover' | 'stretch'\`: How game fits container (default: \`'contain'\` with letterboxing)
522
+ - \`pixelRatio?: 'device' | number\`: (default: \`'device'\`)
523
+ - \`maxPixelRatio?: number\`: (default: \`2\`)
524
+ - \`preventContextMenu?: boolean\`: Prevent right-click context menu on canvas (default: \`true\`)
525
+
526
+ **Default behavior:** Fixed 640\xD7360 (16:9) with letterboxing. Games work identically on all devices.
527
+
528
+ -----
529
+
530
+ ## Recipes
531
+
532
+ ### Recipe 1: UI-Only Game (e.g., Clicker)
533
+
534
+ Use \`game\`, \`on\`, and \`ui\`.
535
+
536
+ \`\`\`ts
537
+ import { game } from 'star-canvas';
538
+
539
+ game(({ on, ui }) => {
540
+ let score = 0;
541
+
542
+ function render() {
543
+ // UI is interactive by default - buttons, scroll, forms all work
544
+ ui.render(\`
545
+ <div class="min-h-[100dvh] grid place-items-center bg-purple-900 text-white">
546
+ <div class="text-center space-y-4">
547
+ <h1 class="text-4xl font-bold">Score: \\\${score}</h1>
548
+ <button id="clickBtn" class="px-8 py-4 rounded-xl bg-cyan-400 text-slate-900 font-bold">
549
+ Click Me!
550
+ </button>
551
+ </div>
552
+ </div>
553
+ \`);
554
+ }
555
+
556
+ // Button clicks work by default
557
+ on('click', '#clickBtn', () => {
558
+ score++;
559
+ render();
560
+ });
561
+
562
+ render();
563
+ });
564
+ \`\`\`
565
+
566
+ ### Recipe 2: Canvas Game (Landscape)
567
+
568
+ Default pattern - fixed 640\xD7360 resolution. Games work identically on all devices.
569
+
570
+ \`\`\`ts
571
+ import { game } from 'star-canvas';
572
+
573
+ game(({ ctx, width, height, loop }) => {
574
+ // width = 640, height = 360 (always, with letterboxing)
575
+ const playerSize = 32;
576
+ const speed = 200; // 200px per second
577
+
578
+ const player = { x: 64, y: 180 }; // Fixed positions work everywhere
579
+
580
+ loop((dt) => {
581
+ player.x += speed * dt;
582
+ if (player.x > width) player.x = -playerSize;
583
+
584
+ ctx.fillStyle = '#0f172a';
585
+ ctx.fillRect(0, 0, width, height);
586
+
587
+ ctx.fillStyle = '#22d3ee';
588
+ ctx.fillRect(player.x, player.y - playerSize/2, playerSize, playerSize);
589
+ });
590
+ });
591
+ // Default: 640\xD7360 landscape with letterboxing
592
+ \`\`\`
593
+
594
+ ### Recipe 3: Canvas Game (Portrait)
595
+
596
+ For puzzle games, card games, match-3, mobile-style games - use portrait preset.
597
+
598
+ \`\`\`ts
599
+ import { game } from 'star-canvas';
600
+
601
+ game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
602
+ // width = 360, height = 640 (always, with letterboxing)
603
+ const cellSize = 40;
604
+ const gridCols = 8;
605
+ const gridRows = 12;
606
+
607
+ // Center the grid
608
+ const gridWidth = gridCols * cellSize;
609
+ const gridX = (width - gridWidth) / 2;
610
+ const gridY = 80;
611
+
612
+ canvas.addEventListener('pointerdown', (e) => {
613
+ const { x, y } = toStagePoint(e);
614
+ // Handle tap on grid...
615
+ });
616
+
617
+ loop((dt) => {
618
+ ctx.fillStyle = '#1e1b4b';
619
+ ctx.fillRect(0, 0, width, height);
620
+
621
+ ctx.strokeStyle = '#4338ca';
622
+ for (let row = 0; row < gridRows; row++) {
623
+ for (let col = 0; col < gridCols; col++) {
624
+ ctx.strokeRect(
625
+ gridX + col * cellSize,
626
+ gridY + row * cellSize,
627
+ cellSize, cellSize
628
+ );
629
+ }
630
+ }
631
+ });
632
+ }, { preset: 'portrait' }); // 360\xD7640 portrait with letterboxing
633
+ \`\`\`
634
+
635
+ ### Recipe 4: Custom Resolution
636
+
637
+ For games that need different dimensions (e.g., pixel art at 320\xD7180).
638
+
639
+ \`\`\`ts
640
+ import { game } from 'star-canvas';
641
+
642
+ game(({ ctx, width, height, loop, toStagePoint, canvas }) => {
643
+ // Custom 320\xD7180 resolution (retro pixel art style)
644
+ const player = { x: 160, y: 90 }; // Center
645
+
646
+ canvas.addEventListener('pointerdown', (e) => {
647
+ const { x, y } = toStagePoint(e);
648
+ console.log('Tapped at:', x, y); // Always 0-320, 0-180
649
+ });
650
+
651
+ loop((dt) => {
652
+ ctx.fillStyle = '#0f172a';
653
+ ctx.fillRect(0, 0, width, height);
654
+
655
+ ctx.fillStyle = '#22d3ee';
656
+ ctx.fillRect(player.x - 8, player.y - 8, 16, 16);
657
+ });
658
+ }, { width: 320, height: 180 }); // Custom resolution with letterboxing
659
+ \`\`\`
660
+
661
+ ### Recipe 5: Complex Game with Canvas + UI + Events (like FLOW)
662
+
663
+ \`\`\`ts
664
+ import { game } from 'star-canvas';
665
+ import { createLeaderboard } from '/star-sdk/v1/leaderboard.js';
666
+
667
+ const leaderboard = createLeaderboard();
668
+
669
+ game(({ ctx, width, height, loop, ui, on, canvas, toStagePoint }) => {
670
+ let score = 0;
671
+ let state = 'menu';
672
+
673
+ function handleTap() {
674
+ if (state === 'menu' || state === 'gameover') {
675
+ startGame();
676
+ } else if (state === 'playing') {
677
+ // ... (player float logic) ...
678
+ }
679
+ }
680
+
681
+ // 1. Listen for screen taps - this makes UI click-through automatically
682
+ canvas.addEventListener('pointerdown', handleTap);
683
+
684
+ // 2. Listen for button clicks - buttons need pointer-events-auto
685
+ on('click', '#leaderboard-btn', (e) => {
686
+ e.stopPropagation();
687
+ leaderboard.show();
688
+ });
689
+
690
+ // 3. Render UI - buttons need pointer-events-auto to intercept clicks
691
+ let lastState = null;
692
+ let lastScore = -1;
693
+
694
+ function updateUI() {
695
+ // CRITICAL: Only render when state/score changes, NOT every frame
696
+ // Calling ui.render() in the loop breaks buttons (DOM recreation)
697
+ if (state === lastState && score === lastScore) return;
698
+ lastState = state;
699
+ lastScore = score;
700
+
701
+ if (state === 'menu') {
702
+ ui.render(\`
703
+ <div class="h-full flex flex-col items-center justify-center text-white">
704
+ <h1 class="text-6xl font-bold mb-4">FLOW</h1>
705
+ <div class="text-2xl animate-pulse">TAP TO START</div>
706
+ </div>\`);
707
+ } else if (state === 'playing') {
708
+ ui.render(\`
709
+ <div class="absolute top-8 left-1/2 -translate-x-1/2 text-white">
710
+ <div class="text-5xl font-bold">\\\${score}</div>
711
+ </div>\`);
712
+ } else if (state === 'gameover') {
713
+ ui.render(\`
714
+ <div class="h-full flex flex-col items-center justify-center text-white">
715
+ <div class="text-3xl mb-4">GAME OVER</div>
716
+ <div class="text-6xl mb-4">\\\${score}</div>
717
+ <button id="leaderboard-btn" class="px-6 py-3 mb-4 bg-purple-600 rounded-lg pointer-events-auto">
718
+ VIEW LEADERBOARD
719
+ </button>
720
+ <div class="text-xl animate-pulse">TAP TO RESTART</div>
721
+ </div>\`);
722
+ }
723
+ }
724
+
725
+ // 4. Call updateUI when state changes (NOT every frame)
726
+ updateUI();
727
+
728
+ // Update when state transitions happen
729
+ function startGame() {
730
+ state = 'playing';
731
+ score = 0;
732
+ updateUI();
733
+ }
734
+
735
+ function endGame() {
736
+ state = 'gameover';
737
+ // Submit score to leaderboard
738
+ leaderboard.submit(score);
739
+ updateUI();
740
+ }
741
+ });
742
+ \`\`\`
743
+
744
+ ### Recipe 6: Safe Canvas Transforms (scoped)
745
+
746
+ When applying temporary transforms (translate, rotate, scale), use \`scoped()\` to automatically restore the context state:
747
+
748
+ \`\`\`ts
749
+ import { game } from 'star-canvas';
750
+
751
+ game(({ ctx, scoped, loop }) => {
752
+ const cards = [
753
+ { x: 100, y: 100, angle: 0.1, visible: true },
754
+ { x: 200, y: 150, angle: -0.2, visible: true },
755
+ ];
756
+
757
+ function drawCard(card) {
758
+ scoped(() => {
759
+ ctx.translate(card.x, card.y);
760
+ ctx.rotate(card.angle);
761
+ if (!card.visible) return; // Safe! restore() still happens
762
+ ctx.fillStyle = '#3b82f6';
763
+ ctx.fillRect(-40, -60, 80, 120);
764
+ });
765
+ }
766
+
767
+ loop(() => {
768
+ ctx.clearRect(0, 0, 800, 600);
769
+ cards.forEach(drawCard);
770
+ });
771
+ });
772
+ \`\`\`
773
+
774
+ **Why use \`scoped()\`:** Prevents transform stack corruption from early returns, exceptions, or forgetting \`ctx.restore()\`. The context is always restored, even if the function exits early.
775
+
776
+ ### Recipe 7: Drag and Drop with createDrag() (RECOMMENDED)
777
+
778
+ Use the \`createDrag()\` helper - it handles coordinate conversion and offset tracking automatically.
779
+
780
+ \`\`\`ts
781
+ import { game } from 'star-canvas';
782
+
783
+ game(({ ctx, width, height, loop, canvas, createDrag }) => {
784
+ // Size relative to height for consistency
785
+ const pieceSize = height * 0.15;
786
+
787
+ const pieces = [
788
+ { x: width * 0.2, y: height * 0.3, color: '#ef4444' },
789
+ { x: width * 0.4, y: height * 0.4, color: '#22c55e' },
790
+ { x: width * 0.6, y: height * 0.3, color: '#3b82f6' },
791
+ ];
792
+
793
+ // Create drag helper - handles coordinate conversion automatically
794
+ const drag = createDrag();
795
+
796
+ function hitTest(x, y) {
797
+ for (let i = pieces.length - 1; i >= 0; i--) {
798
+ const p = pieces[i];
799
+ if (x >= p.x && x < p.x + pieceSize && y >= p.y && y < p.y + pieceSize) {
800
+ return p;
801
+ }
802
+ }
803
+ return null;
804
+ }
805
+
806
+ canvas.addEventListener('pointerdown', (e) => {
807
+ canvas.setPointerCapture(e.pointerId); // IMPORTANT: Ensures drag works outside canvas
808
+ const { x, y } = drag.point(e); // Convert coordinates
809
+ const hit = hitTest(x, y);
810
+ if (hit) {
811
+ drag.grab(e, hit); // Start drag with offset from cursor
812
+ canvas.style.cursor = 'grabbing';
813
+ }
814
+ });
815
+
816
+ canvas.addEventListener('pointermove', (e) => {
817
+ drag.move(e); // Updates grabbed object position
818
+ if (!drag.dragging) {
819
+ const { x, y } = drag.point(e);
820
+ canvas.style.cursor = hitTest(x, y) ? 'grab' : 'default';
821
+ }
822
+ });
823
+
824
+ canvas.addEventListener('pointerup', () => {
825
+ const dropped = drag.release(); // Returns dropped object (or null)
826
+ if (dropped) {
827
+ console.log('Dropped:', dropped);
828
+ }
829
+ canvas.style.cursor = 'default';
830
+ });
831
+
832
+ loop(() => {
833
+ ctx.fillStyle = '#1e293b';
834
+ ctx.fillRect(0, 0, width, height);
835
+
836
+ for (const p of pieces) {
837
+ ctx.fillStyle = drag.dragging === p ? '#fbbf24' : p.color;
838
+ ctx.fillRect(p.x, p.y, pieceSize, pieceSize);
839
+ }
840
+ });
841
+ });
842
+ \`\`\`
843
+
844
+ **CRITICAL: Always use \`setPointerCapture()\`** - This ensures drags work even when the pointer moves outside the canvas. Without it, fast drags can leave objects stuck mid-drag.
845
+
846
+ ### Recipe 8: Drag and Drop (Manual Pattern)
847
+
848
+ If you need more control, here's the manual approach with \`toStagePoint()\`.
849
+
850
+ \`\`\`ts
851
+ import { game } from 'star-canvas';
852
+
853
+ game(({ ctx, width, height, loop, canvas, toStagePoint }) => {
854
+ const pieceSize = height * 0.15;
855
+ const pieces = [
856
+ { x: width * 0.2, y: height * 0.3, color: '#ef4444' },
857
+ { x: width * 0.4, y: height * 0.4, color: '#22c55e' },
858
+ ];
859
+
860
+ // Manual drag state
861
+ let dragging = null;
862
+ let dragOffsetX = 0;
863
+ let dragOffsetY = 0;
864
+
865
+ function hitTest(px, py) {
866
+ for (let i = pieces.length - 1; i >= 0; i--) {
867
+ const p = pieces[i];
868
+ if (px >= p.x && px < p.x + pieceSize && py >= p.y && py < p.y + pieceSize) {
869
+ return p;
870
+ }
871
+ }
872
+ return null;
873
+ }
874
+
875
+ canvas.addEventListener('pointerdown', (e) => {
876
+ canvas.setPointerCapture(e.pointerId); // Ensures drag works outside canvas
877
+ const { x, y } = toStagePoint(e); // CRITICAL: Convert coordinates!
878
+ const hit = hitTest(x, y);
879
+ if (hit) {
880
+ dragging = hit;
881
+ dragOffsetX = x - hit.x; // Store offset
882
+ dragOffsetY = y - hit.y;
883
+ canvas.style.cursor = 'grabbing';
884
+ }
885
+ });
886
+
887
+ canvas.addEventListener('pointermove', (e) => {
888
+ const { x, y } = toStagePoint(e); // CRITICAL: Convert here too!
889
+ if (dragging) {
890
+ dragging.x = x - dragOffsetX;
891
+ dragging.y = y - dragOffsetY;
892
+ } else {
893
+ canvas.style.cursor = hitTest(x, y) ? 'grab' : 'default';
894
+ }
895
+ });
896
+
897
+ canvas.addEventListener('pointerup', () => {
898
+ dragging = null;
899
+ canvas.style.cursor = 'default';
900
+ });
901
+
902
+ loop(() => {
903
+ ctx.fillStyle = '#1e293b';
904
+ ctx.fillRect(0, 0, width, height);
905
+
906
+ for (const p of pieces) {
907
+ ctx.fillStyle = dragging === p ? '#fbbf24' : p.color;
908
+ ctx.fillRect(p.x, p.y, pieceSize, pieceSize);
909
+ }
910
+ });
911
+ });
912
+ \`\`\`
913
+
914
+ **Common Drag-Drop Mistakes:**
915
+
916
+ 1. \u274C Forgetting \`toStagePoint()\` in pointermove \u2192 \`createDrag()\` fixes this
917
+ 2. \u274C No drag offset (piece "jumps" to cursor) \u2192 \`createDrag()\` fixes this
918
+ 3. \u274C Using \`e.clientX/clientY\` directly \u2192 \`createDrag()\` fixes this
919
+ 4. \u274C Not clearing state on pointerup \u2192 \`createDrag()\` fixes this
920
+ 5. \u274C Missing \`setPointerCapture()\` (drags break outside canvas) \u2192 **You must add this!**
921
+
922
+ **Recommendation:** Use \`createDrag()\` + \`setPointerCapture()\` for bulletproof drag-and-drop.`;
923
+ var LEADERBOARD_DOCS = `**Installation**
924
+
925
+ \`\`\`bash
926
+ yarn add star-leaderboard
927
+ \`\`\`
928
+
929
+ ### Star Leaderboard SDK
930
+
931
+ **Simple leaderboards for Star games.** Submit scores, show rankings, share results. Never crashes your game.
932
+
933
+ **Import:**
934
+ \`\`\`javascript
935
+ import { createLeaderboard } from 'star-leaderboard';
936
+ const leaderboard = createLeaderboard();
937
+ \`\`\`
938
+
939
+ **CRITICAL:** Import in JavaScript - don't add \`<script>\` tags.
940
+
941
+ ---
942
+
943
+ **Quick Start:**
944
+
945
+ \`\`\`javascript
946
+ import { createLeaderboard } from 'star-leaderboard';
947
+ import { game } from '/star-sdk/v1/dom.js';
948
+
949
+ const leaderboard = createLeaderboard();
950
+
951
+ game(({ ctx, width, height, loop, ui, on, canvas }) => {
952
+ let score = 0;
953
+ let state = 'playing';
954
+
955
+ function endGame() {
956
+ state = 'gameover';
957
+ // Submit score and show leaderboard
958
+ leaderboard.submit(score);
959
+ leaderboard.show();
960
+ }
961
+
962
+ // Game logic...
963
+ });
964
+ \`\`\`
965
+
966
+ **That's it!** The SDK handles:
967
+ - Score submission (works for guests and logged-in users)
968
+ - Platform leaderboard UI (modal with rankings)
969
+ - Weekly/all-time timeframes
970
+ - AI-detected scoring (score/time/moves - higher or lower is better)
971
+
972
+ ---
973
+
974
+ **API Reference:**
975
+
976
+ **Core Methods:**
977
+ \`\`\`javascript
978
+ leaderboard.submit(score) // Submit score, returns Promise<{ success, rank, scoreId }>
979
+ leaderboard.show() // Show platform leaderboard UI
980
+ leaderboard.getScores(options) // Fetch scores for custom UI
981
+ leaderboard.share(options) // Generate shareable link
982
+ \`\`\`
983
+
984
+ **Properties:**
985
+ \`\`\`javascript
986
+ leaderboard.ready // true when SDK is initialized
987
+ leaderboard.gameId // Current game ID (auto-detected on platform)
988
+ \`\`\`
989
+
990
+ **Aliases (for discoverability):**
991
+ \`\`\`javascript
992
+ leaderboard.submitScore(score) // Same as submit()
993
+ leaderboard.showLeaderboard() // Same as show()
994
+ \`\`\`
995
+
996
+ ---
997
+
998
+ **Patterns:**
999
+
1000
+ ### Pattern 1: Submit and Show (Most Common)
1001
+
1002
+ \`\`\`javascript
1003
+ import { createLeaderboard } from 'star-leaderboard';
1004
+
1005
+ const leaderboard = createLeaderboard();
1006
+
1007
+ function gameOver(finalScore) {
1008
+ // Fire and forget - simplest approach
1009
+ leaderboard.submit(finalScore);
1010
+ leaderboard.show();
1011
+ }
1012
+ \`\`\`
1013
+
1014
+ ### Pattern 2: With Rank Feedback
1015
+
1016
+ \`\`\`javascript
1017
+ import { createLeaderboard } from 'star-leaderboard';
1018
+
1019
+ const leaderboard = createLeaderboard();
1020
+
1021
+ async function gameOver(finalScore) {
1022
+ const { success, rank } = await leaderboard.submit(finalScore);
1023
+
1024
+ if (success && rank) {
1025
+ console.log(\`You ranked #\${rank}!\`);
1026
+ }
1027
+
1028
+ leaderboard.show();
1029
+ }
1030
+ \`\`\`
1031
+
1032
+ ### Pattern 3: Leaderboard Button
1033
+
1034
+ \`\`\`javascript
1035
+ import { createLeaderboard } from 'star-leaderboard';
1036
+ import { game } from '/star-sdk/v1/dom.js';
1037
+
1038
+ const leaderboard = createLeaderboard();
1039
+
1040
+ game(({ ui, on }) => {
1041
+ ui.render(\`
1042
+ <button id="lb-btn" class="pointer-events-auto">
1043
+ View Leaderboard
1044
+ </button>
1045
+ \`);
1046
+
1047
+ on('click', '#lb-btn', (e) => {
1048
+ e.stopPropagation();
1049
+ leaderboard.show();
1050
+ });
1051
+ });
1052
+ \`\`\`
1053
+
1054
+ ### Pattern 4: Custom Leaderboard UI
1055
+
1056
+ \`\`\`javascript
1057
+ import { createLeaderboard } from 'star-leaderboard';
1058
+
1059
+ const leaderboard = createLeaderboard();
1060
+
1061
+ async function showCustomLeaderboard() {
1062
+ const { scores, you, config } = await leaderboard.getScores({
1063
+ timeframe: 'weekly', // or 'all_time'
1064
+ limit: 10
1065
+ });
1066
+
1067
+ // Render your own UI
1068
+ scores.forEach(entry => {
1069
+ console.log(\`#\${entry.rank} \${entry.playerName}: \${entry.score}\`);
1070
+ });
1071
+
1072
+ if (you) {
1073
+ console.log(\`Your rank: #\${you.rank}\`);
1074
+ }
1075
+ }
1076
+ \`\`\`
1077
+
1078
+ ### Pattern 5: Full Game Example
1079
+
1080
+ \`\`\`javascript
1081
+ import { createLeaderboard } from 'star-leaderboard';
1082
+ import { game } from '/star-sdk/v1/dom.js';
1083
+
1084
+ const leaderboard = createLeaderboard();
1085
+
1086
+ game(({ ctx, width, height, loop, ui, on, canvas }) => {
1087
+ let score = 0;
1088
+ let state = 'menu';
1089
+
1090
+ function startGame() {
1091
+ state = 'playing';
1092
+ score = 0;
1093
+ updateUI();
1094
+ }
1095
+
1096
+ function endGame() {
1097
+ state = 'gameover';
1098
+ leaderboard.submit(score);
1099
+ updateUI();
1100
+ }
1101
+
1102
+ // UI with leaderboard button
1103
+ function updateUI() {
1104
+ if (state === 'gameover') {
1105
+ ui.render(\`
1106
+ <div class="h-full flex flex-col items-center justify-center text-white">
1107
+ <div class="text-3xl mb-4">GAME OVER</div>
1108
+ <div class="text-6xl mb-4">\\\${score}</div>
1109
+ <button id="lb-btn" class="px-6 py-3 mb-4 bg-purple-600 rounded-lg pointer-events-auto">
1110
+ VIEW LEADERBOARD
1111
+ </button>
1112
+ <div class="text-xl animate-pulse">TAP TO RESTART</div>
1113
+ </div>
1114
+ \`);
1115
+ }
1116
+ }
1117
+
1118
+ on('click', '#lb-btn', (e) => {
1119
+ e.stopPropagation();
1120
+ leaderboard.show();
1121
+ });
1122
+
1123
+ canvas.addEventListener('pointerdown', () => {
1124
+ if (state === 'menu' || state === 'gameover') startGame();
1125
+ });
1126
+
1127
+ loop((dt) => {
1128
+ // Game logic...
1129
+ });
1130
+ });
1131
+ \`\`\`
1132
+
1133
+ ---
1134
+
1135
+ **Options:**
1136
+
1137
+ \`\`\`javascript
1138
+ // Default - auto-detects gameId on Star platform
1139
+ const leaderboard = createLeaderboard();
1140
+
1141
+ // Standalone use (outside Star platform)
1142
+ const leaderboard = createLeaderboard({
1143
+ gameId: 'your-game-uuid',
1144
+ apiBase: 'https://buildwithstar.com'
1145
+ });
1146
+ \`\`\`
1147
+
1148
+ ---
1149
+
1150
+ **getScores Options:**
1151
+
1152
+ \`\`\`javascript
1153
+ const data = await leaderboard.getScores({
1154
+ timeframe: 'weekly', // 'weekly' (default) or 'all_time'
1155
+ limit: 10 // Number of scores (default: 10)
1156
+ });
1157
+
1158
+ // Returns:
1159
+ {
1160
+ scores: [{ id, playerName, score, rank, submittedAt }],
1161
+ config: { sort: 'DESC', valueType: 'score' },
1162
+ timeframe: 'weekly',
1163
+ you: { ... } | null, // Your score if outside top scores
1164
+ weekResetTime: 1234567890 // Unix ms when weekly resets
1165
+ }
1166
+ \`\`\`
1167
+
1168
+ ---
1169
+
1170
+ **Tips:**
1171
+
1172
+ 1. **Call \`submit()\` before \`show()\`** - Ensures your score appears immediately in the leaderboard.
1173
+
1174
+ 2. **Fire and forget is fine** - \`submit()\` returns a Promise but you don't need to await it.
1175
+
1176
+ 3. **Use \`show()\` for platform UI** - It's the easiest way. Use \`getScores()\` only if you need custom rendering.
1177
+
1178
+ 4. **Don't store leaderboard state** - Just call the SDK methods when needed. The platform handles caching.
1179
+
1180
+ 5. **Works for guests** - Guests get a generated name like "Guest1234". They can sign in later to claim scores.`;
1181
+ var MULTIPLAYER_DOCS = `**Installation**
1182
+
1183
+ \`\`\`bash
1184
+ yarn add star-multiplayer
1185
+ \`\`\`
1186
+
1187
+ ### Star Multiplayer SDK
1188
+
1189
+ **Real-time multiplayer for Star games.** Host-authoritative state sync with automatic room management.
1190
+
1191
+ **Import:**
1192
+ \`\`\`javascript
1193
+ import { createMultiplayer } from 'star-multiplayer';
1194
+ const mp = createMultiplayer();
1195
+ \`\`\`
1196
+
1197
+ **CRITICAL:** Import in JavaScript - don't add \`<script>\` tags.
1198
+
1199
+ **Features:**
1200
+ - **Host-authoritative** - One player runs game logic, others receive state
1201
+ - **Room-based** - Create/join rooms with 6-character invite codes
1202
+ - **Auto host migration** - If host leaves, another player becomes host
1203
+ - **Built-in lobby UI** - No need to build your own
1204
+ - **Works in iframes** - Uses postMessage relay to parent window
1205
+
1206
+ ---
1207
+
1208
+ **Quick Start:**
1209
+
1210
+ \`\`\`javascript
1211
+ import { createMultiplayer } from 'star-multiplayer';
1212
+ import { game } from '/star-sdk/v1/dom.js';
1213
+
1214
+ const mp = createMultiplayer();
1215
+
1216
+ game(async ({ ctx, width, height, loop, canvas }) => {
1217
+ // 1. Start - auto-finds or creates room, starts immediately
1218
+ await mp.start();
1219
+
1220
+ // 2. Game state
1221
+ let state = { players: {}, ball: { x: width/2, y: height/2, vx: 200, vy: 150 } };
1222
+
1223
+ // 3. Receive state updates - update shared objects, keep local player prediction
1224
+ mp.onState((s) => {
1225
+ state.ball = s.ball;
1226
+ for (const [id, p] of Object.entries(s.players)) {
1227
+ if (id !== mp.localPlayerId) {
1228
+ state.players[id] = p;
1229
+ }
1230
+ }
1231
+ });
1232
+
1233
+ // 4. Handle inputs (runs locally for prediction, on host for authoritative state)
1234
+ mp.onInput((playerId, input) => {
1235
+ if (!state.players[playerId]) {
1236
+ state.players[playerId] = { y: height/2 };
1237
+ }
1238
+ state.players[playerId].y = input.y;
1239
+ });
1240
+
1241
+ // 5. Game loop
1242
+ loop((dt) => {
1243
+ // Host: run physics + broadcast state (auto-throttled to 20Hz)
1244
+ mp.hostTick(dt, () => {
1245
+ state.ball.x += state.ball.vx * dt;
1246
+ state.ball.y += state.ball.vy * dt;
1247
+ if (state.ball.y < 0 || state.ball.y > height) state.ball.vy *= -1;
1248
+ return state;
1249
+ });
1250
+
1251
+ // Everyone: render with interpolation for smooth movement
1252
+ const renderState = mp.getInterpolatedState(dt, state);
1253
+ ctx.fillStyle = '#0f172a';
1254
+ ctx.fillRect(0, 0, width, height);
1255
+ for (const p of Object.values(renderState.players)) {
1256
+ ctx.fillStyle = '#22d3ee';
1257
+ ctx.fillRect(20, p.y - 40, 10, 80);
1258
+ }
1259
+ ctx.beginPath();
1260
+ ctx.arc(renderState.ball.x, renderState.ball.y, 10, 0, Math.PI * 2);
1261
+ ctx.fill();
1262
+ });
1263
+
1264
+ // 6. Input - works for EVERYONE (applied locally for instant feedback)
1265
+ canvas.onpointermove = (e) => {
1266
+ const rect = canvas.getBoundingClientRect();
1267
+ mp.input({ y: ((e.clientY - rect.top) / rect.height) * height });
1268
+ };
1269
+ });
1270
+ \`\`\`
1271
+
1272
+ **That's it!** The SDK handles:
1273
+ - Auto-matchmaking (finds open room or creates one)
1274
+ - URL detection (\`?room=CODE\` \u2192 joins specific room)
1275
+ - Throttling state broadcasts to 20Hz
1276
+ - Routing host's inputs to their own handler
1277
+
1278
+ ---
1279
+
1280
+ **Architecture:**
1281
+
1282
+ \`\`\`
1283
+ Host (Player 1) Server Client (Player 2)
1284
+ \u2502 \u2502 \u2502
1285
+ \u2502 \u2500\u2500 state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2500\u2500 state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25B6\u2502
1286
+ \u2502 \u2502 \u2502
1287
+ \u2502\u25C0\u2500\u2500 input \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502\u2500\u2500 input \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2502
1288
+ \u2502 \u2502 \u2502
1289
+ \u2502 (runs game logic) \u2502 (dumb relay) \u2502 (renders state)
1290
+ \`\`\`
1291
+
1292
+ ---
1293
+
1294
+ **API Reference:**
1295
+
1296
+ **State (read-only):**
1297
+ \`\`\`javascript
1298
+ mp.isHost // true if you're the host
1299
+ mp.isReady // true if game has started
1300
+ mp.players // Array of { id: string }
1301
+ mp.localPlayerId // Your player ID
1302
+ mp.roomCode // Current room code
1303
+ \`\`\`
1304
+
1305
+ **Setup:**
1306
+ \`\`\`javascript
1307
+ await mp.start(); // Uses all defaults - auto-matchmaking, 8 players
1308
+
1309
+ // Or customize:
1310
+ await mp.start({
1311
+ maxPlayers: 8, // default: 8 (max players per room)
1312
+ mode: 'auto', // default: 'auto' - finds/creates rooms automatically
1313
+ // 'private' - shows lobby with shareable code
1314
+ showLobby: true, // default: true (only used in 'private' mode)
1315
+ tickRate: 50, // default: 50ms (20Hz state broadcasts)
1316
+ prediction: true // default: true (client-side prediction for instant input)
1317
+ });
1318
+
1319
+ // Examples:
1320
+ await mp.start({ maxPlayers: 2 }); // 1v1 game
1321
+ await mp.start({ maxPlayers: 100 }); // Battle royale
1322
+ await mp.start({ mode: 'private' }); // Friends-only with invite code
1323
+ await mp.start({ mode: 'private', maxPlayers: 4 }); // Small private game
1324
+ \`\`\`
1325
+
1326
+ **Sync:**
1327
+ \`\`\`javascript
1328
+ mp.onState((state) => { ... }) // Receive state (returns unsubscribe)
1329
+ mp.onInput((playerId, input) => { ... }) // Handle continuous input (position, aim)
1330
+ mp.hostTick(dt, () => state) // Run on host, broadcast returned state
1331
+ mp.input(data) // Send continuous input (position, aim)
1332
+ mp.getInterpolatedState(dt, state) // Get smoothly interpolated state for rendering
1333
+ \`\`\`
1334
+
1335
+ **Discrete Events (shooting, jumping, using items):**
1336
+ \`\`\`javascript
1337
+ mp.event(type, data) // Send event - guaranteed delivery
1338
+ mp.onEvent((playerId, type, data) => { }) // Handle events (HOST ONLY, authoritative)
1339
+ \`\`\`
1340
+
1341
+ **Player Updates:**
1342
+ \`\`\`javascript
1343
+ mp.onLocalPlayerUpdate((player, prevPlayer) => { ... }) // Local player changed
1344
+ mp.onPlayerUpdate(playerId, (player, prevPlayer) => { ... }) // Specific player changed
1345
+ \`\`\`
1346
+
1347
+ **Lifecycle Events:**
1348
+ \`\`\`javascript
1349
+ mp.onPlayers((players) => { ... }) // Player list changed
1350
+ mp.onPlayerJoin((player) => { ... }) // Another player joined (use to init their state)
1351
+ mp.onPlayerLeave((player) => { ... }) // A player left
1352
+ mp.onHost((isHost) => { ... }) // Host status changed (for host migration)
1353
+ mp.onError((message) => { ... }) // Error occurred
1354
+ \`\`\`
1355
+
1356
+ **Cleanup:**
1357
+ \`\`\`javascript
1358
+ mp.leave() // Leave room
1359
+ mp.destroy() // Full cleanup
1360
+ \`\`\`
1361
+
1362
+ ---
1363
+
1364
+ **Tips:**
1365
+
1366
+ 1. **Use \`hostTick()\`** - It handles host detection and throttling. Don't use \`setInterval\`.
1367
+
1368
+ 2. **\`input()\` works on host** - Host's \`input()\` calls their \`onInput\` handler directly. Same code for everyone.
1369
+
1370
+ 3. **Handle host migration** - If host leaves, \`onHost(true)\` fires on new host. They should start running physics.
1371
+
1372
+ 4. **State design** - Only sync what changes: positions, scores, game objects. Not constants or textures.
1373
+
1374
+ 5. **Auto mode is default** - Players join instantly without friction. Use \`mode: 'private'\` only if you need invite codes.
1375
+
1376
+ 6. **Scale with maxPlayers** - Set \`maxPlayers: 2\` for 1v1, \`maxPlayers: 50\` for larger games. Default is 8.
1377
+
1378
+ 7. **Built-in prediction** - Your \`onInput\` handler runs immediately for local player input, eliminating input lag. Use \`mp.getInterpolatedState(dt, state)\` for smooth rendering of all players.
1379
+
1380
+ 8. **State pattern** - In \`onState\`, update shared objects (ball, score) but don't overwrite local player (keep your prediction). The SDK handles interpolation for other players.
1381
+
1382
+ 9. **Player count** - Use \`mp.players.length\` for player count in UI. Don't use \`Object.keys(state.players).length\` - that only counts players who've sent input.
1383
+
1384
+ 10. **Use \`mp.event()\` for discrete actions** - \`mp.input()\` is for continuous state (position, aim). Use \`mp.event('shoot', data)\` for discrete actions (shooting, jumping) - SDK guarantees delivery.
1385
+
1386
+ ---
1387
+
1388
+ **Where Code Runs (Important!):**
1389
+
1390
+ | Code Type | Where | Why |
1391
+ |-----------|-------|-----|
1392
+ | Physics, collision | Host (\`hostTick\`) | Single source of truth |
1393
+ | Hit detection, damage | Host (\`onInput\`) | Prevents cheating |
1394
+ | State changes (health, score) | Host | Authoritative |
1395
+ | UI updates, messages | **Everyone** (\`onState\` or \`loop\`) | Each player sees their own |
1396
+ | Visual/sound effects | **Everyone** | Local feedback |
1397
+ | Input sending | **Everyone** (\`mp.input()\`) | All players send inputs |
1398
+
1399
+ **Golden rule:**
1400
+ - **CHANGES state** \u2192 host only
1401
+ - **REACTS to state** \u2192 everyone locally
1402
+
1403
+ \`\`\`javascript
1404
+ // \u274C WRONG - UI in host-only code
1405
+ mp.hostTick(dt, () => {
1406
+ if (player.health <= 0) showDeathMessage(); // Only host sees!
1407
+ return state;
1408
+ });
1409
+
1410
+ // \u2705 RIGHT - React to state on every client
1411
+ mp.onState((state) => {
1412
+ const wasAlive = localPlayer.health > 0;
1413
+ localPlayer.health = state.players[mp.localPlayerId].health;
1414
+ if (wasAlive && localPlayer.health <= 0) {
1415
+ showDeathMessage(); // Everyone sees their own death!
1416
+ }
1417
+ });
1418
+ \`\`\`
1419
+
1420
+ ---
1421
+
1422
+ **Common Patterns:**
1423
+
1424
+ ### Player Initialization (Important!)
1425
+
1426
+ Initialize player state when they join, not when they first send input:
1427
+
1428
+ \`\`\`javascript
1429
+ // \u274C WRONG - Players invisible until they move
1430
+ mp.onInput((playerId, input) => {
1431
+ if (!state.players[playerId]) {
1432
+ state.players[playerId] = { x: 0, y: 0, health: 100 }; // Too late!
1433
+ }
1434
+ });
1435
+
1436
+ // \u2705 RIGHT - Initialize on join
1437
+ mp.onPlayerJoin((player) => {
1438
+ state.players[player.id] = {
1439
+ x: Math.random() * width,
1440
+ y: Math.random() * height,
1441
+ health: 100
1442
+ };
1443
+ });
1444
+
1445
+ mp.onPlayerLeave((player) => {
1446
+ delete state.players[player.id];
1447
+ });
1448
+ \`\`\`
1449
+
1450
+ **Why this matters:** Without \`onPlayerJoin\`, a spectating player (not sending input) would be invisible to everyone.
1451
+
1452
+ ### Position Sync (3D/Complex Games)
1453
+
1454
+ Clients send position, host tracks all players:
1455
+
1456
+ \`\`\`javascript
1457
+ // Everyone: Send position every frame
1458
+ mp.input({
1459
+ pos: [localPlayer.x, localPlayer.y, localPlayer.z],
1460
+ yaw: localPlayer.yaw
1461
+ });
1462
+
1463
+ // Host: Track in onInput
1464
+ const remotePlayers = {};
1465
+ mp.onInput((playerId, input) => {
1466
+ if (!remotePlayers[playerId]) remotePlayers[playerId] = { health: 100 };
1467
+ if (input.pos) {
1468
+ remotePlayers[playerId].pos = input.pos;
1469
+ remotePlayers[playerId].yaw = input.yaw;
1470
+ }
1471
+ });
1472
+ \`\`\`
1473
+
1474
+ ### Actions (Shooting, Jumping, Items) - Use \`mp.event()\`
1475
+
1476
+ **For discrete actions, use \`mp.event()\` instead of \`mp.input()\`.** The SDK guarantees delivery, deduplication, and preserves each event's data.
1477
+
1478
+ **The Pattern:**
1479
+
1480
+ \`\`\`javascript
1481
+ // \u274C WRONG - mp.input() loses rapid discrete actions (30Hz throttling)
1482
+ mp.input({ shoot: true }); // 3 rapid clicks = 1 shot!
1483
+
1484
+ // \u2705 RIGHT - Two parts: cosmetic (instant) + authoritative (host)
1485
+
1486
+ // PART 1: At the trigger point (EVERYONE runs this)
1487
+ if (shooting && canShoot(localPlayer)) {
1488
+ // Cosmetic feedback - instant, local only
1489
+ audio.play('shoot');
1490
+ particles.muzzleFlash(localPlayer.pos);
1491
+
1492
+ // Send event to host for authoritative processing
1493
+ mp.event('shoot', {
1494
+ origin: [x, y, z],
1495
+ dir: [dx, dy, dz]
1496
+ });
1497
+ }
1498
+
1499
+ // PART 2: Authoritative handler (HOST ONLY processes this)
1500
+ mp.onEvent((playerId, type, data) => {
1501
+ if (type === 'shoot') {
1502
+ // This creates the REAL bullet in game state
1503
+ state.bullets.push({
1504
+ owner: playerId,
1505
+ pos: data.origin,
1506
+ dir: data.dir
1507
+ });
1508
+ }
1509
+ });
1510
+ \`\`\`
1511
+
1512
+ **Why two parts?**
1513
+
1514
+ | Part | Who runs it | What it does |
1515
+ |------|-------------|--------------|
1516
+ | Cosmetic (at call site) | Everyone | Instant feedback: sound, particles, animations |
1517
+ | Authoritative (\`onEvent\`) | Host only | State changes: spawn bullets, apply damage |
1518
+
1519
+ **This separation is intentional:**
1520
+ - **Cosmetic:** Your own shots feel instant (no network latency)
1521
+ - **Authoritative:** Game state is consistent (host is source of truth)
1522
+ - **Other players' bullets:** Appear in state broadcast, ~50ms later (acceptable for casual games)
1523
+
1524
+ **Hearing other players' shots (optional):**
1525
+ \`\`\`javascript
1526
+ const seenBullets = new Set();
1527
+ mp.onState((state) => {
1528
+ for (const bullet of state.bullets || []) {
1529
+ if (!seenBullets.has(bullet.id) && bullet.owner !== mp.localPlayerId) {
1530
+ audio.play('shoot'); // Hear remote player's shot
1531
+ seenBullets.add(bullet.id);
1532
+ }
1533
+ }
1534
+ });
1535
+ \`\`\`
1536
+
1537
+ **When to use which:**
1538
+ | Action Type | Method | Example |
1539
+ |-------------|--------|---------|
1540
+ | Continuous state | \`mp.input()\` | Position, aim direction, movement |
1541
+ | Discrete action | \`mp.event()\` | Shooting, jumping, using items, emotes |
1542
+
1543
+ ### Health (Host-Authoritative)
1544
+
1545
+ Host owns health. Clients accept and react:
1546
+
1547
+ \`\`\`javascript
1548
+ // \u274C WRONG - Client sends health (cheatable!)
1549
+ mp.input({ health: localPlayer.health });
1550
+
1551
+ // \u2705 RIGHT - Host applies damage
1552
+ mp.onInput((playerId, input) => {
1553
+ if (hit.playerId) remotePlayers[hit.playerId].health -= 25;
1554
+ });
1555
+
1556
+ // \u2705 RIGHT - Client accepts AND reacts
1557
+ mp.onState((state) => {
1558
+ const myHealth = state.players[mp.localPlayerId]?.health;
1559
+ if (myHealth !== undefined) {
1560
+ if (localPlayer.health > 0 && myHealth <= 0) showDeathMessage();
1561
+ localPlayer.health = myHealth;
1562
+ }
1563
+ });
1564
+ \`\`\`
1565
+
1566
+ ### Reacting to Player State Changes (Death, Score, etc.)
1567
+
1568
+ Use \`onLocalPlayerUpdate\` to react when your player's state changes - the SDK tracks previous state for you:
1569
+
1570
+ \`\`\`javascript
1571
+ mp.onLocalPlayerUpdate((player, prevPlayer) => {
1572
+ // Detect death
1573
+ if (prevPlayer.health > 0 && player.health <= 0) {
1574
+ showDeathScreen();
1575
+ }
1576
+
1577
+ // Detect respawn
1578
+ if (prevPlayer.health <= 0 && player.health > 0) {
1579
+ hideDeathScreen();
1580
+ }
1581
+
1582
+ // Detect score increase
1583
+ if (player.score > (prevPlayer.score || 0)) {
1584
+ showScorePopup(player.score - prevPlayer.score);
1585
+ }
1586
+ });
1587
+ \`\`\`
1588
+
1589
+ For remote player effects (death animations, etc.):
1590
+
1591
+ \`\`\`javascript
1592
+ // Subscribe to each remote player's updates
1593
+ for (const p of mp.players) {
1594
+ if (p.id !== mp.localPlayerId) {
1595
+ mp.onPlayerUpdate(p.id, (player, prev) => {
1596
+ if (prev.health > 0 && player.health <= 0) {
1597
+ playDeathAnimation(p.id);
1598
+ }
1599
+ });
1600
+ }
1601
+ }
1602
+ \`\`\`
1603
+
1604
+ ---
1605
+
1606
+ **Anti-Patterns:**
1607
+
1608
+ ### Using \`mp.input()\` for Discrete Actions
1609
+
1610
+ **Problem**: Using \`mp.input({ shoot: true })\` for shooting, jumping, or any discrete action.
1611
+
1612
+ **Why it breaks**: \`mp.input()\` is throttled to 30Hz. Multiple rapid clicks collapse into a single value, losing inputs.
1613
+
1614
+ \`\`\`javascript
1615
+ // \u274C BAD - mp.input() loses rapid discrete actions
1616
+ mp.input({ shoot: true }); // 3 rapid clicks = 1 shot!
1617
+
1618
+ // \u274C ALSO BAD - counters in mp.input() still have issues
1619
+ mp.input({ shootCount: count }); // Bursts all shots at once, loses timing
1620
+
1621
+ // \u2705 GOOD - Use mp.event() for discrete actions
1622
+ mp.event('shoot', { origin, dir }); // Each event preserved with its data
1623
+ \`\`\`
1624
+
1625
+ **The rule is simple:**
1626
+
1627
+ | Action Type | Method | Why |
1628
+ |-------------|--------|-----|
1629
+ | **Continuous** (position, aim) | \`mp.input()\` | Last value is correct, throttling is fine |
1630
+ | **Discrete** (shoot, jump, use item) | \`mp.event()\` | Each action must be delivered with its data |`;
1631
+
1632
+ // src/cli.ts
6
1633
  var VERSION = "0.1.0";
7
1634
  var API_BASE = process.env.STAR_API_BASE || "https://buildwithstar.com";
8
1635
  var CONFIG_FILE = ".starrc";
@@ -33,11 +1660,22 @@ function showHelp() {
33
1660
  ${colors.bright}Star SDK CLI${colors.reset} v${VERSION}
34
1661
 
35
1662
  ${colors.dim}Commands:${colors.reset}
36
- ${colors.cyan}init <name>${colors.reset} Register a new game
37
- ${colors.cyan}whoami${colors.reset} Show current configuration
1663
+ ${colors.cyan}init <name>${colors.reset} Register a new game
1664
+ ${colors.cyan}install [agent]${colors.reset} Install Star SDK docs for AI coding agents
1665
+ ${colors.cyan}docs [topic]${colors.reset} Print API documentation to stdout
1666
+ ${colors.cyan}whoami${colors.reset} Show current configuration
1667
+
1668
+ ${colors.dim}Supported agents:${colors.reset}
1669
+ ${colors.cyan}claude${colors.reset} ${colors.dim}(default)${colors.reset} Claude Code \u2192 ~/.claude/skills/star-sdk/
1670
+ ${colors.cyan}codex${colors.reset} OpenAI Codex \u2192 ./AGENTS.md
1671
+ ${colors.cyan}cursor${colors.reset} Cursor \u2192 .cursor/rules/star-sdk.mdc
1672
+ ${colors.cyan}windsurf${colors.reset} Windsurf \u2192 .windsurf/rules/star-sdk.md
1673
+ ${colors.cyan}aider${colors.reset} Aider \u2192 ./CONVENTIONS.md
38
1674
 
39
1675
  ${colors.dim}Options:${colors.reset}
40
1676
  --email <email> Email for higher rate limits (optional)
1677
+ --global Install to global location (codex: ~/.codex/)
1678
+ --project Install to project directory
41
1679
  --help Show this help message
42
1680
  --version Show version
43
1681
 
@@ -45,13 +1683,18 @@ ${colors.dim}Examples:${colors.reset}
45
1683
  ${colors.dim}# Register a new game${colors.reset}
46
1684
  npx star-sdk init "My Awesome Game"
47
1685
 
48
- ${colors.dim}# Register with email for higher limits${colors.reset}
49
- npx star-sdk init "My Awesome Game" --email user@example.com
1686
+ ${colors.dim}# Install for different AI agents${colors.reset}
1687
+ npx star-sdk install ${colors.dim}# Claude Code (default)${colors.reset}
1688
+ npx star-sdk install codex ${colors.dim}# OpenAI Codex${colors.reset}
1689
+ npx star-sdk install cursor ${colors.dim}# Cursor${colors.reset}
1690
+ npx star-sdk install windsurf ${colors.dim}# Windsurf${colors.reset}
1691
+ npx star-sdk install aider ${colors.dim}# Aider${colors.reset}
50
1692
 
51
- ${colors.dim}# Using environment variable${colors.reset}
52
- STAR_EMAIL=user@example.com npx star-sdk init "My Awesome Game"
1693
+ ${colors.dim}# Print API docs (works with any LLM)${colors.reset}
1694
+ npx star-sdk docs
1695
+ npx star-sdk docs audio
53
1696
 
54
- ${colors.dim}Learn more:${colors.reset} https://buildwithstar.com/docs/sdk
1697
+ ${colors.dim}Learn more:${colors.reset} https://buildwithstar.com/docs
55
1698
  `);
56
1699
  }
57
1700
  function showVersion() {
@@ -130,6 +1773,12 @@ async function initCommand(name, email) {
130
1773
  log(` ${colors.cyan}import Star from 'star-sdk';${colors.reset}`);
131
1774
  log(` ${colors.cyan}Star.leaderboard.submit(1500);${colors.reset}`);
132
1775
  log("");
1776
+ log(`${colors.dim}Using an AI coding agent?${colors.reset}`);
1777
+ log(` ${colors.cyan}npx star-sdk install${colors.reset} ${colors.dim}Claude Code, Codex, Cursor, Windsurf, Aider${colors.reset}`);
1778
+ log("");
1779
+ log(`${colors.dim}Documentation:${colors.reset}`);
1780
+ log(` ${colors.cyan}https://buildwithstar.com/docs${colors.reset}`);
1781
+ log("");
133
1782
  } catch (err) {
134
1783
  if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
135
1784
  error("Could not connect to Star API. Check your internet connection.");
@@ -160,6 +1809,179 @@ function whoamiCommand() {
160
1809
  }
161
1810
  log("");
162
1811
  }
1812
+ function getAllDocsContent() {
1813
+ return `${SKILL_CONTENT}
1814
+
1815
+ ---
1816
+
1817
+ # Audio API
1818
+
1819
+ ${AUDIO_DOCS}
1820
+
1821
+ ---
1822
+
1823
+ # Canvas API
1824
+
1825
+ ${CANVAS_DOCS}
1826
+
1827
+ ---
1828
+
1829
+ # Leaderboard API
1830
+
1831
+ ${LEADERBOARD_DOCS}
1832
+
1833
+ ---
1834
+
1835
+ # Multiplayer API
1836
+
1837
+ ${MULTIPLAYER_DOCS}`;
1838
+ }
1839
+ function installClaudeCode(scope) {
1840
+ const skillDir = scope === "global" ? path.join(os.homedir(), ".claude", "skills", "star-sdk") : path.join(process.cwd(), ".claude", "skills", "star-sdk");
1841
+ fs.mkdirSync(skillDir, { recursive: true });
1842
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), SKILL_CONTENT);
1843
+ fs.writeFileSync(path.join(skillDir, "audio.md"), AUDIO_DOCS);
1844
+ fs.writeFileSync(path.join(skillDir, "canvas.md"), CANVAS_DOCS);
1845
+ fs.writeFileSync(path.join(skillDir, "leaderboard.md"), LEADERBOARD_DOCS);
1846
+ fs.writeFileSync(path.join(skillDir, "multiplayer.md"), MULTIPLAYER_DOCS);
1847
+ log("");
1848
+ success(`Star SDK skill installed to ${skillDir}`);
1849
+ log("");
1850
+ log(`${colors.dim}Claude Code will now auto-discover the Star SDK when you work on games.${colors.reset}`);
1851
+ log("");
1852
+ log(`${colors.dim}Installed files:${colors.reset}`);
1853
+ log(` SKILL.md ${colors.dim}Main skill entry point${colors.reset}`);
1854
+ log(` audio.md ${colors.dim}Star.audio API docs${colors.reset}`);
1855
+ log(` canvas.md ${colors.dim}Star.game() API docs${colors.reset}`);
1856
+ log(` leaderboard.md ${colors.dim}Star.leaderboard API docs${colors.reset}`);
1857
+ log(` multiplayer.md ${colors.dim}Star.multiplayer API docs${colors.reset}`);
1858
+ log("");
1859
+ }
1860
+ function installCodex(scope) {
1861
+ const content = `# Star SDK - Browser Game Development
1862
+
1863
+ ${getAllDocsContent()}`;
1864
+ const filePath = scope === "global" ? path.join(os.homedir(), ".codex", "AGENTS.md") : path.join(process.cwd(), "AGENTS.md");
1865
+ if (scope === "global") {
1866
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1867
+ }
1868
+ if (fs.existsSync(filePath)) {
1869
+ const existing = fs.readFileSync(filePath, "utf-8");
1870
+ if (existing.includes("Star SDK")) {
1871
+ info(`Star SDK already present in ${filePath}`);
1872
+ return;
1873
+ }
1874
+ fs.writeFileSync(filePath, existing + "\n\n" + content);
1875
+ success(`Star SDK appended to ${filePath}`);
1876
+ } else {
1877
+ fs.writeFileSync(filePath, content);
1878
+ success(`Star SDK installed to ${filePath}`);
1879
+ }
1880
+ log("");
1881
+ log(`${colors.dim}Codex will now have access to Star SDK documentation.${colors.reset}`);
1882
+ log("");
1883
+ }
1884
+ function installCursor() {
1885
+ const rulesDir = path.join(process.cwd(), ".cursor", "rules");
1886
+ const filePath = path.join(rulesDir, "star-sdk.mdc");
1887
+ const content = `---
1888
+ description: Star SDK for browser game development with audio, canvas, leaderboards, and multiplayer
1889
+ globs: ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.html"]
1890
+ alwaysApply: false
1891
+ ---
1892
+
1893
+ ${getAllDocsContent()}`;
1894
+ fs.mkdirSync(rulesDir, { recursive: true });
1895
+ fs.writeFileSync(filePath, content);
1896
+ log("");
1897
+ success(`Star SDK installed to ${filePath}`);
1898
+ log("");
1899
+ log(`${colors.dim}Cursor will now have access to Star SDK documentation.${colors.reset}`);
1900
+ log(`${colors.dim}Use @star-sdk to reference it in Cursor chat.${colors.reset}`);
1901
+ log("");
1902
+ }
1903
+ function installWindsurf() {
1904
+ const rulesDir = path.join(process.cwd(), ".windsurf", "rules");
1905
+ const filePath = path.join(rulesDir, "star-sdk.md");
1906
+ const content = `# Star SDK - Browser Game Development
1907
+
1908
+ ${getAllDocsContent()}`;
1909
+ fs.mkdirSync(rulesDir, { recursive: true });
1910
+ fs.writeFileSync(filePath, content);
1911
+ log("");
1912
+ success(`Star SDK installed to ${filePath}`);
1913
+ log("");
1914
+ log(`${colors.dim}Windsurf will now have access to Star SDK documentation.${colors.reset}`);
1915
+ log("");
1916
+ }
1917
+ function installAider() {
1918
+ const filePath = path.join(process.cwd(), "CONVENTIONS.md");
1919
+ const content = `# Star SDK - Browser Game Development
1920
+
1921
+ ${getAllDocsContent()}`;
1922
+ if (fs.existsSync(filePath)) {
1923
+ const existing = fs.readFileSync(filePath, "utf-8");
1924
+ if (existing.includes("Star SDK")) {
1925
+ info(`Star SDK already present in ${filePath}`);
1926
+ return;
1927
+ }
1928
+ fs.writeFileSync(filePath, existing + "\n\n" + content);
1929
+ success(`Star SDK appended to ${filePath}`);
1930
+ } else {
1931
+ fs.writeFileSync(filePath, content);
1932
+ success(`Star SDK installed to ${filePath}`);
1933
+ }
1934
+ log("");
1935
+ log(`${colors.dim}Add to aider with: aider --read CONVENTIONS.md${colors.reset}`);
1936
+ log("");
1937
+ }
1938
+ function installCommand(agent = "claude", scope = "global") {
1939
+ try {
1940
+ switch (agent) {
1941
+ case "claude":
1942
+ installClaudeCode(scope);
1943
+ break;
1944
+ case "codex":
1945
+ installCodex(scope);
1946
+ break;
1947
+ case "cursor":
1948
+ installCursor();
1949
+ break;
1950
+ case "windsurf":
1951
+ installWindsurf();
1952
+ break;
1953
+ case "aider":
1954
+ installAider();
1955
+ break;
1956
+ default:
1957
+ error(`Unknown agent: ${agent}`);
1958
+ log("");
1959
+ log("Supported agents: claude, codex, cursor, windsurf, aider");
1960
+ process.exit(1);
1961
+ }
1962
+ } catch (err) {
1963
+ error(`Failed to install: ${err.message}`);
1964
+ process.exit(1);
1965
+ }
1966
+ }
1967
+ function docsCommand(topic) {
1968
+ const docs = {
1969
+ audio: AUDIO_DOCS,
1970
+ canvas: CANVAS_DOCS,
1971
+ leaderboard: LEADERBOARD_DOCS,
1972
+ multiplayer: MULTIPLAYER_DOCS
1973
+ };
1974
+ if (!topic) {
1975
+ console.log(SKILL_CONTENT);
1976
+ } else if (docs[topic]) {
1977
+ console.log(docs[topic]);
1978
+ } else {
1979
+ error(`Unknown topic: ${topic}`);
1980
+ log("");
1981
+ log("Available topics: audio, canvas, leaderboard, multiplayer");
1982
+ process.exit(1);
1983
+ }
1984
+ }
163
1985
  function parseArgs(args) {
164
1986
  const flags = {};
165
1987
  const positional = [];
@@ -199,6 +2021,17 @@ async function main() {
199
2021
  case "init":
200
2022
  await initCommand(positional[0], email);
201
2023
  break;
2024
+ case "install": {
2025
+ const agent = positional[0] || "claude";
2026
+ const scope = flags.project ? "project" : "global";
2027
+ installCommand(agent, scope);
2028
+ break;
2029
+ }
2030
+ case "docs":
2031
+ case "skill":
2032
+ case "prompt":
2033
+ docsCommand(positional[0]);
2034
+ break;
202
2035
  case "whoami":
203
2036
  whoamiCommand();
204
2037
  break;