@ysdk/create 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,647 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as path2 from "path";
5
+ import * as fs2 from "fs";
6
+
7
+ // src/scaffold.ts
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+
11
+ // src/templates.ts
12
+ var TEMPLATES = {
13
+ basic: {
14
+ description: 'Simple shared counter - the "Hello World" of multiplayer',
15
+ appTsx: `import { useShared } from '@ysdk/react';
16
+
17
+ export default function App() {
18
+ const [count, setCount] = useShared('count', 0);
19
+
20
+ return (
21
+ <div style={{ fontFamily: 'system-ui', maxWidth: 480, margin: '0 auto', padding: 40 }}>
22
+ <h1>Shared Counter</h1>
23
+ <p style={{ fontSize: 64, textAlign: 'center' }}>{count}</p>
24
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
25
+ <button onClick={() => setCount(count - 1)}>-</button>
26
+ <button onClick={() => setCount(count + 1)}>+</button>
27
+ <button onClick={() => setCount(0)}>Reset</button>
28
+ </div>
29
+ <p style={{ textAlign: 'center', color: '#888', marginTop: 24 }}>
30
+ Open in another tab to see real-time sync
31
+ </p>
32
+ </div>
33
+ );
34
+ }
35
+ `
36
+ },
37
+ todo: {
38
+ description: "Collaborative to-do list with shared array",
39
+ appTsx: `import { useState } from 'react';
40
+ import { useSharedArray, usePresence } from '@ysdk/react';
41
+
42
+ interface Todo {
43
+ id: string;
44
+ text: string;
45
+ done: boolean;
46
+ }
47
+
48
+ export default function App() {
49
+ const [todos, ops] = useSharedArray<Todo>('todos');
50
+ const [input, setInput] = useState('');
51
+ const { peers, count } = usePresence({ name: 'User' });
52
+
53
+ const addTodo = () => {
54
+ if (!input.trim()) return;
55
+ ops.push({ id: crypto.randomUUID(), text: input.trim(), done: false });
56
+ setInput('');
57
+ };
58
+
59
+ const toggleTodo = (index: number) => {
60
+ const todo = todos[index];
61
+ ops.delete(index);
62
+ ops.insert(index, { ...todo, done: !todo.done });
63
+ };
64
+
65
+ const deleteTodo = (index: number) => {
66
+ ops.delete(index);
67
+ };
68
+
69
+ return (
70
+ <div style={{ fontFamily: 'system-ui', maxWidth: 480, margin: '0 auto', padding: 40 }}>
71
+ <h1>Shared To-Do List</h1>
72
+ <p style={{ color: '#888' }}>{count + 1} user(s) connected</p>
73
+
74
+ <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
75
+ <input
76
+ value={input}
77
+ onChange={(e) => setInput(e.target.value)}
78
+ onKeyDown={(e) => e.key === 'Enter' && addTodo()}
79
+ placeholder="Add a task..."
80
+ style={{ flex: 1, padding: 8 }}
81
+ />
82
+ <button onClick={addTodo}>Add</button>
83
+ </div>
84
+
85
+ <ul style={{ listStyle: 'none', padding: 0 }}>
86
+ {todos.map((todo, i) => (
87
+ <li key={todo.id} style={{ display: 'flex', gap: 8, padding: '8px 0', borderBottom: '1px solid #eee' }}>
88
+ <input type="checkbox" checked={todo.done} onChange={() => toggleTodo(i)} />
89
+ <span style={{ flex: 1, textDecoration: todo.done ? 'line-through' : 'none', color: todo.done ? '#aaa' : '#000' }}>
90
+ {todo.text}
91
+ </span>
92
+ <button onClick={() => deleteTodo(i)} style={{ color: 'red', border: 'none', background: 'none', cursor: 'pointer' }}>x</button>
93
+ </li>
94
+ ))}
95
+ </ul>
96
+
97
+ {todos.length === 0 && <p style={{ textAlign: 'center', color: '#aaa' }}>No tasks yet</p>}
98
+ </div>
99
+ );
100
+ }
101
+ `
102
+ },
103
+ whiteboard: {
104
+ description: "Multiplayer drawing canvas with presence cursors",
105
+ appTsx: `import { useRef, useEffect, useState, useCallback } from 'react';
106
+ import { useSharedArray, usePresence, useBroadcast } from '@ysdk/react';
107
+
108
+ interface Point { x: number; y: number }
109
+ interface Stroke { points: Point[]; color: string }
110
+
111
+ export default function App() {
112
+ const canvasRef = useRef<HTMLCanvasElement>(null);
113
+ const [strokes, ops] = useSharedArray<Stroke>('strokes');
114
+ const [drawing, setDrawing] = useState(false);
115
+ const [color, setColor] = useState('#000000');
116
+ const currentStroke = useRef<Point[]>([]);
117
+ const { peers, setPresence } = usePresence<{ name: string; cursor?: Point }>({ name: 'User' });
118
+ const { broadcast, onMessage } = useBroadcast<{ cursor: Point }>('cursor');
119
+
120
+ // Draw all strokes
121
+ useEffect(() => {
122
+ const canvas = canvasRef.current;
123
+ if (!canvas) return;
124
+ const ctx = canvas.getContext('2d')!;
125
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
126
+
127
+ for (const stroke of strokes) {
128
+ if (stroke.points.length < 2) continue;
129
+ ctx.beginPath();
130
+ ctx.strokeStyle = stroke.color;
131
+ ctx.lineWidth = 3;
132
+ ctx.lineCap = 'round';
133
+ ctx.lineJoin = 'round';
134
+ ctx.moveTo(stroke.points[0].x, stroke.points[0].y);
135
+ for (let i = 1; i < stroke.points.length; i++) {
136
+ ctx.lineTo(stroke.points[i].x, stroke.points[i].y);
137
+ }
138
+ ctx.stroke();
139
+ }
140
+ }, [strokes]);
141
+
142
+ const getPos = (e: React.MouseEvent): Point => {
143
+ const rect = canvasRef.current!.getBoundingClientRect();
144
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
145
+ };
146
+
147
+ return (
148
+ <div style={{ fontFamily: 'system-ui', maxWidth: 640, margin: '0 auto', padding: 40 }}>
149
+ <h1>Shared Whiteboard</h1>
150
+ <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
151
+ <input type="color" value={color} onChange={(e) => setColor(e.target.value)} />
152
+ <button onClick={() => ops.clear()}>Clear</button>
153
+ <span style={{ color: '#888', marginLeft: 'auto' }}>{peers.length + 1} connected</span>
154
+ </div>
155
+ <canvas
156
+ ref={canvasRef}
157
+ width={600}
158
+ height={400}
159
+ style={{ border: '1px solid #ccc', cursor: 'crosshair' }}
160
+ onMouseDown={(e) => {
161
+ setDrawing(true);
162
+ currentStroke.current = [getPos(e)];
163
+ }}
164
+ onMouseMove={(e) => {
165
+ const pos = getPos(e);
166
+ broadcast({ cursor: pos });
167
+ if (!drawing) return;
168
+ currentStroke.current.push(pos);
169
+ // Draw locally for smooth feel
170
+ const ctx = canvasRef.current!.getContext('2d')!;
171
+ const pts = currentStroke.current;
172
+ if (pts.length >= 2) {
173
+ ctx.beginPath();
174
+ ctx.strokeStyle = color;
175
+ ctx.lineWidth = 3;
176
+ ctx.lineCap = 'round';
177
+ ctx.moveTo(pts[pts.length - 2].x, pts[pts.length - 2].y);
178
+ ctx.lineTo(pts[pts.length - 1].x, pts[pts.length - 1].y);
179
+ ctx.stroke();
180
+ }
181
+ }}
182
+ onMouseUp={() => {
183
+ if (currentStroke.current.length > 1) {
184
+ ops.push({ points: currentStroke.current, color });
185
+ }
186
+ setDrawing(false);
187
+ currentStroke.current = [];
188
+ }}
189
+ />
190
+ </div>
191
+ );
192
+ }
193
+ `
194
+ },
195
+ chat: {
196
+ description: "Real-time chat room with presence",
197
+ appTsx: `import { useState, useRef, useEffect } from 'react';
198
+ import { useSharedArray, usePresence } from '@ysdk/react';
199
+
200
+ interface Message {
201
+ id: string;
202
+ user: string;
203
+ text: string;
204
+ time: number;
205
+ }
206
+
207
+ const COLORS = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c'];
208
+
209
+ export default function App() {
210
+ const [messages, ops] = useSharedArray<Message>('messages');
211
+ const [input, setInput] = useState('');
212
+ const [name, setName] = useState(() => 'User-' + Math.random().toString(36).slice(2, 5));
213
+ const { peers, setPresence, count } = usePresence<{ name: string; typing?: boolean }>({ name });
214
+ const bottomRef = useRef<HTMLDivElement>(null);
215
+
216
+ useEffect(() => {
217
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
218
+ }, [messages.length]);
219
+
220
+ const send = () => {
221
+ if (!input.trim()) return;
222
+ ops.push({ id: crypto.randomUUID(), user: name, text: input.trim(), time: Date.now() });
223
+ setInput('');
224
+ setPresence({ typing: false });
225
+ };
226
+
227
+ const colorFor = (user: string) => COLORS[Math.abs([...user].reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) % COLORS.length];
228
+ const typingPeers = peers.filter(p => p.typing).map(p => p.name);
229
+
230
+ return (
231
+ <div style={{ fontFamily: 'system-ui', maxWidth: 480, margin: '0 auto', padding: 40, display: 'flex', flexDirection: 'column', height: '100vh' }}>
232
+ <div style={{ display: 'flex', gap: 8, marginBottom: 16, alignItems: 'center' }}>
233
+ <h1 style={{ margin: 0, flex: 1 }}>Chat Room</h1>
234
+ <input
235
+ value={name}
236
+ onChange={(e) => { setName(e.target.value); setPresence({ name: e.target.value }); }}
237
+ style={{ width: 100, padding: 4 }}
238
+ placeholder="Your name"
239
+ />
240
+ <span style={{ color: '#888' }}>{count + 1} online</span>
241
+ </div>
242
+
243
+ <div style={{ flex: 1, overflowY: 'auto', border: '1px solid #eee', borderRadius: 8, padding: 12 }}>
244
+ {messages.map((msg) => (
245
+ <div key={msg.id} style={{ marginBottom: 8 }}>
246
+ <strong style={{ color: colorFor(msg.user) }}>{msg.user}</strong>
247
+ <span style={{ color: '#aaa', fontSize: 12, marginLeft: 8 }}>{new Date(msg.time).toLocaleTimeString()}</span>
248
+ <div>{msg.text}</div>
249
+ </div>
250
+ ))}
251
+ <div ref={bottomRef} />
252
+ </div>
253
+
254
+ {typingPeers.length > 0 && (
255
+ <div style={{ color: '#888', fontSize: 12, padding: 4 }}>
256
+ {typingPeers.join(', ')} {typingPeers.length === 1 ? 'is' : 'are'} typing...
257
+ </div>
258
+ )}
259
+
260
+ <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
261
+ <input
262
+ value={input}
263
+ onChange={(e) => { setInput(e.target.value); setPresence({ typing: e.target.value.length > 0 }); }}
264
+ onKeyDown={(e) => e.key === 'Enter' && send()}
265
+ placeholder="Type a message..."
266
+ style={{ flex: 1, padding: 8 }}
267
+ />
268
+ <button onClick={send}>Send</button>
269
+ </div>
270
+ </div>
271
+ );
272
+ }
273
+ `
274
+ }
275
+ };
276
+ function getTemplateNames() {
277
+ return Object.keys(TEMPLATES);
278
+ }
279
+
280
+ // src/claude-md.ts
281
+ function generateClaudeMd(projectName2, room) {
282
+ return `# CLAUDE.md - ${projectName2}
283
+
284
+ This is a collaborative React app powered by YSDK. You (Claude) are connected to the same multiplayer room as the browser via MCP tools.
285
+
286
+ ## How It Works
287
+
288
+ The React app and Claude share the same Yjs CRDT document. When you call \`shared_set\`, the React app's \`useShared\` hook updates instantly. When the user changes state in the browser, you can read it with \`shared_get\`.
289
+
290
+ **Room:** \`${room}\`
291
+
292
+ ---
293
+
294
+ ## MCP Tools Reference
295
+
296
+ ### Shared State (Y.Map)
297
+ Maps to \`useShared(key, defaultValue)\` in React.
298
+
299
+ | Tool | Description |
300
+ |------|-------------|
301
+ | \`shared_get(key)\` | Read a value. Returns the current value or "undefined". |
302
+ | \`shared_set(key, value)\` | Write a value. All connected clients update instantly. Value can be any JSON type. |
303
+ | \`shared_delete(key)\` | Remove a key from shared state. |
304
+ | \`shared_keys()\` | List all keys in shared state. |
305
+
306
+ **Example:**
307
+ \`\`\`
308
+ shared_set(key: "count", value: 42) -> useShared('count', 0) returns 42
309
+ shared_get(key: "count") -> "42"
310
+ \`\`\`
311
+
312
+ **Important:** Objects are stored as opaque values (last-write-wins on the whole object). If two users set the same key simultaneously, one write wins. Use shared arrays for concurrent insertions.
313
+
314
+ ### Shared Arrays (Y.Array)
315
+ Maps to \`useSharedArray(key)\` in React. Concurrent insertions merge cleanly (CRDT).
316
+
317
+ | Tool | Description |
318
+ |------|-------------|
319
+ | \`array_get(key)\` | Read all items in the array. |
320
+ | \`array_push(key, items)\` | Append items to the end. \`items\` is a JSON array. |
321
+ | \`array_insert(key, index, items)\` | Insert items at a position. |
322
+ | \`array_delete(key, index, count?)\` | Delete items starting at index. Count defaults to 1. |
323
+ | \`array_clear(key)\` | Remove all items from the array. |
324
+
325
+ **Example:**
326
+ \`\`\`
327
+ array_push(key: "todos", items: [{"id": "abc", "text": "Buy milk", "done": false}])
328
+ array_get(key: "todos") -> [{"id": "abc", "text": "Buy milk", "done": false}]
329
+ \`\`\`
330
+
331
+ ### Shared Text (Y.Text)
332
+ Maps to \`useSharedText(key, defaultValue)\` in React. Character-by-character CRDT merge.
333
+
334
+ | Tool | Description |
335
+ |------|-------------|
336
+ | \`text_get(key)\` | Read the text content. |
337
+ | \`text_set(key, value)\` | Replace the entire text. |
338
+ | \`text_insert(key, index, content)\` | Insert text at a character position. |
339
+ | \`text_delete(key, index, length)\` | Delete characters starting at position. |
340
+
341
+ ### Presence & Broadcast
342
+
343
+ | Tool | Description |
344
+ |------|-------------|
345
+ | \`presence_list()\` | List all connected users and their state. |
346
+ | \`broadcast(channel, data)\` | Send ephemeral message to all clients. Received via \`useBroadcast(channel)\`. Not persisted. |
347
+
348
+ ### Utility
349
+
350
+ | Tool | Description |
351
+ |------|-------------|
352
+ | \`room_info()\` | Connection status, sync status, client count, document size. |
353
+ | \`document_snapshot()\` | Full dump of all shared data (maps, arrays, texts). |
354
+
355
+ ---
356
+
357
+ ## React Hooks Reference
358
+
359
+ \`\`\`tsx
360
+ import { YSDKProvider, useShared, useSharedArray, useSharedText, usePresence, useBroadcast } from '@ysdk/react';
361
+
362
+ // Shared state (like useState but multiplayer)
363
+ const [count, setCount] = useShared('count', 0);
364
+
365
+ // Shared array (concurrent insertions merge)
366
+ const [items, ops] = useSharedArray<Item>('items');
367
+ ops.push(item);
368
+ ops.delete(index, count?);
369
+ ops.insert(index, item);
370
+ ops.clear();
371
+
372
+ // Shared text (character-level CRDT)
373
+ const { value, setValue, ytext } = useSharedText('doc', 'initial');
374
+
375
+ // Presence (who's online + ephemeral state)
376
+ const { peers, setPresence, count } = usePresence<{ name: string }>({ name: 'User' });
377
+
378
+ // Broadcast (fire-and-forget messages)
379
+ const { broadcast, onMessage } = useBroadcast<T>('channel');
380
+ broadcast(data);
381
+ onMessage((data, senderId) => { ... });
382
+ \`\`\`
383
+
384
+ ---
385
+
386
+ ## How MCP Maps to Hooks
387
+
388
+ | MCP Tool | React Hook | What Happens |
389
+ |----------|------------|-------------|
390
+ | \`shared_set(key, val)\` | \`useShared(key)\` | Hook re-renders with new value |
391
+ | \`array_push(key, items)\` | \`useSharedArray(key)\` | Hook re-renders with new array |
392
+ | \`text_set(key, val)\` | \`useSharedText(key)\` | Hook re-renders with new text |
393
+ | \`broadcast(ch, data)\` | \`useBroadcast(ch)\` | \`onMessage\` callback fires |
394
+ | \`presence_list()\` | \`usePresence()\` | Shows all peers including Claude |
395
+
396
+ ---
397
+
398
+ ## Common Patterns
399
+
400
+ ### Counter
401
+ \`\`\`
402
+ shared_get(key: "count") # Read current value
403
+ shared_set(key: "count", value: 5) # Set to 5
404
+ \`\`\`
405
+
406
+ ### Todo List
407
+ \`\`\`
408
+ array_get(key: "todos") # Read all todos
409
+ array_push(key: "todos", items: [{"id": "...", "text": "New task", "done": false}])
410
+ array_delete(key: "todos", index: 2) # Delete 3rd item
411
+ \`\`\`
412
+
413
+ ### Collaborative Text
414
+ \`\`\`
415
+ text_get(key: "document") # Read full text
416
+ text_set(key: "document", value: "Hello World") # Replace
417
+ text_insert(key: "document", index: 5, content: ",") # Insert comma
418
+ \`\`\`
419
+
420
+ ---
421
+
422
+ ## Rules
423
+
424
+ 1. **React 18+ required** - hooks use modern React patterns
425
+ 2. **All hooks must be inside \`<YSDKProvider>\`** - this provides the Yjs document context
426
+ 3. **Objects in shared state are LWW** (last-write-wins) - use arrays for concurrent insertions
427
+ 4. **Presence is ephemeral** - disappears when user disconnects
428
+ 5. **Broadcast is fire-and-forget** - no persistence, no delivery guarantee
429
+
430
+ ## Verifying Changes
431
+
432
+ After making changes via MCP tools, you can verify:
433
+ 1. \`shared_get\` / \`array_get\` / \`text_get\` - read back what you wrote
434
+ 2. \`document_snapshot\` - see ALL shared data at once
435
+ 3. \`presence_list\` - confirm you're connected (look for "Claude (MCP)")
436
+ `;
437
+ }
438
+
439
+ // src/mcp-json.ts
440
+ function generateMcpJson(opts) {
441
+ const env = {
442
+ YSDK_ROOM: opts.room
443
+ };
444
+ if (opts.apiKey) {
445
+ env.YSDK_API_KEY = opts.apiKey;
446
+ }
447
+ const config = {
448
+ mcpServers: {
449
+ ysdk: {
450
+ command: "npx",
451
+ args: ["@ysdk/mcp"],
452
+ env
453
+ }
454
+ }
455
+ };
456
+ return JSON.stringify(config, null, 2) + "\n";
457
+ }
458
+
459
+ // src/scaffold.ts
460
+ function scaffold(opts) {
461
+ const { name, dir, template: template2, apiKey: apiKey2 } = opts;
462
+ const tpl2 = TEMPLATES[template2];
463
+ if (!tpl2) {
464
+ throw new Error(`Unknown template: ${template2}. Available: ${Object.keys(TEMPLATES).join(", ")}`);
465
+ }
466
+ const room = name;
467
+ fs.mkdirSync(path.join(dir, "src"), { recursive: true });
468
+ writeFile(dir, "package.json", JSON.stringify({
469
+ name,
470
+ version: "0.1.0",
471
+ private: true,
472
+ type: "module",
473
+ scripts: {
474
+ dev: "vite",
475
+ build: "vite build",
476
+ preview: "vite preview"
477
+ },
478
+ dependencies: {
479
+ react: "^18.2.0",
480
+ "react-dom": "^18.2.0",
481
+ "@ysdk/react": "^0.1.0"
482
+ },
483
+ devDependencies: {
484
+ "@types/react": "^18.2.0",
485
+ "@types/react-dom": "^18.2.0",
486
+ "@vitejs/plugin-react": "^4.0.0",
487
+ typescript: "^5.0.0",
488
+ vite: "^5.0.0"
489
+ }
490
+ }, null, 2) + "\n");
491
+ writeFile(dir, "tsconfig.json", JSON.stringify({
492
+ compilerOptions: {
493
+ target: "ES2020",
494
+ useDefineForClassFields: true,
495
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
496
+ module: "ESNext",
497
+ skipLibCheck: true,
498
+ moduleResolution: "bundler",
499
+ allowImportingTsExtensions: true,
500
+ isolatedModules: true,
501
+ moduleDetection: "force",
502
+ noEmit: true,
503
+ jsx: "react-jsx",
504
+ strict: true,
505
+ noUnusedLocals: false,
506
+ noUnusedParameters: false
507
+ },
508
+ include: ["src"]
509
+ }, null, 2) + "\n");
510
+ writeFile(dir, "vite.config.ts", `import { defineConfig } from 'vite';
511
+ import react from '@vitejs/plugin-react';
512
+
513
+ export default defineConfig({
514
+ plugins: [react()],
515
+ });
516
+ `);
517
+ writeFile(dir, "index.html", `<!DOCTYPE html>
518
+ <html lang="en">
519
+ <head>
520
+ <meta charset="UTF-8" />
521
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
522
+ <title>${name}</title>
523
+ </head>
524
+ <body>
525
+ <div id="root"></div>
526
+ <script type="module" src="/src/main.tsx"></script>
527
+ </body>
528
+ </html>
529
+ `);
530
+ const providerProps = apiKey2 ? `cloud={{ room: '${room}', apiKey: '${apiKey2}' }}` : `url="ws://localhost:1234" room="${room}"`;
531
+ writeFile(dir, "src/main.tsx", `import React from 'react';
532
+ import ReactDOM from 'react-dom/client';
533
+ import { YSDKProvider } from '@ysdk/react';
534
+ import App from './App';
535
+
536
+ ReactDOM.createRoot(document.getElementById('root')!).render(
537
+ <React.StrictMode>
538
+ <YSDKProvider ${providerProps}>
539
+ <App />
540
+ </YSDKProvider>
541
+ </React.StrictMode>,
542
+ );
543
+ `);
544
+ writeFile(dir, "src/App.tsx", tpl2.appTsx);
545
+ writeFile(dir, ".mcp.json", generateMcpJson({ room, apiKey: apiKey2 }));
546
+ writeFile(dir, "CLAUDE.md", generateClaudeMd(name, room));
547
+ writeFile(dir, ".gitignore", `node_modules
548
+ dist
549
+ .env
550
+ .env.local
551
+ `);
552
+ }
553
+ function writeFile(dir, filename, content) {
554
+ fs.writeFileSync(path.join(dir, filename), content);
555
+ }
556
+
557
+ // src/index.ts
558
+ var args = process.argv.slice(2);
559
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
560
+ console.log(`
561
+ Usage: npx @ysdk/create <project-name> [options]
562
+
563
+ Options:
564
+ --template <name> App template (default: basic)
565
+ Available: ${getTemplateNames().join(", ")}
566
+ --api-key <key> YSDK cloud API key (optional, uses localhost without)
567
+ --help, -h Show this help
568
+
569
+ Examples:
570
+ npx @ysdk/create my-app
571
+ npx @ysdk/create my-app --template todo
572
+ npx @ysdk/create my-app --template chat --api-key ysdk_live_...
573
+ `);
574
+ process.exit(0);
575
+ }
576
+ var projectName = args[0];
577
+ if (!projectName || projectName.startsWith("-")) {
578
+ console.error("Error: Please provide a project name.\n npx @ysdk/create my-app");
579
+ process.exit(1);
580
+ }
581
+ function getFlag(name) {
582
+ const idx = args.indexOf(name);
583
+ if (idx === -1) return void 0;
584
+ return args[idx + 1];
585
+ }
586
+ var template = getFlag("--template") || "basic";
587
+ var apiKey = getFlag("--api-key");
588
+ if (!TEMPLATES[template]) {
589
+ console.error(`Error: Unknown template "${template}". Available: ${getTemplateNames().join(", ")}`);
590
+ process.exit(1);
591
+ }
592
+ var targetDir = path2.resolve(process.cwd(), projectName);
593
+ if (fs2.existsSync(targetDir)) {
594
+ const contents = fs2.readdirSync(targetDir);
595
+ if (contents.length > 0) {
596
+ console.error(`Error: Directory "${projectName}" already exists and is not empty.`);
597
+ process.exit(1);
598
+ }
599
+ }
600
+ console.log(`
601
+ Creating ${projectName} with "${template}" template...
602
+ `);
603
+ try {
604
+ scaffold({
605
+ name: projectName,
606
+ dir: targetDir,
607
+ template,
608
+ apiKey
609
+ });
610
+ } catch (err) {
611
+ console.error(`Error: ${err.message}`);
612
+ process.exit(1);
613
+ }
614
+ var tpl = TEMPLATES[template];
615
+ var useYarn = process.env.npm_config_user_agent?.startsWith("yarn");
616
+ var usePnpm = process.env.npm_config_user_agent?.startsWith("pnpm");
617
+ var pm = usePnpm ? "pnpm" : useYarn ? "yarn" : "npm";
618
+ console.log(` Created files:
619
+ package.json
620
+ tsconfig.json
621
+ vite.config.ts
622
+ index.html
623
+ src/main.tsx
624
+ src/App.tsx (${tpl.description})
625
+ .mcp.json (Claude MCP config)
626
+ CLAUDE.md (AI assistant guide)
627
+ .gitignore
628
+ `);
629
+ console.log(` Next steps:
630
+ `);
631
+ console.log(` cd ${projectName}`);
632
+ console.log(` ${pm} install`);
633
+ if (!apiKey) {
634
+ console.log(` npx y-websocket (start local sync server)`);
635
+ }
636
+ console.log(` ${pm} run dev (start dev server)
637
+ `);
638
+ if (apiKey) {
639
+ console.log(` Cloud sync configured with API key.`);
640
+ } else {
641
+ console.log(` Using local sync (ws://localhost:1234).`);
642
+ console.log(` Add --api-key for cloud sync.
643
+ `);
644
+ }
645
+ console.log(` Open in Claude Code for God Mode - .mcp.json is ready.
646
+ `);
647
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/scaffold.ts","../src/templates.ts","../src/claude-md.ts","../src/mcp-json.ts"],"sourcesContent":["import * as path from 'node:path';\r\nimport * as fs from 'node:fs';\r\nimport { scaffold } from './scaffold.js';\r\nimport { getTemplateNames, TEMPLATES } from './templates.js';\r\n\r\n// Parse CLI args\r\nconst args = process.argv.slice(2);\r\n\r\nif (args.includes('--help') || args.includes('-h') || args.length === 0) {\r\n console.log(`\r\n Usage: npx @ysdk/create <project-name> [options]\r\n\r\n Options:\r\n --template <name> App template (default: basic)\r\n Available: ${getTemplateNames().join(', ')}\r\n --api-key <key> YSDK cloud API key (optional, uses localhost without)\r\n --help, -h Show this help\r\n\r\n Examples:\r\n npx @ysdk/create my-app\r\n npx @ysdk/create my-app --template todo\r\n npx @ysdk/create my-app --template chat --api-key ysdk_live_...\r\n`);\r\n process.exit(0);\r\n}\r\n\r\n// Parse project name and flags\r\nconst projectName = args[0];\r\n\r\nif (!projectName || projectName.startsWith('-')) {\r\n console.error('Error: Please provide a project name.\\n npx @ysdk/create my-app');\r\n process.exit(1);\r\n}\r\n\r\nfunction getFlag(name: string): string | undefined {\r\n const idx = args.indexOf(name);\r\n if (idx === -1) return undefined;\r\n return args[idx + 1];\r\n}\r\n\r\nconst template = getFlag('--template') || 'basic';\r\nconst apiKey = getFlag('--api-key');\r\n\r\n// Validate template\r\nif (!TEMPLATES[template]) {\r\n console.error(`Error: Unknown template \"${template}\". Available: ${getTemplateNames().join(', ')}`);\r\n process.exit(1);\r\n}\r\n\r\n// Create project directory\r\nconst targetDir = path.resolve(process.cwd(), projectName);\r\n\r\nif (fs.existsSync(targetDir)) {\r\n const contents = fs.readdirSync(targetDir);\r\n if (contents.length > 0) {\r\n console.error(`Error: Directory \"${projectName}\" already exists and is not empty.`);\r\n process.exit(1);\r\n }\r\n}\r\n\r\nconsole.log(`\\nCreating ${projectName} with \"${template}\" template...\\n`);\r\n\r\ntry {\r\n scaffold({\r\n name: projectName,\r\n dir: targetDir,\r\n template,\r\n apiKey,\r\n });\r\n} catch (err: any) {\r\n console.error(`Error: ${err.message}`);\r\n process.exit(1);\r\n}\r\n\r\n// Print next steps\r\nconst tpl = TEMPLATES[template];\r\nconst useYarn = process.env.npm_config_user_agent?.startsWith('yarn');\r\nconst usePnpm = process.env.npm_config_user_agent?.startsWith('pnpm');\r\nconst pm = usePnpm ? 'pnpm' : useYarn ? 'yarn' : 'npm';\r\n\r\nconsole.log(` Created files:\r\n package.json\r\n tsconfig.json\r\n vite.config.ts\r\n index.html\r\n src/main.tsx\r\n src/App.tsx (${tpl.description})\r\n .mcp.json (Claude MCP config)\r\n CLAUDE.md (AI assistant guide)\r\n .gitignore\r\n`);\r\n\r\nconsole.log(` Next steps:\\n`);\r\nconsole.log(` cd ${projectName}`);\r\nconsole.log(` ${pm} install`);\r\n\r\nif (!apiKey) {\r\n console.log(` npx y-websocket (start local sync server)`);\r\n}\r\n\r\nconsole.log(` ${pm} run dev (start dev server)\\n`);\r\n\r\nif (apiKey) {\r\n console.log(` Cloud sync configured with API key.`);\r\n} else {\r\n console.log(` Using local sync (ws://localhost:1234).`);\r\n console.log(` Add --api-key for cloud sync.\\n`);\r\n}\r\n\r\nconsole.log(` Open in Claude Code for God Mode - .mcp.json is ready.\\n`);\r\n","import * as fs from 'node:fs';\r\nimport * as path from 'node:path';\r\nimport { TEMPLATES } from './templates.js';\r\nimport { generateClaudeMd } from './claude-md.js';\r\nimport { generateMcpJson } from './mcp-json.js';\r\n\r\nexport interface ScaffoldOptions {\r\n name: string;\r\n dir: string;\r\n template: string;\r\n apiKey?: string;\r\n}\r\n\r\nexport function scaffold(opts: ScaffoldOptions) {\r\n const { name, dir, template, apiKey } = opts;\r\n const tpl = TEMPLATES[template];\r\n if (!tpl) {\r\n throw new Error(`Unknown template: ${template}. Available: ${Object.keys(TEMPLATES).join(', ')}`);\r\n }\r\n\r\n const room = name; // Use project name as default room\r\n\r\n // Create directory structure\r\n fs.mkdirSync(path.join(dir, 'src'), { recursive: true });\r\n\r\n // package.json\r\n writeFile(dir, 'package.json', JSON.stringify({\r\n name,\r\n version: '0.1.0',\r\n private: true,\r\n type: 'module',\r\n scripts: {\r\n dev: 'vite',\r\n build: 'vite build',\r\n preview: 'vite preview',\r\n },\r\n dependencies: {\r\n react: '^18.2.0',\r\n 'react-dom': '^18.2.0',\r\n '@ysdk/react': '^0.1.0',\r\n },\r\n devDependencies: {\r\n '@types/react': '^18.2.0',\r\n '@types/react-dom': '^18.2.0',\r\n '@vitejs/plugin-react': '^4.0.0',\r\n typescript: '^5.0.0',\r\n vite: '^5.0.0',\r\n },\r\n }, null, 2) + '\\n');\r\n\r\n // tsconfig.json\r\n writeFile(dir, 'tsconfig.json', JSON.stringify({\r\n compilerOptions: {\r\n target: 'ES2020',\r\n useDefineForClassFields: true,\r\n lib: ['ES2020', 'DOM', 'DOM.Iterable'],\r\n module: 'ESNext',\r\n skipLibCheck: true,\r\n moduleResolution: 'bundler',\r\n allowImportingTsExtensions: true,\r\n isolatedModules: true,\r\n moduleDetection: 'force',\r\n noEmit: true,\r\n jsx: 'react-jsx',\r\n strict: true,\r\n noUnusedLocals: false,\r\n noUnusedParameters: false,\r\n },\r\n include: ['src'],\r\n }, null, 2) + '\\n');\r\n\r\n // vite.config.ts\r\n writeFile(dir, 'vite.config.ts', `import { defineConfig } from 'vite';\r\nimport react from '@vitejs/plugin-react';\r\n\r\nexport default defineConfig({\r\n plugins: [react()],\r\n});\r\n`);\r\n\r\n // index.html\r\n writeFile(dir, 'index.html', `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n <head>\r\n <meta charset=\"UTF-8\" />\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\r\n <title>${name}</title>\r\n </head>\r\n <body>\r\n <div id=\"root\"></div>\r\n <script type=\"module\" src=\"/src/main.tsx\"></script>\r\n </body>\r\n</html>\r\n`);\r\n\r\n // src/main.tsx\r\n const providerProps = apiKey\r\n ? `cloud={{ room: '${room}', apiKey: '${apiKey}' }}`\r\n : `url=\"ws://localhost:1234\" room=\"${room}\"`;\r\n\r\n writeFile(dir, 'src/main.tsx', `import React from 'react';\r\nimport ReactDOM from 'react-dom/client';\r\nimport { YSDKProvider } from '@ysdk/react';\r\nimport App from './App';\r\n\r\nReactDOM.createRoot(document.getElementById('root')!).render(\r\n <React.StrictMode>\r\n <YSDKProvider ${providerProps}>\r\n <App />\r\n </YSDKProvider>\r\n </React.StrictMode>,\r\n);\r\n`);\r\n\r\n // src/App.tsx\r\n writeFile(dir, 'src/App.tsx', tpl.appTsx);\r\n\r\n // .mcp.json\r\n writeFile(dir, '.mcp.json', generateMcpJson({ room, apiKey }));\r\n\r\n // CLAUDE.md\r\n writeFile(dir, 'CLAUDE.md', generateClaudeMd(name, room));\r\n\r\n // .gitignore\r\n writeFile(dir, '.gitignore', `node_modules\r\ndist\r\n.env\r\n.env.local\r\n`);\r\n}\r\n\r\nfunction writeFile(dir: string, filename: string, content: string) {\r\n fs.writeFileSync(path.join(dir, filename), content);\r\n}\r\n","// App.tsx content for each project template\r\n\r\nexport const TEMPLATES: Record<string, { description: string; appTsx: string }> = {\r\n basic: {\r\n description: 'Simple shared counter - the \"Hello World\" of multiplayer',\r\n appTsx: `import { useShared } from '@ysdk/react';\r\n\r\nexport default function App() {\r\n const [count, setCount] = useShared('count', 0);\r\n\r\n return (\r\n <div style={{ fontFamily: 'system-ui', maxWidth: 480, margin: '0 auto', padding: 40 }}>\r\n <h1>Shared Counter</h1>\r\n <p style={{ fontSize: 64, textAlign: 'center' }}>{count}</p>\r\n <div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>\r\n <button onClick={() => setCount(count - 1)}>-</button>\r\n <button onClick={() => setCount(count + 1)}>+</button>\r\n <button onClick={() => setCount(0)}>Reset</button>\r\n </div>\r\n <p style={{ textAlign: 'center', color: '#888', marginTop: 24 }}>\r\n Open in another tab to see real-time sync\r\n </p>\r\n </div>\r\n );\r\n}\r\n`,\r\n },\r\n\r\n todo: {\r\n description: 'Collaborative to-do list with shared array',\r\n appTsx: `import { useState } from 'react';\r\nimport { useSharedArray, usePresence } from '@ysdk/react';\r\n\r\ninterface Todo {\r\n id: string;\r\n text: string;\r\n done: boolean;\r\n}\r\n\r\nexport default function App() {\r\n const [todos, ops] = useSharedArray<Todo>('todos');\r\n const [input, setInput] = useState('');\r\n const { peers, count } = usePresence({ name: 'User' });\r\n\r\n const addTodo = () => {\r\n if (!input.trim()) return;\r\n ops.push({ id: crypto.randomUUID(), text: input.trim(), done: false });\r\n setInput('');\r\n };\r\n\r\n const toggleTodo = (index: number) => {\r\n const todo = todos[index];\r\n ops.delete(index);\r\n ops.insert(index, { ...todo, done: !todo.done });\r\n };\r\n\r\n const deleteTodo = (index: number) => {\r\n ops.delete(index);\r\n };\r\n\r\n return (\r\n <div style={{ fontFamily: 'system-ui', maxWidth: 480, margin: '0 auto', padding: 40 }}>\r\n <h1>Shared To-Do List</h1>\r\n <p style={{ color: '#888' }}>{count + 1} user(s) connected</p>\r\n\r\n <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>\r\n <input\r\n value={input}\r\n onChange={(e) => setInput(e.target.value)}\r\n onKeyDown={(e) => e.key === 'Enter' && addTodo()}\r\n placeholder=\"Add a task...\"\r\n style={{ flex: 1, padding: 8 }}\r\n />\r\n <button onClick={addTodo}>Add</button>\r\n </div>\r\n\r\n <ul style={{ listStyle: 'none', padding: 0 }}>\r\n {todos.map((todo, i) => (\r\n <li key={todo.id} style={{ display: 'flex', gap: 8, padding: '8px 0', borderBottom: '1px solid #eee' }}>\r\n <input type=\"checkbox\" checked={todo.done} onChange={() => toggleTodo(i)} />\r\n <span style={{ flex: 1, textDecoration: todo.done ? 'line-through' : 'none', color: todo.done ? '#aaa' : '#000' }}>\r\n {todo.text}\r\n </span>\r\n <button onClick={() => deleteTodo(i)} style={{ color: 'red', border: 'none', background: 'none', cursor: 'pointer' }}>x</button>\r\n </li>\r\n ))}\r\n </ul>\r\n\r\n {todos.length === 0 && <p style={{ textAlign: 'center', color: '#aaa' }}>No tasks yet</p>}\r\n </div>\r\n );\r\n}\r\n`,\r\n },\r\n\r\n whiteboard: {\r\n description: 'Multiplayer drawing canvas with presence cursors',\r\n appTsx: `import { useRef, useEffect, useState, useCallback } from 'react';\r\nimport { useSharedArray, usePresence, useBroadcast } from '@ysdk/react';\r\n\r\ninterface Point { x: number; y: number }\r\ninterface Stroke { points: Point[]; color: string }\r\n\r\nexport default function App() {\r\n const canvasRef = useRef<HTMLCanvasElement>(null);\r\n const [strokes, ops] = useSharedArray<Stroke>('strokes');\r\n const [drawing, setDrawing] = useState(false);\r\n const [color, setColor] = useState('#000000');\r\n const currentStroke = useRef<Point[]>([]);\r\n const { peers, setPresence } = usePresence<{ name: string; cursor?: Point }>({ name: 'User' });\r\n const { broadcast, onMessage } = useBroadcast<{ cursor: Point }>('cursor');\r\n\r\n // Draw all strokes\r\n useEffect(() => {\r\n const canvas = canvasRef.current;\r\n if (!canvas) return;\r\n const ctx = canvas.getContext('2d')!;\r\n ctx.clearRect(0, 0, canvas.width, canvas.height);\r\n\r\n for (const stroke of strokes) {\r\n if (stroke.points.length < 2) continue;\r\n ctx.beginPath();\r\n ctx.strokeStyle = stroke.color;\r\n ctx.lineWidth = 3;\r\n ctx.lineCap = 'round';\r\n ctx.lineJoin = 'round';\r\n ctx.moveTo(stroke.points[0].x, stroke.points[0].y);\r\n for (let i = 1; i < stroke.points.length; i++) {\r\n ctx.lineTo(stroke.points[i].x, stroke.points[i].y);\r\n }\r\n ctx.stroke();\r\n }\r\n }, [strokes]);\r\n\r\n const getPos = (e: React.MouseEvent): Point => {\r\n const rect = canvasRef.current!.getBoundingClientRect();\r\n return { x: e.clientX - rect.left, y: e.clientY - rect.top };\r\n };\r\n\r\n return (\r\n <div style={{ fontFamily: 'system-ui', maxWidth: 640, margin: '0 auto', padding: 40 }}>\r\n <h1>Shared Whiteboard</h1>\r\n <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>\r\n <input type=\"color\" value={color} onChange={(e) => setColor(e.target.value)} />\r\n <button onClick={() => ops.clear()}>Clear</button>\r\n <span style={{ color: '#888', marginLeft: 'auto' }}>{peers.length + 1} connected</span>\r\n </div>\r\n <canvas\r\n ref={canvasRef}\r\n width={600}\r\n height={400}\r\n style={{ border: '1px solid #ccc', cursor: 'crosshair' }}\r\n onMouseDown={(e) => {\r\n setDrawing(true);\r\n currentStroke.current = [getPos(e)];\r\n }}\r\n onMouseMove={(e) => {\r\n const pos = getPos(e);\r\n broadcast({ cursor: pos });\r\n if (!drawing) return;\r\n currentStroke.current.push(pos);\r\n // Draw locally for smooth feel\r\n const ctx = canvasRef.current!.getContext('2d')!;\r\n const pts = currentStroke.current;\r\n if (pts.length >= 2) {\r\n ctx.beginPath();\r\n ctx.strokeStyle = color;\r\n ctx.lineWidth = 3;\r\n ctx.lineCap = 'round';\r\n ctx.moveTo(pts[pts.length - 2].x, pts[pts.length - 2].y);\r\n ctx.lineTo(pts[pts.length - 1].x, pts[pts.length - 1].y);\r\n ctx.stroke();\r\n }\r\n }}\r\n onMouseUp={() => {\r\n if (currentStroke.current.length > 1) {\r\n ops.push({ points: currentStroke.current, color });\r\n }\r\n setDrawing(false);\r\n currentStroke.current = [];\r\n }}\r\n />\r\n </div>\r\n );\r\n}\r\n`,\r\n },\r\n\r\n chat: {\r\n description: 'Real-time chat room with presence',\r\n appTsx: `import { useState, useRef, useEffect } from 'react';\r\nimport { useSharedArray, usePresence } from '@ysdk/react';\r\n\r\ninterface Message {\r\n id: string;\r\n user: string;\r\n text: string;\r\n time: number;\r\n}\r\n\r\nconst COLORS = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c'];\r\n\r\nexport default function App() {\r\n const [messages, ops] = useSharedArray<Message>('messages');\r\n const [input, setInput] = useState('');\r\n const [name, setName] = useState(() => 'User-' + Math.random().toString(36).slice(2, 5));\r\n const { peers, setPresence, count } = usePresence<{ name: string; typing?: boolean }>({ name });\r\n const bottomRef = useRef<HTMLDivElement>(null);\r\n\r\n useEffect(() => {\r\n bottomRef.current?.scrollIntoView({ behavior: 'smooth' });\r\n }, [messages.length]);\r\n\r\n const send = () => {\r\n if (!input.trim()) return;\r\n ops.push({ id: crypto.randomUUID(), user: name, text: input.trim(), time: Date.now() });\r\n setInput('');\r\n setPresence({ typing: false });\r\n };\r\n\r\n const colorFor = (user: string) => COLORS[Math.abs([...user].reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) % COLORS.length];\r\n const typingPeers = peers.filter(p => p.typing).map(p => p.name);\r\n\r\n return (\r\n <div style={{ fontFamily: 'system-ui', maxWidth: 480, margin: '0 auto', padding: 40, display: 'flex', flexDirection: 'column', height: '100vh' }}>\r\n <div style={{ display: 'flex', gap: 8, marginBottom: 16, alignItems: 'center' }}>\r\n <h1 style={{ margin: 0, flex: 1 }}>Chat Room</h1>\r\n <input\r\n value={name}\r\n onChange={(e) => { setName(e.target.value); setPresence({ name: e.target.value }); }}\r\n style={{ width: 100, padding: 4 }}\r\n placeholder=\"Your name\"\r\n />\r\n <span style={{ color: '#888' }}>{count + 1} online</span>\r\n </div>\r\n\r\n <div style={{ flex: 1, overflowY: 'auto', border: '1px solid #eee', borderRadius: 8, padding: 12 }}>\r\n {messages.map((msg) => (\r\n <div key={msg.id} style={{ marginBottom: 8 }}>\r\n <strong style={{ color: colorFor(msg.user) }}>{msg.user}</strong>\r\n <span style={{ color: '#aaa', fontSize: 12, marginLeft: 8 }}>{new Date(msg.time).toLocaleTimeString()}</span>\r\n <div>{msg.text}</div>\r\n </div>\r\n ))}\r\n <div ref={bottomRef} />\r\n </div>\r\n\r\n {typingPeers.length > 0 && (\r\n <div style={{ color: '#888', fontSize: 12, padding: 4 }}>\r\n {typingPeers.join(', ')} {typingPeers.length === 1 ? 'is' : 'are'} typing...\r\n </div>\r\n )}\r\n\r\n <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>\r\n <input\r\n value={input}\r\n onChange={(e) => { setInput(e.target.value); setPresence({ typing: e.target.value.length > 0 }); }}\r\n onKeyDown={(e) => e.key === 'Enter' && send()}\r\n placeholder=\"Type a message...\"\r\n style={{ flex: 1, padding: 8 }}\r\n />\r\n <button onClick={send}>Send</button>\r\n </div>\r\n </div>\r\n );\r\n}\r\n`,\r\n },\r\n};\r\n\r\nexport function getTemplateNames(): string[] {\r\n return Object.keys(TEMPLATES);\r\n}\r\n","// Generates CLAUDE.md for the scaffolded project\r\n\r\nexport function generateClaudeMd(projectName: string, room: string): string {\r\n return `# CLAUDE.md - ${projectName}\r\n\r\nThis is a collaborative React app powered by YSDK. You (Claude) are connected to the same multiplayer room as the browser via MCP tools.\r\n\r\n## How It Works\r\n\r\nThe React app and Claude share the same Yjs CRDT document. When you call \\`shared_set\\`, the React app's \\`useShared\\` hook updates instantly. When the user changes state in the browser, you can read it with \\`shared_get\\`.\r\n\r\n**Room:** \\`${room}\\`\r\n\r\n---\r\n\r\n## MCP Tools Reference\r\n\r\n### Shared State (Y.Map)\r\nMaps to \\`useShared(key, defaultValue)\\` in React.\r\n\r\n| Tool | Description |\r\n|------|-------------|\r\n| \\`shared_get(key)\\` | Read a value. Returns the current value or \"undefined\". |\r\n| \\`shared_set(key, value)\\` | Write a value. All connected clients update instantly. Value can be any JSON type. |\r\n| \\`shared_delete(key)\\` | Remove a key from shared state. |\r\n| \\`shared_keys()\\` | List all keys in shared state. |\r\n\r\n**Example:**\r\n\\`\\`\\`\r\nshared_set(key: \"count\", value: 42) -> useShared('count', 0) returns 42\r\nshared_get(key: \"count\") -> \"42\"\r\n\\`\\`\\`\r\n\r\n**Important:** Objects are stored as opaque values (last-write-wins on the whole object). If two users set the same key simultaneously, one write wins. Use shared arrays for concurrent insertions.\r\n\r\n### Shared Arrays (Y.Array)\r\nMaps to \\`useSharedArray(key)\\` in React. Concurrent insertions merge cleanly (CRDT).\r\n\r\n| Tool | Description |\r\n|------|-------------|\r\n| \\`array_get(key)\\` | Read all items in the array. |\r\n| \\`array_push(key, items)\\` | Append items to the end. \\`items\\` is a JSON array. |\r\n| \\`array_insert(key, index, items)\\` | Insert items at a position. |\r\n| \\`array_delete(key, index, count?)\\` | Delete items starting at index. Count defaults to 1. |\r\n| \\`array_clear(key)\\` | Remove all items from the array. |\r\n\r\n**Example:**\r\n\\`\\`\\`\r\narray_push(key: \"todos\", items: [{\"id\": \"abc\", \"text\": \"Buy milk\", \"done\": false}])\r\narray_get(key: \"todos\") -> [{\"id\": \"abc\", \"text\": \"Buy milk\", \"done\": false}]\r\n\\`\\`\\`\r\n\r\n### Shared Text (Y.Text)\r\nMaps to \\`useSharedText(key, defaultValue)\\` in React. Character-by-character CRDT merge.\r\n\r\n| Tool | Description |\r\n|------|-------------|\r\n| \\`text_get(key)\\` | Read the text content. |\r\n| \\`text_set(key, value)\\` | Replace the entire text. |\r\n| \\`text_insert(key, index, content)\\` | Insert text at a character position. |\r\n| \\`text_delete(key, index, length)\\` | Delete characters starting at position. |\r\n\r\n### Presence & Broadcast\r\n\r\n| Tool | Description |\r\n|------|-------------|\r\n| \\`presence_list()\\` | List all connected users and their state. |\r\n| \\`broadcast(channel, data)\\` | Send ephemeral message to all clients. Received via \\`useBroadcast(channel)\\`. Not persisted. |\r\n\r\n### Utility\r\n\r\n| Tool | Description |\r\n|------|-------------|\r\n| \\`room_info()\\` | Connection status, sync status, client count, document size. |\r\n| \\`document_snapshot()\\` | Full dump of all shared data (maps, arrays, texts). |\r\n\r\n---\r\n\r\n## React Hooks Reference\r\n\r\n\\`\\`\\`tsx\r\nimport { YSDKProvider, useShared, useSharedArray, useSharedText, usePresence, useBroadcast } from '@ysdk/react';\r\n\r\n// Shared state (like useState but multiplayer)\r\nconst [count, setCount] = useShared('count', 0);\r\n\r\n// Shared array (concurrent insertions merge)\r\nconst [items, ops] = useSharedArray<Item>('items');\r\nops.push(item);\r\nops.delete(index, count?);\r\nops.insert(index, item);\r\nops.clear();\r\n\r\n// Shared text (character-level CRDT)\r\nconst { value, setValue, ytext } = useSharedText('doc', 'initial');\r\n\r\n// Presence (who's online + ephemeral state)\r\nconst { peers, setPresence, count } = usePresence<{ name: string }>({ name: 'User' });\r\n\r\n// Broadcast (fire-and-forget messages)\r\nconst { broadcast, onMessage } = useBroadcast<T>('channel');\r\nbroadcast(data);\r\nonMessage((data, senderId) => { ... });\r\n\\`\\`\\`\r\n\r\n---\r\n\r\n## How MCP Maps to Hooks\r\n\r\n| MCP Tool | React Hook | What Happens |\r\n|----------|------------|-------------|\r\n| \\`shared_set(key, val)\\` | \\`useShared(key)\\` | Hook re-renders with new value |\r\n| \\`array_push(key, items)\\` | \\`useSharedArray(key)\\` | Hook re-renders with new array |\r\n| \\`text_set(key, val)\\` | \\`useSharedText(key)\\` | Hook re-renders with new text |\r\n| \\`broadcast(ch, data)\\` | \\`useBroadcast(ch)\\` | \\`onMessage\\` callback fires |\r\n| \\`presence_list()\\` | \\`usePresence()\\` | Shows all peers including Claude |\r\n\r\n---\r\n\r\n## Common Patterns\r\n\r\n### Counter\r\n\\`\\`\\`\r\nshared_get(key: \"count\") # Read current value\r\nshared_set(key: \"count\", value: 5) # Set to 5\r\n\\`\\`\\`\r\n\r\n### Todo List\r\n\\`\\`\\`\r\narray_get(key: \"todos\") # Read all todos\r\narray_push(key: \"todos\", items: [{\"id\": \"...\", \"text\": \"New task\", \"done\": false}])\r\narray_delete(key: \"todos\", index: 2) # Delete 3rd item\r\n\\`\\`\\`\r\n\r\n### Collaborative Text\r\n\\`\\`\\`\r\ntext_get(key: \"document\") # Read full text\r\ntext_set(key: \"document\", value: \"Hello World\") # Replace\r\ntext_insert(key: \"document\", index: 5, content: \",\") # Insert comma\r\n\\`\\`\\`\r\n\r\n---\r\n\r\n## Rules\r\n\r\n1. **React 18+ required** - hooks use modern React patterns\r\n2. **All hooks must be inside \\`<YSDKProvider>\\`** - this provides the Yjs document context\r\n3. **Objects in shared state are LWW** (last-write-wins) - use arrays for concurrent insertions\r\n4. **Presence is ephemeral** - disappears when user disconnects\r\n5. **Broadcast is fire-and-forget** - no persistence, no delivery guarantee\r\n\r\n## Verifying Changes\r\n\r\nAfter making changes via MCP tools, you can verify:\r\n1. \\`shared_get\\` / \\`array_get\\` / \\`text_get\\` - read back what you wrote\r\n2. \\`document_snapshot\\` - see ALL shared data at once\r\n3. \\`presence_list\\` - confirm you're connected (look for \"Claude (MCP)\")\r\n`;\r\n}\r\n","// Generates .mcp.json for the scaffolded project\r\n\r\nexport interface McpJsonOptions {\r\n room: string;\r\n apiKey?: string;\r\n}\r\n\r\nexport function generateMcpJson(opts: McpJsonOptions): string {\r\n const env: Record<string, string> = {\r\n YSDK_ROOM: opts.room,\r\n };\r\n\r\n if (opts.apiKey) {\r\n env.YSDK_API_KEY = opts.apiKey;\r\n }\r\n\r\n const config = {\r\n mcpServers: {\r\n ysdk: {\r\n command: 'npx',\r\n args: ['@ysdk/mcp'],\r\n env,\r\n },\r\n },\r\n };\r\n\r\n return JSON.stringify(config, null, 2) + '\\n';\r\n}\r\n"],"mappings":"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","names":["path","fs","projectName","template","apiKey","tpl"]}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@ysdk/create",
3
+ "version": "0.1.0",
4
+ "description": "Create a new YSDK collaborative app with Claude integration.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-ysdk": "./dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch"
16
+ },
17
+ "devDependencies": {
18
+ "tsup": "^8.0.0",
19
+ "typescript": "^5.0.0"
20
+ },
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/aoatkinson/ysdk"
25
+ },
26
+ "keywords": [
27
+ "yjs",
28
+ "crdt",
29
+ "create",
30
+ "scaffold",
31
+ "collaborative",
32
+ "multiplayer"
33
+ ]
34
+ }