add-velt 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/bin/velt.js ADDED
@@ -0,0 +1,1664 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ // Enhanced package manager detection (checks parent directories for workspaces)
8
+ function detectPackageManager(cwd) {
9
+ // Check current directory first
10
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
11
+ if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
12
+ if (fs.existsSync(path.join(cwd, 'package-lock.json'))) return 'npm';
13
+
14
+ // Check parent directories for workspace lockfiles
15
+ let dir = cwd;
16
+ const root = path.parse(dir).root;
17
+ while (dir !== root) {
18
+ dir = path.dirname(dir);
19
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
20
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
21
+ if (fs.existsSync(path.join(dir, 'package-lock.json'))) return 'npm';
22
+ const pkgPath = path.join(dir, 'package.json');
23
+ if (fs.existsSync(pkgPath)) {
24
+ try {
25
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
26
+ if (pkg.workspaces) break;
27
+ } catch {}
28
+ }
29
+ }
30
+ return 'npm';
31
+ }
32
+
33
+ // ============================================================================
34
+ // DEPENDENCY DEFINITIONS (derived from sample apps)
35
+ // ============================================================================
36
+
37
+ const DEPS = {
38
+ // Baseline (always) - use v4.x.x versions
39
+ baseline: {
40
+ production: { '@veltdev/react': '^4.0.0' },
41
+ development: { '@veltdev/types': '^4.0.0' }
42
+ },
43
+ // Comments: no extra deps beyond baseline
44
+ comments: {
45
+ production: {},
46
+ development: {}
47
+ },
48
+ // Notifications: no extra deps beyond baseline
49
+ notifications: {
50
+ production: {},
51
+ development: {}
52
+ },
53
+ // ReactFlow CRDT - use v4.x.x versions
54
+ 'reactflow-crdt': {
55
+ production: {
56
+ '@veltdev/reactflow-crdt': '^4.0.0',
57
+ 'yjs': 'latest'
58
+ },
59
+ development: {}
60
+ },
61
+ // Tiptap CRDT - use v4.x.x versions
62
+ 'tiptap-crdt': {
63
+ production: {
64
+ '@veltdev/tiptap-crdt': '^4.0.0',
65
+ '@veltdev/tiptap-crdt-react': '^4.0.0',
66
+ '@veltdev/tiptap-velt-comments': '^4.0.0',
67
+ 'yjs': 'latest',
68
+ 'y-prosemirror': 'latest'
69
+ },
70
+ development: {}
71
+ },
72
+ // CodeMirror CRDT - use v4.x.x versions
73
+ 'codemirror-crdt': {
74
+ production: {
75
+ '@veltdev/codemirror-crdt': '^4.0.0',
76
+ '@veltdev/codemirror-crdt-react': '^4.0.0',
77
+ 'yjs': 'latest',
78
+ 'y-codemirror.next': 'latest'
79
+ },
80
+ development: {}
81
+ }
82
+ };
83
+
84
+ // ============================================================================
85
+ // PROJECT ANALYSIS
86
+ // ============================================================================
87
+
88
+ function analyzeProject(cwd) {
89
+ const analysis = {
90
+ hasNextConfig: fs.existsSync(path.join(cwd, 'next.config.js')) || fs.existsSync(path.join(cwd, 'next.config.ts')),
91
+ hasAppRouter: fs.existsSync(path.join(cwd, 'app')),
92
+ hasPagesRouter: fs.existsSync(path.join(cwd, 'pages')),
93
+ hasComponents: fs.existsSync(path.join(cwd, 'components')),
94
+ packageManager: detectPackageManager(cwd),
95
+ existingDeps: {}
96
+ };
97
+
98
+ const pkg = readPkg(cwd);
99
+ if (pkg) {
100
+ analysis.existingDeps = { ...pkg.dependencies, ...pkg.devDependencies };
101
+ }
102
+
103
+ return analysis;
104
+ }
105
+
106
+ // ============================================================================
107
+ // FEATURE PARSING
108
+ // ============================================================================
109
+
110
+ function parseFeatures(args) {
111
+ const features = {
112
+ comments: false,
113
+ notifications: false,
114
+ presence: false,
115
+ cursors: false,
116
+ crdt: 'none' // 'none' | 'reactflow' | 'tiptap' | 'codemirror'
117
+ };
118
+
119
+ // Check for CRDT type flags
120
+ const crdtFlags = [];
121
+ if (args['reactflow-crdt']) crdtFlags.push('reactflow');
122
+ if (args['tiptap-crdt']) crdtFlags.push('tiptap');
123
+ if (args['codemirror-crdt']) crdtFlags.push('codemirror');
124
+
125
+ // Enforce only one CRDT type
126
+ if (crdtFlags.length > 1) {
127
+ console.error('Error: Only one CRDT type flag can be specified at a time.');
128
+ console.error('Choose one of: --reactflow-crdt, --tiptap-crdt, --codemirror-crdt');
129
+ process.exit(1);
130
+ }
131
+
132
+ if (crdtFlags.length === 1) {
133
+ features.crdt = crdtFlags[0];
134
+ }
135
+
136
+ // Handle --all flag
137
+ if (args.all) {
138
+ if (features.crdt === 'none') {
139
+ console.error('Error: --all requires a CRDT type flag.');
140
+ console.error('Usage: npx add-velt --all --reactflow-crdt');
141
+ console.error(' npx add-velt --all --tiptap-crdt');
142
+ console.error(' npx add-velt --all --codemirror-crdt');
143
+ process.exit(1);
144
+ }
145
+ features.comments = true;
146
+ features.notifications = true;
147
+ features.presence = true;
148
+ features.cursors = true;
149
+ } else {
150
+ features.comments = !!args.comments;
151
+ features.notifications = !!args.notifications;
152
+ features.presence = !!args.presence;
153
+ features.cursors = !!args.cursors;
154
+ }
155
+
156
+ return features;
157
+ }
158
+
159
+ function hasAnyFeature(features) {
160
+ return features.comments || features.notifications || features.presence || features.cursors || features.crdt !== 'none';
161
+ }
162
+
163
+ // ============================================================================
164
+ // DEPENDENCY RESOLUTION
165
+ // ============================================================================
166
+
167
+ function resolveDependencies(analysis, features) {
168
+ const deps = {
169
+ production: [],
170
+ development: []
171
+ };
172
+
173
+ // Helper: format package for install (use pkg@latest or just pkg for latest)
174
+ const formatPkg = (pkg, version) => {
175
+ // For 'latest', just use the package name without version to let npm resolve
176
+ if (version === 'latest') {
177
+ return pkg;
178
+ }
179
+ return `${pkg}@${version}`;
180
+ };
181
+
182
+ // Always include baseline
183
+ for (const [pkg, version] of Object.entries(DEPS.baseline.production)) {
184
+ if (!analysis.existingDeps[pkg]) {
185
+ deps.production.push(formatPkg(pkg, version));
186
+ }
187
+ }
188
+ for (const [pkg, version] of Object.entries(DEPS.baseline.development)) {
189
+ if (!analysis.existingDeps[pkg]) {
190
+ deps.development.push(formatPkg(pkg, version));
191
+ }
192
+ }
193
+
194
+ // Comments deps (none extra beyond baseline)
195
+ if (features.comments) {
196
+ for (const [pkg, version] of Object.entries(DEPS.comments.production)) {
197
+ if (!analysis.existingDeps[pkg]) {
198
+ deps.production.push(formatPkg(pkg, version));
199
+ }
200
+ }
201
+ }
202
+
203
+ // Notifications deps (none extra beyond baseline)
204
+ if (features.notifications) {
205
+ for (const [pkg, version] of Object.entries(DEPS.notifications.production)) {
206
+ if (!analysis.existingDeps[pkg]) {
207
+ deps.production.push(formatPkg(pkg, version));
208
+ }
209
+ }
210
+ }
211
+
212
+ // CRDT type deps
213
+ if (features.crdt !== 'none') {
214
+ const crdtKey = `${features.crdt}-crdt`;
215
+ for (const [pkg, version] of Object.entries(DEPS[crdtKey].production)) {
216
+ if (!analysis.existingDeps[pkg]) {
217
+ deps.production.push(formatPkg(pkg, version));
218
+ }
219
+ }
220
+ for (const [pkg, version] of Object.entries(DEPS[crdtKey].development)) {
221
+ if (!analysis.existingDeps[pkg]) {
222
+ deps.development.push(formatPkg(pkg, version));
223
+ }
224
+ }
225
+ }
226
+
227
+ return deps;
228
+ }
229
+
230
+ // ============================================================================
231
+ // SMART INSTALL
232
+ // ============================================================================
233
+
234
+ function smartInstall(pm, deps, isDev, cwd, flags) {
235
+ if (!deps.length) return;
236
+
237
+ const installStrategies = [
238
+ { args: [], description: 'standard install' },
239
+ { args: ['--force'], description: 'with --force flag' },
240
+ ...(pm === 'npm' ? [{ args: ['--legacy-peer-deps'], description: 'with --legacy-peer-deps' }] : []),
241
+ ...(pm === 'npm' ? [{ args: ['--force', '--legacy-peer-deps'], description: 'with --force --legacy-peer-deps' }] : [])
242
+ ];
243
+
244
+ for (const strategy of installStrategies) {
245
+ try {
246
+ console.log(`Attempting ${strategy.description}...`);
247
+
248
+ let cmd;
249
+ const extraArgs = [...strategy.args];
250
+ if (flags.force && !extraArgs.includes('--force')) extraArgs.push('--force');
251
+ if (flags['legacy-peer-deps'] && pm === 'npm' && !extraArgs.includes('--legacy-peer-deps')) {
252
+ extraArgs.push('--legacy-peer-deps');
253
+ }
254
+
255
+ switch (pm) {
256
+ case 'pnpm':
257
+ cmd = `pnpm add ${isDev ? '-D' : ''} ${deps.join(' ')} ${extraArgs.join(' ')}`.trim();
258
+ break;
259
+ case 'yarn':
260
+ cmd = `yarn add ${isDev ? '-D' : ''} ${deps.join(' ')} ${extraArgs.filter(a => a !== '--legacy-peer-deps').join(' ')}`.trim();
261
+ break;
262
+ default:
263
+ cmd = `npm install ${isDev ? '--save-dev' : ''} ${deps.join(' ')} ${extraArgs.join(' ')}`.trim();
264
+ }
265
+
266
+ console.log(`> ${cmd}`);
267
+ execSync(cmd, { stdio: 'inherit', cwd });
268
+ console.log(`Success with ${strategy.description}`);
269
+ return;
270
+ } catch (error) {
271
+ console.warn(`Failed with ${strategy.description}`);
272
+ if (strategy === installStrategies[installStrategies.length - 1]) {
273
+ throw new Error(`All installation strategies failed. Last error: ${error.message}`);
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ // ============================================================================
280
+ // FILE TEMPLATES
281
+ // ============================================================================
282
+
283
+ // CORE: VeltInitializeDocument.tsx (same for all)
284
+ const TEMPLATE_VELT_INITIALIZE_DOCUMENT = `import { useEffect } from 'react';
285
+ import { useSetDocuments } from '@veltdev/react';
286
+
287
+ // [Velt] Initialize Velt document
288
+ export default function VeltInitializeDocument() {
289
+ // TODO: Replace with your own document ID source
290
+ const documentId = 'your-document-id';
291
+ const documentName = 'your-document-name';
292
+
293
+ const { setDocuments } = useSetDocuments();
294
+
295
+ useEffect(() => {
296
+ if (!documentId) return;
297
+ setDocuments([
298
+ { id: documentId, metadata: { documentName } },
299
+ ]);
300
+ }, [setDocuments, documentId]);
301
+
302
+ return null;
303
+ }
304
+ `;
305
+
306
+ // CORE: VeltInitializeUser.tsx (same for all)
307
+ const TEMPLATE_VELT_INITIALIZE_USER = `"use client";
308
+
309
+ import type { VeltAuthProvider } from "@veltdev/types";
310
+ import { useMemo } from "react";
311
+
312
+ // [Velt] User authentication hook
313
+ // TODO: Replace with your own user retrieval logic
314
+ export function useVeltAuthProvider() {
315
+ const user = {
316
+ userId: 'user-id',
317
+ organizationId: 'organization-id',
318
+ email: 'user@example.com',
319
+ };
320
+
321
+ const authProvider: VeltAuthProvider | undefined = useMemo(() => {
322
+ if (!user) return undefined;
323
+ return {
324
+ user,
325
+ retryConfig: { retryCount: 3, retryDelay: 1000 },
326
+ // TODO: Add generateToken for JWT auth
327
+ };
328
+ }, [user]);
329
+
330
+ return { authProvider };
331
+ }
332
+ `;
333
+
334
+ // CORE ONLY: VeltCollaboration.tsx (minimal)
335
+ const TEMPLATE_VELT_COLLABORATION_CORE = `import VeltInitializeDocument from './VeltInitializeDocument';
336
+
337
+ export function VeltCollaboration() {
338
+ return (
339
+ <>
340
+ <VeltInitializeDocument />
341
+ </>
342
+ );
343
+ }
344
+
345
+ export default VeltCollaboration;
346
+ `;
347
+
348
+ // COMMENTS ONLY: VeltCollaboration.tsx
349
+ const TEMPLATE_VELT_COLLABORATION_COMMENTS = `"use client";
350
+ import { VeltComments, VeltCommentsSidebar } from "@veltdev/react";
351
+ import VeltInitializeDocument from "./VeltInitializeDocument";
352
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
353
+
354
+ export function VeltCollaboration() {
355
+ return (
356
+ <>
357
+ <VeltInitializeDocument />
358
+ <VeltComments
359
+ popoverMode={true}
360
+ shadowDom={false}
361
+ textMode={false}
362
+ commentPinHighlighter={false}
363
+ dialogOnHover={false}
364
+ />
365
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
366
+ <VeltCustomization />
367
+ </>
368
+ );
369
+ }
370
+
371
+ export default VeltCollaboration;
372
+ `;
373
+
374
+ // NOTIFICATIONS ONLY: VeltCollaboration.tsx
375
+ const TEMPLATE_VELT_COLLABORATION_NOTIFICATIONS = `"use client";
376
+ import VeltInitializeDocument from "./VeltInitializeDocument";
377
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
378
+
379
+ export function VeltCollaboration() {
380
+ return (
381
+ <>
382
+ <VeltInitializeDocument />
383
+ <VeltCustomization />
384
+ </>
385
+ );
386
+ }
387
+
388
+ export default VeltCollaboration;
389
+ `;
390
+
391
+ // COMMENTS + NOTIFICATIONS: VeltCollaboration.tsx
392
+ const TEMPLATE_VELT_COLLABORATION_COMMENTS_NOTIFICATIONS = `"use client";
393
+ import { VeltComments, VeltCommentsSidebar } from "@veltdev/react";
394
+ import VeltInitializeDocument from "./VeltInitializeDocument";
395
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
396
+
397
+ export function VeltCollaboration() {
398
+ return (
399
+ <>
400
+ <VeltInitializeDocument />
401
+ <VeltComments
402
+ popoverMode={true}
403
+ shadowDom={false}
404
+ textMode={false}
405
+ commentPinHighlighter={false}
406
+ dialogOnHover={false}
407
+ />
408
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
409
+ <VeltCustomization />
410
+ </>
411
+ );
412
+ }
413
+
414
+ export default VeltCollaboration;
415
+ `;
416
+
417
+ // REACTFLOW CRDT: VeltCollaboration.tsx
418
+ const TEMPLATE_VELT_COLLABORATION_REACTFLOW = `"use client";
419
+ import { VeltCursor, VeltComments, VeltCommentsSidebar } from "@veltdev/react";
420
+ import VeltInitializeDocument from "./VeltInitializeDocument";
421
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
422
+
423
+ export function VeltCollaboration() {
424
+ return (
425
+ <>
426
+ <VeltInitializeDocument />
427
+ <VeltComments
428
+ popoverMode={true}
429
+ textMode={false}
430
+ commentPinHighlighter={false}
431
+ dialogOnHover={false}
432
+ popoverTriangleComponent={false}
433
+ />
434
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
435
+ <VeltCursor />
436
+ <VeltCustomization />
437
+ </>
438
+ );
439
+ }
440
+
441
+ export default VeltCollaboration;
442
+ `;
443
+
444
+ // TIPTAP CRDT: VeltCollaboration.tsx (includes VeltCursor)
445
+ const TEMPLATE_VELT_COLLABORATION_TIPTAP = `"use client";
446
+ import { VeltComments, VeltCommentsSidebar, VeltCursor } from "@veltdev/react";
447
+ import VeltInitializeDocument from "./VeltInitializeDocument";
448
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
449
+
450
+ export function VeltCollaboration() {
451
+ return (
452
+ <>
453
+ <VeltInitializeDocument />
454
+ <VeltComments
455
+ shadowDom={false}
456
+ textMode={false}
457
+ />
458
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
459
+ <VeltCursor />
460
+ <VeltCustomization />
461
+ </>
462
+ );
463
+ }
464
+
465
+ export default VeltCollaboration;
466
+ `;
467
+
468
+ // CODEMIRROR CRDT: VeltCollaboration.tsx (includes VeltCursor)
469
+ const TEMPLATE_VELT_COLLABORATION_CODEMIRROR = `"use client";
470
+ import { VeltComments, VeltCommentsSidebar, VeltCursor } from "@veltdev/react";
471
+ import VeltInitializeDocument from "./VeltInitializeDocument";
472
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
473
+
474
+ export function VeltCollaboration() {
475
+ return (
476
+ <>
477
+ <VeltInitializeDocument />
478
+ <VeltComments textMode={false} />
479
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
480
+ <VeltCursor />
481
+ <VeltCustomization />
482
+ </>
483
+ );
484
+ }
485
+
486
+ export default VeltCollaboration;
487
+ `;
488
+
489
+ // ALL (comments + notifications + CRDT): VeltCollaboration variants
490
+ const TEMPLATE_VELT_COLLABORATION_ALL_REACTFLOW = `"use client";
491
+ import { VeltCursor, VeltComments, VeltCommentsSidebar } from "@veltdev/react";
492
+ import VeltInitializeDocument from "./VeltInitializeDocument";
493
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
494
+
495
+ export function VeltCollaboration() {
496
+ return (
497
+ <>
498
+ <VeltInitializeDocument />
499
+ <VeltComments
500
+ popoverMode={true}
501
+ shadowDom={false}
502
+ textMode={false}
503
+ commentPinHighlighter={false}
504
+ dialogOnHover={false}
505
+ popoverTriangleComponent={false}
506
+ />
507
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
508
+ <VeltCursor />
509
+ <VeltCustomization />
510
+ </>
511
+ );
512
+ }
513
+
514
+ export default VeltCollaboration;
515
+ `;
516
+
517
+ const TEMPLATE_VELT_COLLABORATION_ALL_TIPTAP = `"use client";
518
+ import { VeltComments, VeltCommentsSidebar, VeltCursor } from "@veltdev/react";
519
+ import VeltInitializeDocument from "./VeltInitializeDocument";
520
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
521
+
522
+ export function VeltCollaboration() {
523
+ return (
524
+ <>
525
+ <VeltInitializeDocument />
526
+ <VeltComments
527
+ shadowDom={false}
528
+ textMode={false}
529
+ />
530
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
531
+ <VeltCursor />
532
+ <VeltCustomization />
533
+ </>
534
+ );
535
+ }
536
+
537
+ export default VeltCollaboration;
538
+ `;
539
+
540
+ const TEMPLATE_VELT_COLLABORATION_ALL_CODEMIRROR = `"use client";
541
+ import { VeltComments, VeltCommentsSidebar, VeltCursor } from "@veltdev/react";
542
+ import VeltInitializeDocument from "./VeltInitializeDocument";
543
+ import { VeltCustomization } from "./ui-customization/VeltCustomization";
544
+
545
+ export function VeltCollaboration() {
546
+ return (
547
+ <>
548
+ <VeltInitializeDocument />
549
+ <VeltComments textMode={false} />
550
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
551
+ <VeltCursor />
552
+ <VeltCustomization />
553
+ </>
554
+ );
555
+ }
556
+
557
+ export default VeltCollaboration;
558
+ `;
559
+
560
+ // VeltTools: COMMENTS ONLY (no notifications)
561
+ const TEMPLATE_VELT_TOOLS_COMMENTS = `"use client";
562
+ import { VeltPresence, VeltSidebarButton } from "@veltdev/react";
563
+
564
+ function VeltTools() {
565
+ return (
566
+ <>
567
+ <VeltPresence />
568
+ <VeltSidebarButton />
569
+ </>
570
+ );
571
+ }
572
+
573
+ export default VeltTools;
574
+ `;
575
+
576
+ // VeltTools: NOTIFICATIONS ONLY (no sidebar)
577
+ const TEMPLATE_VELT_TOOLS_NOTIFICATIONS = `"use client";
578
+ import { VeltPresence, VeltNotificationsTool } from "@veltdev/react";
579
+
580
+ function VeltTools() {
581
+ return (
582
+ <>
583
+ <VeltPresence />
584
+ <VeltNotificationsTool
585
+ settings={true}
586
+ shadowDom={false}
587
+ tabConfig={{
588
+ forYou: { name: "For You", enable: true },
589
+ documents: { name: "Documents", enable: true },
590
+ all: { name: "All", enable: true },
591
+ }}
592
+ />
593
+ </>
594
+ );
595
+ }
596
+
597
+ export default VeltTools;
598
+ `;
599
+
600
+ // VeltTools: COMMENTS + NOTIFICATIONS
601
+ const TEMPLATE_VELT_TOOLS_COMMENTS_NOTIFICATIONS = `"use client";
602
+ import { VeltPresence, VeltSidebarButton, VeltNotificationsTool } from "@veltdev/react";
603
+
604
+ function VeltTools() {
605
+ return (
606
+ <>
607
+ <VeltPresence />
608
+ <VeltSidebarButton />
609
+ <VeltNotificationsTool
610
+ settings={true}
611
+ shadowDom={false}
612
+ tabConfig={{
613
+ forYou: { name: "For You", enable: true },
614
+ documents: { name: "Documents", enable: true },
615
+ all: { name: "All", enable: true },
616
+ }}
617
+ />
618
+ </>
619
+ );
620
+ }
621
+
622
+ export default VeltTools;
623
+ `;
624
+
625
+ // VeltTools: CRDT with huddle (for reactflow)
626
+ const TEMPLATE_VELT_TOOLS_CRDT_REACTFLOW = `"use client";
627
+ import { VeltPresence, VeltSidebarButton, VeltNotificationsTool, VeltHuddleTool } from "@veltdev/react";
628
+
629
+ function VeltTools() {
630
+ return (
631
+ <>
632
+ <VeltPresence />
633
+ <VeltSidebarButton />
634
+ <VeltNotificationsTool
635
+ settings={true}
636
+ shadowDom={false}
637
+ tabConfig={{
638
+ forYou: { name: "For You", enable: true },
639
+ documents: { name: "Documents", enable: true },
640
+ all: { name: "All", enable: true },
641
+ }}
642
+ />
643
+ <VeltHuddleTool type='all' />
644
+ </>
645
+ );
646
+ }
647
+
648
+ export default VeltTools;
649
+ `;
650
+
651
+ // VeltTools: CRDT without huddle (for text editors)
652
+ const TEMPLATE_VELT_TOOLS_CRDT_TEXT = `"use client";
653
+ import { VeltPresence, VeltSidebarButton, VeltNotificationsTool } from "@veltdev/react";
654
+
655
+ function VeltTools() {
656
+ return (
657
+ <>
658
+ <VeltPresence />
659
+ <VeltSidebarButton />
660
+ <VeltNotificationsTool
661
+ settings={true}
662
+ shadowDom={false}
663
+ tabConfig={{
664
+ forYou: { name: "For You", enable: true },
665
+ documents: { name: "Documents", enable: true },
666
+ all: { name: "All", enable: true },
667
+ }}
668
+ />
669
+ </>
670
+ );
671
+ }
672
+
673
+ export default VeltTools;
674
+ `;
675
+
676
+ // UI CUSTOMIZATION: Comments only
677
+ const TEMPLATE_VELT_CUSTOMIZATION_COMMENTS = `"use client";
678
+ import { useVeltClient, VeltWireframe } from "@veltdev/react";
679
+ import VeltCommentBubbleWf from "./VeltCommentBubbleWf";
680
+ import VeltCommentToolWf from "./VeltCommentToolWf";
681
+ import "./styles.css";
682
+ import { useEffect } from "react";
683
+
684
+ export function VeltCustomization() {
685
+ const { client } = useVeltClient();
686
+
687
+ useEffect(() => {
688
+ if (client) {
689
+ client.setDarkMode(true);
690
+ }
691
+ }, [client]);
692
+
693
+ return (
694
+ <VeltWireframe>
695
+ <VeltCommentBubbleWf />
696
+ <VeltCommentToolWf />
697
+ </VeltWireframe>
698
+ );
699
+ }
700
+ `;
701
+
702
+ // UI CUSTOMIZATION: Notifications only
703
+ const TEMPLATE_VELT_CUSTOMIZATION_NOTIFICATIONS = `"use client";
704
+ import { useVeltClient, VeltWireframe } from "@veltdev/react";
705
+ import VeltNotificationsToolWf from "./VeltNotificationsToolWf";
706
+ import "./styles.css";
707
+ import { useEffect } from "react";
708
+
709
+ export function VeltCustomization() {
710
+ const { client } = useVeltClient();
711
+
712
+ useEffect(() => {
713
+ if (client) {
714
+ client.setDarkMode(true);
715
+ }
716
+ }, [client]);
717
+
718
+ return (
719
+ <VeltWireframe>
720
+ <VeltNotificationsToolWf />
721
+ </VeltWireframe>
722
+ );
723
+ }
724
+ `;
725
+
726
+ // UI CUSTOMIZATION: Comments + Notifications
727
+ const TEMPLATE_VELT_CUSTOMIZATION_COMMENTS_NOTIFICATIONS = `"use client";
728
+ import { useVeltClient, VeltWireframe } from "@veltdev/react";
729
+ import VeltCommentBubbleWf from "./VeltCommentBubbleWf";
730
+ import VeltCommentToolWf from "./VeltCommentToolWf";
731
+ import VeltNotificationsToolWf from "./VeltNotificationsToolWf";
732
+ import VeltSidebarButtonWf from "./VeltSidebarButtonWf";
733
+ import "./styles.css";
734
+ import { useEffect } from "react";
735
+
736
+ export function VeltCustomization() {
737
+ const { client } = useVeltClient();
738
+
739
+ useEffect(() => {
740
+ if (client) {
741
+ client.setDarkMode(true);
742
+ }
743
+ }, [client]);
744
+
745
+ return (
746
+ <VeltWireframe>
747
+ <VeltCommentBubbleWf />
748
+ <VeltCommentToolWf />
749
+ <VeltNotificationsToolWf />
750
+ <VeltSidebarButtonWf />
751
+ </VeltWireframe>
752
+ );
753
+ }
754
+ `;
755
+
756
+ // UI CUSTOMIZATION: CRDT (all wireframes)
757
+ const TEMPLATE_VELT_CUSTOMIZATION_CRDT = `"use client";
758
+ import { useVeltClient, VeltWireframe } from "@veltdev/react";
759
+ import VeltCommentBubbleWf from "./VeltCommentBubbleWf";
760
+ import VeltCommentToolWf from "./VeltCommentToolWf";
761
+ import VeltNotificationsToolWf from "./VeltNotificationsToolWf";
762
+ import VeltSidebarButtonWf from "./VeltSidebarButtonWf";
763
+ import "./styles.css";
764
+ import { useEffect } from "react";
765
+
766
+ export function VeltCustomization() {
767
+ const { client } = useVeltClient();
768
+
769
+ useEffect(() => {
770
+ if (client) {
771
+ client.setDarkMode(true);
772
+ }
773
+ }, [client]);
774
+
775
+ return (
776
+ <VeltWireframe>
777
+ <VeltCommentBubbleWf />
778
+ <VeltCommentToolWf />
779
+ <VeltNotificationsToolWf />
780
+ <VeltSidebarButtonWf />
781
+ </VeltWireframe>
782
+ );
783
+ }
784
+ `;
785
+
786
+ // Wireframe components
787
+ const TEMPLATE_VELT_COMMENT_BUBBLE_WF = `"use client";
788
+ import { VeltCommentBubbleWireframe, VeltIf } from '@veltdev/react';
789
+
790
+ const VeltCommentBubbleWf = () => {
791
+ return (
792
+ <VeltCommentBubbleWireframe>
793
+ <div style={{ position: 'relative', cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
794
+ <VeltIf condition="{commentAnnotation.comments.length} > 0">
795
+ <div style={{
796
+ background: 'rgba(255, 255, 255, 0.08)',
797
+ borderRadius: '18.667px',
798
+ padding: '4.667px 8px 4.667px 6.222px',
799
+ display: 'flex',
800
+ alignItems: 'center',
801
+ justifyContent: 'center',
802
+ gap: '3.111px',
803
+ height: '28px',
804
+ flexShrink: 0,
805
+ boxSizing: 'border-box'
806
+ }}>
807
+ <div style={{
808
+ width: '15.556px',
809
+ height: '15.556px',
810
+ position: 'relative',
811
+ overflow: 'clip',
812
+ flexShrink: 0
813
+ }}>
814
+ <div style={{
815
+ position: 'absolute',
816
+ left: 'calc(50% + 0.492px)',
817
+ top: '50%',
818
+ transform: 'translate(-50%, -50%)',
819
+ width: '11px',
820
+ height: '11px',
821
+ border: '1.5px solid white',
822
+ borderRadius: '6px 6px 6px 1px',
823
+ boxSizing: 'border-box'
824
+ }} />
825
+ </div>
826
+ <p style={{
827
+ fontFamily: 'Urbanist, sans-serif',
828
+ fontWeight: 700,
829
+ fontSize: '14px',
830
+ lineHeight: '1.3',
831
+ color: 'white',
832
+ margin: 0,
833
+ whiteSpace: 'pre',
834
+ flexShrink: 0
835
+ }}>
836
+ <VeltCommentBubbleWireframe.CommentsCount />
837
+ </p>
838
+ </div>
839
+ </VeltIf>
840
+ <VeltIf condition="{commentAnnotation.comments.length} === 0">
841
+ <div style={{ width: '0px', height: '0px', flexShrink: 0 }} />
842
+ </VeltIf>
843
+ </div>
844
+ </VeltCommentBubbleWireframe>
845
+ );
846
+ };
847
+
848
+ export default VeltCommentBubbleWf;
849
+ `;
850
+
851
+ const TEMPLATE_VELT_COMMENT_TOOL_WF = `"use client";
852
+ import { VeltCommentToolWireframe } from '@veltdev/react';
853
+
854
+ const VeltCommentToolWf = () => {
855
+ return (
856
+ <VeltCommentToolWireframe>
857
+ <div style={{
858
+ background: '#141414',
859
+ border: 'none',
860
+ padding: '4.667px 6.32px 4.667px 6.222px',
861
+ cursor: 'pointer',
862
+ display: 'flex',
863
+ alignItems: 'center',
864
+ justifyContent: 'center',
865
+ width: '28px',
866
+ height: '28px',
867
+ borderRadius: '18.667px',
868
+ boxSizing: 'border-box'
869
+ }}>
870
+ <div style={{
871
+ width: '15.556px',
872
+ height: '15.556px',
873
+ position: 'relative',
874
+ overflow: 'clip',
875
+ flexShrink: 0
876
+ }}>
877
+ <div style={{
878
+ position: 'absolute',
879
+ left: 'calc(50% + 0.492px)',
880
+ top: '50%',
881
+ transform: 'translate(-50%, -50%)',
882
+ width: '11px',
883
+ height: '11px',
884
+ border: '0.972px solid white',
885
+ borderRadius: '6px 6px 6px 1px',
886
+ boxSizing: 'border-box'
887
+ }} />
888
+ </div>
889
+ </div>
890
+ </VeltCommentToolWireframe>
891
+ );
892
+ };
893
+
894
+ export default VeltCommentToolWf;
895
+ `;
896
+
897
+ const TEMPLATE_VELT_NOTIFICATIONS_TOOL_WF = `"use client";
898
+ import { VeltNotificationsToolWireframe } from '@veltdev/react';
899
+
900
+ const bellIcon = "/icons/bell-icon.svg";
901
+
902
+ const VeltNotificationsToolWf = () => {
903
+ return (
904
+ <VeltNotificationsToolWireframe>
905
+ <div style={{
906
+ background: '#141414',
907
+ borderRadius: '23.333px',
908
+ padding: '5.833px 4px 5.833px 7px',
909
+ display: 'flex',
910
+ alignItems: 'center',
911
+ justifyContent: 'center',
912
+ gap: '3.889px',
913
+ cursor: 'pointer',
914
+ boxSizing: 'border-box',
915
+ flexShrink: 0
916
+ }}>
917
+ <div style={{ width: '19.444px', height: '19.444px', position: 'relative', flexShrink: 0 }}>
918
+ <img src={bellIcon} alt="" style={{ width: '100%', height: '100%', display: 'block' }} />
919
+ </div>
920
+ <div style={{
921
+ fontFamily: 'Poppins, sans-serif',
922
+ fontWeight: 500,
923
+ fontSize: '14px',
924
+ lineHeight: '1.5',
925
+ color: 'white',
926
+ whiteSpace: 'pre',
927
+ flexShrink: 0
928
+ }}>
929
+ <VeltNotificationsToolWireframe.UnreadCount />
930
+ </div>
931
+ </div>
932
+ </VeltNotificationsToolWireframe>
933
+ );
934
+ };
935
+
936
+ export default VeltNotificationsToolWf;
937
+ `;
938
+
939
+ const TEMPLATE_VELT_SIDEBAR_BUTTON_WF = `"use client";
940
+ import { VeltSidebarButtonWireframe } from '@veltdev/react';
941
+
942
+ const inboxIcon = "/icons/inbox-icon.svg";
943
+
944
+ const VeltSidebarButtonWf = () => {
945
+ return (
946
+ <VeltSidebarButtonWireframe>
947
+ <div style={{
948
+ background: '#141414',
949
+ borderRadius: '24px',
950
+ padding: '6px 7px 6px 12px',
951
+ display: 'flex',
952
+ alignItems: 'center',
953
+ justifyContent: 'center',
954
+ gap: '4px',
955
+ height: '28px',
956
+ cursor: 'pointer',
957
+ boxSizing: 'border-box',
958
+ flexShrink: 0
959
+ }}>
960
+ <div style={{ width: '19.444px', height: '19.444px', position: 'relative', flexShrink: 0 }}>
961
+ <img src={inboxIcon} alt="" style={{ width: '100%', height: '100%', display: 'block' }} />
962
+ </div>
963
+ <div style={{
964
+ fontFamily: 'Poppins, sans-serif',
965
+ fontWeight: 500,
966
+ fontSize: '14px',
967
+ lineHeight: '1.5',
968
+ color: 'white',
969
+ whiteSpace: 'pre',
970
+ flexShrink: 0
971
+ }}>
972
+ <VeltSidebarButtonWireframe.CommentsCount />
973
+ </div>
974
+ </div>
975
+ </VeltSidebarButtonWireframe>
976
+ );
977
+ };
978
+
979
+ export default VeltSidebarButtonWf;
980
+ `;
981
+
982
+ const TEMPLATE_STYLES_CSS = `/* [Velt] UI customization styles */
983
+
984
+ velt-comment-dialog::part(triangle) {
985
+ border-color: rgb(255, 205, 46) !important;
986
+ }
987
+
988
+ velt-comment-dialog::part(triangle-fill) {
989
+ fill: rgb(255, 205, 46) !important;
990
+ }
991
+
992
+ /* Dark Mode Theme */
993
+ --velt-dark-mode-accent: #141414;
994
+ --velt-dark-mode-accent-text: #ffffff;
995
+ --velt-dark-mode-background-0: #000000;
996
+ --velt-dark-mode-background-2: #141414;
997
+ --velt-dark-mode-text-0: #ffffff;
998
+ `;
999
+
1000
+ // ============================================================================
1001
+ // DYNAMIC TEMPLATE GENERATORS
1002
+ // ============================================================================
1003
+
1004
+ function generateVeltCollaborationTemplate(features) {
1005
+ const hasComments = features.comments;
1006
+ const hasCursors = features.cursors;
1007
+ const hasCrdt = features.crdt !== 'none';
1008
+ const crdtType = features.crdt;
1009
+
1010
+ // Build imports
1011
+ const veltImports = [];
1012
+ if (hasComments) veltImports.push('VeltComments', 'VeltCommentsSidebar');
1013
+ if (hasCursors) veltImports.push('VeltCursor');
1014
+
1015
+ const hasVeltImports = veltImports.length > 0;
1016
+ const hasCustomization = hasComments || features.notifications;
1017
+
1018
+ // Determine comment props based on CRDT type
1019
+ let commentProps = '';
1020
+ if (hasComments) {
1021
+ if (crdtType === 'reactflow') {
1022
+ commentProps = `
1023
+ popoverMode={true}
1024
+ textMode={false}
1025
+ commentPinHighlighter={false}
1026
+ dialogOnHover={false}
1027
+ popoverTriangleComponent={false}`;
1028
+ } else if (crdtType === 'tiptap') {
1029
+ commentProps = `
1030
+ shadowDom={false}
1031
+ textMode={false}`;
1032
+ } else if (crdtType === 'codemirror') {
1033
+ commentProps = ` textMode={false}`;
1034
+ } else {
1035
+ commentProps = `
1036
+ popoverMode={true}
1037
+ shadowDom={false}
1038
+ textMode={false}
1039
+ commentPinHighlighter={false}
1040
+ dialogOnHover={false}`;
1041
+ }
1042
+ }
1043
+
1044
+ // Build template
1045
+ let template = `"use client";
1046
+ `;
1047
+
1048
+ if (hasVeltImports) {
1049
+ template += `import { ${veltImports.join(', ')} } from "@veltdev/react";
1050
+ `;
1051
+ }
1052
+
1053
+ template += `import VeltInitializeDocument from "./VeltInitializeDocument";
1054
+ `;
1055
+
1056
+ if (hasCustomization) {
1057
+ template += `import { VeltCustomization } from "./ui-customization/VeltCustomization";
1058
+ `;
1059
+ }
1060
+
1061
+ template += `
1062
+ export function VeltCollaboration() {
1063
+ return (
1064
+ <>
1065
+ <VeltInitializeDocument />
1066
+ `;
1067
+
1068
+ if (hasComments) {
1069
+ template += ` <VeltComments${commentProps}
1070
+ />
1071
+ <VeltCommentsSidebar groupConfig={{ enable: false }} />
1072
+ `;
1073
+ }
1074
+
1075
+ if (hasCursors) {
1076
+ template += ` <VeltCursor />
1077
+ `;
1078
+ }
1079
+
1080
+ if (hasCustomization) {
1081
+ template += ` <VeltCustomization />
1082
+ `;
1083
+ }
1084
+
1085
+ template += ` </>
1086
+ );
1087
+ }
1088
+
1089
+ export default VeltCollaboration;
1090
+ `;
1091
+
1092
+ return template;
1093
+ }
1094
+
1095
+ function generateVeltToolsTemplate(features) {
1096
+ const hasPresence = features.presence;
1097
+ const hasComments = features.comments;
1098
+ const hasNotifications = features.notifications;
1099
+ const hasCrdt = features.crdt !== 'none';
1100
+ const crdtType = features.crdt;
1101
+
1102
+ // Build imports
1103
+ const veltImports = [];
1104
+ if (hasPresence) veltImports.push('VeltPresence');
1105
+ if (hasComments) veltImports.push('VeltSidebarButton');
1106
+ if (hasNotifications) veltImports.push('VeltNotificationsTool');
1107
+ if (hasCrdt && crdtType === 'reactflow') veltImports.push('VeltHuddleTool');
1108
+
1109
+ if (veltImports.length === 0) {
1110
+ return null; // No VeltTools needed
1111
+ }
1112
+
1113
+ let template = `"use client";
1114
+ import { ${veltImports.join(', ')} } from "@veltdev/react";
1115
+
1116
+ function VeltTools() {
1117
+ return (
1118
+ <>
1119
+ `;
1120
+
1121
+ if (hasPresence) {
1122
+ template += ` <VeltPresence />
1123
+ `;
1124
+ }
1125
+
1126
+ if (hasComments) {
1127
+ template += ` <VeltSidebarButton />
1128
+ `;
1129
+ }
1130
+
1131
+ if (hasNotifications) {
1132
+ template += ` <VeltNotificationsTool
1133
+ settings={true}
1134
+ shadowDom={false}
1135
+ tabConfig={{
1136
+ forYou: { name: "For You", enable: true },
1137
+ documents: { name: "Documents", enable: true },
1138
+ all: { name: "All", enable: true },
1139
+ }}
1140
+ />
1141
+ `;
1142
+ }
1143
+
1144
+ if (hasCrdt && crdtType === 'reactflow') {
1145
+ template += ` <VeltHuddleTool type='all' />
1146
+ `;
1147
+ }
1148
+
1149
+ template += ` </>
1150
+ );
1151
+ }
1152
+
1153
+ export default VeltTools;
1154
+ `;
1155
+
1156
+ return template;
1157
+ }
1158
+
1159
+ // ============================================================================
1160
+ // FILE GENERATION LOGIC
1161
+ // ============================================================================
1162
+
1163
+ function generateVeltConfig(features) {
1164
+ const files = {};
1165
+
1166
+ // Always include core files
1167
+ files['components/velt/VeltInitializeDocument.tsx'] = TEMPLATE_VELT_INITIALIZE_DOCUMENT;
1168
+ files['components/velt/VeltInitializeUser.tsx'] = TEMPLATE_VELT_INITIALIZE_USER;
1169
+
1170
+ const hasComments = features.comments;
1171
+ const hasNotifications = features.notifications;
1172
+ const hasPresence = features.presence;
1173
+ const hasCursors = features.cursors;
1174
+ const hasCrdt = features.crdt !== 'none';
1175
+
1176
+ // Generate VeltCollaboration dynamically based on features
1177
+ files['components/velt/VeltCollaboration.tsx'] = generateVeltCollaborationTemplate(features);
1178
+
1179
+ // Generate VeltTools dynamically based on features
1180
+ const veltToolsTemplate = generateVeltToolsTemplate(features);
1181
+ if (veltToolsTemplate) {
1182
+ files['components/velt/VeltTools.tsx'] = veltToolsTemplate;
1183
+ }
1184
+
1185
+ // Add UI customization files based on features
1186
+ if (hasCrdt || (hasComments && hasNotifications)) {
1187
+ // Full customization
1188
+ files['components/velt/ui-customization/styles.css'] = TEMPLATE_STYLES_CSS;
1189
+ files['components/velt/ui-customization/VeltCustomization.tsx'] = TEMPLATE_VELT_CUSTOMIZATION_CRDT;
1190
+ files['components/velt/ui-customization/VeltCommentBubbleWf.tsx'] = TEMPLATE_VELT_COMMENT_BUBBLE_WF;
1191
+ files['components/velt/ui-customization/VeltCommentToolWf.tsx'] = TEMPLATE_VELT_COMMENT_TOOL_WF;
1192
+ files['components/velt/ui-customization/VeltNotificationsToolWf.tsx'] = TEMPLATE_VELT_NOTIFICATIONS_TOOL_WF;
1193
+ files['components/velt/ui-customization/VeltSidebarButtonWf.tsx'] = TEMPLATE_VELT_SIDEBAR_BUTTON_WF;
1194
+ } else if (hasComments) {
1195
+ // Comments only
1196
+ files['components/velt/ui-customization/styles.css'] = TEMPLATE_STYLES_CSS;
1197
+ files['components/velt/ui-customization/VeltCustomization.tsx'] = TEMPLATE_VELT_CUSTOMIZATION_COMMENTS;
1198
+ files['components/velt/ui-customization/VeltCommentBubbleWf.tsx'] = TEMPLATE_VELT_COMMENT_BUBBLE_WF;
1199
+ files['components/velt/ui-customization/VeltCommentToolWf.tsx'] = TEMPLATE_VELT_COMMENT_TOOL_WF;
1200
+ } else if (hasNotifications) {
1201
+ // Notifications only
1202
+ files['components/velt/ui-customization/styles.css'] = TEMPLATE_STYLES_CSS;
1203
+ files['components/velt/ui-customization/VeltCustomization.tsx'] = TEMPLATE_VELT_CUSTOMIZATION_NOTIFICATIONS;
1204
+ files['components/velt/ui-customization/VeltNotificationsToolWf.tsx'] = TEMPLATE_VELT_NOTIFICATIONS_TOOL_WF;
1205
+ }
1206
+
1207
+ return files;
1208
+ }
1209
+
1210
+ // ============================================================================
1211
+ // MAIN FUNCTIONS
1212
+ // ============================================================================
1213
+
1214
+ function main() {
1215
+ const args = parseArgs(process.argv);
1216
+
1217
+ if (args.help || args.h) {
1218
+ printUsage();
1219
+ process.exit(0);
1220
+ }
1221
+
1222
+ const cwd = process.cwd();
1223
+ console.log('Analyzing existing project...');
1224
+ const analysis = analyzeProject(cwd);
1225
+ const features = parseFeatures(args);
1226
+
1227
+ console.log(`Project Analysis:
1228
+ - Package Manager: ${analysis.packageManager}
1229
+ - Router Type: ${analysis.hasAppRouter ? 'App Router' : analysis.hasPagesRouter ? 'Pages Router' : 'Unknown'}
1230
+ - Has Components: ${analysis.hasComponents ? 'Yes' : 'No'}
1231
+ `);
1232
+
1233
+ // Show enabled features
1234
+ const enabledFeatures = [];
1235
+ if (features.presence) enabledFeatures.push('presence');
1236
+ if (features.cursors) enabledFeatures.push('cursors');
1237
+ if (features.comments) enabledFeatures.push('comments');
1238
+ if (features.notifications) enabledFeatures.push('notifications');
1239
+ if (features.crdt !== 'none') enabledFeatures.push(`${features.crdt}-crdt`);
1240
+
1241
+ if (enabledFeatures.length > 0) {
1242
+ console.log(`Features enabled: ${enabledFeatures.join(', ')}\n`);
1243
+ } else {
1244
+ console.log('Features enabled: core only\n');
1245
+ }
1246
+
1247
+ const flags = {
1248
+ force: !!args.force || !!args.f,
1249
+ 'legacy-peer-deps': !!args['legacy-peer-deps'],
1250
+ };
1251
+
1252
+ try {
1253
+ console.log('Generating Velt integration files...');
1254
+ const filesWritten = applyScaffold({ cwd, flags, features });
1255
+
1256
+ console.log('\nInstalling dependencies...');
1257
+ const deps = resolveDependencies(analysis, features);
1258
+
1259
+ if (deps.production.length) {
1260
+ smartInstall(analysis.packageManager, deps.production, false, cwd, flags);
1261
+ }
1262
+ if (deps.development.length) {
1263
+ smartInstall(analysis.packageManager, deps.development, true, cwd, flags);
1264
+ }
1265
+
1266
+ // Copy icon assets if features use them
1267
+ if (hasAnyFeature(features)) {
1268
+ console.log('\nCopying icon assets...');
1269
+ copyIconAssets(cwd);
1270
+ }
1271
+
1272
+ generateSetupInstructions(analysis, features, filesWritten, deps);
1273
+ console.log(`\nVelt integration complete!`);
1274
+
1275
+ } catch (error) {
1276
+ console.error(`Installation failed: ${error.message}`);
1277
+ console.log('\nTroubleshooting tips:');
1278
+ console.log(' - Try: npx add-velt --force');
1279
+ console.log(' - Or: npx add-velt --legacy-peer-deps');
1280
+ process.exit(1);
1281
+ }
1282
+ }
1283
+
1284
+ function applyScaffold({ cwd, flags, features }) {
1285
+ const files = generateVeltConfig(features);
1286
+ const filesWritten = [];
1287
+
1288
+ Object.entries(files).forEach(([relPath, content]) => {
1289
+ const outPath = path.join(cwd, relPath);
1290
+ const dir = path.dirname(outPath);
1291
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1292
+ if (fs.existsSync(outPath) && !flags.force) {
1293
+ console.log(`Skipping existing file: ${relPath}`);
1294
+ return;
1295
+ }
1296
+
1297
+ fs.writeFileSync(outPath, content, 'utf8');
1298
+ console.log(`Wrote ${relPath}`);
1299
+ filesWritten.push(relPath);
1300
+ });
1301
+
1302
+ return filesWritten;
1303
+ }
1304
+
1305
+ // ============================================================================
1306
+ // AUTO-WIRING: VeltProvider + VeltCollaboration
1307
+ // ============================================================================
1308
+
1309
+ function autoWireVeltProvider(cwd, analysis) {
1310
+ // Detect layout file
1311
+ const appLayoutPaths = [
1312
+ path.join(cwd, 'app', 'layout.tsx'),
1313
+ path.join(cwd, 'app', 'layout.jsx'),
1314
+ path.join(cwd, 'src', 'app', 'layout.tsx'),
1315
+ path.join(cwd, 'src', 'app', 'layout.jsx')
1316
+ ];
1317
+
1318
+ const pagesAppPaths = [
1319
+ path.join(cwd, 'pages', '_app.tsx'),
1320
+ path.join(cwd, 'pages', '_app.jsx'),
1321
+ path.join(cwd, 'src', 'pages', '_app.tsx'),
1322
+ path.join(cwd, 'src', 'pages', '_app.jsx')
1323
+ ];
1324
+
1325
+ let layoutPath = null;
1326
+ let isAppRouter = false;
1327
+
1328
+ // Prefer App Router
1329
+ for (const p of appLayoutPaths) {
1330
+ if (fs.existsSync(p)) {
1331
+ layoutPath = p;
1332
+ isAppRouter = true;
1333
+ break;
1334
+ }
1335
+ }
1336
+
1337
+ // Fall back to Pages Router
1338
+ if (!layoutPath) {
1339
+ for (const p of pagesAppPaths) {
1340
+ if (fs.existsSync(p)) {
1341
+ layoutPath = p;
1342
+ isAppRouter = false;
1343
+ break;
1344
+ }
1345
+ }
1346
+ }
1347
+
1348
+ if (!layoutPath) {
1349
+ console.log('\nNo layout file found. Skipping auto-wiring.');
1350
+ console.log('You will need to manually wrap your app with VeltProvider.');
1351
+ return { success: false, reason: 'no-layout' };
1352
+ }
1353
+
1354
+ let content = fs.readFileSync(layoutPath, 'utf8');
1355
+ const relPath = path.relative(cwd, layoutPath);
1356
+
1357
+ // Check if already wired
1358
+ if (content.includes('VeltProvider') && content.includes('VeltCollaboration')) {
1359
+ console.log(`\nVelt already wired in ${relPath}. Skipping.`);
1360
+ return { success: true, already: true, file: relPath };
1361
+ }
1362
+
1363
+ // Determine import path for components
1364
+ const isInSrc = layoutPath.includes(path.join('src', 'app')) || layoutPath.includes(path.join('src', 'pages'));
1365
+ const componentImportBase = isInSrc ? '@/components' : '@/components';
1366
+
1367
+ // Build the required imports
1368
+ const veltImports = [];
1369
+
1370
+ if (!content.includes("from '@veltdev/react'") && !content.includes('from "@veltdev/react"')) {
1371
+ veltImports.push(`import { VeltProvider } from '@veltdev/react';`);
1372
+ } else if (!content.includes('VeltProvider')) {
1373
+ // VeltProvider needs to be added to existing import
1374
+ content = content.replace(
1375
+ /(import\s*\{[^}]*)(}\s*from\s*['"]@veltdev\/react['"])/,
1376
+ '$1, VeltProvider$2'
1377
+ );
1378
+ }
1379
+
1380
+ if (!content.includes('useVeltAuthProvider')) {
1381
+ veltImports.push(`import { useVeltAuthProvider } from '${componentImportBase}/velt/VeltInitializeUser';`);
1382
+ }
1383
+
1384
+ if (!content.includes('VeltCollaboration')) {
1385
+ veltImports.push(`import { VeltCollaboration } from '${componentImportBase}/velt/VeltCollaboration';`);
1386
+ }
1387
+
1388
+ // Insert imports at the top (after existing imports)
1389
+ if (veltImports.length > 0) {
1390
+ // Find the last import statement
1391
+ const importMatches = [...content.matchAll(/^import\s+.*?['"][^'"]+['"];?\s*$/gm)];
1392
+ if (importMatches.length > 0) {
1393
+ const lastImport = importMatches[importMatches.length - 1];
1394
+ const insertPos = lastImport.index + lastImport[0].length;
1395
+ content = content.slice(0, insertPos) + '\n' + veltImports.join('\n') + content.slice(insertPos);
1396
+ } else {
1397
+ // No imports found, add at the very top
1398
+ content = veltImports.join('\n') + '\n\n' + content;
1399
+ }
1400
+ }
1401
+
1402
+ // Add VeltClientWrapper component if not present
1403
+ if (!content.includes('VeltClientWrapper')) {
1404
+ const wrapperComponent = `
1405
+ // Velt wrapper component for client-side provider
1406
+ "use client";
1407
+ function VeltClientWrapper({ children }: { children: React.ReactNode }) {
1408
+ const { authProvider } = useVeltAuthProvider();
1409
+ return (
1410
+ <VeltProvider apiKey={process.env.NEXT_PUBLIC_VELT_API_KEY || 'YOUR_API_KEY'} authProvider={authProvider}>
1411
+ <VeltCollaboration />
1412
+ {children}
1413
+ </VeltProvider>
1414
+ );
1415
+ }
1416
+ `;
1417
+ // Insert before the default export
1418
+ const exportMatch = content.match(/export\s+default\s+function\s+\w+/);
1419
+ if (exportMatch) {
1420
+ content = content.slice(0, exportMatch.index) + wrapperComponent + '\n' + content.slice(exportMatch.index);
1421
+ }
1422
+ }
1423
+
1424
+ // Wrap the body children with VeltClientWrapper
1425
+ if (isAppRouter) {
1426
+ // For App Router, wrap body children
1427
+ // Pattern: <body ...>{children}</body> -> <body ...><VeltClientWrapper>{children}</VeltClientWrapper></body>
1428
+ if (!content.includes('<VeltClientWrapper>') && !content.includes('<VeltProvider')) {
1429
+ // Try to wrap {children} inside body
1430
+ content = content.replace(
1431
+ /(<body[^>]*>)([\s\S]*?)(\{children\})([\s\S]*?)(<\/body>)/,
1432
+ '$1$2<VeltClientWrapper>$3</VeltClientWrapper>$4$5'
1433
+ );
1434
+ }
1435
+ } else {
1436
+ // For Pages Router, wrap Component
1437
+ if (!content.includes('<VeltClientWrapper>') && !content.includes('<VeltProvider')) {
1438
+ content = content.replace(
1439
+ /(<Component\s+\{\.\.\.pageProps\}\s*\/>)/,
1440
+ '<VeltClientWrapper><Component {...pageProps} /></VeltClientWrapper>'
1441
+ );
1442
+ }
1443
+ }
1444
+
1445
+ // Write the modified content
1446
+ fs.writeFileSync(layoutPath, content, 'utf8');
1447
+ console.log(`\nAuto-wired VeltProvider + VeltCollaboration in ${relPath}`);
1448
+
1449
+ return { success: true, file: relPath };
1450
+ }
1451
+
1452
+ // Copy icon assets from CLI to user's public/icons
1453
+ function copyIconAssets(cwd) {
1454
+ try {
1455
+ const publicIconsDir = path.join(cwd, 'public', 'icons');
1456
+ if (!fs.existsSync(publicIconsDir)) {
1457
+ fs.mkdirSync(publicIconsDir, { recursive: true });
1458
+ }
1459
+
1460
+ // Get the CLI's assets directory
1461
+ const cliAssetsDir = path.join(__dirname, '..', 'assets', 'icons');
1462
+
1463
+ const iconFiles = ['bell-icon.svg', 'inbox-icon.svg'];
1464
+ iconFiles.forEach(iconFile => {
1465
+ const srcPath = path.join(cliAssetsDir, iconFile);
1466
+ const destPath = path.join(publicIconsDir, iconFile);
1467
+
1468
+ if (fs.existsSync(srcPath) && !fs.existsSync(destPath)) {
1469
+ fs.copyFileSync(srcPath, destPath);
1470
+ console.log(`Copied public/icons/${iconFile}`);
1471
+ }
1472
+ });
1473
+ } catch (error) {
1474
+ console.warn(`Warning: Failed to copy icon files: ${error.message}`);
1475
+ }
1476
+ }
1477
+
1478
+ function generateSetupInstructions(analysis, features, filesWritten, deps) {
1479
+ console.log('\n----------------------------------------');
1480
+ console.log('FILES CREATED:');
1481
+ console.log('----------------------------------------');
1482
+
1483
+ const coreFiles = filesWritten.filter(f => !f.includes('ui-customization'));
1484
+ const uiFiles = filesWritten.filter(f => f.includes('ui-customization'));
1485
+
1486
+ console.log('\nCore components:');
1487
+ coreFiles.forEach(f => console.log(` - ${f}`));
1488
+
1489
+ if (uiFiles.length > 0) {
1490
+ console.log('\nUI customization:');
1491
+ uiFiles.forEach(f => console.log(` - ${f}`));
1492
+ }
1493
+
1494
+ console.log('\n----------------------------------------');
1495
+ console.log('DEPENDENCIES INSTALLED:');
1496
+ console.log('----------------------------------------');
1497
+ if (deps.production.length) {
1498
+ console.log('\nProduction:');
1499
+ deps.production.forEach(d => console.log(` - ${d}`));
1500
+ }
1501
+ if (deps.development.length) {
1502
+ console.log('\nDevelopment:');
1503
+ deps.development.forEach(d => console.log(` - ${d}`));
1504
+ }
1505
+
1506
+ console.log('\n----------------------------------------');
1507
+ console.log('NEXT STEPS:');
1508
+ console.log('----------------------------------------');
1509
+
1510
+ let stepNum = 1;
1511
+
1512
+ console.log(`\n${stepNum++}. Get your API key from https://console.velt.dev`);
1513
+ console.log(`\n${stepNum++}. Wrap your app with VeltProvider in your layout file`);
1514
+ console.log(` Set NEXT_PUBLIC_VELT_API_KEY in your .env.local file`);
1515
+
1516
+ console.log(`\n${stepNum++}. Update VeltInitializeDocument.tsx with your document ID source`);
1517
+ console.log(`\n${stepNum++}. Update VeltInitializeUser.tsx with your user authentication logic`);
1518
+
1519
+ if (hasAnyFeature(features)) {
1520
+ console.log(`\n${stepNum++}. Add <VeltTools /> to your toolbar area for Presence, Sidebar, and Notifications`);
1521
+ }
1522
+
1523
+ if (features.crdt !== 'none') {
1524
+ console.log(`\n${stepNum++}. For ${features.crdt.toUpperCase()} CRDT integration, see:`);
1525
+ if (features.crdt === 'reactflow') {
1526
+ console.log(' https://docs.velt.dev/async-collaboration/comments/setup/react-flow');
1527
+ } else if (features.crdt === 'tiptap') {
1528
+ console.log(' https://docs.velt.dev/realtime-collaboration/single-editor-mode/tiptap');
1529
+ } else {
1530
+ console.log(' https://docs.velt.dev/realtime-collaboration/single-editor-mode/codemirror');
1531
+ }
1532
+ }
1533
+
1534
+ // Features summary
1535
+ console.log('\n----------------------------------------');
1536
+ console.log('FEATURES ENABLED:');
1537
+ console.log('----------------------------------------');
1538
+ if (features.presence) {
1539
+ console.log('\n - Presence: VeltPresence (shows online users)');
1540
+ }
1541
+ if (features.cursors) {
1542
+ console.log(' - Cursors: VeltCursor (shows live cursor positions)');
1543
+ }
1544
+ if (features.comments) {
1545
+ console.log(' - Comments: VeltComments + VeltCommentsSidebar');
1546
+ }
1547
+ if (features.notifications) {
1548
+ console.log(' - Notifications: VeltNotificationsTool');
1549
+ }
1550
+ if (features.crdt !== 'none') {
1551
+ console.log(` - CRDT: ${features.crdt} real-time sync`);
1552
+ }
1553
+ if (!hasAnyFeature(features)) {
1554
+ console.log('\n - Core only (VeltProvider + VeltInitialize)');
1555
+ }
1556
+ }
1557
+
1558
+ function parseArgs(argv) {
1559
+ const args = argv.slice(2);
1560
+ const result = { _: [] };
1561
+ for (let i = 0; i < args.length; i++) {
1562
+ const token = args[i];
1563
+ if (token.startsWith('--')) {
1564
+ if (token.includes('=')) {
1565
+ const [fullKey, ...valueParts] = token.split('=');
1566
+ const key = fullKey.replace(/^--/, '');
1567
+ let value = valueParts.join('=');
1568
+ if ((value.startsWith('"') && value.endsWith('"')) ||
1569
+ (value.startsWith("'") && value.endsWith("'"))) {
1570
+ value = value.slice(1, -1);
1571
+ }
1572
+ result[key] = value;
1573
+ } else {
1574
+ const key = token.replace(/^--/, '');
1575
+ const next = args[i + 1];
1576
+ if (next && !next.startsWith('-')) {
1577
+ let value = next;
1578
+ if ((value.startsWith('"') && value.endsWith('"')) ||
1579
+ (value.startsWith("'") && value.endsWith("'"))) {
1580
+ value = value.slice(1, -1);
1581
+ }
1582
+ result[key] = value;
1583
+ i++;
1584
+ } else {
1585
+ result[key] = true;
1586
+ }
1587
+ }
1588
+ } else if (token.startsWith('-')) {
1589
+ const key = token.replace(/^-+/, '');
1590
+ result[key] = true;
1591
+ } else {
1592
+ result._.push(token);
1593
+ }
1594
+ }
1595
+ return result;
1596
+ }
1597
+
1598
+ function readPkg(cwd) {
1599
+ const pkgPath = path.join(cwd, 'package.json');
1600
+ if (!fs.existsSync(pkgPath)) return null;
1601
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
1602
+ }
1603
+
1604
+ function printUsage() {
1605
+ console.log(`
1606
+ add-velt - Add Velt collaboration to Next.js apps
1607
+
1608
+ Usage:
1609
+ npx add-velt [options]
1610
+
1611
+ Feature Flags:
1612
+ --presence Add presence (VeltPresence - shows online users)
1613
+ --cursors Add cursors (VeltCursor - shows live cursor positions)
1614
+ --comments Add comments (VeltComments, VeltCommentsSidebar)
1615
+ --notifications Add notifications (VeltNotificationsTool)
1616
+
1617
+ CRDT Type Flags (choose one):
1618
+ --reactflow-crdt Add ReactFlow CRDT (canvas collaboration)
1619
+ --tiptap-crdt Add Tiptap CRDT (rich text editor)
1620
+ --codemirror-crdt Add CodeMirror CRDT (code editor)
1621
+
1622
+ Combined:
1623
+ --all Enable presence + cursors + comments + notifications + CRDT
1624
+ (requires a CRDT type flag)
1625
+
1626
+ Installation Flags:
1627
+ --force, -f Force overwrite existing files
1628
+ --legacy-peer-deps Use legacy peer deps (npm only)
1629
+ --help, -h Show this help message
1630
+
1631
+ Examples:
1632
+ npx add-velt # Core only
1633
+ npx add-velt --presence # Presence only
1634
+ npx add-velt --cursors # Cursors only
1635
+ npx add-velt --comments # Comments only
1636
+ npx add-velt --notifications # Notifications only
1637
+ npx add-velt --presence --cursors # Presence + Cursors
1638
+ npx add-velt --comments --notifications # Comments + Notifications
1639
+ npx add-velt --reactflow-crdt # ReactFlow CRDT only
1640
+ npx add-velt --tiptap-crdt # Tiptap CRDT only
1641
+ npx add-velt --codemirror-crdt # CodeMirror CRDT only
1642
+ npx add-velt --all --reactflow-crdt # All features with ReactFlow
1643
+ npx add-velt --all --tiptap-crdt # All features with Tiptap
1644
+ npx add-velt --all --codemirror-crdt # All features with CodeMirror
1645
+
1646
+ Note:
1647
+ - Only ONE CRDT type can be selected at a time
1648
+ - --all requires a CRDT type flag
1649
+ - Get your API key from https://console.velt.dev
1650
+ `);
1651
+ }
1652
+
1653
+ if (require.main === module) {
1654
+ main();
1655
+ }
1656
+
1657
+ module.exports = {
1658
+ analyzeProject,
1659
+ resolveDependencies,
1660
+ detectPackageManager,
1661
+ smartInstall,
1662
+ parseFeatures,
1663
+ generateVeltConfig
1664
+ };