pdfx-cli 0.0.1 → 0.4.1

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,4446 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync2 } from "fs";
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import chalk11 from "chalk";
8
+ import { Command as Command3 } from "commander";
9
+
10
+ // src/commands/add.ts
11
+ import fs5 from "fs";
12
+ import path4 from "path";
13
+
14
+ // ../shared/src/schemas.ts
15
+ import { z } from "zod";
16
+ var colorTokensSchema = z.object({
17
+ foreground: z.string().min(1),
18
+ background: z.string().min(1),
19
+ muted: z.string().min(1),
20
+ mutedForeground: z.string().min(1),
21
+ primary: z.string().min(1),
22
+ primaryForeground: z.string().min(1),
23
+ border: z.string().min(1),
24
+ accent: z.string().min(1),
25
+ destructive: z.string().min(1),
26
+ success: z.string().min(1),
27
+ warning: z.string().min(1),
28
+ info: z.string().min(1)
29
+ });
30
+ var headingFontSizeSchema = z.object({
31
+ h1: z.number().positive(),
32
+ h2: z.number().positive(),
33
+ h3: z.number().positive(),
34
+ h4: z.number().positive(),
35
+ h5: z.number().positive(),
36
+ h6: z.number().positive()
37
+ });
38
+ var typographyTokensSchema = z.object({
39
+ body: z.object({
40
+ fontFamily: z.string().min(1),
41
+ fontSize: z.number().positive(),
42
+ lineHeight: z.number().positive()
43
+ }),
44
+ heading: z.object({
45
+ fontFamily: z.string().min(1),
46
+ fontWeight: z.number().int().min(100).max(900),
47
+ lineHeight: z.number().positive(),
48
+ fontSize: headingFontSizeSchema
49
+ })
50
+ });
51
+ var spacingTokensSchema = z.object({
52
+ page: z.object({
53
+ marginTop: z.number().min(0),
54
+ marginRight: z.number().min(0),
55
+ marginBottom: z.number().min(0),
56
+ marginLeft: z.number().min(0)
57
+ }),
58
+ sectionGap: z.number().min(0),
59
+ paragraphGap: z.number().min(0),
60
+ componentGap: z.number().min(0)
61
+ });
62
+ var pageTokensSchema = z.object({
63
+ size: z.enum(["A4", "LETTER", "LEGAL"]),
64
+ orientation: z.enum(["portrait", "landscape"])
65
+ });
66
+ var primitiveTokensSchema = z.object({
67
+ typography: z.record(z.number()),
68
+ spacing: z.record(z.number()),
69
+ fontWeights: z.object({
70
+ regular: z.number(),
71
+ medium: z.number(),
72
+ semibold: z.number(),
73
+ bold: z.number()
74
+ }),
75
+ lineHeights: z.object({
76
+ tight: z.number(),
77
+ normal: z.number(),
78
+ relaxed: z.number()
79
+ }),
80
+ borderRadius: z.object({
81
+ none: z.number(),
82
+ sm: z.number(),
83
+ md: z.number(),
84
+ lg: z.number(),
85
+ full: z.number()
86
+ }),
87
+ letterSpacing: z.object({
88
+ tight: z.number(),
89
+ normal: z.number(),
90
+ wide: z.number(),
91
+ wider: z.number()
92
+ })
93
+ });
94
+ var themeSchema = z.object({
95
+ name: z.string().min(1),
96
+ primitives: primitiveTokensSchema,
97
+ colors: colorTokensSchema,
98
+ typography: typographyTokensSchema,
99
+ spacing: spacingTokensSchema,
100
+ page: pageTokensSchema
101
+ });
102
+ var configSchema = z.object({
103
+ $schema: z.string().optional(),
104
+ componentDir: z.string().min(1, "componentDir must not be empty"),
105
+ registry: z.string().url("registry must be a valid URL"),
106
+ theme: z.string().min(1).optional(),
107
+ blockDir: z.string().min(1).optional()
108
+ });
109
+ var registryFileTypes = [
110
+ "registry:component",
111
+ "registry:lib",
112
+ "registry:style",
113
+ "registry:template",
114
+ "registry:block",
115
+ "registry:file"
116
+ ];
117
+ var registryFileSchema = z.object({
118
+ path: z.string().min(1),
119
+ content: z.string(),
120
+ type: z.enum(registryFileTypes)
121
+ });
122
+ var registryItemSchema = z.object({
123
+ name: z.string().min(1),
124
+ type: z.string().optional(),
125
+ title: z.string().optional(),
126
+ description: z.string().optional(),
127
+ files: z.array(registryFileSchema).min(1, "Component must have at least one file"),
128
+ dependencies: z.array(z.string()).optional(),
129
+ devDependencies: z.array(z.string()).optional(),
130
+ registryDependencies: z.array(z.string()).optional(),
131
+ peerComponents: z.array(z.string()).optional()
132
+ });
133
+ var registryIndexItemSchema = z.object({
134
+ name: z.string().min(1),
135
+ type: z.string(),
136
+ title: z.string(),
137
+ description: z.string(),
138
+ files: z.array(
139
+ z.object({
140
+ path: z.string().min(1),
141
+ type: z.string()
142
+ })
143
+ ),
144
+ dependencies: z.array(z.string()).optional(),
145
+ devDependencies: z.array(z.string()).optional(),
146
+ registryDependencies: z.array(z.string()).optional(),
147
+ peerComponents: z.array(z.string()).optional()
148
+ });
149
+ var registrySchema = z.object({
150
+ $schema: z.string(),
151
+ name: z.string(),
152
+ homepage: z.string(),
153
+ items: z.array(registryIndexItemSchema)
154
+ });
155
+ var componentNameSchema = z.string().regex(
156
+ /^[a-z][a-z0-9-]*$/,
157
+ "Component name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens"
158
+ );
159
+
160
+ // ../shared/src/themes/primitives.ts
161
+ var defaultPrimitives = {
162
+ typography: {
163
+ xs: 10,
164
+ sm: 12,
165
+ base: 15,
166
+ lg: 18,
167
+ xl: 22,
168
+ "2xl": 28,
169
+ "3xl": 36
170
+ },
171
+ spacing: {
172
+ 0: 0,
173
+ 0.5: 2,
174
+ 1: 4,
175
+ 2: 8,
176
+ 3: 12,
177
+ 4: 16,
178
+ 5: 20,
179
+ 6: 24,
180
+ 8: 32,
181
+ 10: 40,
182
+ 12: 48,
183
+ 16: 64
184
+ },
185
+ fontWeights: {
186
+ regular: 400,
187
+ medium: 500,
188
+ semibold: 600,
189
+ bold: 700
190
+ },
191
+ lineHeights: {
192
+ tight: 1.2,
193
+ normal: 1.4,
194
+ relaxed: 1.6
195
+ },
196
+ borderRadius: {
197
+ none: 0,
198
+ sm: 2,
199
+ md: 4,
200
+ lg: 8,
201
+ full: 9999
202
+ },
203
+ letterSpacing: {
204
+ tight: -0.025,
205
+ normal: 0,
206
+ wide: 0.025,
207
+ wider: 0.05
208
+ }
209
+ };
210
+
211
+ // ../shared/src/themes/professional.ts
212
+ var professionalTheme = {
213
+ name: "professional",
214
+ primitives: defaultPrimitives,
215
+ colors: {
216
+ foreground: "#18181b",
217
+ background: "#ffffff",
218
+ muted: "#f4f4f5",
219
+ mutedForeground: "#71717a",
220
+ primary: "#18181b",
221
+ primaryForeground: "#ffffff",
222
+ border: "#e4e4e7",
223
+ accent: "#3b82f6",
224
+ destructive: "#dc2626",
225
+ success: "#16a34a",
226
+ warning: "#d97706",
227
+ info: "#0ea5e9"
228
+ },
229
+ typography: {
230
+ body: {
231
+ fontFamily: "Helvetica",
232
+ fontSize: 11,
233
+ lineHeight: 1.6
234
+ },
235
+ heading: {
236
+ fontFamily: "Times-Roman",
237
+ fontWeight: 700,
238
+ lineHeight: 1.25,
239
+ fontSize: {
240
+ h1: 32,
241
+ h2: 24,
242
+ h3: 20,
243
+ h4: 16,
244
+ h5: 14,
245
+ h6: 12
246
+ }
247
+ }
248
+ },
249
+ spacing: {
250
+ page: {
251
+ marginTop: 56,
252
+ marginRight: 48,
253
+ marginBottom: 56,
254
+ marginLeft: 48
255
+ },
256
+ sectionGap: 28,
257
+ paragraphGap: 10,
258
+ componentGap: 14
259
+ },
260
+ page: {
261
+ size: "A4",
262
+ orientation: "portrait"
263
+ }
264
+ };
265
+
266
+ // ../shared/src/themes/modern.ts
267
+ var modernTheme = {
268
+ name: "modern",
269
+ primitives: defaultPrimitives,
270
+ colors: {
271
+ foreground: "#0f172a",
272
+ background: "#ffffff",
273
+ muted: "#f1f5f9",
274
+ mutedForeground: "#64748b",
275
+ primary: "#334155",
276
+ primaryForeground: "#ffffff",
277
+ border: "#e2e8f0",
278
+ accent: "#6366f1",
279
+ destructive: "#ef4444",
280
+ success: "#22c55e",
281
+ warning: "#f59e0b",
282
+ info: "#3b82f6"
283
+ },
284
+ typography: {
285
+ body: {
286
+ fontFamily: "Helvetica",
287
+ fontSize: 11,
288
+ lineHeight: 1.6
289
+ },
290
+ heading: {
291
+ fontFamily: "Helvetica",
292
+ fontWeight: 600,
293
+ lineHeight: 1.25,
294
+ fontSize: {
295
+ h1: 28,
296
+ h2: 22,
297
+ h3: 18,
298
+ h4: 16,
299
+ h5: 14,
300
+ h6: 12
301
+ }
302
+ }
303
+ },
304
+ spacing: {
305
+ page: {
306
+ marginTop: 40,
307
+ marginRight: 40,
308
+ marginBottom: 40,
309
+ marginLeft: 40
310
+ },
311
+ sectionGap: 24,
312
+ paragraphGap: 10,
313
+ componentGap: 12
314
+ },
315
+ page: {
316
+ size: "A4",
317
+ orientation: "portrait"
318
+ }
319
+ };
320
+
321
+ // ../shared/src/themes/minimal.ts
322
+ var minimalTheme = {
323
+ name: "minimal",
324
+ primitives: defaultPrimitives,
325
+ colors: {
326
+ foreground: "#18181b",
327
+ background: "#ffffff",
328
+ muted: "#fafafa",
329
+ mutedForeground: "#a1a1aa",
330
+ primary: "#18181b",
331
+ primaryForeground: "#ffffff",
332
+ border: "#e4e4e7",
333
+ accent: "#71717a",
334
+ destructive: "#b91c1c",
335
+ success: "#15803d",
336
+ warning: "#a16207",
337
+ info: "#0369a1"
338
+ },
339
+ typography: {
340
+ body: {
341
+ fontFamily: "Helvetica",
342
+ fontSize: 11,
343
+ lineHeight: 1.65
344
+ },
345
+ heading: {
346
+ fontFamily: "Courier",
347
+ fontWeight: 600,
348
+ lineHeight: 1.25,
349
+ fontSize: {
350
+ h1: 24,
351
+ h2: 20,
352
+ h3: 16,
353
+ h4: 14,
354
+ h5: 12,
355
+ h6: 10
356
+ }
357
+ }
358
+ },
359
+ spacing: {
360
+ page: {
361
+ marginTop: 72,
362
+ marginRight: 56,
363
+ marginBottom: 72,
364
+ marginLeft: 56
365
+ },
366
+ sectionGap: 36,
367
+ paragraphGap: 14,
368
+ componentGap: 18
369
+ },
370
+ page: {
371
+ size: "A4",
372
+ orientation: "portrait"
373
+ }
374
+ };
375
+
376
+ // ../shared/src/themes/index.ts
377
+ var themePresets = {
378
+ professional: professionalTheme,
379
+ modern: modernTheme,
380
+ minimal: minimalTheme
381
+ };
382
+
383
+ // ../shared/src/errors.ts
384
+ var PdfxError = class extends Error {
385
+ constructor(message, code, suggestion) {
386
+ super(message);
387
+ this.code = code;
388
+ this.suggestion = suggestion;
389
+ this.name = "PdfxError";
390
+ }
391
+ };
392
+ var ConfigError = class extends PdfxError {
393
+ constructor(message, suggestion) {
394
+ super(message, "CONFIG_ERROR", suggestion);
395
+ this.name = "ConfigError";
396
+ }
397
+ };
398
+ var RegistryError = class extends PdfxError {
399
+ constructor(message, suggestion) {
400
+ super(message, "REGISTRY_ERROR", suggestion);
401
+ this.name = "RegistryError";
402
+ }
403
+ };
404
+ var NetworkError = class extends PdfxError {
405
+ constructor(message, suggestion) {
406
+ super(
407
+ message,
408
+ "NETWORK_ERROR",
409
+ suggestion ?? "Check your internet connection and registry URL"
410
+ );
411
+ this.name = "NetworkError";
412
+ }
413
+ };
414
+ var ValidationError = class extends PdfxError {
415
+ constructor(message, suggestion) {
416
+ super(message, "VALIDATION_ERROR", suggestion);
417
+ this.name = "ValidationError";
418
+ }
419
+ };
420
+
421
+ // src/commands/add.ts
422
+ import chalk from "chalk";
423
+ import { execa } from "execa";
424
+ import ora from "ora";
425
+ import prompts from "prompts";
426
+
427
+ // src/constants.ts
428
+ var DEFAULTS = {
429
+ REGISTRY_URL: "https://pdfx.akashpise.dev/r",
430
+ SCHEMA_URL: "https://pdfx.akashpise.dev/schema.json",
431
+ COMPONENT_DIR: "./src/components/pdfx",
432
+ THEME_FILE: "./src/lib/pdfx-theme.ts",
433
+ BLOCK_DIR: "./src/blocks/pdfx"
434
+ };
435
+ var REGISTRY_SUBPATHS = {
436
+ BLOCKS: "blocks"
437
+ };
438
+ var REQUIRED_VERSIONS = {
439
+ "@react-pdf/renderer": ">=3.0.0",
440
+ react: ">=16.8.0",
441
+ node: ">=20.0.0"
442
+ };
443
+ var FETCH_TIMEOUT_MS = 1e4;
444
+
445
+ // src/utils/dependency-validator.ts
446
+ import fs2 from "fs";
447
+ import path from "path";
448
+ import semver from "semver";
449
+
450
+ // src/utils/read-json.ts
451
+ import fs from "fs";
452
+ function readJsonFile(filePath) {
453
+ try {
454
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
455
+ } catch (error) {
456
+ const details = error instanceof Error ? error.message : String(error);
457
+ throw new ConfigError(`Failed to read ${filePath}: ${details}`);
458
+ }
459
+ }
460
+
461
+ // src/utils/dependency-validator.ts
462
+ function getPackageJson(cwd = process.cwd()) {
463
+ const pkgPath = path.join(cwd, "package.json");
464
+ if (!fs2.existsSync(pkgPath)) {
465
+ return null;
466
+ }
467
+ return readJsonFile(pkgPath);
468
+ }
469
+ function getInstalledVersion(packageName, cwd = process.cwd(), pkg) {
470
+ const resolved = pkg !== void 0 ? pkg : getPackageJson(cwd);
471
+ if (!resolved) return null;
472
+ const deps = {
473
+ ...resolved.dependencies,
474
+ ...resolved.devDependencies
475
+ };
476
+ const version = deps[packageName];
477
+ if (!version) return null;
478
+ return semver.clean(version) || semver.coerce(version)?.version || null;
479
+ }
480
+ function validateReactPdfRenderer(cwd = process.cwd(), pkg) {
481
+ const version = getInstalledVersion("@react-pdf/renderer", cwd, pkg);
482
+ const required = REQUIRED_VERSIONS["@react-pdf/renderer"];
483
+ if (!version) {
484
+ return {
485
+ valid: false,
486
+ installed: false,
487
+ requiredVersion: required,
488
+ message: "@react-pdf/renderer is not installed"
489
+ };
490
+ }
491
+ const isCompatible = semver.satisfies(version, required);
492
+ return {
493
+ valid: isCompatible,
494
+ installed: true,
495
+ currentVersion: version,
496
+ requiredVersion: required,
497
+ message: isCompatible ? "@react-pdf/renderer version is compatible" : `@react-pdf/renderer version ${version} does not meet requirement ${required}`
498
+ };
499
+ }
500
+ function validateReact(cwd = process.cwd(), pkg) {
501
+ const version = getInstalledVersion("react", cwd, pkg);
502
+ const required = REQUIRED_VERSIONS.react;
503
+ if (!version) {
504
+ return {
505
+ valid: false,
506
+ installed: false,
507
+ requiredVersion: required,
508
+ message: "React is not installed"
509
+ };
510
+ }
511
+ const isCompatible = semver.satisfies(version, required);
512
+ return {
513
+ valid: isCompatible,
514
+ installed: true,
515
+ currentVersion: version,
516
+ requiredVersion: required,
517
+ message: isCompatible ? "React version is compatible" : `React version ${version} does not meet requirement ${required}`
518
+ };
519
+ }
520
+ function validateNodeVersion() {
521
+ const version = process.version;
522
+ const required = REQUIRED_VERSIONS.node;
523
+ const cleanVersion = semver.clean(version);
524
+ if (!cleanVersion) {
525
+ return {
526
+ valid: false,
527
+ installed: true,
528
+ currentVersion: version,
529
+ requiredVersion: required,
530
+ message: `Unable to parse Node.js version: ${version}`
531
+ };
532
+ }
533
+ const isCompatible = semver.satisfies(cleanVersion, required);
534
+ return {
535
+ valid: isCompatible,
536
+ installed: true,
537
+ currentVersion: cleanVersion,
538
+ requiredVersion: required,
539
+ message: isCompatible ? "Node.js version is compatible" : `Node.js version ${cleanVersion} does not meet requirement ${required}`
540
+ };
541
+ }
542
+ function validateDependencies(cwd = process.cwd()) {
543
+ const pkg = getPackageJson(cwd);
544
+ return {
545
+ reactPdfRenderer: validateReactPdfRenderer(cwd, pkg),
546
+ react: validateReact(cwd, pkg),
547
+ nodeJs: validateNodeVersion()
548
+ };
549
+ }
550
+
551
+ // src/utils/file-system.ts
552
+ import fs3 from "fs";
553
+ import path2 from "path";
554
+ function ensureDir(dir) {
555
+ fs3.mkdirSync(dir, { recursive: true });
556
+ }
557
+ function writeFile(filePath, content) {
558
+ const dir = path2.dirname(filePath);
559
+ ensureDir(dir);
560
+ fs3.writeFileSync(filePath, content, "utf-8");
561
+ }
562
+ function checkFileExists(filePath) {
563
+ return fs3.existsSync(filePath);
564
+ }
565
+ function normalizePath(...segments) {
566
+ return path2.resolve(...segments);
567
+ }
568
+ function isPathWithinDirectory(resolvedPath, targetDir) {
569
+ const normalizedTarget = normalizePath(targetDir);
570
+ const normalizedResolved = normalizePath(resolvedPath);
571
+ if (normalizedResolved === normalizedTarget) return true;
572
+ const prefix = normalizedTarget.endsWith(path2.sep) ? normalizedTarget : normalizedTarget + path2.sep;
573
+ return normalizedResolved.startsWith(prefix);
574
+ }
575
+ function safePath(targetDir, fileName) {
576
+ const resolved = normalizePath(targetDir, fileName);
577
+ const normalizedTarget = normalizePath(targetDir);
578
+ if (!isPathWithinDirectory(resolved, normalizedTarget)) {
579
+ throw new Error(`Path "${fileName}" escapes target directory "${normalizedTarget}"`);
580
+ }
581
+ return resolved;
582
+ }
583
+
584
+ // src/utils/generate-theme.ts
585
+ function generateThemeFile(preset) {
586
+ return `// Generated by pdfx init.
587
+ // See: https://pdfx.akashpise.dev/installation#theming
588
+ //
589
+ // This module is the single source of truth for PDFX styling tokens.
590
+ // Update the exported \`theme\` object to customize component styles.
591
+
592
+ interface PdfxTheme {
593
+ name: string;
594
+ primitives: {
595
+ typography: Record<string, number>;
596
+ spacing: Record<string | number, number>;
597
+ fontWeights: { regular: number; medium: number; semibold: number; bold: number };
598
+ lineHeights: { tight: number; normal: number; relaxed: number };
599
+ borderRadius: { none: number; sm: number; md: number; lg: number; full: number };
600
+ letterSpacing: { tight: number; normal: number; wide: number; wider: number };
601
+ };
602
+ colors: {
603
+ foreground: string;
604
+ background: string;
605
+ muted: string;
606
+ mutedForeground: string;
607
+ primary: string;
608
+ primaryForeground: string;
609
+ border: string;
610
+ accent: string;
611
+ destructive: string;
612
+ success: string;
613
+ warning: string;
614
+ info: string;
615
+ };
616
+ typography: {
617
+ body: { fontFamily: string; fontSize: number; lineHeight: number };
618
+ heading: {
619
+ fontFamily: string;
620
+ fontWeight: number;
621
+ lineHeight: number;
622
+ fontSize: { h1: number; h2: number; h3: number; h4: number; h5: number; h6: number };
623
+ };
624
+ };
625
+ spacing: {
626
+ page: { marginTop: number; marginRight: number; marginBottom: number; marginLeft: number };
627
+ sectionGap: number;
628
+ paragraphGap: number;
629
+ componentGap: number;
630
+ };
631
+ page: {
632
+ size: 'A4' | 'LETTER' | 'LEGAL';
633
+ orientation: 'portrait' | 'landscape';
634
+ };
635
+ }
636
+
637
+ export const theme: PdfxTheme = ${serializeTheme(preset)};
638
+ `;
639
+ }
640
+ function serializeTheme(t) {
641
+ return `{
642
+ name: '${t.name}',
643
+
644
+ // Primitive scales.
645
+ primitives: {
646
+ typography: {
647
+ xs: ${t.primitives.typography.xs},
648
+ sm: ${t.primitives.typography.sm},
649
+ base: ${t.primitives.typography.base},
650
+ lg: ${t.primitives.typography.lg},
651
+ xl: ${t.primitives.typography.xl},
652
+ '2xl': ${t.primitives.typography["2xl"]},
653
+ '3xl': ${t.primitives.typography["3xl"]},
654
+ },
655
+ spacing: {
656
+ 0: ${t.primitives.spacing[0]},
657
+ 0.5: ${t.primitives.spacing[0.5]},
658
+ 1: ${t.primitives.spacing[1]},
659
+ 2: ${t.primitives.spacing[2]},
660
+ 3: ${t.primitives.spacing[3]},
661
+ 4: ${t.primitives.spacing[4]},
662
+ 5: ${t.primitives.spacing[5]},
663
+ 6: ${t.primitives.spacing[6]},
664
+ 8: ${t.primitives.spacing[8]},
665
+ 10: ${t.primitives.spacing[10]},
666
+ 12: ${t.primitives.spacing[12]},
667
+ 16: ${t.primitives.spacing[16]},
668
+ },
669
+ fontWeights: {
670
+ regular: ${t.primitives.fontWeights.regular},
671
+ medium: ${t.primitives.fontWeights.medium},
672
+ semibold: ${t.primitives.fontWeights.semibold},
673
+ bold: ${t.primitives.fontWeights.bold},
674
+ },
675
+ lineHeights: {
676
+ tight: ${t.primitives.lineHeights.tight},
677
+ normal: ${t.primitives.lineHeights.normal},
678
+ relaxed: ${t.primitives.lineHeights.relaxed},
679
+ },
680
+ borderRadius: {
681
+ none: ${t.primitives.borderRadius.none},
682
+ sm: ${t.primitives.borderRadius.sm},
683
+ md: ${t.primitives.borderRadius.md},
684
+ lg: ${t.primitives.borderRadius.lg},
685
+ full: ${t.primitives.borderRadius.full},
686
+ },
687
+ letterSpacing: {
688
+ tight: ${t.primitives.letterSpacing.tight},
689
+ normal: ${t.primitives.letterSpacing.normal},
690
+ wide: ${t.primitives.letterSpacing.wide},
691
+ wider: ${t.primitives.letterSpacing.wider},
692
+ },
693
+ },
694
+
695
+ // Semantic colors. Values must be valid for react-pdf.
696
+ colors: {
697
+ foreground: '${t.colors.foreground}',
698
+ background: '${t.colors.background}',
699
+ muted: '${t.colors.muted}',
700
+ mutedForeground: '${t.colors.mutedForeground}',
701
+ primary: '${t.colors.primary}',
702
+ primaryForeground: '${t.colors.primaryForeground}',
703
+ border: '${t.colors.border}',
704
+ accent: '${t.colors.accent}',
705
+ destructive: '${t.colors.destructive}',
706
+ success: '${t.colors.success}',
707
+ warning: '${t.colors.warning}',
708
+ info: '${t.colors.info}',
709
+ },
710
+
711
+ // Typography defaults.
712
+ typography: {
713
+ body: {
714
+ fontFamily: '${t.typography.body.fontFamily}',
715
+ fontSize: ${t.typography.body.fontSize},
716
+ lineHeight: ${t.typography.body.lineHeight},
717
+ },
718
+ heading: {
719
+ fontFamily: '${t.typography.heading.fontFamily}',
720
+ fontWeight: ${t.typography.heading.fontWeight},
721
+ lineHeight: ${t.typography.heading.lineHeight},
722
+ fontSize: {
723
+ h1: ${t.typography.heading.fontSize.h1},
724
+ h2: ${t.typography.heading.fontSize.h2},
725
+ h3: ${t.typography.heading.fontSize.h3},
726
+ h4: ${t.typography.heading.fontSize.h4},
727
+ h5: ${t.typography.heading.fontSize.h5},
728
+ h6: ${t.typography.heading.fontSize.h6},
729
+ },
730
+ },
731
+ },
732
+
733
+ // Layout spacing.
734
+ spacing: {
735
+ page: {
736
+ marginTop: ${t.spacing.page.marginTop},
737
+ marginRight: ${t.spacing.page.marginRight},
738
+ marginBottom: ${t.spacing.page.marginBottom},
739
+ marginLeft: ${t.spacing.page.marginLeft},
740
+ },
741
+ sectionGap: ${t.spacing.sectionGap},
742
+ paragraphGap: ${t.spacing.paragraphGap},
743
+ componentGap: ${t.spacing.componentGap},
744
+ },
745
+
746
+ // Page defaults.
747
+ page: {
748
+ size: '${t.page.size}',
749
+ orientation: '${t.page.orientation}',
750
+ },
751
+ }`;
752
+ }
753
+ function generateThemeContextFile() {
754
+ return `// Generated by pdfx init.
755
+ // See: https://pdfx.akashpise.dev/installation#theming
756
+ //
757
+ // Provides runtime theme overrides via React context.
758
+ // Wrap a subtree in <PdfxThemeProvider theme={myTheme}> to override defaults.
759
+
760
+ /* eslint-disable react-refresh/only-export-components */
761
+ // Intentional: this module exports both a component and hooks/context.
762
+ // Keeping a single import surface preserves the generated public API.
763
+
764
+ import { type DependencyList, type ReactNode, createContext, useContext } from 'react';
765
+ import { theme as defaultTheme } from './pdfx-theme';
766
+
767
+ type PdfxTheme = typeof defaultTheme;
768
+
769
+ export const PdfxThemeContext = createContext<PdfxTheme>(defaultTheme);
770
+
771
+ export interface PdfxThemeProviderProps {
772
+ theme?: PdfxTheme;
773
+ children: ReactNode;
774
+ }
775
+
776
+ export function PdfxThemeProvider({ theme, children }: PdfxThemeProviderProps) {
777
+ const resolvedTheme = theme ?? defaultTheme;
778
+ return <PdfxThemeContext.Provider value={resolvedTheme}>{children}</PdfxThemeContext.Provider>;
779
+ }
780
+
781
+ /**
782
+ * Returns the active theme from context.
783
+ *
784
+ * When called outside a React render tree (for example in plain-function tests),
785
+ * React may throw an invalid hook error. In that case we return defaultTheme.
786
+ * Unexpected errors are re-thrown.
787
+ */
788
+ export function usePdfxTheme(): PdfxTheme {
789
+ try {
790
+ return useContext(PdfxThemeContext);
791
+ } catch (error) {
792
+ // Suppress only known React dispatcher errors.
793
+ if (
794
+ error instanceof Error &&
795
+ /invalid hook call|useContext|cannot read properties of null/i.test(error.message)
796
+ ) {
797
+ return defaultTheme;
798
+ }
799
+ throw error;
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Calls factory() and returns the result.
805
+ * The deps parameter is accepted for API compatibility with existing callers.
806
+ */
807
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
808
+ export function useSafeMemo<T>(factory: () => T, _deps: DependencyList): T {
809
+ return factory();
810
+ }
811
+ `;
812
+ }
813
+
814
+ // src/utils/package-manager.ts
815
+ import fs4 from "fs";
816
+ import path3 from "path";
817
+ var PACKAGE_MANAGERS = {
818
+ pnpm: {
819
+ name: "pnpm",
820
+ lockfile: "pnpm-lock.yaml",
821
+ installCommand: "pnpm add"
822
+ },
823
+ yarn: {
824
+ name: "yarn",
825
+ lockfile: "yarn.lock",
826
+ installCommand: "yarn add"
827
+ },
828
+ bun: {
829
+ name: "bun",
830
+ lockfile: "bun.lock",
831
+ installCommand: "bun add"
832
+ },
833
+ npm: {
834
+ name: "npm",
835
+ lockfile: "package-lock.json",
836
+ installCommand: "npm install"
837
+ }
838
+ };
839
+ function findPackageRoot(startDir) {
840
+ let dir = path3.resolve(startDir);
841
+ const fsRoot = path3.parse(dir).root;
842
+ while (dir !== fsRoot) {
843
+ const pkgPath = path3.join(dir, "package.json");
844
+ if (fs4.existsSync(pkgPath)) {
845
+ try {
846
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
847
+ const hasWorkspacesField = Array.isArray(pkg.workspaces) || typeof pkg.workspaces === "object" && pkg.workspaces !== null;
848
+ const hasPnpmWorkspaceFile = fs4.existsSync(path3.join(dir, "pnpm-workspace.yaml"));
849
+ if (!hasWorkspacesField && !hasPnpmWorkspaceFile) {
850
+ return dir;
851
+ }
852
+ } catch {
853
+ return dir;
854
+ }
855
+ }
856
+ dir = path3.dirname(dir);
857
+ }
858
+ return startDir;
859
+ }
860
+ function detectPackageManager(startDir = process.cwd()) {
861
+ const managers = ["pnpm", "yarn", "bun", "npm"];
862
+ let dir = path3.resolve(startDir);
863
+ const fsRoot = path3.parse(dir).root;
864
+ while (dir !== fsRoot) {
865
+ for (const manager of managers) {
866
+ const info = PACKAGE_MANAGERS[manager];
867
+ if (fs4.existsSync(path3.join(dir, info.lockfile))) {
868
+ return info;
869
+ }
870
+ }
871
+ if (fs4.existsSync(path3.join(dir, "bun.lockb"))) {
872
+ return PACKAGE_MANAGERS.bun;
873
+ }
874
+ dir = path3.dirname(dir);
875
+ }
876
+ return PACKAGE_MANAGERS.npm;
877
+ }
878
+ function getInstallCommand(packageManager, packages, devDependency = false) {
879
+ const pm = PACKAGE_MANAGERS[packageManager];
880
+ const devFlag = devDependency ? packageManager === "npm" ? "--save-dev" : "-D" : "";
881
+ return `${pm.installCommand} ${packages.join(" ")} ${devFlag}`.trim();
882
+ }
883
+
884
+ // src/commands/add.ts
885
+ function readConfig(configPath) {
886
+ const raw = readJsonFile(configPath);
887
+ const result = configSchema.safeParse(raw);
888
+ if (!result.success) {
889
+ const issues = result.error.issues.map((i) => {
890
+ const fieldPath = i.path.length > 0 ? i.path.join(".") : "root";
891
+ return `"${fieldPath}": ${i.message}`;
892
+ }).join("; ");
893
+ throw new ConfigError(
894
+ `Invalid pdfx.json: ${issues}`,
895
+ `Fix the config or re-run ${chalk.cyan("npx pdfx-cli@latest init")}`
896
+ );
897
+ }
898
+ return result.data;
899
+ }
900
+ async function fetchComponent(name, registryUrl) {
901
+ const url = `${registryUrl}/${name}.json`;
902
+ let response;
903
+ try {
904
+ response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
905
+ } catch (err) {
906
+ const isTimeout = err instanceof Error && err.name === "TimeoutError";
907
+ throw new NetworkError(
908
+ isTimeout ? `Registry request timed out after 10 seconds.
909
+ ${chalk.dim("Check your internet connection or try again later.")}` : `Could not reach registry at ${registryUrl}
910
+ ${chalk.dim("Verify the URL is correct and you have internet access.")}`
911
+ );
912
+ }
913
+ if (!response.ok) {
914
+ throw new RegistryError(
915
+ response.status === 404 ? `Component "${name}" not found in registry` : `Registry returned HTTP ${response.status}`
916
+ );
917
+ }
918
+ let data;
919
+ try {
920
+ data = await response.json();
921
+ } catch {
922
+ throw new RegistryError(`Invalid response for "${name}": not valid JSON`);
923
+ }
924
+ const result = registryItemSchema.safeParse(data);
925
+ if (!result.success) {
926
+ throw new RegistryError(
927
+ `Invalid registry entry for "${name}": ${result.error.issues[0]?.message}`
928
+ );
929
+ }
930
+ return result.data;
931
+ }
932
+ function resolveThemeImport(componentDir, themePath, fileContent) {
933
+ const absComponentDir = path4.resolve(process.cwd(), componentDir);
934
+ const absThemePath = path4.resolve(process.cwd(), themePath);
935
+ const themeImportTarget = absThemePath.replace(/\.tsx?$/, "");
936
+ let relativePath = path4.relative(absComponentDir, themeImportTarget);
937
+ if (!relativePath.startsWith(".")) {
938
+ relativePath = `./${relativePath}`;
939
+ }
940
+ const absContextPath = path4.join(path4.dirname(absThemePath), "pdfx-theme-context");
941
+ let relativeContextPath = path4.relative(absComponentDir, absContextPath);
942
+ if (!relativeContextPath.startsWith(".")) {
943
+ relativeContextPath = `./${relativeContextPath}`;
944
+ }
945
+ let content = fileContent.replace(
946
+ /from\s+['"]\.\.\/lib\/pdfx-theme['"]/g,
947
+ `from '${relativePath}'`
948
+ );
949
+ content = content.replace(
950
+ /from\s+['"]\.\.\/lib\/pdfx-theme-context['"]/g,
951
+ `from '${relativeContextPath}'`
952
+ );
953
+ return content;
954
+ }
955
+ function collectComponentDependencies(items) {
956
+ const runtime = /* @__PURE__ */ new Set();
957
+ const dev = /* @__PURE__ */ new Set();
958
+ for (const item of items) {
959
+ for (const dep of item.dependencies ?? []) {
960
+ runtime.add(dep);
961
+ }
962
+ for (const dep of item.devDependencies ?? []) {
963
+ dev.add(dep);
964
+ }
965
+ }
966
+ return {
967
+ runtime: [...runtime],
968
+ dev: [...dev]
969
+ };
970
+ }
971
+ function getDeclaredDependencies(pkg) {
972
+ const deps = pkg.dependencies ?? {};
973
+ const devDeps = pkg.devDependencies ?? {};
974
+ return /* @__PURE__ */ new Set([...Object.keys(deps), ...Object.keys(devDeps)]);
975
+ }
976
+ function findMissingDependencies(requirements, pkg) {
977
+ const installed = getDeclaredDependencies(pkg);
978
+ return {
979
+ runtime: requirements.runtime.filter((dep) => !installed.has(dep)),
980
+ dev: requirements.dev.filter((dep) => !installed.has(dep))
981
+ };
982
+ }
983
+ function resolveDependencyInstallMode(options) {
984
+ if (options.installDeps === true) return "always";
985
+ if (options.installDeps === false) return "never";
986
+ return "prompt";
987
+ }
988
+ async function installDependencySet(packageRoot, runtimeDeps, devDeps) {
989
+ const pm = detectPackageManager(packageRoot);
990
+ const installArgs = pm.installCommand.split(" ").slice(1);
991
+ if (runtimeDeps.length > 0) {
992
+ await execa(pm.name, [...installArgs, ...runtimeDeps], {
993
+ cwd: packageRoot,
994
+ stdio: "pipe"
995
+ });
996
+ }
997
+ if (devDeps.length > 0) {
998
+ const devFlag = pm.name === "npm" ? "--save-dev" : "-D";
999
+ await execa(pm.name, [...installArgs, ...devDeps, devFlag], {
1000
+ cwd: packageRoot,
1001
+ stdio: "pipe"
1002
+ });
1003
+ }
1004
+ }
1005
+ function formatDependencyInstallHint(packageRoot, runtimeDeps, devDeps) {
1006
+ const pm = detectPackageManager(packageRoot);
1007
+ const commands = [];
1008
+ if (runtimeDeps.length > 0) {
1009
+ commands.push(getInstallCommand(pm.name, runtimeDeps));
1010
+ }
1011
+ if (devDeps.length > 0) {
1012
+ commands.push(getInstallCommand(pm.name, devDeps, true));
1013
+ }
1014
+ return commands.map((cmd) => ` ${cmd}`).join("\n");
1015
+ }
1016
+ async function ensureComponentDependencies(requirements, options) {
1017
+ const packageRoot = findPackageRoot(process.cwd());
1018
+ const pkgPath = path4.join(packageRoot, "package.json");
1019
+ if (!fs5.existsSync(pkgPath)) {
1020
+ throw new ValidationError(
1021
+ `Missing package.json at ${packageRoot}`,
1022
+ "Run this command inside a Node.js project"
1023
+ );
1024
+ }
1025
+ const pkg = readJsonFile(pkgPath);
1026
+ const missing = findMissingDependencies(requirements, pkg);
1027
+ if (missing.runtime.length === 0 && missing.dev.length === 0) {
1028
+ return;
1029
+ }
1030
+ const installMode = resolveDependencyInstallMode(options);
1031
+ const strictDeps = options.strictDeps ?? false;
1032
+ const installHint = formatDependencyInstallHint(packageRoot, missing.runtime, missing.dev);
1033
+ if (installMode === "never") {
1034
+ const hasBlocking = missing.runtime.length > 0 || strictDeps && missing.dev.length > 0;
1035
+ const missingMessage = [
1036
+ missing.runtime.length > 0 ? `runtime: ${missing.runtime.join(", ")}` : void 0,
1037
+ missing.dev.length > 0 ? `dev: ${missing.dev.join(", ")}` : void 0
1038
+ ].filter(Boolean).join("; ");
1039
+ if (hasBlocking) {
1040
+ throw new ValidationError(
1041
+ `Missing component dependencies (${missingMessage})`,
1042
+ `Install manually:
1043
+ ${installHint}`
1044
+ );
1045
+ }
1046
+ console.log(
1047
+ chalk.yellow(
1048
+ `
1049
+ \u26A0 Missing devDependencies (${missing.dev.join(", ")})
1050
+ ${chalk.dim("\u2192")} Install manually:
1051
+ ${chalk.cyan(installHint)}
1052
+ `
1053
+ )
1054
+ );
1055
+ return;
1056
+ }
1057
+ let shouldInstall = installMode === "always";
1058
+ if (installMode === "prompt") {
1059
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1060
+ if (!interactive) {
1061
+ throw new ValidationError(
1062
+ "Missing component dependencies in non-interactive mode",
1063
+ `Use --install-deps or install manually:
1064
+ ${installHint}`
1065
+ );
1066
+ }
1067
+ const depSummary = [
1068
+ missing.runtime.length > 0 ? `runtime: ${missing.runtime.join(", ")}` : void 0,
1069
+ missing.dev.length > 0 ? `dev: ${missing.dev.join(", ")}` : void 0
1070
+ ].filter(Boolean).join("\n ");
1071
+ console.log(chalk.yellow("\n Missing component dependencies detected:\n"));
1072
+ console.log(chalk.dim(` ${depSummary}
1073
+ `));
1074
+ const response = await prompts({
1075
+ type: "confirm",
1076
+ name: "install",
1077
+ message: "Install missing dependencies now?",
1078
+ initial: true
1079
+ });
1080
+ shouldInstall = response.install === true;
1081
+ }
1082
+ if (!shouldInstall) {
1083
+ throw new ValidationError(
1084
+ "Dependency installation cancelled",
1085
+ `Install manually:
1086
+ ${installHint}`
1087
+ );
1088
+ }
1089
+ const spinner = ora("Installing missing component dependencies...").start();
1090
+ try {
1091
+ await installDependencySet(packageRoot, missing.runtime, missing.dev);
1092
+ spinner.succeed("Installed component dependencies");
1093
+ } catch (error) {
1094
+ spinner.fail("Failed to install component dependencies");
1095
+ const message = error instanceof Error ? error.message : String(error);
1096
+ if (strictDeps || missing.runtime.length > 0) {
1097
+ throw new ValidationError(
1098
+ `Dependency installation failed: ${message}`,
1099
+ `Install manually:
1100
+ ${installHint}`
1101
+ );
1102
+ }
1103
+ console.log(
1104
+ chalk.yellow(
1105
+ `
1106
+ \u26A0 Could not install devDependencies automatically
1107
+ ${chalk.dim("\u2192")} Install manually:
1108
+ ${chalk.cyan(installHint)}
1109
+ `
1110
+ )
1111
+ );
1112
+ }
1113
+ }
1114
+ async function installComponent(component, config, force) {
1115
+ const targetDir = path4.resolve(process.cwd(), config.componentDir);
1116
+ const componentDir = path4.join(targetDir, component.name);
1117
+ ensureDir(componentDir);
1118
+ const componentRelDir = path4.join(config.componentDir, component.name);
1119
+ const filesToWrite = [];
1120
+ for (const file of component.files) {
1121
+ const fileName = path4.basename(file.path);
1122
+ const filePath = safePath(componentDir, fileName);
1123
+ let content = file.content;
1124
+ if (config.theme && (content.includes("pdfx-theme") || content.includes("pdfx-theme-context"))) {
1125
+ content = resolveThemeImport(componentRelDir, config.theme, content);
1126
+ }
1127
+ filesToWrite.push({ filePath, content });
1128
+ }
1129
+ if (!force) {
1130
+ const existing = filesToWrite.filter((f) => checkFileExists(f.filePath));
1131
+ if (existing.length > 0) {
1132
+ const fileNames = existing.map((f) => path4.basename(f.filePath)).join(", ");
1133
+ throw new ValidationError(
1134
+ `${component.name}: already exists (${fileNames}), skipped install (use --force to overwrite)`
1135
+ );
1136
+ }
1137
+ }
1138
+ for (const file of filesToWrite) {
1139
+ writeFile(file.filePath, file.content);
1140
+ }
1141
+ if (config.theme) {
1142
+ const absThemePath = path4.resolve(process.cwd(), config.theme);
1143
+ const contextPath = path4.join(path4.dirname(absThemePath), "pdfx-theme-context.tsx");
1144
+ if (!checkFileExists(contextPath)) {
1145
+ ensureDir(path4.dirname(contextPath));
1146
+ writeFile(contextPath, generateThemeContextFile());
1147
+ }
1148
+ }
1149
+ }
1150
+ async function add(components, options = {}) {
1151
+ if (!components || components.length === 0) {
1152
+ console.error(chalk.red("Error: Component name required"));
1153
+ console.log(chalk.dim("Usage: npx pdfx-cli@latest add <component...>"));
1154
+ console.log(chalk.dim("Example: npx pdfx-cli@latest add heading text table\n"));
1155
+ process.exit(1);
1156
+ }
1157
+ const reactPdfCheck = validateReactPdfRenderer();
1158
+ if (!reactPdfCheck.installed) {
1159
+ console.error(chalk.red("\nError: @react-pdf/renderer is not installed\n"));
1160
+ console.log(chalk.yellow(" PDFx components require @react-pdf/renderer to work.\n"));
1161
+ console.log(chalk.cyan(" Run: npx pdfx-cli@latest init"));
1162
+ console.log(chalk.dim(" or install manually: npm install @react-pdf/renderer\n"));
1163
+ process.exit(1);
1164
+ }
1165
+ if (!reactPdfCheck.valid) {
1166
+ console.log(
1167
+ chalk.yellow(
1168
+ `
1169
+ \u26A0 Warning: ${reactPdfCheck.message}
1170
+ ${chalk.dim("\u2192")} You may encounter compatibility issues
1171
+ `
1172
+ )
1173
+ );
1174
+ }
1175
+ const configPath = path4.join(process.cwd(), "pdfx.json");
1176
+ if (!checkFileExists(configPath)) {
1177
+ console.error(chalk.red("Error: pdfx.json not found"));
1178
+ console.log(chalk.yellow("Run: npx pdfx-cli@latest init"));
1179
+ process.exit(1);
1180
+ }
1181
+ let config;
1182
+ try {
1183
+ config = readConfig(configPath);
1184
+ if (options.registry) {
1185
+ config = { ...config, registry: options.registry };
1186
+ }
1187
+ } catch (error) {
1188
+ if (error instanceof ConfigError) {
1189
+ console.error(chalk.red(error.message));
1190
+ if (error.suggestion) console.log(chalk.yellow(` Hint: ${error.suggestion}`));
1191
+ } else {
1192
+ const message = error instanceof Error ? error.message : String(error);
1193
+ console.error(chalk.red(message));
1194
+ }
1195
+ process.exit(1);
1196
+ }
1197
+ const force = options.force ?? false;
1198
+ const failed = [];
1199
+ let installedCount = 0;
1200
+ const resolvedComponents = [];
1201
+ const validNames = [];
1202
+ for (const componentName of components) {
1203
+ const nameResult = componentNameSchema.safeParse(componentName);
1204
+ if (!nameResult.success) {
1205
+ console.error(chalk.red(`Invalid component name: "${componentName}"`));
1206
+ console.log(
1207
+ chalk.dim(' Names must be lowercase alphanumeric with hyphens (e.g., "data-table")')
1208
+ );
1209
+ failed.push(componentName);
1210
+ continue;
1211
+ }
1212
+ validNames.push(componentName);
1213
+ }
1214
+ for (const componentName of validNames) {
1215
+ const spinner = ora(`Resolving ${componentName}...`).start();
1216
+ try {
1217
+ const component = await fetchComponent(componentName, config.registry);
1218
+ resolvedComponents.push(component);
1219
+ spinner.succeed(`Resolved ${componentName}`);
1220
+ } catch (error) {
1221
+ spinner.fail(`Failed to resolve ${componentName}`);
1222
+ const message = error instanceof Error ? error.message : String(error);
1223
+ console.error(chalk.dim(` ${message}`));
1224
+ failed.push(componentName);
1225
+ }
1226
+ }
1227
+ if (resolvedComponents.length > 0) {
1228
+ try {
1229
+ const requirements = collectComponentDependencies(resolvedComponents);
1230
+ await ensureComponentDependencies(requirements, options);
1231
+ } catch (error) {
1232
+ const message = error instanceof Error ? error.message : String(error);
1233
+ console.error(chalk.red(message));
1234
+ if (error instanceof ValidationError && error.suggestion) {
1235
+ console.log(chalk.yellow(` Hint: ${error.suggestion}`));
1236
+ }
1237
+ process.exit(1);
1238
+ }
1239
+ }
1240
+ for (const component of resolvedComponents) {
1241
+ const spinner = ora(`Adding ${component.name}...`).start();
1242
+ try {
1243
+ await installComponent(component, config, force);
1244
+ installedCount++;
1245
+ spinner.succeed(`Added ${component.name}`);
1246
+ } catch (error) {
1247
+ let shouldMarkAsFailed = true;
1248
+ if (error instanceof ValidationError && error.message.includes("already exists")) {
1249
+ spinner.info(error.message);
1250
+ shouldMarkAsFailed = false;
1251
+ } else if (error instanceof NetworkError || error instanceof RegistryError || error instanceof ValidationError) {
1252
+ spinner.fail(error.message);
1253
+ if (error.suggestion) {
1254
+ console.log(chalk.dim(` Hint: ${error.suggestion}`));
1255
+ }
1256
+ } else {
1257
+ spinner.fail(`Failed to add ${component.name}`);
1258
+ const message = error instanceof Error ? error.message : String(error);
1259
+ console.error(chalk.dim(` ${message}`));
1260
+ }
1261
+ if (shouldMarkAsFailed) {
1262
+ failed.push(component.name);
1263
+ }
1264
+ }
1265
+ }
1266
+ console.log();
1267
+ if (failed.length > 0) {
1268
+ console.log(chalk.yellow(`Failed to add: ${failed.join(", ")}`));
1269
+ }
1270
+ if (installedCount > 0) {
1271
+ const resolvedDir = path4.resolve(process.cwd(), config.componentDir);
1272
+ console.log(chalk.green("Done!"));
1273
+ console.log(chalk.dim(`Components installed to: ${resolvedDir}
1274
+ `));
1275
+ }
1276
+ if (failed.length > 0) {
1277
+ process.exit(1);
1278
+ }
1279
+ }
1280
+
1281
+ // src/commands/block.ts
1282
+ import path5 from "path";
1283
+ import chalk2 from "chalk";
1284
+ import ora2 from "ora";
1285
+ import prompts2 from "prompts";
1286
+ async function fetchBlock(name, registryUrl) {
1287
+ const url = `${registryUrl}/${REGISTRY_SUBPATHS.BLOCKS}/${name}.json`;
1288
+ let response;
1289
+ try {
1290
+ response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
1291
+ } catch (err) {
1292
+ const isTimeout = err instanceof Error && err.name === "TimeoutError";
1293
+ throw new NetworkError(
1294
+ isTimeout ? "Registry request timed out" : `Could not reach ${registryUrl}`
1295
+ );
1296
+ }
1297
+ if (!response.ok) {
1298
+ throw new RegistryError(
1299
+ response.status === 404 ? `Block "${name}" not found in registry` : `Registry returned HTTP ${response.status}`,
1300
+ response.status === 404 ? 'Run "npx pdfx-cli@latest block list" to see all available blocks' : void 0
1301
+ );
1302
+ }
1303
+ let data;
1304
+ try {
1305
+ data = await response.json();
1306
+ } catch {
1307
+ throw new RegistryError(`Invalid response for "${name}": not valid JSON`);
1308
+ }
1309
+ const result = registryItemSchema.safeParse(data);
1310
+ if (!result.success) {
1311
+ throw new RegistryError(
1312
+ `Invalid registry entry for "${name}": ${result.error.issues[0]?.message}`
1313
+ );
1314
+ }
1315
+ return result.data;
1316
+ }
1317
+ function resolveBlockImports(content, blockName, config) {
1318
+ const cwd = process.cwd();
1319
+ const blockSubdir = path5.resolve(cwd, config.blockDir ?? DEFAULTS.BLOCK_DIR, blockName);
1320
+ let result = content.replace(
1321
+ /from '\.\.\/\.\.\/components\/pdfx\/([a-z][a-z0-9-]*)\/pdfx-([a-z][a-z0-9-]*)'/g,
1322
+ (_match, componentName) => {
1323
+ const absCompFile = path5.resolve(
1324
+ cwd,
1325
+ config.componentDir,
1326
+ componentName,
1327
+ `pdfx-${componentName}`
1328
+ );
1329
+ let rel = path5.relative(blockSubdir, absCompFile);
1330
+ if (!rel.startsWith(".")) rel = `./${rel}`;
1331
+ return `from '${rel}'`;
1332
+ }
1333
+ );
1334
+ if (config.theme) {
1335
+ const absThemePath = path5.resolve(cwd, config.theme).replace(/\.tsx?$/, "");
1336
+ let relTheme = path5.relative(blockSubdir, absThemePath);
1337
+ if (!relTheme.startsWith(".")) relTheme = `./${relTheme}`;
1338
+ const absContextPath = path5.join(
1339
+ path5.dirname(path5.resolve(cwd, config.theme)),
1340
+ "pdfx-theme-context"
1341
+ );
1342
+ let relContext = path5.relative(blockSubdir, absContextPath);
1343
+ if (!relContext.startsWith(".")) relContext = `./${relContext}`;
1344
+ result = result.replace(/from '\.\.\/\.\.\/lib\/pdfx-theme'/g, `from '${relTheme}'`);
1345
+ result = result.replace(/from '\.\.\/\.\.\/lib\/pdfx-theme-context'/g, `from '${relContext}'`);
1346
+ }
1347
+ return result;
1348
+ }
1349
+ async function resolveConflict(fileName, currentDecision) {
1350
+ if (currentDecision === "overwrite-all") return "overwrite-all";
1351
+ const { action } = await prompts2({
1352
+ type: "select",
1353
+ name: "action",
1354
+ message: `${chalk2.yellow(fileName)} already exists. What would you like to do?`,
1355
+ choices: [
1356
+ { title: "Skip", value: "skip", description: "Keep the existing file unchanged" },
1357
+ { title: "Overwrite", value: "overwrite", description: "Replace this file only" },
1358
+ {
1359
+ title: "Overwrite all",
1360
+ value: "overwrite-all",
1361
+ description: "Replace all conflicting files"
1362
+ }
1363
+ ],
1364
+ initial: 0
1365
+ });
1366
+ if (!action) throw new ValidationError("Cancelled by user");
1367
+ return action;
1368
+ }
1369
+ async function ensurePeerComponents(block, config, force) {
1370
+ const installedPeers = [];
1371
+ const peerWarnings = [];
1372
+ if (!block.peerComponents || block.peerComponents.length === 0) {
1373
+ return { installedPeers, peerWarnings, allSkipped: false };
1374
+ }
1375
+ const componentBaseDir = path5.resolve(process.cwd(), config.componentDir);
1376
+ for (const componentName of block.peerComponents) {
1377
+ const componentDir = path5.join(componentBaseDir, componentName);
1378
+ const expectedMain = path5.join(componentDir, `pdfx-${componentName}.tsx`);
1379
+ if (checkFileExists(componentDir)) {
1380
+ if (!checkFileExists(expectedMain)) {
1381
+ peerWarnings.push(
1382
+ `${componentName}: directory exists but expected file missing (${path5.basename(expectedMain)})`
1383
+ );
1384
+ continue;
1385
+ }
1386
+ if (!force) {
1387
+ peerWarnings.push(
1388
+ `${componentName}: already exists, skipped install (use "npx pdfx-cli@latest diff ${componentName}" to verify freshness)`
1389
+ );
1390
+ continue;
1391
+ }
1392
+ }
1393
+ const component = await fetchComponent(componentName, config.registry);
1394
+ ensureDir(componentDir);
1395
+ const componentRelDir = path5.join(config.componentDir, component.name);
1396
+ for (const file of component.files) {
1397
+ const fileName = path5.basename(file.path);
1398
+ const filePath = safePath(componentDir, fileName);
1399
+ let content = file.content;
1400
+ if (config.theme && (content.includes("pdfx-theme") || content.includes("pdfx-theme-context"))) {
1401
+ content = resolveThemeImport(componentRelDir, config.theme, content);
1402
+ }
1403
+ writeFile(filePath, content);
1404
+ }
1405
+ installedPeers.push(componentName);
1406
+ }
1407
+ return { installedPeers, peerWarnings, allSkipped: false };
1408
+ }
1409
+ async function installBlock(name, config, force, spinner) {
1410
+ const block = await fetchBlock(name, config.registry);
1411
+ const peerResult = await ensurePeerComponents(block, config, force);
1412
+ const blockBaseDir = path5.resolve(process.cwd(), config.blockDir ?? DEFAULTS.BLOCK_DIR);
1413
+ const blockDir = path5.join(blockBaseDir, block.name);
1414
+ ensureDir(blockDir);
1415
+ const filesToWrite = [];
1416
+ let allSkipped = false;
1417
+ let globalDecision = null;
1418
+ const resolved = [];
1419
+ for (const file of block.files) {
1420
+ const fileName = path5.basename(file.path);
1421
+ const filePath = safePath(blockDir, fileName);
1422
+ let content = file.content;
1423
+ if (/\.(tsx?|jsx?)$/.test(fileName) && content.includes("../../")) {
1424
+ content = resolveBlockImports(content, block.name, config);
1425
+ }
1426
+ filesToWrite.push({ filePath, content });
1427
+ }
1428
+ if (!force) {
1429
+ const hasConflicts = filesToWrite.some((f) => checkFileExists(f.filePath));
1430
+ if (hasConflicts) {
1431
+ spinner.stop();
1432
+ }
1433
+ for (const file of filesToWrite) {
1434
+ if (checkFileExists(file.filePath)) {
1435
+ const decision = await resolveConflict(path5.basename(file.filePath), globalDecision);
1436
+ if (decision === "overwrite-all") {
1437
+ globalDecision = "overwrite-all";
1438
+ }
1439
+ resolved.push({ ...file, skip: decision === "skip" });
1440
+ } else {
1441
+ resolved.push({ ...file, skip: false });
1442
+ }
1443
+ }
1444
+ allSkipped = resolved.every((f) => f.skip);
1445
+ for (const file of resolved) {
1446
+ if (!file.skip) {
1447
+ writeFile(file.filePath, file.content);
1448
+ } else {
1449
+ console.log(chalk2.dim(` skipped ${path5.basename(file.filePath)}`));
1450
+ }
1451
+ }
1452
+ } else {
1453
+ for (const file of filesToWrite) {
1454
+ writeFile(file.filePath, file.content);
1455
+ }
1456
+ }
1457
+ if (config.theme) {
1458
+ const absThemePath = path5.resolve(process.cwd(), config.theme);
1459
+ const contextPath = path5.join(path5.dirname(absThemePath), "pdfx-theme-context.tsx");
1460
+ if (!checkFileExists(contextPath)) {
1461
+ ensureDir(path5.dirname(contextPath));
1462
+ writeFile(contextPath, generateThemeContextFile());
1463
+ }
1464
+ }
1465
+ return { ...peerResult, allSkipped };
1466
+ }
1467
+ async function blockAdd(names, options = {}) {
1468
+ const configPath = path5.join(process.cwd(), "pdfx.json");
1469
+ let config;
1470
+ const force = options.force ?? false;
1471
+ const failed = [];
1472
+ let installedCount = 0;
1473
+ if (!checkFileExists(configPath)) {
1474
+ console.error(chalk2.red("Error: pdfx.json not found"));
1475
+ console.log(chalk2.yellow("Run: npx pdfx-cli@latest init"));
1476
+ process.exit(1);
1477
+ }
1478
+ try {
1479
+ config = readConfig(configPath);
1480
+ } catch (error) {
1481
+ if (error instanceof ConfigError) {
1482
+ console.error(chalk2.red(error.message));
1483
+ if (error.suggestion) console.log(chalk2.yellow(` Hint: ${error.suggestion}`));
1484
+ } else {
1485
+ const message = error instanceof Error ? error.message : String(error);
1486
+ console.error(chalk2.red(message));
1487
+ }
1488
+ process.exit(1);
1489
+ }
1490
+ for (const blockName of names) {
1491
+ const nameResult = componentNameSchema.safeParse(blockName);
1492
+ if (!nameResult.success) {
1493
+ console.error(chalk2.red(`Invalid block name: "${blockName}"`));
1494
+ console.log(
1495
+ chalk2.dim(' Names must be lowercase alphanumeric with hyphens (e.g., "invoice-classic")')
1496
+ );
1497
+ failed.push(blockName);
1498
+ continue;
1499
+ }
1500
+ const spinner = ora2(`Adding block ${blockName}...`).start();
1501
+ try {
1502
+ const result = await installBlock(blockName, config, force, spinner);
1503
+ if (result.allSkipped) {
1504
+ spinner.info(
1505
+ `Skipped ${chalk2.cyan(blockName)} \u2014 all files already exist (use ${chalk2.cyan("--force")} to overwrite)`
1506
+ );
1507
+ } else {
1508
+ installedCount++;
1509
+ spinner.succeed(`Added block ${chalk2.cyan(blockName)}`);
1510
+ if (result.installedPeers.length > 0) {
1511
+ console.log(
1512
+ chalk2.green(` Installed required components: ${result.installedPeers.join(", ")}`)
1513
+ );
1514
+ }
1515
+ for (const warning of result.peerWarnings) {
1516
+ console.log(chalk2.yellow(` Warning: ${warning}`));
1517
+ }
1518
+ }
1519
+ } catch (error) {
1520
+ if (error instanceof ValidationError && error.message.includes("Cancelled")) {
1521
+ spinner.info("Cancelled");
1522
+ process.exit(0);
1523
+ } else if (error instanceof NetworkError || error instanceof RegistryError || error instanceof ValidationError) {
1524
+ spinner.fail(error.message);
1525
+ if (error.suggestion) {
1526
+ console.log(chalk2.dim(` Hint: ${error.suggestion}`));
1527
+ }
1528
+ } else {
1529
+ spinner.fail(`Failed to add block ${blockName}`);
1530
+ const message = error instanceof Error ? error.message : String(error);
1531
+ console.error(chalk2.dim(` ${message}`));
1532
+ }
1533
+ failed.push(blockName);
1534
+ }
1535
+ }
1536
+ console.log();
1537
+ if (failed.length > 0) {
1538
+ console.log(chalk2.yellow(`Failed: ${failed.join(", ")}`));
1539
+ }
1540
+ if (installedCount > 0) {
1541
+ const resolvedDir = path5.resolve(process.cwd(), config.blockDir ?? DEFAULTS.BLOCK_DIR);
1542
+ console.log(chalk2.green("Done!"));
1543
+ console.log(chalk2.dim(`Blocks installed to: ${resolvedDir}
1544
+ `));
1545
+ }
1546
+ if (failed.length > 0) {
1547
+ process.exit(1);
1548
+ }
1549
+ }
1550
+ async function blockList() {
1551
+ const configPath = path5.join(process.cwd(), "pdfx.json");
1552
+ if (!checkFileExists(configPath)) {
1553
+ console.error(chalk2.red("Error: pdfx.json not found"));
1554
+ console.log(chalk2.yellow("Run: npx pdfx-cli@latest init"));
1555
+ process.exit(1);
1556
+ }
1557
+ let config;
1558
+ try {
1559
+ config = readConfig(configPath);
1560
+ } catch (error) {
1561
+ if (error instanceof ConfigError) {
1562
+ console.error(chalk2.red(error.message));
1563
+ if (error.suggestion) console.log(chalk2.yellow(` Hint: ${error.suggestion}`));
1564
+ } else {
1565
+ const message = error instanceof Error ? error.message : String(error);
1566
+ console.error(chalk2.red(message));
1567
+ }
1568
+ process.exit(1);
1569
+ }
1570
+ const spinner = ora2("Fetching block list...").start();
1571
+ try {
1572
+ let response;
1573
+ try {
1574
+ response = await fetch(`${config.registry}/index.json`, {
1575
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
1576
+ });
1577
+ } catch (err) {
1578
+ const isTimeout = err instanceof Error && err.name === "TimeoutError";
1579
+ throw new NetworkError(
1580
+ isTimeout ? "Registry request timed out" : `Could not reach ${config.registry}`
1581
+ );
1582
+ }
1583
+ if (!response.ok) {
1584
+ throw new RegistryError(`Registry returned HTTP ${response.status}`);
1585
+ }
1586
+ let data;
1587
+ try {
1588
+ data = await response.json();
1589
+ } catch {
1590
+ throw new RegistryError("Invalid response from registry: not valid JSON");
1591
+ }
1592
+ const result = registrySchema.safeParse(data);
1593
+ if (!result.success) {
1594
+ throw new RegistryError("Invalid registry format");
1595
+ }
1596
+ spinner.stop();
1597
+ const blocks = result.data.items.filter((item) => item.type === "registry:block");
1598
+ if (blocks.length === 0) {
1599
+ console.log(chalk2.dim("\n No blocks available in the registry.\n"));
1600
+ return;
1601
+ }
1602
+ const blockBaseDir = path5.resolve(process.cwd(), config.blockDir ?? DEFAULTS.BLOCK_DIR);
1603
+ console.log(chalk2.bold(`
1604
+ Available Blocks (${blocks.length})
1605
+ `));
1606
+ for (const item of blocks) {
1607
+ const blockDir = path5.join(blockBaseDir, item.name);
1608
+ const installed = checkFileExists(blockDir);
1609
+ const status = installed ? chalk2.green("[installed]") : chalk2.dim("[not installed]");
1610
+ console.log(` ${chalk2.cyan(item.name.padEnd(22))} ${item.description ?? ""}`);
1611
+ console.log(` ${"".padEnd(22)} ${status}`);
1612
+ if (item.peerComponents && item.peerComponents.length > 0) {
1613
+ console.log(chalk2.dim(` ${"".padEnd(22)} requires: ${item.peerComponents.join(", ")}`));
1614
+ }
1615
+ console.log();
1616
+ }
1617
+ console.log(
1618
+ chalk2.dim(` Install with: ${chalk2.cyan("npx pdfx-cli@latest block add <block-name>")}
1619
+ `)
1620
+ );
1621
+ } catch (error) {
1622
+ const message = error instanceof Error ? error.message : String(error);
1623
+ spinner.fail(message);
1624
+ process.exit(1);
1625
+ }
1626
+ }
1627
+
1628
+ // src/commands/diff.ts
1629
+ import fs6 from "fs";
1630
+ import path6 from "path";
1631
+ import chalk3 from "chalk";
1632
+ import ora3 from "ora";
1633
+ async function diff(components) {
1634
+ const configPath = path6.join(process.cwd(), "pdfx.json");
1635
+ let hasFailures = false;
1636
+ if (!checkFileExists(configPath)) {
1637
+ console.error(chalk3.red("Error: pdfx.json not found"));
1638
+ console.log(chalk3.yellow("Run: npx pdfx-cli@latest init"));
1639
+ process.exit(1);
1640
+ }
1641
+ let config;
1642
+ try {
1643
+ config = readConfig(configPath);
1644
+ } catch (error) {
1645
+ const message = error instanceof Error ? error.message : String(error);
1646
+ console.error(chalk3.red(message));
1647
+ process.exit(1);
1648
+ }
1649
+ const targetDir = path6.resolve(process.cwd(), config.componentDir);
1650
+ for (const componentName of components) {
1651
+ const nameResult = componentNameSchema.safeParse(componentName);
1652
+ if (!nameResult.success) {
1653
+ console.error(chalk3.red(`Invalid component name: "${componentName}"`));
1654
+ hasFailures = true;
1655
+ continue;
1656
+ }
1657
+ const spinner = ora3(`Comparing ${componentName}...`).start();
1658
+ try {
1659
+ const component = await fetchComponent(componentName, config.registry);
1660
+ spinner.stop();
1661
+ const componentSubDir = path6.join(targetDir, component.name);
1662
+ for (const file of component.files) {
1663
+ const fileName = path6.basename(file.path);
1664
+ const localPath = safePath(componentSubDir, fileName);
1665
+ if (!checkFileExists(localPath)) {
1666
+ console.log(chalk3.yellow(` ${fileName}: not installed locally`));
1667
+ continue;
1668
+ }
1669
+ const localContent = fs6.readFileSync(localPath, "utf-8");
1670
+ const registryContent = config.theme && (file.content.includes("pdfx-theme") || file.content.includes("pdfx-theme-context")) ? resolveThemeImport(
1671
+ path6.join(config.componentDir, component.name),
1672
+ config.theme,
1673
+ file.content
1674
+ ) : file.content;
1675
+ if (localContent === registryContent) {
1676
+ console.log(chalk3.green(` ${fileName}: up to date`));
1677
+ } else {
1678
+ console.log(chalk3.yellow(` ${fileName}: differs from registry`));
1679
+ const localLines = localContent.split("\n");
1680
+ const registryLines = registryContent.split("\n");
1681
+ const lineDiff = localLines.length - registryLines.length;
1682
+ console.log(chalk3.dim(` Local: ${localLines.length} lines`));
1683
+ console.log(chalk3.dim(` Registry: ${registryLines.length} lines`));
1684
+ if (lineDiff !== 0) {
1685
+ const diffText = lineDiff > 0 ? `${Math.abs(lineDiff)} line${Math.abs(lineDiff) > 1 ? "s" : ""} added locally` : `${Math.abs(lineDiff)} line${Math.abs(lineDiff) > 1 ? "s" : ""} removed locally`;
1686
+ console.log(chalk3.dim(` \u2192 ${diffText}`));
1687
+ }
1688
+ }
1689
+ }
1690
+ console.log();
1691
+ } catch (error) {
1692
+ const message = error instanceof Error ? error.message : String(error);
1693
+ spinner.fail(message);
1694
+ hasFailures = true;
1695
+ }
1696
+ }
1697
+ if (hasFailures) {
1698
+ process.exit(1);
1699
+ }
1700
+ }
1701
+
1702
+ // src/commands/init.ts
1703
+ import fs7 from "fs";
1704
+ import path9 from "path";
1705
+ import chalk6 from "chalk";
1706
+ import ora5 from "ora";
1707
+ import prompts4 from "prompts";
1708
+
1709
+ // src/utils/install-dependencies.ts
1710
+ import chalk4 from "chalk";
1711
+ import { execa as execa2 } from "execa";
1712
+ import ora4 from "ora";
1713
+ import prompts3 from "prompts";
1714
+ async function promptAndInstallReactPdf(validation, cwd = process.cwd()) {
1715
+ if (validation.installed && validation.valid) {
1716
+ return {
1717
+ success: true,
1718
+ message: "@react-pdf/renderer is already installed and compatible"
1719
+ };
1720
+ }
1721
+ const packageRoot = findPackageRoot(cwd);
1722
+ const pm = detectPackageManager(cwd);
1723
+ const packageName = "@react-pdf/renderer";
1724
+ const installCmd2 = getInstallCommand(pm.name, [packageName]);
1725
+ console.log(chalk4.yellow("\n \u26A0 @react-pdf/renderer is required but not installed\n"));
1726
+ console.log(chalk4.dim(` Package root: ${packageRoot}`));
1727
+ console.log(chalk4.dim(` This command will run: ${installCmd2}
1728
+ `));
1729
+ const { shouldInstall } = await prompts3({
1730
+ type: "confirm",
1731
+ name: "shouldInstall",
1732
+ message: "Install @react-pdf/renderer now?",
1733
+ initial: true
1734
+ });
1735
+ if (!shouldInstall) {
1736
+ return {
1737
+ success: false,
1738
+ message: `Installation cancelled. Please install manually:
1739
+ ${chalk4.cyan(installCmd2)}`
1740
+ };
1741
+ }
1742
+ const spinner = ora4("Installing @react-pdf/renderer...").start();
1743
+ try {
1744
+ const installArgs = pm.installCommand.split(" ").slice(1);
1745
+ await execa2(pm.name, [...installArgs, packageName], {
1746
+ cwd: packageRoot,
1747
+ stdio: "pipe"
1748
+ });
1749
+ spinner.succeed("Installed @react-pdf/renderer");
1750
+ return {
1751
+ success: true,
1752
+ message: "@react-pdf/renderer installed successfully"
1753
+ };
1754
+ } catch (error) {
1755
+ spinner.fail("Failed to install @react-pdf/renderer");
1756
+ const message = error instanceof Error ? error.message : String(error);
1757
+ return {
1758
+ success: false,
1759
+ message: `Installation failed: ${message}
1760
+ Try manually: ${chalk4.cyan(installCmd2)}`
1761
+ };
1762
+ }
1763
+ }
1764
+ async function ensureReactPdfRenderer(validation, cwd = process.cwd()) {
1765
+ if (!validation.installed) {
1766
+ const result = await promptAndInstallReactPdf(validation, cwd);
1767
+ if (!result.success) {
1768
+ console.error(chalk4.red(`
1769
+ ${result.message}
1770
+ `));
1771
+ return false;
1772
+ }
1773
+ return true;
1774
+ }
1775
+ if (!validation.valid) {
1776
+ console.log(
1777
+ chalk4.yellow(
1778
+ `
1779
+ \u26A0 ${validation.message}
1780
+ ${chalk4.dim("\u2192")} You may encounter compatibility issues
1781
+ `
1782
+ )
1783
+ );
1784
+ }
1785
+ return true;
1786
+ }
1787
+
1788
+ // src/utils/pre-flight.ts
1789
+ import chalk5 from "chalk";
1790
+
1791
+ // src/utils/environment-validator.ts
1792
+ import path7 from "path";
1793
+ function validatePackageJson(cwd = process.cwd()) {
1794
+ const pkgPath = path7.join(cwd, "package.json");
1795
+ const exists = checkFileExists(pkgPath);
1796
+ return {
1797
+ valid: exists,
1798
+ message: exists ? "package.json found" : "No package.json found in current directory",
1799
+ fixCommand: exists ? void 0 : 'Run "npm init" or "pnpm init" to create a package.json'
1800
+ };
1801
+ }
1802
+ function validateReactProject(cwd = process.cwd()) {
1803
+ const pkgPath = path7.join(cwd, "package.json");
1804
+ if (!checkFileExists(pkgPath)) {
1805
+ return {
1806
+ valid: false,
1807
+ message: "Cannot validate React project without package.json"
1808
+ };
1809
+ }
1810
+ try {
1811
+ const pkg = readJsonFile(pkgPath);
1812
+ const deps = {
1813
+ ...pkg.dependencies,
1814
+ ...pkg.devDependencies
1815
+ };
1816
+ const hasReact = "react" in deps;
1817
+ return {
1818
+ valid: hasReact,
1819
+ message: hasReact ? "React is installed" : "This does not appear to be a React project",
1820
+ fixCommand: hasReact ? void 0 : "Install React: npx create-vite@latest or npx create-react-app"
1821
+ };
1822
+ } catch {
1823
+ return {
1824
+ valid: false,
1825
+ message: "Failed to read package.json"
1826
+ };
1827
+ }
1828
+ }
1829
+ function validateEnvironment(cwd = process.cwd()) {
1830
+ return {
1831
+ hasPackageJson: validatePackageJson(cwd),
1832
+ isReactProject: validateReactProject(cwd)
1833
+ };
1834
+ }
1835
+
1836
+ // src/utils/pre-flight.ts
1837
+ function runPreFlightChecks(cwd = process.cwd()) {
1838
+ const environment = validateEnvironment(cwd);
1839
+ const dependencies = validateDependencies(cwd);
1840
+ const blockingErrors = [];
1841
+ const warnings = [];
1842
+ if (!environment.hasPackageJson.valid) {
1843
+ blockingErrors.push(
1844
+ `${environment.hasPackageJson.message}
1845
+ ${chalk5.dim("\u2192")} ${environment.hasPackageJson.fixCommand}`
1846
+ );
1847
+ } else if (!environment.isReactProject.valid) {
1848
+ blockingErrors.push(
1849
+ `${environment.isReactProject.message}
1850
+ ${chalk5.dim("\u2192")} ${environment.isReactProject.fixCommand}`
1851
+ );
1852
+ } else {
1853
+ if (!dependencies.react.valid && dependencies.react.installed) {
1854
+ warnings.push(
1855
+ `${dependencies.react.message}
1856
+ ${chalk5.dim("\u2192")} Current: ${dependencies.react.currentVersion}, Required: ${dependencies.react.requiredVersion}`
1857
+ );
1858
+ } else if (!dependencies.react.installed) {
1859
+ blockingErrors.push(
1860
+ `${dependencies.react.message}
1861
+ ${chalk5.dim("\u2192")} Install React: npm install react react-dom`
1862
+ );
1863
+ }
1864
+ }
1865
+ if (!dependencies.nodeJs.valid) {
1866
+ blockingErrors.push(
1867
+ `${dependencies.nodeJs.message}
1868
+ ${chalk5.dim("\u2192")} Current: ${dependencies.nodeJs.currentVersion}, Required: ${dependencies.nodeJs.requiredVersion}
1869
+ ${chalk5.dim("\u2192")} Visit https://nodejs.org to upgrade`
1870
+ );
1871
+ }
1872
+ if (dependencies.reactPdfRenderer.installed && !dependencies.reactPdfRenderer.valid) {
1873
+ warnings.push(
1874
+ `${dependencies.reactPdfRenderer.message}
1875
+ ${chalk5.dim("\u2192")} Consider upgrading: npm install @react-pdf/renderer@latest`
1876
+ );
1877
+ }
1878
+ return {
1879
+ environment,
1880
+ dependencies,
1881
+ blockingErrors,
1882
+ warnings,
1883
+ canProceed: blockingErrors.length === 0
1884
+ };
1885
+ }
1886
+ function displayPreFlightResults(result) {
1887
+ console.log(chalk5.bold("\n Pre-flight Checks:\n"));
1888
+ if (result.blockingErrors.length > 0) {
1889
+ console.log(chalk5.red(" \u2717 Blocking Issues:\n"));
1890
+ for (const error of result.blockingErrors) {
1891
+ console.log(chalk5.red(` \u2022 ${error}
1892
+ `));
1893
+ }
1894
+ }
1895
+ if (result.warnings.length > 0) {
1896
+ console.log(chalk5.yellow(" \u26A0 Warnings:\n"));
1897
+ for (const warning of result.warnings) {
1898
+ console.log(chalk5.yellow(` \u2022 ${warning}
1899
+ `));
1900
+ }
1901
+ }
1902
+ if (result.blockingErrors.length === 0 && result.warnings.length === 0) {
1903
+ console.log(chalk5.green(" \u2713 All checks passed!\n"));
1904
+ }
1905
+ }
1906
+
1907
+ // src/utils/theme-path.ts
1908
+ import path8 from "path";
1909
+ function normalizeThemePath(value) {
1910
+ const trimmed = value.trim();
1911
+ if (trimmed && !path8.isAbsolute(trimmed) && !trimmed.startsWith(".")) {
1912
+ return `./${trimmed}`;
1913
+ }
1914
+ return trimmed;
1915
+ }
1916
+ function validateThemePath(value) {
1917
+ const trimmed = value.trim();
1918
+ if (!trimmed) return "Theme path is required";
1919
+ if (path8.isAbsolute(trimmed)) {
1920
+ return "Please use a relative path (e.g., ./src/lib/pdfx-theme.ts)";
1921
+ }
1922
+ if (trimmed.startsWith(".") && !trimmed.startsWith("./") && !trimmed.startsWith("../")) {
1923
+ return "Path must start with ./ or ../ (e.g., ./src/lib/pdfx-theme.ts)";
1924
+ }
1925
+ if (!trimmed.endsWith(".ts") && !trimmed.endsWith(".tsx")) {
1926
+ return "Theme file must have a .ts or .tsx extension";
1927
+ }
1928
+ return true;
1929
+ }
1930
+
1931
+ // src/commands/init.ts
1932
+ async function init(options = {}) {
1933
+ console.log(chalk6.bold.cyan("\n Welcome to the pdfx cli\n"));
1934
+ const preFlightResult = runPreFlightChecks();
1935
+ displayPreFlightResults(preFlightResult);
1936
+ if (!preFlightResult.canProceed) {
1937
+ console.error(
1938
+ chalk6.red("\n Cannot proceed due to blocking issues. Please fix them and try again.\n")
1939
+ );
1940
+ process.exit(1);
1941
+ }
1942
+ const hasReactPdf = await ensureReactPdfRenderer(preFlightResult.dependencies.reactPdfRenderer);
1943
+ if (!hasReactPdf) {
1944
+ console.error(
1945
+ chalk6.red("\n @react-pdf/renderer is required. Please install it and try again.\n")
1946
+ );
1947
+ process.exit(1);
1948
+ }
1949
+ const existingConfig = path9.join(process.cwd(), "pdfx.json");
1950
+ if (fs7.existsSync(existingConfig)) {
1951
+ if (options.yes) {
1952
+ console.log(chalk6.dim(" pdfx.json already exists \u2014 overwriting (--yes)."));
1953
+ } else {
1954
+ const { overwrite } = await prompts4({
1955
+ type: "confirm",
1956
+ name: "overwrite",
1957
+ message: "pdfx.json already exists. Overwrite?",
1958
+ initial: false
1959
+ });
1960
+ if (!overwrite) {
1961
+ console.log(chalk6.yellow("Init cancelled \u2014 existing config preserved."));
1962
+ return;
1963
+ }
1964
+ }
1965
+ }
1966
+ const answers = options.yes ? {
1967
+ componentDir: DEFAULTS.COMPONENT_DIR,
1968
+ blockDir: DEFAULTS.BLOCK_DIR,
1969
+ registry: DEFAULTS.REGISTRY_URL,
1970
+ themePreset: "professional",
1971
+ themePath: normalizeThemePath(DEFAULTS.THEME_FILE)
1972
+ } : await prompts4(
1973
+ [
1974
+ {
1975
+ type: "text",
1976
+ name: "componentDir",
1977
+ message: "Where should we install components?",
1978
+ initial: DEFAULTS.COMPONENT_DIR,
1979
+ validate: (value) => {
1980
+ if (!value || value.trim().length === 0) {
1981
+ return "Component directory is required";
1982
+ }
1983
+ if (path9.isAbsolute(value)) {
1984
+ return "Please use a relative path (e.g., ./src/components/pdfx)";
1985
+ }
1986
+ if (!value.startsWith(".")) {
1987
+ return "Path should start with ./ or ../ (e.g., ./src/components/pdfx)";
1988
+ }
1989
+ return true;
1990
+ }
1991
+ },
1992
+ {
1993
+ type: "text",
1994
+ name: "blockDir",
1995
+ message: "Where should we install blocks?",
1996
+ initial: DEFAULTS.BLOCK_DIR,
1997
+ validate: (value) => {
1998
+ if (!value || value.trim().length === 0) {
1999
+ return "Block directory is required";
2000
+ }
2001
+ if (path9.isAbsolute(value)) {
2002
+ return "Please use a relative path (e.g., ./src/blocks/pdfx)";
2003
+ }
2004
+ if (!value.startsWith(".")) {
2005
+ return "Path should start with ./ or ../ (e.g., ./src/blocks/pdfx)";
2006
+ }
2007
+ return true;
2008
+ }
2009
+ },
2010
+ {
2011
+ type: "text",
2012
+ name: "registry",
2013
+ message: "Registry URL:",
2014
+ initial: DEFAULTS.REGISTRY_URL,
2015
+ validate: (value) => {
2016
+ if (!value || !value.startsWith("http")) {
2017
+ return "Please enter a valid HTTP(S) URL";
2018
+ }
2019
+ return true;
2020
+ }
2021
+ },
2022
+ {
2023
+ type: "select",
2024
+ name: "themePreset",
2025
+ message: "Choose a theme:",
2026
+ choices: [
2027
+ {
2028
+ title: "Professional",
2029
+ description: "Serif headings, navy colors, generous margins",
2030
+ value: "professional"
2031
+ },
2032
+ {
2033
+ title: "Modern",
2034
+ description: "Sans-serif, vibrant purple, tight spacing",
2035
+ value: "modern"
2036
+ },
2037
+ {
2038
+ title: "Minimal",
2039
+ description: "Monospace headings, stark black, maximum whitespace",
2040
+ value: "minimal"
2041
+ }
2042
+ ],
2043
+ initial: 0
2044
+ },
2045
+ {
2046
+ type: "text",
2047
+ name: "themePath",
2048
+ message: "Where should we create the theme file?",
2049
+ initial: DEFAULTS.THEME_FILE,
2050
+ format: normalizeThemePath,
2051
+ validate: validateThemePath
2052
+ }
2053
+ ],
2054
+ {
2055
+ onCancel: () => {
2056
+ console.log(chalk6.yellow("\nSetup cancelled."));
2057
+ process.exit(0);
2058
+ }
2059
+ }
2060
+ );
2061
+ if (!answers.componentDir || !answers.registry) {
2062
+ console.error(chalk6.red("Missing required fields. Run npx pdfx-cli@latest init again."));
2063
+ process.exit(1);
2064
+ }
2065
+ const config = {
2066
+ $schema: DEFAULTS.SCHEMA_URL,
2067
+ componentDir: answers.componentDir,
2068
+ registry: answers.registry,
2069
+ theme: answers.themePath || DEFAULTS.THEME_FILE,
2070
+ blockDir: answers.blockDir || DEFAULTS.BLOCK_DIR
2071
+ };
2072
+ const validation = configSchema.safeParse(config);
2073
+ if (!validation.success) {
2074
+ const issues = validation.error.issues.map((i) => {
2075
+ const fieldPath = i.path.length > 0 ? i.path.join(".") : "root";
2076
+ return `"${fieldPath}": ${i.message}`;
2077
+ }).join("; ");
2078
+ console.error(chalk6.red(`Invalid configuration: ${issues}`));
2079
+ process.exit(1);
2080
+ }
2081
+ const spinner = ora5("Creating config and theme files...").start();
2082
+ try {
2083
+ const componentDirPath = path9.resolve(process.cwd(), answers.componentDir);
2084
+ ensureDir(componentDirPath);
2085
+ fs7.writeFileSync(path9.join(process.cwd(), "pdfx.json"), JSON.stringify(config, null, 2));
2086
+ const presetName = answers.themePreset || "professional";
2087
+ const preset = themePresets[presetName];
2088
+ const themePath = path9.resolve(process.cwd(), config.theme);
2089
+ ensureDir(path9.dirname(themePath));
2090
+ fs7.writeFileSync(themePath, generateThemeFile(preset), "utf-8");
2091
+ const contextPath = path9.join(path9.dirname(themePath), "pdfx-theme-context.tsx");
2092
+ fs7.writeFileSync(contextPath, generateThemeContextFile(), "utf-8");
2093
+ spinner.succeed(`Created pdfx.json + ${config.theme} (${presetName} theme)`);
2094
+ console.log(chalk6.green("\nSuccess! You can now run:"));
2095
+ console.log(chalk6.cyan(" npx pdfx-cli@latest add heading"));
2096
+ console.log(chalk6.cyan(" npx pdfx-cli@latest block add invoice-classic"));
2097
+ console.log(chalk6.dim(`
2098
+ Components: ${path9.resolve(process.cwd(), answers.componentDir)}`));
2099
+ console.log(chalk6.dim(` Blocks: ${path9.resolve(process.cwd(), config.blockDir)}`));
2100
+ console.log(chalk6.dim(` Theme: ${path9.resolve(process.cwd(), config.theme)}
2101
+ `));
2102
+ } catch (error) {
2103
+ spinner.fail("Failed to create config");
2104
+ const message = error instanceof Error ? error.message : String(error);
2105
+ console.error(chalk6.dim(` ${message}`));
2106
+ process.exit(1);
2107
+ }
2108
+ }
2109
+
2110
+ // src/commands/list.ts
2111
+ import path10 from "path";
2112
+ import chalk7 from "chalk";
2113
+ import ora6 from "ora";
2114
+ async function list() {
2115
+ const configPath = path10.join(process.cwd(), "pdfx.json");
2116
+ let config;
2117
+ let hasLocalProject = false;
2118
+ if (checkFileExists(configPath)) {
2119
+ try {
2120
+ config = readConfig(configPath);
2121
+ hasLocalProject = true;
2122
+ } catch (error) {
2123
+ if (error instanceof ConfigError) {
2124
+ console.error(chalk7.red(error.message));
2125
+ if (error.suggestion) console.log(chalk7.yellow(` Hint: ${error.suggestion}`));
2126
+ } else {
2127
+ console.error(chalk7.red("Invalid pdfx.json"));
2128
+ }
2129
+ process.exit(1);
2130
+ }
2131
+ } else {
2132
+ config = {
2133
+ registry: DEFAULTS.REGISTRY_URL,
2134
+ componentDir: DEFAULTS.COMPONENT_DIR,
2135
+ theme: DEFAULTS.THEME_FILE,
2136
+ blockDir: DEFAULTS.BLOCK_DIR
2137
+ };
2138
+ console.log(chalk7.dim("No pdfx.json found. Listing from default registry.\n"));
2139
+ }
2140
+ const spinner = ora6("Fetching registry...").start();
2141
+ try {
2142
+ let response;
2143
+ try {
2144
+ response = await fetch(`${config.registry}/index.json`, {
2145
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2146
+ });
2147
+ } catch (err) {
2148
+ const isTimeout = err instanceof Error && err.name === "TimeoutError";
2149
+ throw new NetworkError(
2150
+ isTimeout ? `Registry request timed out after 10 seconds.
2151
+ ${chalk7.dim("Check your internet connection or try again later.")}` : `Could not reach registry at ${config.registry}
2152
+ ${chalk7.dim("Verify the URL is correct and you have internet access.")}`
2153
+ );
2154
+ }
2155
+ if (!response.ok) {
2156
+ throw new RegistryError(`Registry returned HTTP ${response.status}`);
2157
+ }
2158
+ const data = await response.json();
2159
+ const result = registrySchema.safeParse(data);
2160
+ if (!result.success) {
2161
+ throw new RegistryError("Invalid registry format");
2162
+ }
2163
+ spinner.stop();
2164
+ const components = result.data.items.filter((item) => item.type === "registry:ui");
2165
+ const blocks = result.data.items.filter((item) => item.type === "registry:block");
2166
+ const componentBaseDir = path10.resolve(process.cwd(), config.componentDir);
2167
+ const blockBaseDir = path10.resolve(process.cwd(), config.blockDir ?? DEFAULTS.BLOCK_DIR);
2168
+ console.log(chalk7.bold(`
2169
+ Components (${components.length})`));
2170
+ console.log(chalk7.dim(" Install with: npx pdfx-cli@latest add <component>\n"));
2171
+ for (const item of components) {
2172
+ const componentSubDir = path10.join(componentBaseDir, item.name);
2173
+ const localPath = safePath(componentSubDir, `pdfx-${item.name}.tsx`);
2174
+ const installed = hasLocalProject && checkFileExists(localPath);
2175
+ const status = installed ? chalk7.green("[installed]") : chalk7.dim("[not installed]");
2176
+ console.log(` ${chalk7.cyan(item.name.padEnd(20))} ${item.description}`);
2177
+ if (hasLocalProject) {
2178
+ console.log(` ${"".padEnd(20)} ${status}`);
2179
+ }
2180
+ console.log();
2181
+ }
2182
+ console.log(chalk7.bold(` Blocks (${blocks.length})`));
2183
+ console.log(
2184
+ chalk7.dim(" Copy-paste designs. Install with: npx pdfx-cli@latest block add <block>\n")
2185
+ );
2186
+ for (const item of blocks) {
2187
+ const blockDir = path10.join(blockBaseDir, item.name);
2188
+ const installed = hasLocalProject && checkFileExists(blockDir);
2189
+ const status = installed ? chalk7.green("[installed]") : chalk7.dim("[not installed]");
2190
+ console.log(` ${chalk7.cyan(item.name.padEnd(22))} ${item.description ?? ""}`);
2191
+ if (hasLocalProject) {
2192
+ console.log(` ${"".padEnd(22)} ${status}`);
2193
+ }
2194
+ if (item.peerComponents && item.peerComponents.length > 0) {
2195
+ console.log(chalk7.dim(` ${"".padEnd(22)} requires: ${item.peerComponents.join(", ")}`));
2196
+ }
2197
+ console.log();
2198
+ }
2199
+ console.log(chalk7.dim(" Quick Start:"));
2200
+ console.log(
2201
+ chalk7.dim(
2202
+ ` npx pdfx-cli@latest add heading table ${chalk7.dim("# Add components")}`
2203
+ )
2204
+ );
2205
+ console.log(
2206
+ chalk7.dim(` npx pdfx-cli@latest block add invoice-classic ${chalk7.dim("# Add a block")}`)
2207
+ );
2208
+ console.log();
2209
+ } catch (error) {
2210
+ const message = error instanceof Error ? error.message : String(error);
2211
+ spinner.fail(message);
2212
+ process.exit(1);
2213
+ }
2214
+ }
2215
+
2216
+ // src/commands/mcp.ts
2217
+ import { existsSync } from "fs";
2218
+ import { mkdir, readFile, writeFile as writeFile2 } from "fs/promises";
2219
+ import path11 from "path";
2220
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2221
+ import chalk8 from "chalk";
2222
+ import { Command } from "commander";
2223
+ import prompts5 from "prompts";
2224
+
2225
+ // src/mcp/index.ts
2226
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2227
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2228
+ import { z as z8 } from "zod";
2229
+ import { zodToJsonSchema } from "zod-to-json-schema";
2230
+
2231
+ // src/mcp/tools/add-command.ts
2232
+ import dedent from "dedent";
2233
+ import { z as z2 } from "zod";
2234
+
2235
+ // src/mcp/utils.ts
2236
+ var REGISTRY_BASE = DEFAULTS.REGISTRY_URL;
2237
+ var BLOCKS_BASE = `${DEFAULTS.REGISTRY_URL}/${REGISTRY_SUBPATHS.BLOCKS}`;
2238
+ async function fetchRegistryIndex() {
2239
+ let response;
2240
+ try {
2241
+ response = await fetch(`${REGISTRY_BASE}/index.json`, {
2242
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2243
+ });
2244
+ } catch (err) {
2245
+ const isTimeout = err instanceof Error && err.name === "TimeoutError";
2246
+ throw new NetworkError(
2247
+ isTimeout ? "Registry request timed out. Check your internet connection." : "Could not reach the PDFx registry. Check your internet connection."
2248
+ );
2249
+ }
2250
+ if (!response.ok) {
2251
+ throw new RegistryError(`Registry returned HTTP ${response.status}`);
2252
+ }
2253
+ let data;
2254
+ try {
2255
+ data = await response.json();
2256
+ } catch {
2257
+ throw new RegistryError("Registry returned invalid JSON");
2258
+ }
2259
+ const result = registrySchema.safeParse(data);
2260
+ if (!result.success) {
2261
+ throw new RegistryError("Registry index has an unexpected format");
2262
+ }
2263
+ return result.data.items;
2264
+ }
2265
+ async function fetchRegistryItem(name, base = REGISTRY_BASE) {
2266
+ const url = `${base}/${name}.json`;
2267
+ let response;
2268
+ try {
2269
+ response = await fetch(url, {
2270
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2271
+ });
2272
+ } catch (err) {
2273
+ const isTimeout = err instanceof Error && err.name === "TimeoutError";
2274
+ throw new NetworkError(
2275
+ isTimeout ? `Registry request timed out for "${name}".` : "Could not reach the PDFx registry. Check your internet connection."
2276
+ );
2277
+ }
2278
+ if (!response.ok) {
2279
+ if (response.status === 404) {
2280
+ throw new RegistryError(
2281
+ `"${name}" not found in the registry. Use list_components or list_blocks to see available items.`
2282
+ );
2283
+ }
2284
+ throw new RegistryError(`Registry returned HTTP ${response.status} for "${name}"`);
2285
+ }
2286
+ let data;
2287
+ try {
2288
+ data = await response.json();
2289
+ } catch {
2290
+ throw new RegistryError(`Registry returned invalid JSON for "${name}"`);
2291
+ }
2292
+ const result = registryItemSchema.safeParse(data);
2293
+ if (!result.success) {
2294
+ throw new RegistryError(`Unexpected registry format for "${name}"`);
2295
+ }
2296
+ return result.data;
2297
+ }
2298
+ function textResponse(text) {
2299
+ return { content: [{ type: "text", text }] };
2300
+ }
2301
+ function errorResponse(error) {
2302
+ const message = error instanceof Error ? error.message : String(error);
2303
+ return {
2304
+ content: [{ type: "text", text: `Error: ${message}` }],
2305
+ isError: true
2306
+ };
2307
+ }
2308
+
2309
+ // src/mcp/tools/add-command.ts
2310
+ var getAddCommandSchema = z2.object({
2311
+ items: z2.array(z2.string().min(1)).min(1).describe(
2312
+ "Item names to add, e.g. ['table', 'heading'] for components or ['invoice-modern'] for blocks"
2313
+ ),
2314
+ type: z2.enum(["component", "block"]).describe("Whether the items are components or blocks")
2315
+ });
2316
+ async function getAddCommand(args) {
2317
+ const isBlock = args.type === "block";
2318
+ const cmd = isBlock ? `npx pdfx-cli block add ${args.items.join(" ")}` : `npx pdfx-cli add ${args.items.join(" ")}`;
2319
+ const installDir = isBlock ? "src/blocks/pdfx/" : "src/components/pdfx/";
2320
+ const inspectTool = isBlock ? "get_block" : "get_component";
2321
+ const itemList = args.items.map((i) => `- \`${i}\``).join("\n");
2322
+ return textResponse(dedent`
2323
+ # Add Command
2324
+
2325
+ \`\`\`bash
2326
+ ${cmd}
2327
+ \`\`\`
2328
+
2329
+ **Items:**
2330
+ ${itemList}
2331
+
2332
+ **What this does:**
2333
+ - Copies source files into \`${installDir}\`
2334
+ - You own the code — no runtime package is added
2335
+ ${isBlock ? "- The block includes a complete document layout ready to customize" : "- Each component gets its own subdirectory inside componentDir"}
2336
+
2337
+ **Before running:** make sure \`pdfx.json\` exists. Run \`npx pdfx-cli init\` if not.
2338
+
2339
+ **See source first:** call \`${inspectTool}\` with the item name to review the code before adding.
2340
+
2341
+ **After adding:** call \`get_audit_checklist\` to verify your setup is correct.
2342
+ `);
2343
+ }
2344
+
2345
+ // src/mcp/tools/audit.ts
2346
+ import dedent2 from "dedent";
2347
+ async function getAuditChecklist() {
2348
+ return textResponse(dedent2`
2349
+ # PDFx Setup Audit Checklist
2350
+
2351
+ Work through this after adding components or generating PDF document code.
2352
+
2353
+ ## Configuration
2354
+ - [ ] \`pdfx.json\` exists in the project root
2355
+ - [ ] \`componentDir\` path in \`pdfx.json\` is correct (default: \`./src/components/pdfx\`)
2356
+ - [ ] Theme file exists at the path set in \`pdfx.json\` (default: \`./src/lib/pdfx-theme.ts\`)
2357
+
2358
+ ## Dependencies
2359
+ - [ ] \`@react-pdf/renderer\` is installed — run \`npm ls @react-pdf/renderer\` to confirm
2360
+ - [ ] Version is ≥ 3.0.0 (PDFx requires react-pdf v3+)
2361
+
2362
+ ## Imports
2363
+ - [ ] PDFx components use **named exports**: \`import { Table, Heading } from '@/components/pdfx/...'\`
2364
+ - [ ] \`Document\` and \`Page\` are imported from \`@react-pdf/renderer\`, not from PDFx
2365
+ - [ ] No Tailwind classes, CSS variables, or DOM APIs are used inside PDF components
2366
+ - [ ] All styles use \`StyleSheet.create({})\` from \`@react-pdf/renderer\`
2367
+
2368
+ ## Rendering
2369
+ - [ ] The root PDF component is **not** inside a React Server Component
2370
+ - [ ] Using \`renderToBuffer\`, \`PDFViewer\`, or \`PDFDownloadLink\` to render the document
2371
+ - [ ] Root component returns \`<Document><Page>...</Page></Document>\`
2372
+ - [ ] No console errors about missing fonts
2373
+
2374
+ ## TypeScript
2375
+ - [ ] No TypeScript errors in component files
2376
+ - [ ] Theme is typed as \`PdfxTheme\` (imported as a type from \`@pdfx/shared\`)
2377
+
2378
+ ---
2379
+
2380
+ ## Common Issues & Fixes
2381
+
2382
+ ### "Cannot find module @/components/pdfx/..."
2383
+ The component hasn't been added yet. Run:
2384
+ \`\`\`bash
2385
+ npx pdfx-cli add <component-name>
2386
+ \`\`\`
2387
+
2388
+ ### "Invalid hook call"
2389
+ PDFx components render to PDF, not to the DOM — React hooks are not supported inside them.
2390
+ Move hook calls to the parent component and pass data down as props.
2391
+
2392
+ ### "Text strings must be rendered inside \`<Text>\` component"
2393
+ Wrap all string literals in \`<Text>\` from \`@react-pdf/renderer\`:
2394
+ \`\`\`tsx
2395
+ import { Text } from '@react-pdf/renderer';
2396
+ // ✗ Wrong: <View>Hello</View>
2397
+ // ✓ Correct: <View><Text>Hello</Text></View>
2398
+ \`\`\`
2399
+
2400
+ ### Fonts not loading / rendering as a fallback
2401
+ Register custom fonts in your theme file:
2402
+ \`\`\`tsx
2403
+ import { Font } from '@react-pdf/renderer';
2404
+
2405
+ Font.register({
2406
+ family: 'Inter',
2407
+ src: 'https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2',
2408
+ });
2409
+ \`\`\`
2410
+
2411
+ ### PDF renders blank or empty
2412
+ Ensure your root component returns a \`<Document>\` with at least one \`<Page>\` inside:
2413
+ \`\`\`tsx
2414
+ import { Document, Page } from '@react-pdf/renderer';
2415
+
2416
+ export function MyDocument() {
2417
+ return (
2418
+ <Document>
2419
+ <Page size="A4">
2420
+ {/* content here */}
2421
+ </Page>
2422
+ </Document>
2423
+ );
2424
+ }
2425
+ \`\`\`
2426
+
2427
+ ### @react-pdf/renderer TypeScript errors
2428
+ Install the types package:
2429
+ \`\`\`bash
2430
+ npm install --save-dev @react-pdf/types
2431
+ \`\`\`
2432
+
2433
+ ---
2434
+
2435
+ ## @react-pdf/renderer Layout Constraints
2436
+
2437
+ These are **fundamental PDF rendering limitations** — they cannot be fixed with CSS-like
2438
+ style tweaks. Understanding them will save hours of debugging.
2439
+
2440
+ ### ⚠️ CRITICAL: Do NOT mix \`<View>\` and \`<Text>\` in the same flex row
2441
+
2442
+ In HTML, inline elements (spans, badges) can sit next to block text freely.
2443
+ In \`@react-pdf/renderer\`, \`View\` and \`Text\` are fundamentally different node types.
2444
+ Placing a \`View\`-based component (e.g. \`<Badge>\`, \`<PdfAlert>\`) **inline** alongside
2445
+ a \`<Text>\` node in the same flex row causes irrecoverable misalignment, overlap, and
2446
+ overflow that no amount of padding, margin, or \`alignItems\` can fix.
2447
+
2448
+ **Wrong — will cause layout corruption:**
2449
+ \`\`\`tsx
2450
+ {/* Badge is a View; Text is a Text node — they CANNOT share a flex row */}
2451
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
2452
+ <Text>INV-2026-001</Text>
2453
+ <Badge label="PAID" variant="success" />
2454
+ </View>
2455
+ \`\`\`
2456
+
2457
+ **Correct — place View-based components on their own line:**
2458
+ \`\`\`tsx
2459
+ <View style={{ flexDirection: 'column', gap: 2 }}>
2460
+ <Text>INV-2026-001</Text>
2461
+ <Badge label="PAID" variant="success" />
2462
+ </View>
2463
+ \`\`\`
2464
+
2465
+ PDFx components that are \`View\`-based (cannot be mixed inline with \`<Text>\`):
2466
+ \`Badge\`, \`PdfAlert\`, \`Card\`, \`Divider\`, \`KeyValue\`, \`Section\`, \`Table\`, \`DataTable\`,
2467
+ \`PdfGraph\`, \`PdfImage\`, \`PdfSignatureBlock\`, \`PdfList\`
2468
+
2469
+ ### No \`position: absolute\` stacking inside \`<Text>\` nodes
2470
+ Absolute positioning works on \`View\` elements but not inside \`Text\` runs.
2471
+
2472
+ ### \`gap\` only works between \`View\` siblings
2473
+ Use \`gap\` on a \`View\` container whose children are all \`View\` elements.
2474
+ If any child is a raw \`Text\` node, use \`marginBottom\` on siblings instead.
2475
+
2476
+ ### No percentage-based font sizes
2477
+ \`@react-pdf/renderer\` requires numeric pt/px values for \`fontSize\`. Do not use strings like \`"1rem"\` or \`"120%"\`.
2478
+ `);
2479
+ }
2480
+
2481
+ // src/mcp/tools/blocks.ts
2482
+ import dedent3 from "dedent";
2483
+ import { z as z3 } from "zod";
2484
+ var listBlocksSchema = z3.object({});
2485
+ async function listBlocks() {
2486
+ const items = await fetchRegistryIndex();
2487
+ const blocks = items.filter((i) => i.type === "registry:block");
2488
+ const invoices = blocks.filter((b) => b.name.startsWith("invoice-"));
2489
+ const reports = blocks.filter((b) => b.name.startsWith("report-"));
2490
+ const others = blocks.filter(
2491
+ (b) => !b.name.startsWith("invoice-") && !b.name.startsWith("report-")
2492
+ );
2493
+ const formatBlock = (b) => {
2494
+ const peers = b.peerComponents?.length ? ` _(requires: ${b.peerComponents.join(", ")})_` : "";
2495
+ return `- **${b.name}** \u2014 ${b.description ?? "No description"}${peers}`;
2496
+ };
2497
+ const sections = [];
2498
+ if (invoices.length > 0) {
2499
+ sections.push(
2500
+ `### Invoice Blocks (${invoices.length})
2501
+ ${invoices.map(formatBlock).join("\n")}`
2502
+ );
2503
+ }
2504
+ if (reports.length > 0) {
2505
+ sections.push(`### Report Blocks (${reports.length})
2506
+ ${reports.map(formatBlock).join("\n")}`);
2507
+ }
2508
+ if (others.length > 0) {
2509
+ sections.push(`### Other Blocks (${others.length})
2510
+ ${others.map(formatBlock).join("\n")}`);
2511
+ }
2512
+ return textResponse(dedent3`
2513
+ # PDFx Blocks (${blocks.length})
2514
+
2515
+ Blocks are complete, copy-paste ready document layouts. Unlike components, they are full documents ready to customize.
2516
+
2517
+ ${sections.join("\n\n")}
2518
+
2519
+ ---
2520
+ Add a block: \`npx pdfx-cli block add <name>\`
2521
+ See full source: call \`get_block\` with the block name
2522
+ `);
2523
+ }
2524
+ var getBlockSchema = z3.object({
2525
+ block: z3.string().min(1).describe("Block name, e.g. 'invoice-modern', 'report-financial'")
2526
+ });
2527
+ async function getBlock(args) {
2528
+ const item = await fetchRegistryItem(args.block, BLOCKS_BASE);
2529
+ const fileList = item.files.map((f) => `- \`${f.path}\``).join("\n");
2530
+ const peers = item.peerComponents?.length ? item.peerComponents.join(", ") : "none";
2531
+ const fileSources = item.files.map(
2532
+ (f) => dedent3`
2533
+ ### \`${f.path}\`
2534
+ \`\`\`tsx
2535
+ ${f.content}
2536
+ \`\`\`
2537
+ `
2538
+ ).join("\n\n");
2539
+ return textResponse(dedent3`
2540
+ # ${item.title ?? item.name}
2541
+
2542
+ ${item.description ?? ""}
2543
+
2544
+ ## Files
2545
+ ${fileList}
2546
+
2547
+ ## Required PDFx Components
2548
+ ${peers}
2549
+
2550
+ ## Add Command
2551
+ \`\`\`bash
2552
+ npx pdfx-cli block add ${args.block}
2553
+ \`\`\`
2554
+
2555
+ ## Source Code
2556
+ ${fileSources}
2557
+ `);
2558
+ }
2559
+
2560
+ // src/mcp/tools/components.ts
2561
+ import dedent4 from "dedent";
2562
+ import { z as z4 } from "zod";
2563
+ var listComponentsSchema = z4.object({});
2564
+ async function listComponents() {
2565
+ const items = await fetchRegistryIndex();
2566
+ const components = items.filter((i) => i.type === "registry:ui");
2567
+ const rows = components.map((c) => `- **${c.name}** \u2014 ${c.description ?? "No description"}`).join("\n");
2568
+ return textResponse(dedent4`
2569
+ # PDFx Components (${components.length})
2570
+
2571
+ ${rows}
2572
+
2573
+ ---
2574
+ Add a component: \`npx pdfx-cli add <name>\`
2575
+ See full source, props, and exact export name: call \`get_component\` with the component name
2576
+ `);
2577
+ }
2578
+ var getComponentSchema = z4.object({
2579
+ component: z4.string().min(1).describe("Component name, e.g. 'table', 'heading', 'data-table'")
2580
+ });
2581
+ async function getComponent(args) {
2582
+ const item = await fetchRegistryItem(args.component);
2583
+ const fileList = item.files.map((f) => `- \`${f.path}\``).join("\n");
2584
+ const deps = item.dependencies?.length ? item.dependencies.join(", ") : "none";
2585
+ const devDeps = item.devDependencies?.length ? item.devDependencies.join(", ") : "none";
2586
+ const registryDeps = item.registryDependencies?.length ? item.registryDependencies.join(", ") : "none";
2587
+ const primaryContent = item.files[0]?.content ?? "";
2588
+ const primaryPath = item.files[0]?.path ?? "";
2589
+ const exportNames = extractAllExportNames(primaryContent);
2590
+ const mainExport = extractExportName(primaryContent);
2591
+ const exportSection = exportNames.length > 0 ? dedent4`
2592
+ ## Exports
2593
+ **Main component export:** \`${mainExport ?? exportNames[0]}\`
2594
+
2595
+ All named exports from \`${primaryPath}\`:
2596
+ ${exportNames.map((n) => `- \`${n}\``).join("\n")}
2597
+
2598
+ **Import after \`npx pdfx-cli@latest add ${args.component}\`:**
2599
+ \`\`\`tsx
2600
+ import { ${mainExport ?? exportNames[0]} } from './components/pdfx/${args.component}/pdfx-${args.component}';
2601
+ \`\`\`
2602
+ ` : "";
2603
+ const fileSources = item.files.map(
2604
+ (f) => dedent4`
2605
+ ### \`${f.path}\`
2606
+ \`\`\`tsx
2607
+ ${f.content}
2608
+ \`\`\`
2609
+ `
2610
+ ).join("\n\n");
2611
+ return textResponse(dedent4`
2612
+ # ${item.title ?? item.name}
2613
+
2614
+ ${item.description ?? ""}
2615
+
2616
+ ## Files
2617
+ ${fileList}
2618
+
2619
+ ## Dependencies
2620
+ - Runtime: ${deps}
2621
+ - Dev: ${devDeps}
2622
+ - Other PDFx components required: ${registryDeps}
2623
+
2624
+ ${exportSection}
2625
+
2626
+ ## Add Command
2627
+ \`\`\`bash
2628
+ npx pdfx-cli add ${args.component}
2629
+ \`\`\`
2630
+
2631
+ ## Source Code
2632
+ ${fileSources}
2633
+ `);
2634
+ }
2635
+ function extractExportName(source) {
2636
+ if (!source) return null;
2637
+ const matches = [...source.matchAll(/export\s+(?:function|const)\s+([A-Z][A-Za-z0-9]*)/g)];
2638
+ if (matches.length === 0) return null;
2639
+ return matches[0][1] ?? null;
2640
+ }
2641
+ function extractAllExportNames(source) {
2642
+ const seen = /* @__PURE__ */ new Set();
2643
+ const results = [];
2644
+ for (const m of source.matchAll(/export\s+(?:function|const|class)\s+([A-Za-z][A-Za-z0-9]*)/g)) {
2645
+ const name = m[1];
2646
+ if (name && !seen.has(name)) {
2647
+ seen.add(name);
2648
+ results.push(name);
2649
+ }
2650
+ }
2651
+ for (const m of source.matchAll(/export\s+\{([^}]+)\}/g)) {
2652
+ for (const part of m[1].split(",")) {
2653
+ const name = part.trim().split(/\s+as\s+/).pop()?.trim();
2654
+ if (name && /^[A-Za-z]/.test(name) && !seen.has(name)) {
2655
+ seen.add(name);
2656
+ results.push(name);
2657
+ }
2658
+ }
2659
+ }
2660
+ return results;
2661
+ }
2662
+
2663
+ // src/mcp/tools/installation.ts
2664
+ import dedent5 from "dedent";
2665
+ import { z as z5 } from "zod";
2666
+ var getInstallationSchema = z5.object({
2667
+ framework: z5.enum(["nextjs", "react", "vite", "remix", "other"]).describe("Target framework"),
2668
+ package_manager: z5.enum(["npm", "pnpm", "yarn", "bun"]).describe("Package manager to use")
2669
+ });
2670
+ function installCmd(pm, pkg, dev = false) {
2671
+ const base = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[pm];
2672
+ const devFlag = { npm: "--save-dev", pnpm: "-D", yarn: "--dev", bun: "-d" }[pm];
2673
+ return dev ? `${base} ${devFlag} ${pkg}` : `${base} ${pkg}`;
2674
+ }
2675
+ var FRAMEWORK_NOTES = {
2676
+ nextjs: dedent5`
2677
+ ## Next.js Notes
2678
+
2679
+ **App Router** — Use a Route Handler to serve PDFs:
2680
+ \`\`\`tsx
2681
+ // app/api/pdf/route.ts
2682
+ import { renderToBuffer } from '@react-pdf/renderer';
2683
+ import { MyDocument } from '@/components/pdfx/my-document';
2684
+
2685
+ export async function GET() {
2686
+ const buffer = await renderToBuffer(<MyDocument />);
2687
+ return new Response(buffer, {
2688
+ headers: { 'Content-Type': 'application/pdf' },
2689
+ });
2690
+ }
2691
+ \`\`\`
2692
+
2693
+ **Important:** Do NOT render PDFx components inside React Server Components.
2694
+ Always use a \`'use client'\` boundary or a Route Handler.
2695
+
2696
+ **Pages Router** — Use an API route:
2697
+ \`\`\`tsx
2698
+ // pages/api/pdf.ts
2699
+ import type { NextApiRequest, NextApiResponse } from 'next';
2700
+ import { renderToBuffer } from '@react-pdf/renderer';
2701
+ import { MyDocument } from '@/components/pdfx/my-document';
2702
+
2703
+ export default async function handler(req: NextApiRequest, res: NextApiResponse) {
2704
+ const buffer = await renderToBuffer(<MyDocument />);
2705
+ res.setHeader('Content-Type', 'application/pdf');
2706
+ res.send(buffer);
2707
+ }
2708
+ \`\`\`
2709
+ `,
2710
+ react: dedent5`
2711
+ ## React Notes
2712
+
2713
+ Display a PDF inline with \`PDFViewer\`:
2714
+ \`\`\`tsx
2715
+ import { PDFViewer } from '@react-pdf/renderer';
2716
+ import { MyDocument } from './components/pdfx/my-document';
2717
+
2718
+ export function App() {
2719
+ return (
2720
+ <PDFViewer width="100%" height="600px">
2721
+ <MyDocument />
2722
+ </PDFViewer>
2723
+ );
2724
+ }
2725
+ \`\`\`
2726
+
2727
+ Trigger a download with \`PDFDownloadLink\`:
2728
+ \`\`\`tsx
2729
+ import { PDFDownloadLink } from '@react-pdf/renderer';
2730
+ import { MyDocument } from './components/pdfx/my-document';
2731
+
2732
+ export function DownloadButton() {
2733
+ return (
2734
+ <PDFDownloadLink document={<MyDocument />} fileName="document.pdf">
2735
+ {({ loading }) => (loading ? 'Generating PDF...' : 'Download PDF')}
2736
+ </PDFDownloadLink>
2737
+ );
2738
+ }
2739
+ \`\`\`
2740
+ `,
2741
+ vite: dedent5`
2742
+ ## Vite Notes
2743
+
2744
+ Works with both \`vite + react\` and \`vite + react-swc\` templates.
2745
+
2746
+ For client-side rendering, use \`PDFViewer\` or \`PDFDownloadLink\` from \`@react-pdf/renderer\`.
2747
+
2748
+ For server-side generation, use a separate Node.js server or Vite's server-side features.
2749
+ `,
2750
+ remix: dedent5`
2751
+ ## Remix Notes
2752
+
2753
+ Use a resource route to serve PDFs:
2754
+ \`\`\`tsx
2755
+ // app/routes/pdf.tsx
2756
+ import { renderToStream } from '@react-pdf/renderer';
2757
+ import { MyDocument } from '~/components/pdfx/my-document';
2758
+
2759
+ export async function loader() {
2760
+ const stream = await renderToStream(<MyDocument />);
2761
+ return new Response(stream as unknown as ReadableStream, {
2762
+ headers: { 'Content-Type': 'application/pdf' },
2763
+ });
2764
+ }
2765
+ \`\`\`
2766
+ `,
2767
+ other: dedent5`
2768
+ ## General Notes
2769
+
2770
+ \`@react-pdf/renderer\` works in any Node.js ≥ 18 environment.
2771
+
2772
+ - **Buffer output**: \`await renderToBuffer(<MyDocument />)\`
2773
+ - **Stream output**: \`await renderToStream(<MyDocument />)\`
2774
+ - **Client-side**: Use \`PDFViewer\` or \`PDFDownloadLink\` from \`@react-pdf/renderer\`
2775
+ `
2776
+ };
2777
+ async function getInstallation(args) {
2778
+ const pm = args.package_manager;
2779
+ const fw = args.framework;
2780
+ return textResponse(dedent5`
2781
+ # PDFx Setup Guide: ${fw} + ${pm}
2782
+
2783
+ ## Step 1 — Install the peer dependency
2784
+
2785
+ \`\`\`bash
2786
+ ${installCmd(pm, "@react-pdf/renderer")}
2787
+ \`\`\`
2788
+
2789
+ ## Step 2 — Initialize PDFx in your project
2790
+
2791
+ \`\`\`bash
2792
+ npx pdfx-cli init
2793
+ \`\`\`
2794
+
2795
+ This creates \`pdfx.json\` in your project root and generates a theme file at \`src/lib/pdfx-theme.ts\`.
2796
+
2797
+ ## Step 3 — Add your first component
2798
+
2799
+ \`\`\`bash
2800
+ npx pdfx-cli add heading text table
2801
+ \`\`\`
2802
+
2803
+ Components are copied into \`src/components/pdfx/\`. You own the source — there is no runtime package dependency.
2804
+
2805
+ ## Step 4 — Or start with a complete document block
2806
+
2807
+ \`\`\`bash
2808
+ npx pdfx-cli block add invoice-modern
2809
+ \`\`\`
2810
+
2811
+ ${FRAMEWORK_NOTES[fw]}
2812
+
2813
+ ## Generated pdfx.json
2814
+
2815
+ \`\`\`json
2816
+ {
2817
+ "$schema": "https://pdfx.akashpise.dev/schema.json",
2818
+ "componentDir": "./src/components/pdfx",
2819
+ "blockDir": "./src/blocks/pdfx",
2820
+ "registry": "https://pdfx.akashpise.dev/r",
2821
+ "theme": "./src/lib/pdfx-theme.ts"
2822
+ }
2823
+ \`\`\`
2824
+
2825
+ ## pdfx.json Field Reference
2826
+
2827
+ All four fields are **required**. Relative paths must start with \`./\` or \`../\`.
2828
+
2829
+ | Field | Type | Description | Default |
2830
+ |-------|------|-------------|---------|
2831
+ | \`componentDir\` | string | Where individual components are installed | \`./src/components/pdfx\` |
2832
+ | \`blockDir\` | string | Where full document blocks are installed | \`./src/blocks/pdfx\` |
2833
+ | \`registry\` | string (URL) | Registry base URL (must start with http) | \`https://pdfx.akashpise.dev/r\` |
2834
+ | \`theme\` | string | Path to your generated theme file | \`./src/lib/pdfx-theme.ts\` |
2835
+
2836
+ > **Non-interactive init (CI / AI agents):** pass \`--yes\` to accept all defaults:
2837
+ > \`\`\`bash
2838
+ > npx pdfx-cli init --yes
2839
+ > \`\`\`
2840
+
2841
+ ## Troubleshooting
2842
+
2843
+ | Problem | Fix |
2844
+ |---------|-----|
2845
+ | TypeScript errors on \`@react-pdf/renderer\` | \`${installCmd(pm, "@react-pdf/types", true)}\` |
2846
+ | "Cannot find module @/components/pdfx/..." | Run \`npx pdfx-cli@latest add <component>\` to install it |
2847
+ | PDF renders blank | Ensure root returns \`<Document><Page>...</Page></Document>\` |
2848
+ | "Invalid hook call" | PDFx components cannot use React hooks — pass data as props |
2849
+
2850
+ ---
2851
+ Next: call \`get_audit_checklist\` to verify your setup is correct.
2852
+ `);
2853
+ }
2854
+
2855
+ // src/mcp/tools/search.ts
2856
+ import dedent6 from "dedent";
2857
+ import { z as z6 } from "zod";
2858
+ var searchRegistrySchema = z6.object({
2859
+ query: z6.string().min(1).describe("Search query \u2014 matched against component name, title, and description"),
2860
+ type: z6.enum(["all", "component", "block"]).optional().default("all").describe("Filter by item type (default: all)"),
2861
+ limit: z6.number().int().positive().max(50).optional().default(20).describe("Maximum number of results to return (default: 20, max: 50)")
2862
+ });
2863
+ async function searchRegistry(args) {
2864
+ const items = await fetchRegistryIndex();
2865
+ const q = args.query.toLowerCase();
2866
+ let pool = items;
2867
+ if (args.type === "component") {
2868
+ pool = items.filter((i) => i.type === "registry:ui");
2869
+ } else if (args.type === "block") {
2870
+ pool = items.filter((i) => i.type === "registry:block");
2871
+ }
2872
+ const scored = pool.map((item) => {
2873
+ const name = item.name.toLowerCase();
2874
+ const title = (item.title ?? "").toLowerCase();
2875
+ const desc = (item.description ?? "").toLowerCase();
2876
+ let score = 0;
2877
+ if (name === q) score = 100;
2878
+ else if (name.startsWith(q)) score = 80;
2879
+ else if (name.includes(q)) score = 60;
2880
+ else if (title.includes(q)) score = 40;
2881
+ else if (desc.includes(q)) score = 20;
2882
+ return { item, score };
2883
+ }).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, args.limit);
2884
+ if (scored.length === 0) {
2885
+ return textResponse(dedent6`
2886
+ # Search: "${args.query}"
2887
+
2888
+ No results found. Try a broader query or browse all items:
2889
+ - \`list_components\` — see all 24 components
2890
+ - \`list_blocks\` — see all 10 blocks
2891
+ `);
2892
+ }
2893
+ const rows = scored.map(({ item }) => {
2894
+ const typeLabel2 = item.type === "registry:ui" ? "component" : "block";
2895
+ const addCmd = item.type === "registry:ui" ? `npx pdfx-cli add ${item.name}` : `npx pdfx-cli block add ${item.name}`;
2896
+ return dedent6`
2897
+ - **${item.name}** _(${typeLabel2})_ — ${item.description ?? "No description"}
2898
+ \`${addCmd}\`
2899
+ `;
2900
+ });
2901
+ const typeLabel = args.type === "all" ? "components + blocks" : args.type === "component" ? "components" : "blocks";
2902
+ return textResponse(dedent6`
2903
+ # Search: "${args.query}" — ${scored.length} result${scored.length === 1 ? "" : "s"} (${typeLabel})
2904
+
2905
+ ${rows.join("\n")}
2906
+ `);
2907
+ }
2908
+
2909
+ // src/mcp/tools/theme.ts
2910
+ import dedent7 from "dedent";
2911
+ import { z as z7 } from "zod";
2912
+ var getThemeSchema = z7.object({
2913
+ theme: z7.enum(["professional", "modern", "minimal"]).describe("Theme preset name")
2914
+ });
2915
+ async function getTheme(args) {
2916
+ const preset = themePresets[args.theme];
2917
+ const { colors, typography, spacing, page } = preset;
2918
+ return textResponse(dedent7`
2919
+ # PDFx Theme: ${args.theme}
2920
+
2921
+ ## Colors
2922
+ | Token | Value |
2923
+ |-------|-------|
2924
+ | foreground | \`${colors.foreground}\` |
2925
+ | background | \`${colors.background}\` |
2926
+ | primary | \`${colors.primary}\` |
2927
+ | primaryForeground | \`${colors.primaryForeground}\` |
2928
+ | accent | \`${colors.accent}\` |
2929
+ | muted | \`${colors.muted}\` |
2930
+ | mutedForeground | \`${colors.mutedForeground}\` |
2931
+ | border | \`${colors.border}\` |
2932
+ | destructive | \`${colors.destructive}\` |
2933
+ | success | \`${colors.success}\` |
2934
+ | warning | \`${colors.warning}\` |
2935
+ | info | \`${colors.info}\` |
2936
+
2937
+ ## Typography
2938
+
2939
+ ### Body
2940
+ - Font family: \`${typography.body.fontFamily}\`
2941
+ - Font size: ${typography.body.fontSize}pt
2942
+ - Line height: ${typography.body.lineHeight}
2943
+
2944
+ ### Headings
2945
+ - Font family: \`${typography.heading.fontFamily}\`
2946
+ - Font weight: ${typography.heading.fontWeight}
2947
+ - Line height: ${typography.heading.lineHeight}
2948
+ - h1: ${typography.heading.fontSize.h1}pt
2949
+ - h2: ${typography.heading.fontSize.h2}pt
2950
+ - h3: ${typography.heading.fontSize.h3}pt
2951
+ - h4: ${typography.heading.fontSize.h4}pt
2952
+ - h5: ${typography.heading.fontSize.h5}pt
2953
+ - h6: ${typography.heading.fontSize.h6}pt
2954
+
2955
+ ## Spacing
2956
+ - Page margins: top=${spacing.page.marginTop}pt · right=${spacing.page.marginRight}pt · bottom=${spacing.page.marginBottom}pt · left=${spacing.page.marginLeft}pt
2957
+ - Section gap: ${spacing.sectionGap}pt
2958
+ - Paragraph gap: ${spacing.paragraphGap}pt
2959
+ - Component gap: ${spacing.componentGap}pt
2960
+
2961
+ ## Page
2962
+ - Size: ${page.size}
2963
+ - Orientation: ${page.orientation}
2964
+
2965
+ ## Apply This Theme
2966
+ \`\`\`bash
2967
+ npx pdfx-cli theme switch ${args.theme}
2968
+ \`\`\`
2969
+
2970
+ ## Usage in Components
2971
+ \`\`\`tsx
2972
+ // Access theme values in a PDFx component
2973
+ import type { PdfxTheme } from '@pdfx/shared';
2974
+
2975
+ interface Props {
2976
+ theme: PdfxTheme;
2977
+ }
2978
+
2979
+ export function MyComponent({ theme }: Props) {
2980
+ return (
2981
+ <View style={{ backgroundColor: theme.colors.background }}>
2982
+ <Text style={{ color: theme.colors.foreground, fontSize: theme.typography.body.fontSize }}>
2983
+ Content
2984
+ </Text>
2985
+ </View>
2986
+ );
2987
+ }
2988
+ \`\`\`
2989
+ `);
2990
+ }
2991
+
2992
+ // src/mcp/index.ts
2993
+ var server = new Server(
2994
+ { name: "pdfx", version: "1.0.0" },
2995
+ { capabilities: { tools: {} } }
2996
+ );
2997
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2998
+ tools: [
2999
+ {
3000
+ name: "list_components",
3001
+ description: "List all available PDFx PDF components with names and descriptions. Call this first to discover what components exist before adding any.",
3002
+ inputSchema: zodToJsonSchema(listComponentsSchema)
3003
+ },
3004
+ {
3005
+ name: "get_component",
3006
+ description: "Get the full source code, files, and dependencies for a specific PDFx component. Use this to understand the API and props before using it in generated code.",
3007
+ inputSchema: zodToJsonSchema(getComponentSchema)
3008
+ },
3009
+ {
3010
+ name: "list_blocks",
3011
+ description: "List all PDFx pre-built document blocks (complete invoice and report layouts ready to customize).",
3012
+ inputSchema: zodToJsonSchema(listBlocksSchema)
3013
+ },
3014
+ {
3015
+ name: "get_block",
3016
+ description: "Get the full source code for a PDFx document block. Returns the complete layout code ready to customize for your use case.",
3017
+ inputSchema: zodToJsonSchema(getBlockSchema)
3018
+ },
3019
+ {
3020
+ name: "search_registry",
3021
+ description: "Search PDFx components and blocks by name or description. Use this when you know what you need but not the exact item name.",
3022
+ inputSchema: zodToJsonSchema(searchRegistrySchema)
3023
+ },
3024
+ {
3025
+ name: "get_theme",
3026
+ description: "Get the full design token values for a PDFx theme preset (professional, modern, or minimal). Use this to understand colors, typography, and spacing before customizing documents.",
3027
+ inputSchema: zodToJsonSchema(getThemeSchema)
3028
+ },
3029
+ {
3030
+ name: "get_installation",
3031
+ description: "Get step-by-step PDFx setup instructions for a specific framework and package manager. Use this when setting up PDFx in a new project.",
3032
+ inputSchema: zodToJsonSchema(getInstallationSchema)
3033
+ },
3034
+ {
3035
+ name: "get_add_command",
3036
+ description: "Get the exact CLI command string to add specific PDFx components or blocks to a project.",
3037
+ inputSchema: zodToJsonSchema(getAddCommandSchema)
3038
+ },
3039
+ {
3040
+ name: "get_audit_checklist",
3041
+ description: "Get a post-generation checklist to verify PDFx is set up correctly. Call this after adding components or generating PDF document code.",
3042
+ inputSchema: zodToJsonSchema(z8.object({}))
3043
+ }
3044
+ ]
3045
+ }));
3046
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3047
+ const args = request.params.arguments ?? {};
3048
+ try {
3049
+ switch (request.params.name) {
3050
+ case "list_components":
3051
+ return await listComponents();
3052
+ case "get_component":
3053
+ return await getComponent(getComponentSchema.parse(args));
3054
+ case "list_blocks":
3055
+ return await listBlocks();
3056
+ case "get_block":
3057
+ return await getBlock(getBlockSchema.parse(args));
3058
+ case "search_registry":
3059
+ return await searchRegistry(searchRegistrySchema.parse(args));
3060
+ case "get_theme":
3061
+ return await getTheme(getThemeSchema.parse(args));
3062
+ case "get_installation":
3063
+ return await getInstallation(getInstallationSchema.parse(args));
3064
+ case "get_add_command":
3065
+ return await getAddCommand(getAddCommandSchema.parse(args));
3066
+ case "get_audit_checklist":
3067
+ return await getAuditChecklist();
3068
+ default:
3069
+ return errorResponse(new Error(`Unknown tool: ${request.params.name}`));
3070
+ }
3071
+ } catch (error) {
3072
+ return errorResponse(error);
3073
+ }
3074
+ });
3075
+
3076
+ // src/commands/mcp.ts
3077
+ var PDFX_CLI_PACKAGE = "pdfx-cli";
3078
+ var PDFX_MCP_VERSION = "latest";
3079
+ var CLIENTS = [
3080
+ {
3081
+ name: "claude",
3082
+ label: "Claude Code",
3083
+ configPath: ".mcp.json",
3084
+ config: {
3085
+ mcpServers: {
3086
+ pdfx: {
3087
+ command: "npx",
3088
+ args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
3089
+ }
3090
+ }
3091
+ }
3092
+ },
3093
+ {
3094
+ name: "cursor",
3095
+ label: "Cursor",
3096
+ configPath: ".cursor/mcp.json",
3097
+ config: {
3098
+ mcpServers: {
3099
+ pdfx: {
3100
+ command: "npx",
3101
+ args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
3102
+ }
3103
+ }
3104
+ }
3105
+ },
3106
+ {
3107
+ name: "vscode",
3108
+ label: "VS Code",
3109
+ configPath: ".vscode/mcp.json",
3110
+ config: {
3111
+ servers: {
3112
+ pdfx: {
3113
+ type: "stdio",
3114
+ command: "npx",
3115
+ args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
3116
+ }
3117
+ }
3118
+ }
3119
+ },
3120
+ {
3121
+ name: "windsurf",
3122
+ label: "Windsurf",
3123
+ configPath: "mcp_config.json",
3124
+ config: {
3125
+ mcpServers: {
3126
+ pdfx: {
3127
+ command: "npx",
3128
+ args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
3129
+ }
3130
+ }
3131
+ }
3132
+ },
3133
+ {
3134
+ name: "qoder",
3135
+ label: "Qoder",
3136
+ configPath: ".qoder/mcp.json",
3137
+ config: {
3138
+ mcpServers: {
3139
+ pdfx: {
3140
+ command: "npx",
3141
+ args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
3142
+ }
3143
+ }
3144
+ }
3145
+ },
3146
+ {
3147
+ name: "opencode",
3148
+ label: "opencode",
3149
+ configPath: "opencode.json",
3150
+ config: {
3151
+ $schema: "https://opencode.ai/config.json",
3152
+ mcp: {
3153
+ pdfx: {
3154
+ type: "local",
3155
+ command: ["npx", "-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
3156
+ }
3157
+ }
3158
+ }
3159
+ },
3160
+ {
3161
+ name: "antigravity",
3162
+ label: "Antigravity",
3163
+ configPath: ".antigravity/mcp.json",
3164
+ config: {
3165
+ mcpServers: {
3166
+ pdfx: {
3167
+ command: "npx",
3168
+ args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
3169
+ }
3170
+ }
3171
+ }
3172
+ },
3173
+ {
3174
+ name: "other",
3175
+ label: "Generic (any MCP client)",
3176
+ configPath: "pdfx-mcp.json",
3177
+ config: {
3178
+ mcpServers: {
3179
+ pdfx: {
3180
+ command: "npx",
3181
+ args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
3182
+ }
3183
+ }
3184
+ }
3185
+ }
3186
+ ];
3187
+ function mergeDeep(target, source) {
3188
+ const result = { ...target };
3189
+ for (const [key, value] of Object.entries(source)) {
3190
+ const existing = result[key];
3191
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
3192
+ result[key] = mergeDeep(
3193
+ existing,
3194
+ value
3195
+ );
3196
+ } else {
3197
+ result[key] = value;
3198
+ }
3199
+ }
3200
+ return result;
3201
+ }
3202
+ async function readJsonConfig(filePath) {
3203
+ try {
3204
+ const content = await readFile(filePath, "utf-8");
3205
+ return JSON.parse(content);
3206
+ } catch {
3207
+ return {};
3208
+ }
3209
+ }
3210
+ function isPdfxAlreadyConfigured(config) {
3211
+ const withMcpServers = config;
3212
+ const withServers = config;
3213
+ const withMcp = config;
3214
+ return withMcpServers.mcpServers?.pdfx !== void 0 || withServers.servers?.pdfx !== void 0 || withMcp.mcp?.pdfx !== void 0;
3215
+ }
3216
+ async function initMcpConfig(opts) {
3217
+ const preFlightResult = runPreFlightChecks();
3218
+ displayPreFlightResults(preFlightResult);
3219
+ if (!preFlightResult.canProceed) {
3220
+ console.error(
3221
+ chalk8.red("\n Cannot proceed due to blocking issues. Please fix them and try again.\n")
3222
+ );
3223
+ process.exit(1);
3224
+ }
3225
+ let clientName = opts.client;
3226
+ if (!clientName) {
3227
+ const response = await prompts5({
3228
+ type: "select",
3229
+ name: "client",
3230
+ message: "Which AI editor are you configuring?",
3231
+ choices: CLIENTS.map((c) => ({ title: c.label, value: c.name }))
3232
+ });
3233
+ if (!response.client) {
3234
+ process.exit(0);
3235
+ }
3236
+ clientName = response.client;
3237
+ }
3238
+ const client = CLIENTS.find((c) => c.name === clientName);
3239
+ if (!client) {
3240
+ process.stderr.write(`Unknown client: "${clientName}"
3241
+ `);
3242
+ process.stderr.write(`Valid options: ${CLIENTS.map((c) => c.name).join(", ")}
3243
+ `);
3244
+ process.exit(1);
3245
+ }
3246
+ const configPath = path11.resolve(process.cwd(), client.configPath);
3247
+ const configDir = path11.dirname(configPath);
3248
+ const existing = await readJsonConfig(configPath);
3249
+ if (isPdfxAlreadyConfigured(existing)) {
3250
+ const { overwrite } = await prompts5({
3251
+ type: "confirm",
3252
+ name: "overwrite",
3253
+ message: `PDFx MCP is already configured in ${client.configPath}. Overwrite?`,
3254
+ initial: false
3255
+ });
3256
+ if (!overwrite) {
3257
+ process.stdout.write("\nSkipped \u2014 existing configuration kept.\n");
3258
+ return;
3259
+ }
3260
+ }
3261
+ if (!existsSync(configDir)) {
3262
+ await mkdir(configDir, { recursive: true });
3263
+ }
3264
+ const merged = mergeDeep(existing, client.config);
3265
+ await writeFile2(configPath, `${JSON.stringify(merged, null, 2)}
3266
+ `, "utf-8");
3267
+ process.stdout.write(`
3268
+ \u2713 Wrote MCP configuration to ${client.configPath}
3269
+ `);
3270
+ process.stdout.write(`
3271
+ Restart ${client.label} to activate the PDFx MCP server.
3272
+ `);
3273
+ if (client.name === "claude") {
3274
+ process.stdout.write("\nAlternatively, run:\n claude mcp add pdfx -- npx -y pdfx-cli mcp\n");
3275
+ }
3276
+ if (client.name === "opencode") {
3277
+ process.stdout.write("\nVerify: run `opencode mcp list` \u2014 pdfx should appear.\n");
3278
+ process.stdout.write(
3279
+ "Tip: move opencode.json to ~/.config/opencode/opencode.json for global access.\n"
3280
+ );
3281
+ }
3282
+ if (client.name === "antigravity") {
3283
+ process.stdout.write("\nVerify: open Antigravity \u2192 MCP panel \u2192 pdfx should appear.\n");
3284
+ }
3285
+ if (client.name === "other") {
3286
+ process.stdout.write("\nThis file contains the PDFx MCP server configuration.\n");
3287
+ process.stdout.write(
3288
+ "Copy the pdfx entry into your editor's native MCP config file, or reference this file directly.\n"
3289
+ );
3290
+ }
3291
+ process.stdout.write("\n");
3292
+ }
3293
+ var mcpCommand = new Command().name("mcp").description("Start the PDFx MCP server for AI editor integration").action(async () => {
3294
+ const transport = new StdioServerTransport();
3295
+ await server.connect(transport);
3296
+ });
3297
+ mcpCommand.command("init").description("Add PDFx MCP server config to your AI editor").option(
3298
+ "--client <name>",
3299
+ `AI editor to configure. One of: ${CLIENTS.map((c) => c.name).join(", ")}`
3300
+ ).action(initMcpConfig);
3301
+
3302
+ // src/commands/skills.ts
3303
+ import { existsSync as existsSync2, readFileSync } from "fs";
3304
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
3305
+ import path12 from "path";
3306
+ import chalk9 from "chalk";
3307
+ import { Command as Command2 } from "commander";
3308
+ import prompts6 from "prompts";
3309
+
3310
+ // src/skills-content.ts
3311
+ var PDFX_SKILLS_CONTENT = `# PDFx \u2014 AI Context Guide
3312
+ # Version: 1.0 | Updated: 2026 | License: MIT
3313
+ # Or run: npx pdfx-cli@latest skills init (handles editor-specific paths & frontmatter)
3314
+
3315
+ ## What is PDFx?
3316
+
3317
+ PDFx is an open-source, shadcn/ui-style PDF component library for React. It is built on
3318
+ @react-pdf/renderer and provides 24 type-safe components, 10 pre-built document blocks,
3319
+ 3 themes, and a CLI. Components are copied into your project (not installed as npm imports
3320
+ that expose a public API).
3321
+
3322
+ Key facts:
3323
+ - Package: pdfx-cli (the CLI that installs components)
3324
+ - Registry: https://pdfx.akashpise.dev/r/
3325
+ - Runtime: Works in browser AND Node.js (Next.js App Router, Express, etc.)
3326
+ - React version: 16.8+ (hooks required)
3327
+ - Peer dep: @react-pdf/renderer ^3.x
3328
+
3329
+ ---
3330
+
3331
+ ## Installation (one-time project setup)
3332
+
3333
+ \`\`\`bash
3334
+ # 1. Initialize PDFx \u2014 creates src/lib/pdfx-theme.ts and installs @pdfx/shared
3335
+ npx pdfx-cli@latest init
3336
+
3337
+ # 2. Add components you need
3338
+ npx pdfx-cli@latest add heading text table
3339
+
3340
+ # 3. Add a pre-built block
3341
+ npx pdfx-cli@latest block add invoice-modern
3342
+ \`\`\`
3343
+
3344
+ The init command adds a theme file at src/lib/pdfx-theme.ts. All components read from this file.
3345
+
3346
+ ---
3347
+
3348
+ ## How components work
3349
+
3350
+ PDFx components are React components that render @react-pdf/renderer primitives (View, Text,
3351
+ Page, Document, etc.). They CANNOT render HTML or DOM elements \u2014 they only work inside a
3352
+ <Document> from @react-pdf/renderer.
3353
+
3354
+ Usage pattern:
3355
+ \`\`\`tsx
3356
+ import { Document, Page } from '@react-pdf/renderer';
3357
+ import { Heading } from '@/components/pdfx/heading/pdfx-heading';
3358
+ import { Text } from '@/components/pdfx/text/pdfx-text';
3359
+ import { Table } from '@/components/pdfx/table/pdfx-table';
3360
+
3361
+ export function MyDocument() {
3362
+ return (
3363
+ <Document>
3364
+ <Page size="A4" style={{ padding: 40 }}>
3365
+ <Heading level={1}>Invoice #001</Heading>
3366
+ <Text>Thank you for your business.</Text>
3367
+ <Table
3368
+ headers={['Item', 'Qty', 'Price']}
3369
+ rows={[['Design work', '1', '$4,800']]}
3370
+ />
3371
+ </Page>
3372
+ </Document>
3373
+ );
3374
+ }
3375
+ \`\`\`
3376
+
3377
+ Rendering to PDF:
3378
+ \`\`\`tsx
3379
+ // Browser: live preview
3380
+ import { PDFViewer } from '@react-pdf/renderer';
3381
+ <PDFViewer><MyDocument /></PDFViewer>
3382
+
3383
+ // Browser: download button
3384
+ import { PDFDownloadLink } from '@react-pdf/renderer';
3385
+ <PDFDownloadLink document={<MyDocument />} fileName="output.pdf">Download</PDFDownloadLink>
3386
+
3387
+ // Server (Next.js App Router):
3388
+ import { renderToBuffer } from '@react-pdf/renderer';
3389
+ export async function GET() {
3390
+ const buf = await renderToBuffer(<MyDocument />);
3391
+ return new Response(buf, { headers: { 'Content-Type': 'application/pdf' } });
3392
+ }
3393
+ \`\`\`
3394
+
3395
+ ---
3396
+
3397
+ ## All 24 Components \u2014 Props Reference
3398
+
3399
+ CRITICAL: These are the EXACT props. Do not invent additional props.
3400
+
3401
+ ### Heading
3402
+ \`\`\`tsx
3403
+ import { Heading } from '@/components/pdfx/heading/pdfx-heading';
3404
+ <Heading
3405
+ level={1} // 1 | 2 | 3 | 4 | 5 | 6 \u2014 default: 1
3406
+ align="left" // 'left' | 'center' | 'right' \u2014 default: 'left'
3407
+ weight="bold" // 'normal' | 'bold' \u2014 default: 'bold'
3408
+ tracking="normal" // 'tight' | 'normal' | 'wide' \u2014 default: 'normal'
3409
+ color="#000" // string \u2014 default: theme.colors.foreground
3410
+ gutterBottom // boolean \u2014 adds bottom margin
3411
+ >
3412
+ My Heading
3413
+ </Heading>
3414
+ \`\`\`
3415
+
3416
+ ### Text
3417
+ \`\`\`tsx
3418
+ import { Text } from '@/components/pdfx/text/pdfx-text';
3419
+ <Text
3420
+ size="md" // 'xs' | 'sm' | 'md' | 'lg' | 'xl' \u2014 default: 'md'
3421
+ weight="normal" // 'normal' | 'medium' | 'semibold' | 'bold' \u2014 default: 'normal'
3422
+ color="#000" // string
3423
+ align="left" // 'left' | 'center' | 'right' | 'justify'
3424
+ italic // boolean
3425
+ muted // boolean \u2014 applies theme.colors.mutedForeground
3426
+ gutterBottom // boolean
3427
+ >
3428
+ Paragraph text here.
3429
+ </Text>
3430
+ \`\`\`
3431
+
3432
+ ### Link
3433
+ \`\`\`tsx
3434
+ import { Link } from '@/components/pdfx/link/pdfx-link';
3435
+ <Link
3436
+ href="https://example.com" // string \u2014 required
3437
+ size="md" // same as Text size
3438
+ color="#0000ff" // default: theme.colors.accent
3439
+ >
3440
+ Click here
3441
+ </Link>
3442
+ \`\`\`
3443
+
3444
+ ### Divider
3445
+ \`\`\`tsx
3446
+ import { Divider } from '@/components/pdfx/divider/pdfx-divider';
3447
+ <Divider
3448
+ thickness={1} // number in pt \u2014 default: 1
3449
+ color="#e4e4e7" // string \u2014 default: theme.colors.border
3450
+ spacing="md" // 'sm' | 'md' | 'lg' \u2014 vertical margin
3451
+ style="solid" // 'solid' | 'dashed' | 'dotted'
3452
+ />
3453
+ \`\`\`
3454
+
3455
+ ### PageBreak
3456
+ \`\`\`tsx
3457
+ import { PageBreak } from '@/components/pdfx/page-break/pdfx-page-break';
3458
+ <PageBreak /> // No props. Forces a new page.
3459
+ \`\`\`
3460
+
3461
+ ### Stack
3462
+ \`\`\`tsx
3463
+ import { Stack } from '@/components/pdfx/stack/pdfx-stack';
3464
+ <Stack
3465
+ direction="column" // 'row' | 'column' \u2014 default: 'column'
3466
+ gap={8} // number in pt \u2014 default: 0
3467
+ align="flex-start" // flexbox align-items
3468
+ justify="flex-start"// flexbox justify-content
3469
+ wrap // boolean \u2014 flex-wrap
3470
+ >
3471
+ {children}
3472
+ </Stack>
3473
+ \`\`\`
3474
+
3475
+ ### Section
3476
+ \`\`\`tsx
3477
+ import { Section } from '@/components/pdfx/section/pdfx-section';
3478
+ <Section
3479
+ title="Section Title" // string \u2014 optional
3480
+ titleLevel={2} // 1\u20136 \u2014 default: 2
3481
+ padding={16} // number | {top,right,bottom,left} \u2014 default: 0
3482
+ bordered // boolean \u2014 adds border around section
3483
+ background="#f9f9f9" // string \u2014 background color
3484
+ >
3485
+ {children}
3486
+ </Section>
3487
+ \`\`\`
3488
+
3489
+ ### Table
3490
+ \`\`\`tsx
3491
+ import { Table } from '@/components/pdfx/table/pdfx-table';
3492
+ <Table
3493
+ headers={['Column A', 'Column B', 'Column C']} // string[] \u2014 required
3494
+ rows={[['R1C1', 'R1C2', 'R1C3']]} // string[][] \u2014 required
3495
+ striped // boolean \u2014 alternating row colors
3496
+ bordered // boolean \u2014 cell borders
3497
+ compact // boolean \u2014 smaller padding
3498
+ headerBg="#18181b" // string \u2014 header background
3499
+ headerColor="#fff" // string \u2014 header text color
3500
+ columnWidths={[2, 1, 1]} // number[] \u2014 flex ratios
3501
+ caption="Table 1" // string \u2014 caption below table
3502
+ />
3503
+ \`\`\`
3504
+
3505
+ ### DataTable
3506
+ \`\`\`tsx
3507
+ import { DataTable } from '@/components/pdfx/data-table/pdfx-data-table';
3508
+ <DataTable
3509
+ columns={[
3510
+ { key: 'name', header: 'Name', width: 2 },
3511
+ { key: 'amount', header: 'Amount', width: 1, align: 'right' },
3512
+ ]}
3513
+ data={[{ name: 'Item A', amount: '$100' }]}
3514
+ striped
3515
+ bordered
3516
+ compact
3517
+ />
3518
+ \`\`\`
3519
+
3520
+ ### List
3521
+ \`\`\`tsx
3522
+ import { List } from '@/components/pdfx/list/pdfx-list';
3523
+ <List
3524
+ items={['First item', 'Second item', 'Third item']} // string[] \u2014 required
3525
+ ordered // boolean \u2014 numbered list (default: bulleted)
3526
+ bullet="\u2022" // string \u2014 custom bullet character
3527
+ indent={16} // number \u2014 left indent in pt
3528
+ spacing="sm" // 'sm' | 'md' | 'lg' \u2014 gap between items
3529
+ />
3530
+ \`\`\`
3531
+
3532
+ ### Card
3533
+ \`\`\`tsx
3534
+ import { Card } from '@/components/pdfx/card/pdfx-card';
3535
+ <Card
3536
+ padding={16} // number \u2014 default: 16
3537
+ bordered // boolean \u2014 default: true
3538
+ shadow // boolean
3539
+ background="#fff" // string
3540
+ borderColor="#e4e4e7" // string
3541
+ borderRadius={4} // number in pt
3542
+ >
3543
+ {children}
3544
+ </Card>
3545
+ \`\`\`
3546
+
3547
+ ### Form (read-only form fields for PDFs)
3548
+ \`\`\`tsx
3549
+ import { Form } from '@/components/pdfx/form/pdfx-form';
3550
+ <Form
3551
+ fields={[
3552
+ { label: 'Full Name', value: 'John Doe' },
3553
+ { label: 'Email', value: 'john@example.com' },
3554
+ { label: 'Notes', value: 'Some notes here', multiline: true },
3555
+ ]}
3556
+ columns={2} // 1 | 2 \u2014 default: 1
3557
+ bordered // boolean
3558
+ />
3559
+ \`\`\`
3560
+
3561
+ ### Signature
3562
+ \`\`\`tsx
3563
+ import { PdfSignatureBlock } from '@/components/pdfx/signature/pdfx-signature';
3564
+ <PdfSignatureBlock
3565
+ name="Sarah Chen" // string \u2014 printed name below line
3566
+ title="Engineering Lead" // string \u2014 title/role below name
3567
+ date="2024-12-12" // string \u2014 formatted date
3568
+ lineWidth={120} // number \u2014 signature line width in pt
3569
+ showDate // boolean \u2014 default: true
3570
+ />
3571
+ \`\`\`
3572
+
3573
+ ### PageHeader
3574
+ \`\`\`tsx
3575
+ import { PageHeader } from '@/components/pdfx/page-header/pdfx-page-header';
3576
+ <PageHeader
3577
+ title="Document Title"
3578
+ subtitle="Subtitle or tagline"
3579
+ logo={{ src: 'https://...', width: 60, height: 30 }}
3580
+ bordered // boolean \u2014 adds bottom border
3581
+ />
3582
+ \`\`\`
3583
+
3584
+ ### PageFooter
3585
+ \`\`\`tsx
3586
+ import { PageFooter } from '@/components/pdfx/page-footer/pdfx-page-footer';
3587
+ <PageFooter
3588
+ left="\xA9 2024 Acme Corp"
3589
+ center="Confidential"
3590
+ right="Page 1 of 1"
3591
+ bordered // boolean \u2014 top border
3592
+ />
3593
+ \`\`\`
3594
+
3595
+ ### Badge
3596
+ \`\`\`tsx
3597
+ import { Badge } from '@/components/pdfx/badge/pdfx-badge';
3598
+ // Use label prop OR children (string only \u2014 not a React node)
3599
+ <Badge
3600
+ label="PAID" // string \u2014 preferred API
3601
+ variant="success" // 'default' | 'success' | 'warning' | 'error' | 'info' | 'outline'
3602
+ size="md" // 'sm' | 'md' | 'lg'
3603
+ />
3604
+ // OR
3605
+ <Badge variant="success">PAID</Badge>
3606
+ \`\`\`
3607
+
3608
+ ### KeyValue
3609
+ \`\`\`tsx
3610
+ import { KeyValue } from '@/components/pdfx/key-value/pdfx-key-value';
3611
+ <KeyValue
3612
+ items={[
3613
+ { label: 'Invoice #', value: 'INV-001' },
3614
+ { label: 'Due Date', value: 'Jan 31, 2025' },
3615
+ ]}
3616
+ columns={2} // 1 | 2 | 3 \u2014 default: 1
3617
+ labelWidth={80} // number in pt
3618
+ colon // boolean \u2014 adds colon after label
3619
+ />
3620
+ \`\`\`
3621
+
3622
+ ### KeepTogether
3623
+ \`\`\`tsx
3624
+ import { KeepTogether } from '@/components/pdfx/keep-together/pdfx-keep-together';
3625
+ // Prevents page breaks inside its children
3626
+ <KeepTogether>
3627
+ <Heading level={3}>Section that must not split</Heading>
3628
+ <Table headers={[...]} rows={[...]} />
3629
+ </KeepTogether>
3630
+ \`\`\`
3631
+
3632
+ ### PdfImage
3633
+ \`\`\`tsx
3634
+ import { PdfImage } from '@/components/pdfx/pdf-image/pdfx-pdf-image';
3635
+ <PdfImage
3636
+ src="https://example.com/image.png" // string | base64 \u2014 required
3637
+ width={200} // number in pt
3638
+ height={150} // optional \u2014 maintains aspect ratio if omitted
3639
+ alt="Description" // string \u2014 for accessibility
3640
+ objectFit="cover" // 'cover' | 'contain' | 'fill'
3641
+ borderRadius={4} // number
3642
+ />
3643
+ \`\`\`
3644
+
3645
+ ### Graph
3646
+ \`\`\`tsx
3647
+ import { Graph } from '@/components/pdfx/graph/pdfx-graph';
3648
+ <Graph
3649
+ type="bar" // 'bar' | 'line' | 'pie' | 'donut'
3650
+ data={[
3651
+ { label: 'Q1', value: 4200 },
3652
+ { label: 'Q2', value: 6100 },
3653
+ ]}
3654
+ width={400} // number in pt
3655
+ height={200} // number in pt
3656
+ title="Revenue" // string \u2014 optional
3657
+ showValues // boolean \u2014 show value labels
3658
+ showLegend // boolean
3659
+ colors={['#18181b', '#71717a']} // string[] \u2014 bar/slice colors
3660
+ />
3661
+ \`\`\`
3662
+
3663
+ ### PageNumber
3664
+ \`\`\`tsx
3665
+ import { PageNumber } from '@/components/pdfx/page-number/pdfx-page-number';
3666
+ <PageNumber
3667
+ format="Page {current} of {total}" // string template
3668
+ size="sm"
3669
+ align="right"
3670
+ />
3671
+ \`\`\`
3672
+
3673
+ ### Watermark
3674
+ \`\`\`tsx
3675
+ import { PdfWatermark } from '@/components/pdfx/watermark/pdfx-watermark';
3676
+ <PdfWatermark
3677
+ text="CONFIDENTIAL" // string \u2014 required
3678
+ opacity={0.08} // number 0\u20131 \u2014 default: 0.08
3679
+ angle={-35} // number in degrees \u2014 default: -35
3680
+ fontSize={72} // number \u2014 default: 72
3681
+ color="#000000" // string
3682
+ />
3683
+ \`\`\`
3684
+
3685
+ ### QRCode
3686
+ \`\`\`tsx
3687
+ import { PdfQRCode } from '@/components/pdfx/qrcode/pdfx-qrcode';
3688
+ <PdfQRCode
3689
+ value="https://example.com" // string \u2014 required
3690
+ size={80} // number in pt \u2014 default: 80
3691
+ errorCorrectionLevel="M" // 'L' | 'M' | 'Q' | 'H'
3692
+ />
3693
+ \`\`\`
3694
+
3695
+ ### Alert
3696
+ \`\`\`tsx
3697
+ import { Alert } from '@/components/pdfx/alert/pdfx-alert';
3698
+ <Alert
3699
+ variant="info" // 'info' | 'success' | 'warning' | 'error'
3700
+ title="Note" // string \u2014 optional bold title
3701
+ >
3702
+ This is an informational note.
3703
+ </Alert>
3704
+ \`\`\`
3705
+
3706
+ ---
3707
+
3708
+ ## Pre-built Blocks
3709
+
3710
+ Blocks are complete document templates. Add them with:
3711
+ \`\`\`bash
3712
+ npx pdfx-cli@latest block add <block-name>
3713
+ \`\`\`
3714
+
3715
+ ### Invoice Blocks
3716
+ - invoice-modern \u2014 Clean two-column layout with totals table
3717
+ - invoice-minimal \u2014 Stripped-down, typography-focused
3718
+ - invoice-corporate \u2014 Header with logo area, full itemization
3719
+ - invoice-creative \u2014 Accent colors, bold layout
3720
+
3721
+ ### Report Blocks
3722
+ - report-executive \u2014 KPI cards + summary table, 2-page
3723
+ - report-annual \u2014 Multi-section with charts and appendix
3724
+ - report-financial \u2014 P&L / balance sheet focus
3725
+ - report-marketing \u2014 Campaign metrics with graphs
3726
+ - report-technical \u2014 Code-friendly, monospace sections
3727
+
3728
+ ### Contract Block
3729
+ - contract-standard \u2014 Signature page, numbered clauses, party info
3730
+
3731
+ Blocks are added as full React components in your project. Customize all content props.
3732
+
3733
+ ---
3734
+
3735
+ ## Theming
3736
+
3737
+ ### The theme file
3738
+
3739
+ After \`npx pdfx-cli@latest init\`, a file is created at src/lib/pdfx-theme.ts.
3740
+ Every PDFx component reads from this file \u2014 change a token once, all components update.
3741
+
3742
+ \`\`\`typescript
3743
+ export const theme: PdfxTheme = {
3744
+ name: 'my-brand',
3745
+ colors: {
3746
+ primary: '#2563eb',
3747
+ accent: '#7c3aed',
3748
+ foreground: '#1a1a1a',
3749
+ background: '#ffffff',
3750
+ muted: '#f4f4f5',
3751
+ mutedForeground: '#71717a',
3752
+ primaryForeground: '#ffffff',
3753
+ border: '#e4e4e7',
3754
+ destructive: '#dc2626',
3755
+ success: '#16a34a',
3756
+ warning: '#d97706',
3757
+ info: '#0ea5e9',
3758
+ },
3759
+ typography: {
3760
+ heading: { fontFamily: 'Helvetica-Bold', fontWeight: 700, lineHeight: 1.2,
3761
+ fontSize: { h1: 36, h2: 28, h3: 22, h4: 18, h5: 15, h6: 12 } },
3762
+ body: { fontFamily: 'Helvetica', fontSize: 11, lineHeight: 1.5 },
3763
+ },
3764
+ // primitives, spacing, page \u2014 all required (scaffolded by init)
3765
+ };
3766
+ \`\`\`
3767
+
3768
+ ### Theme presets
3769
+ \`\`\`bash
3770
+ npx pdfx-cli@latest theme init # scaffold blank theme
3771
+ npx pdfx-cli@latest theme switch modern # switch preset: professional | modern | minimal
3772
+ npx pdfx-cli@latest theme validate # validate your theme file
3773
+ \`\`\`
3774
+
3775
+ ---
3776
+
3777
+ ## CLI Reference
3778
+
3779
+ \`\`\`bash
3780
+ # Setup
3781
+ npx pdfx-cli@latest init # Initialize PDFx in project
3782
+ npx pdfx-cli@latest add <component> # Add a component
3783
+ npx pdfx-cli@latest add <comp1> <comp2> # Add multiple
3784
+ npx pdfx-cli@latest block add <block> # Add a block
3785
+
3786
+ # Theme
3787
+ npx pdfx-cli@latest theme init # Create theme file
3788
+ npx pdfx-cli@latest theme switch professional # Switch preset
3789
+ npx pdfx-cli@latest theme validate # Validate theme
3790
+
3791
+ # MCP (AI editor integration)
3792
+ npx pdfx-cli@latest mcp # Start MCP server
3793
+ npx pdfx-cli@latest mcp init # Configure editor (interactive)
3794
+ npx pdfx-cli@latest mcp init --client claude # Claude Code (.mcp.json)
3795
+ npx pdfx-cli@latest mcp init --client cursor # Cursor (.cursor/mcp.json)
3796
+ npx pdfx-cli@latest mcp init --client vscode # VS Code (.vscode/mcp.json)
3797
+ npx pdfx-cli@latest mcp init --client windsurf # Windsurf (mcp_config.json)
3798
+ npx pdfx-cli@latest mcp init --client qoder # Qoder (.qoder/mcp.json)
3799
+ npx pdfx-cli@latest mcp init --client opencode # opencode (opencode.json)
3800
+ npx pdfx-cli@latest mcp init --client antigravity # Antigravity (.antigravity/mcp.json)
3801
+
3802
+ # Skills file (AI context document)
3803
+ npx pdfx-cli@latest skills init # Write skills file (interactive)
3804
+ npx pdfx-cli@latest skills init --platform claude # CLAUDE.md
3805
+ npx pdfx-cli@latest skills init --platform cursor # .cursor/rules/pdfx.mdc
3806
+ npx pdfx-cli@latest skills init --platform vscode # .github/copilot-instructions.md
3807
+ npx pdfx-cli@latest skills init --platform windsurf # .windsurf/rules/pdfx.md
3808
+ npx pdfx-cli@latest skills init --platform opencode # AGENTS.md
3809
+ npx pdfx-cli@latest skills init --platform antigravity # .antigravity/context.md
3810
+ \`\`\`
3811
+
3812
+ ---
3813
+
3814
+ ## Common patterns
3815
+
3816
+ ### Full invoice from scratch
3817
+ \`\`\`tsx
3818
+ import { Document, Page } from '@react-pdf/renderer';
3819
+ import { Heading } from '@/components/pdfx/heading/pdfx-heading';
3820
+ import { KeyValue } from '@/components/pdfx/key-value/pdfx-key-value';
3821
+ import { Table } from '@/components/pdfx/table/pdfx-table';
3822
+ import { Divider } from '@/components/pdfx/divider/pdfx-divider';
3823
+ import { Badge } from '@/components/pdfx/badge/pdfx-badge';
3824
+ import { PageFooter } from '@/components/pdfx/page-footer/pdfx-page-footer';
3825
+
3826
+ export function InvoiceDoc() {
3827
+ return (
3828
+ <Document>
3829
+ <Page size="A4" style={{ padding: 48, fontFamily: 'Helvetica' }}>
3830
+ <Heading level={1}>Invoice #INV-001</Heading>
3831
+ <KeyValue
3832
+ items={[
3833
+ { label: 'Date', value: 'Jan 1, 2025' },
3834
+ { label: 'Due', value: 'Jan 31, 2025' },
3835
+ ]}
3836
+ columns={2}
3837
+ />
3838
+ <Divider spacing="md" />
3839
+ <Table
3840
+ headers={['Description', 'Qty', 'Total']}
3841
+ rows={[
3842
+ ['Design System', '1', '$4,800'],
3843
+ ['Development', '2', '$9,600'],
3844
+ ]}
3845
+ striped
3846
+ bordered
3847
+ columnWidths={[3, 1, 1]}
3848
+ />
3849
+ <Badge label="PAID" variant="success" />
3850
+ <PageFooter left="Acme Corp" right="Page 1 of 1" bordered />
3851
+ </Page>
3852
+ </Document>
3853
+ );
3854
+ }
3855
+ \`\`\`
3856
+
3857
+ ### Preventing page splits
3858
+ \`\`\`tsx
3859
+ // Wrap anything that must stay together across page boundaries
3860
+ <KeepTogether>
3861
+ <Heading level={3}>Q3 Summary</Heading>
3862
+ <Table headers={['Metric', 'Value']} rows={data} />
3863
+ </KeepTogether>
3864
+ \`\`\`
3865
+
3866
+ ### Server-side generation (Next.js)
3867
+ \`\`\`typescript
3868
+ // app/api/invoice/route.ts
3869
+ import { renderToBuffer } from '@react-pdf/renderer';
3870
+ import { InvoiceDoc } from '@/components/pdf/invoice';
3871
+
3872
+ export async function GET(req: Request) {
3873
+ const { searchParams } = new URL(req.url);
3874
+ const id = searchParams.get('id');
3875
+ const data = await fetchInvoice(id);
3876
+ const buf = await renderToBuffer(<InvoiceDoc data={data} />);
3877
+ return new Response(buf, {
3878
+ headers: {
3879
+ 'Content-Type': 'application/pdf',
3880
+ 'Content-Disposition': \`inline; filename="invoice-\${id}.pdf"\`,
3881
+ },
3882
+ });
3883
+ }
3884
+ \`\`\`
3885
+
3886
+ ---
3887
+
3888
+ ## react-pdf layout constraints (CRITICAL)
3889
+
3890
+ @react-pdf/renderer enforces strict separation between layout containers and text:
3891
+
3892
+ - **View** is a layout container (like a div). It can contain other Views and Text nodes.
3893
+ - **Text** is a text container. It can contain strings or nested Text nodes.
3894
+ - **NEVER mix View and inline text in a flex row.** This causes irrecoverable layout failures.
3895
+
3896
+ \`\`\`tsx
3897
+ // \u2717 WRONG \u2014 mixing View and text siblings in a flex row
3898
+ <View style={{ flexDirection: 'row' }}>
3899
+ <View style={{ width: 100 }}>...</View>
3900
+ Some text here {/* \u2190 this text sibling crashes the layout */}
3901
+ </View>
3902
+
3903
+ // \u2713 CORRECT \u2014 wrap all text siblings in <Text>
3904
+ <View style={{ flexDirection: 'row' }}>
3905
+ <View style={{ width: 100 }}>...</View>
3906
+ <Text>Some text here</Text>
3907
+ </View>
3908
+ \`\`\`
3909
+
3910
+ ---
3911
+
3912
+ ## Anti-patterns to avoid
3913
+
3914
+ - DO NOT use HTML elements inside PDFx components (no <div>, <p>, <span>)
3915
+ - DO NOT import from @react-pdf/renderer inside PDFx component files \u2014 they already wrap it
3916
+ - DO NOT use CSS classes or Tailwind inside PDF components \u2014 use style props or theme tokens
3917
+ - DO NOT use window, document, or browser APIs in server-rendered PDF routes
3918
+ - DO NOT install components with npm \u2014 always use the CLI: npx pdfx-cli@latest add <name>
3919
+ - DO NOT place raw text siblings next to View elements in a flex row (react-pdf constraint)
3920
+ - DO NOT pass React nodes (JSX) as Badge children \u2014 only plain strings are supported
3921
+
3922
+ ---
3923
+
3924
+ ## MCP Server (for AI editors)
3925
+
3926
+ The PDFx MCP server gives AI editors live access to the entire registry:
3927
+ \`\`\`bash
3928
+ npx pdfx-cli@latest mcp init # interactive setup for your editor
3929
+ \`\`\`
3930
+ Supported: Claude Code, Cursor, VS Code, Windsurf, Qoder, opencode, Antigravity
3931
+
3932
+ Tools: list_components, get_component, list_blocks, get_block, search_registry,
3933
+ get_theme, get_installation, get_add_command, get_audit_checklist
3934
+
3935
+ ---
3936
+ # End of PDFx AI Context Guide
3937
+ `;
3938
+
3939
+ // src/commands/skills.ts
3940
+ var PLATFORMS = [
3941
+ {
3942
+ name: "claude",
3943
+ label: "Claude Code",
3944
+ file: "CLAUDE.md",
3945
+ hint: "Project-level CLAUDE.md \u2014 read automatically by Claude Code",
3946
+ verifyStep: "Claude Code picks up CLAUDE.md automatically on next session."
3947
+ },
3948
+ {
3949
+ // .cursor/rules/*.mdc with alwaysApply: true — Cursor's current rules format (not .cursorrules).
3950
+ name: "cursor",
3951
+ label: "Cursor",
3952
+ file: ".cursor/rules/pdfx.mdc",
3953
+ hint: ".cursor/rules/pdfx.mdc \u2014 Cursor rules file with alwaysApply frontmatter",
3954
+ verifyStep: "Cursor reads .cursor/rules/*.mdc automatically. Restart Cursor to be sure.",
3955
+ frontmatter: "---\ndescription: PDFx PDF component library AI context\nglobs: \nalwaysApply: true\n---\n\n"
3956
+ },
3957
+ {
3958
+ name: "vscode",
3959
+ label: "VS Code (Copilot)",
3960
+ file: ".github/copilot-instructions.md",
3961
+ hint: ".github/copilot-instructions.md \u2014 read by GitHub Copilot Chat",
3962
+ verifyStep: "GitHub Copilot reads copilot-instructions.md from .github/. Commit the file so teammates get it too."
3963
+ },
3964
+ {
3965
+ name: "windsurf",
3966
+ label: "Windsurf",
3967
+ file: ".windsurfrules",
3968
+ hint: ".windsurfrules \u2014 applied to all Cascade AI interactions",
3969
+ verifyStep: "Windsurf applies .windsurfrules automatically."
3970
+ },
3971
+ {
3972
+ name: "qoder",
3973
+ label: "Qoder",
3974
+ file: ".qoder/rules.md",
3975
+ hint: ".qoder/rules.md \u2014 Qoder project-level AI context",
3976
+ verifyStep: "Restart Qoder and check that the AI context is loaded from .qoder/rules.md."
3977
+ },
3978
+ {
3979
+ name: "opencode",
3980
+ label: "opencode",
3981
+ file: "AGENTS.md",
3982
+ hint: "AGENTS.md \u2014 opencode project context file",
3983
+ verifyStep: "opencode reads AGENTS.md from the project root. Start a new session to verify."
3984
+ },
3985
+ {
3986
+ name: "antigravity",
3987
+ label: "Antigravity",
3988
+ file: ".antigravity/context.md",
3989
+ hint: ".antigravity/context.md \u2014 Antigravity project context",
3990
+ verifyStep: "Verify the context path against your Antigravity version. Restart to activate."
3991
+ },
3992
+ {
3993
+ name: "other",
3994
+ label: "Generic (any AI tool)",
3995
+ file: "pdfx-context.md",
3996
+ hint: "pdfx-context.md \u2014 reference this from your editor's rules file",
3997
+ verifyStep: "Reference pdfx-context.md from your editor's rules file (e.g. .cursorrules, CLAUDE.md)."
3998
+ }
3999
+ ];
4000
+ var PDFX_MARKER = "# PDFx \u2014 AI Context Guide";
4001
+ function fileHasPdfxContent(filePath) {
4002
+ try {
4003
+ return readFileSync(filePath, "utf-8").includes(PDFX_MARKER);
4004
+ } catch {
4005
+ return false;
4006
+ }
4007
+ }
4008
+ async function skillsInit(opts) {
4009
+ const preFlightResult = runPreFlightChecks();
4010
+ displayPreFlightResults(preFlightResult);
4011
+ if (!preFlightResult.canProceed) {
4012
+ console.error(
4013
+ chalk9.red("\n Cannot proceed due to blocking issues. Please fix them and try again.\n")
4014
+ );
4015
+ process.exit(1);
4016
+ }
4017
+ let platformName = opts.platform;
4018
+ if (!platformName) {
4019
+ const response = await prompts6({
4020
+ type: "select",
4021
+ name: "platform",
4022
+ message: "Which AI editor are you targeting?",
4023
+ choices: PLATFORMS.map((p) => ({
4024
+ title: p.label,
4025
+ value: p.name,
4026
+ description: p.hint
4027
+ }))
4028
+ });
4029
+ if (!response.platform) {
4030
+ process.exit(0);
4031
+ }
4032
+ platformName = response.platform;
4033
+ }
4034
+ const platform = PLATFORMS.find((p) => p.name === platformName);
4035
+ if (!platform) {
4036
+ process.stderr.write(chalk9.red(`\u2716 Unknown platform: "${platformName}"
4037
+ `));
4038
+ process.stderr.write(
4039
+ chalk9.dim(` Valid options: ${PLATFORMS.map((p) => p.name).join(", ")}
4040
+ `)
4041
+ );
4042
+ process.exit(1);
4043
+ }
4044
+ const filePath = path12.resolve(process.cwd(), platform.file);
4045
+ const fileDir = path12.dirname(filePath);
4046
+ const relativeFile = platform.file;
4047
+ const alreadyExists = existsSync2(filePath);
4048
+ const hasPdfxContent = alreadyExists && fileHasPdfxContent(filePath);
4049
+ let shouldAppend = opts.append ?? false;
4050
+ if (alreadyExists) {
4051
+ if (shouldAppend || opts.yes) {
4052
+ } else if (hasPdfxContent) {
4053
+ const { action: action2 } = await prompts6({
4054
+ type: "select",
4055
+ name: "action",
4056
+ message: `${relativeFile} already contains PDFx context. What would you like to do?`,
4057
+ choices: [
4058
+ { title: "Overwrite (replace PDFx section with latest content)", value: "overwrite" },
4059
+ { title: "Skip", value: "skip" }
4060
+ ]
4061
+ });
4062
+ if (!action2 || action2 === "skip") {
4063
+ process.stdout.write(chalk9.dim("\nSkipped \u2014 existing file kept.\n\n"));
4064
+ return;
4065
+ }
4066
+ } else {
4067
+ const { action: action2 } = await prompts6({
4068
+ type: "select",
4069
+ name: "action",
4070
+ message: `${relativeFile} already exists. What would you like to do?`,
4071
+ choices: [
4072
+ {
4073
+ title: "Append (add PDFx context at the end, keep existing content)",
4074
+ value: "append"
4075
+ },
4076
+ { title: "Overwrite (replace entire file with PDFx context)", value: "overwrite" },
4077
+ { title: "Skip", value: "skip" }
4078
+ ]
4079
+ });
4080
+ if (!action2 || action2 === "skip") {
4081
+ process.stdout.write(chalk9.dim("\nSkipped \u2014 existing file kept.\n\n"));
4082
+ return;
4083
+ }
4084
+ if (action2 === "append") {
4085
+ shouldAppend = true;
4086
+ }
4087
+ }
4088
+ }
4089
+ if (!existsSync2(fileDir)) {
4090
+ await mkdir2(fileDir, { recursive: true });
4091
+ }
4092
+ const appendContent = `
4093
+
4094
+ ---
4095
+
4096
+ ${PDFX_SKILLS_CONTENT}`;
4097
+ const content = shouldAppend ? appendContent : PDFX_SKILLS_CONTENT;
4098
+ if (shouldAppend && alreadyExists) {
4099
+ const existing = readFileSync(filePath, "utf-8");
4100
+ await writeFile3(filePath, `${existing.trimEnd()}${content}
4101
+ `, "utf-8");
4102
+ } else {
4103
+ await writeFile3(filePath, `${content}
4104
+ `, "utf-8");
4105
+ }
4106
+ const action = shouldAppend && alreadyExists ? "Appended PDFx context to" : "Wrote";
4107
+ process.stdout.write(`
4108
+ ${chalk9.green("\u2713")} ${action} ${chalk9.cyan(relativeFile)}
4109
+ `);
4110
+ process.stdout.write(`
4111
+ ${chalk9.dim(platform.verifyStep)}
4112
+
4113
+ `);
4114
+ if (platform.name === "claude") {
4115
+ process.stdout.write(
4116
+ chalk9.dim(
4117
+ "Tip: if you also use the MCP server, CLAUDE.md + MCP gives the AI\nthe best possible PDFx knowledge \u2014 static props reference + live registry.\n\n"
4118
+ )
4119
+ );
4120
+ }
4121
+ }
4122
+ var skillsCommand = new Command2().name("skills").description("Manage the PDFx AI context (skills) file for your editor");
4123
+ skillsCommand.command("init").description("Write the PDFx skills file to your AI editor's context file").option(
4124
+ "-p, --platform <name>",
4125
+ `Target AI editor. One of: ${PLATFORMS.map((p) => p.name).join(", ")}`
4126
+ ).option("-y, --yes", "Overwrite existing file without prompting").option("-a, --append", "Append PDFx context to an existing file instead of overwriting").action(skillsInit);
4127
+ skillsCommand.command("list").description("List all supported AI editor platforms").action(() => {
4128
+ process.stdout.write("\n");
4129
+ process.stdout.write(chalk9.bold(" Supported platforms\n\n"));
4130
+ for (const p of PLATFORMS) {
4131
+ process.stdout.write(` ${chalk9.cyan(p.name.padEnd(12))} ${chalk9.dim("\u2192")} ${p.file}
4132
+ `);
4133
+ }
4134
+ process.stdout.write("\n");
4135
+ process.stdout.write(
4136
+ chalk9.dim(" Usage: npx pdfx-cli@latest skills init --platform <name>\n\n")
4137
+ );
4138
+ });
4139
+
4140
+ // src/commands/theme.ts
4141
+ import fs8 from "fs";
4142
+ import path13 from "path";
4143
+ import chalk10 from "chalk";
4144
+ import ora7 from "ora";
4145
+ import prompts7 from "prompts";
4146
+ import ts from "typescript";
4147
+ async function themeInit() {
4148
+ const configPath = path13.join(process.cwd(), "pdfx.json");
4149
+ if (!checkFileExists(configPath)) {
4150
+ console.error(chalk10.red("\nError: pdfx.json not found"));
4151
+ console.log(chalk10.yellow("\n PDFx is not initialized in this project.\n"));
4152
+ console.log(chalk10.cyan(" Run: pdfx init"));
4153
+ console.log(chalk10.dim(" This will set up your project configuration and theme.\n"));
4154
+ process.exit(1);
4155
+ }
4156
+ console.log(chalk10.bold.cyan("\n PDFx Theme Setup\n"));
4157
+ const answers = await prompts7(
4158
+ [
4159
+ {
4160
+ type: "select",
4161
+ name: "preset",
4162
+ message: "Choose a theme preset:",
4163
+ choices: [
4164
+ {
4165
+ title: "Professional",
4166
+ description: "Serif headings, navy colors, generous margins",
4167
+ value: "professional"
4168
+ },
4169
+ {
4170
+ title: "Modern",
4171
+ description: "Sans-serif, vibrant purple, tight spacing",
4172
+ value: "modern"
4173
+ },
4174
+ {
4175
+ title: "Minimal",
4176
+ description: "Monospace headings, stark black, maximum whitespace",
4177
+ value: "minimal"
4178
+ }
4179
+ ],
4180
+ initial: 0
4181
+ },
4182
+ {
4183
+ type: "text",
4184
+ name: "themePath",
4185
+ message: "Where should we create the theme file?",
4186
+ initial: DEFAULTS.THEME_FILE,
4187
+ format: normalizeThemePath,
4188
+ validate: validateThemePath
4189
+ }
4190
+ ],
4191
+ {
4192
+ onCancel: () => {
4193
+ console.log(chalk10.yellow("\nTheme setup cancelled."));
4194
+ process.exit(0);
4195
+ }
4196
+ }
4197
+ );
4198
+ if (!answers.preset || !answers.themePath) {
4199
+ console.error(chalk10.red("Missing required fields."));
4200
+ process.exit(1);
4201
+ }
4202
+ const presetName = answers.preset;
4203
+ const themePath = answers.themePath;
4204
+ const preset = themePresets[presetName];
4205
+ const spinner = ora7(`Scaffolding ${presetName} theme...`).start();
4206
+ try {
4207
+ const absThemePath = path13.resolve(process.cwd(), themePath);
4208
+ writeFile(absThemePath, generateThemeFile(preset));
4209
+ const contextPath = path13.join(path13.dirname(absThemePath), "pdfx-theme-context.tsx");
4210
+ writeFile(contextPath, generateThemeContextFile());
4211
+ spinner.succeed(`Created ${themePath} with ${presetName} theme`);
4212
+ if (checkFileExists(configPath)) {
4213
+ try {
4214
+ const rawConfig = readJsonFile(configPath);
4215
+ const result = configSchema.safeParse(rawConfig);
4216
+ if (result.success) {
4217
+ const updatedConfig = { ...result.data, theme: themePath };
4218
+ writeFile(configPath, JSON.stringify(updatedConfig, null, 2));
4219
+ console.log(chalk10.green(" Updated pdfx.json with theme path"));
4220
+ }
4221
+ } catch {
4222
+ console.log(chalk10.yellow(' Could not update pdfx.json \u2014 add "theme" field manually'));
4223
+ }
4224
+ }
4225
+ console.log(chalk10.dim(`
4226
+ Edit ${themePath} to customize your theme.
4227
+ `));
4228
+ } catch (error) {
4229
+ spinner.fail("Failed to create theme file");
4230
+ const message = error instanceof Error ? error.message : String(error);
4231
+ console.error(chalk10.dim(` ${message}`));
4232
+ process.exit(1);
4233
+ }
4234
+ }
4235
+ async function themeSwitch(presetName) {
4236
+ const resolvedPreset = presetName === "default" ? "professional" : presetName;
4237
+ const validPresets = Object.keys(themePresets);
4238
+ if (!validPresets.includes(resolvedPreset)) {
4239
+ console.error(chalk10.red(`\u2716 Invalid theme preset: "${presetName}"`));
4240
+ console.log(chalk10.dim(` Available presets: ${validPresets.join(", ")}, default
4241
+ `));
4242
+ console.log(chalk10.dim(" Usage: pdfx theme switch <preset>"));
4243
+ process.exit(1);
4244
+ }
4245
+ const validatedPreset = resolvedPreset;
4246
+ const configPath = path13.join(process.cwd(), "pdfx.json");
4247
+ if (!checkFileExists(configPath)) {
4248
+ console.error(chalk10.red('No pdfx.json found. Run "npx pdfx-cli@latest init" first.'));
4249
+ process.exit(1);
4250
+ }
4251
+ const rawConfig = readJsonFile(configPath);
4252
+ const result = configSchema.safeParse(rawConfig);
4253
+ if (!result.success) {
4254
+ console.error(chalk10.red("Invalid pdfx.json configuration."));
4255
+ process.exit(1);
4256
+ }
4257
+ const config = result.data;
4258
+ if (!config.theme) {
4259
+ console.error(
4260
+ chalk10.red(
4261
+ 'No theme path in pdfx.json. Run "npx pdfx-cli@latest theme init" to set up theming.'
4262
+ )
4263
+ );
4264
+ process.exit(1);
4265
+ }
4266
+ const answer = await prompts7({
4267
+ type: "confirm",
4268
+ name: "confirm",
4269
+ message: `This will overwrite ${config.theme} with the ${validatedPreset} preset. Continue?`,
4270
+ initial: false
4271
+ });
4272
+ if (!answer.confirm) {
4273
+ console.log(chalk10.yellow("Cancelled."));
4274
+ return;
4275
+ }
4276
+ const spinner = ora7(`Switching to ${validatedPreset} theme...`).start();
4277
+ try {
4278
+ const preset = themePresets[validatedPreset];
4279
+ const absThemePath = path13.resolve(process.cwd(), config.theme);
4280
+ writeFile(absThemePath, generateThemeFile(preset));
4281
+ const contextPath = path13.join(path13.dirname(absThemePath), "pdfx-theme-context.tsx");
4282
+ if (!checkFileExists(contextPath)) {
4283
+ writeFile(contextPath, generateThemeContextFile());
4284
+ }
4285
+ spinner.succeed(`Switched to ${validatedPreset} theme`);
4286
+ } catch (error) {
4287
+ spinner.fail("Failed to switch theme");
4288
+ const message = error instanceof Error ? error.message : String(error);
4289
+ console.error(chalk10.dim(` ${message}`));
4290
+ process.exit(1);
4291
+ }
4292
+ }
4293
+ function toPlainValue(node) {
4294
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isParenthesizedExpression(node)) {
4295
+ return toPlainValue(node.expression);
4296
+ }
4297
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
4298
+ return node.text;
4299
+ }
4300
+ if (ts.isNumericLiteral(node)) {
4301
+ return Number(node.text);
4302
+ }
4303
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
4304
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
4305
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
4306
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) {
4307
+ const n = toPlainValue(node.operand);
4308
+ return typeof n === "number" ? -n : void 0;
4309
+ }
4310
+ if (ts.isArrayLiteralExpression(node)) {
4311
+ return node.elements.map(
4312
+ (el) => ts.isSpreadElement(el) ? void 0 : toPlainValue(el)
4313
+ );
4314
+ }
4315
+ if (ts.isObjectLiteralExpression(node)) {
4316
+ const out = {};
4317
+ for (const prop of node.properties) {
4318
+ if (!ts.isPropertyAssignment(prop)) return void 0;
4319
+ if (ts.isComputedPropertyName(prop.name)) return void 0;
4320
+ const key = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) || ts.isNumericLiteral(prop.name) ? prop.name.text : void 0;
4321
+ if (!key) return void 0;
4322
+ const value = toPlainValue(prop.initializer);
4323
+ if (value === void 0) return void 0;
4324
+ out[key] = value;
4325
+ }
4326
+ return out;
4327
+ }
4328
+ return void 0;
4329
+ }
4330
+ function parseThemeObject(themePath) {
4331
+ const content = fs8.readFileSync(themePath, "utf-8");
4332
+ const sourceFile = ts.createSourceFile(
4333
+ themePath,
4334
+ content,
4335
+ ts.ScriptTarget.Latest,
4336
+ true,
4337
+ ts.ScriptKind.TS
4338
+ );
4339
+ for (const stmt of sourceFile.statements) {
4340
+ if (!ts.isVariableStatement(stmt)) continue;
4341
+ const isExported = stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
4342
+ if (!isExported) continue;
4343
+ for (const decl of stmt.declarationList.declarations) {
4344
+ if (!ts.isIdentifier(decl.name) || decl.name.text !== "theme" || !decl.initializer) continue;
4345
+ const parsed = toPlainValue(decl.initializer);
4346
+ if (parsed === void 0) {
4347
+ throw new Error(
4348
+ "Could not statically parse exported theme object. Keep `export const theme = { ... }` as a plain object literal."
4349
+ );
4350
+ }
4351
+ return parsed;
4352
+ }
4353
+ }
4354
+ throw new Error("No exported `theme` object found.");
4355
+ }
4356
+ async function themeValidate() {
4357
+ const configPath = path13.join(process.cwd(), "pdfx.json");
4358
+ if (!checkFileExists(configPath)) {
4359
+ console.error(chalk10.red('No pdfx.json found. Run "npx pdfx-cli@latest init" first.'));
4360
+ process.exit(1);
4361
+ }
4362
+ const rawConfig = readJsonFile(configPath);
4363
+ const configResult = configSchema.safeParse(rawConfig);
4364
+ if (!configResult.success) {
4365
+ console.error(chalk10.red("Invalid pdfx.json configuration."));
4366
+ process.exit(1);
4367
+ }
4368
+ if (!configResult.data.theme) {
4369
+ console.error(
4370
+ chalk10.red(
4371
+ 'No theme path in pdfx.json. Run "npx pdfx-cli@latest theme init" to set up theming.'
4372
+ )
4373
+ );
4374
+ process.exit(1);
4375
+ }
4376
+ const absThemePath = path13.resolve(process.cwd(), configResult.data.theme);
4377
+ if (!checkFileExists(absThemePath)) {
4378
+ console.error(chalk10.red(`Theme file not found: ${configResult.data.theme}`));
4379
+ process.exit(1);
4380
+ }
4381
+ const spinner = ora7("Validating theme file...").start();
4382
+ try {
4383
+ const parsedTheme = parseThemeObject(absThemePath);
4384
+ const result = themeSchema.safeParse(parsedTheme);
4385
+ if (!result.success) {
4386
+ const issues = result.error.issues.map((issue) => ` \u2192 ${chalk10.yellow(issue.path.join("."))}: ${issue.message}`).join("\n");
4387
+ spinner.fail("Theme validation failed");
4388
+ console.log(chalk10.red("\n Missing or invalid fields:\n"));
4389
+ console.log(issues);
4390
+ console.log(chalk10.dim("\n Fix these fields in your theme file and run validate again.\n"));
4391
+ process.exit(1);
4392
+ }
4393
+ spinner.succeed("Theme file is valid");
4394
+ console.log(chalk10.dim(`
4395
+ Validated: ${configResult.data.theme}
4396
+ `));
4397
+ } catch (error) {
4398
+ spinner.fail("Failed to validate theme");
4399
+ const message = error instanceof Error ? error.message : String(error);
4400
+ console.error(chalk10.dim(` ${message}`));
4401
+ process.exit(1);
4402
+ }
4403
+ }
4404
+
4405
+ // src/index.ts
4406
+ function getVersion() {
4407
+ try {
4408
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../package.json");
4409
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
4410
+ return pkg.version ?? "0.0.0";
4411
+ } catch {
4412
+ return "0.0.0";
4413
+ }
4414
+ }
4415
+ var program = new Command3();
4416
+ program.name("pdfx").description("CLI for PDFx components").version(getVersion());
4417
+ program.configureOutput({
4418
+ writeErr: (str) => {
4419
+ const message = str.replace(/^error:\s*/i, "").trimEnd();
4420
+ process.stderr.write(chalk11.red(`\u2716 ${message}
4421
+ `));
4422
+ }
4423
+ });
4424
+ program.command("init").description("Initialize pdfx in your project").option("-y, --yes", "Accept all defaults without prompting (non-interactive / CI mode)").action((options) => init(options));
4425
+ program.command("add <components...>").description("Add components to your project").option("-f, --force", "Overwrite existing files without prompting").option("-r, --registry <url>", "Override registry URL").option("--install-deps", "Install missing component dependencies without prompting").option("--strict-deps", "Fail when any runtime or dev dependency is missing").action(
4426
+ (components, options) => add(components, options)
4427
+ );
4428
+ program.command("list").description("List available components from registry").action(list);
4429
+ program.command("diff <components...>").description("Compare local components with registry versions").action(diff);
4430
+ var themeCmd = program.command("theme").description("Manage PDF themes");
4431
+ themeCmd.command("init").description("Initialize or replace the theme file").action(themeInit);
4432
+ themeCmd.command("switch <preset>").description("Switch to a preset theme (professional, modern, minimal)").action(themeSwitch);
4433
+ themeCmd.command("validate").description("Validate your theme file").action(themeValidate);
4434
+ program.addCommand(mcpCommand);
4435
+ program.addCommand(skillsCommand);
4436
+ var blockCmd = program.command("block").description("Manage PDF blocks (copy-paste designs)");
4437
+ blockCmd.command("add <blocks...>").description("Add blocks to your project").option("-f, --force", "Overwrite existing files without prompting").action((blocks, options) => blockAdd(blocks, options));
4438
+ blockCmd.command("list").description("List available blocks from registry").action(blockList);
4439
+ try {
4440
+ await program.parseAsync();
4441
+ } catch (err) {
4442
+ const message = err instanceof Error ? err.message : String(err);
4443
+ process.stderr.write(chalk11.red(`\u2716 ${message}
4444
+ `));
4445
+ process.exitCode = 1;
4446
+ }