gbs-add-block 2.0.1 → 2.0.2

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/index.cjs CHANGED
@@ -1,665 +1,1019 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require("fs-extra");
4
- const path = require("path");
5
- const crypto = require("crypto");
6
- const yargs = require("yargs/yargs");
7
- const { hideBin } = require("yargs/helpers");
8
- const readline = require("readline");
9
-
10
- // Configuration
11
- const CONFIG = {
12
- components: [
13
- "Card",
14
- "Select",
15
- "SideBar",
16
- "MultiSelect",
17
- "Button",
18
- "DatePicker",
19
- "Checkbox",
20
- "DarkMode",
21
- "Dialog",
22
- "Input",
23
- "Modal",
24
- "Spinner",
25
- "Toast",
26
- "Tabs",
27
- "Uploader",
28
- "FormRenderer",
29
- "MaterialInput",
30
- "TextArea",
31
- "ContextMenu",
32
- "Navbar",
33
- "DataGrid",
34
- "BreadCrumb",
35
- "Bargraph",
36
- "UsePaginatedData",
37
- "UseUploader",
38
- ],
39
- betaComponents: [
40
- "DataGrid",
41
- "Combobox",
42
- "DatePicker",
43
- "Toaster",
44
- "FileUploader",
45
- "Dialog",
46
- "Input",
47
- "Modal",
48
- "Textarea",
49
- "Button",
50
- "Breadcrumb",
51
- "Checkbox",
52
- "Tabs",
53
- "Spinner",
54
- "Menu",
55
- "Tooltip",
56
- "Popover",
57
- "Card",
58
- "Skeleton",
59
- "NumberInput",
60
- "RadioGroup",
61
- "Switch",
62
- "Accordion",
63
- "Alert",
64
- "Avatar",
65
- "Badge",
66
- "Progress",
67
- ],
68
- // Beta folder names that are not simply the lowercased component name.
69
- betaFolders: {
70
- DataGrid: "data-grid",
71
- DatePicker: "date-picker",
72
- FileUploader: "file-uploader",
73
- NumberInput: "number-input",
74
- RadioGroup: "radio-group",
75
- },
76
- // Define component dependencies
77
- dependencies: {
78
- FormRenderer: ["Select", "MultiSelect", "Input", "DatePicker"],
79
- },
80
- docs: "https://gramprokit.vercel.app/",
81
- };
82
-
83
- const SOURCE_PATH = path.join(__dirname, "source", "components");
84
- const BETA_SOURCE_PATH = path.join(__dirname, "source", "beta-components");
85
- const DEFAULT_DEST_PATH = path.join(process.cwd(), "component-lib");
86
-
87
- /**
88
- * Every beta component imports its shared helpers from `../../shared`, so this
89
- * folder is installed alongside whatever the user picked. It is not a component
90
- * and never appears in the component list.
91
- */
92
- const SHARED_DIR = "shared";
93
- const MANIFEST_FILE = ".install-manifest.json";
94
-
95
- /**
96
- * Tests belong to this repo, not to the projects we install into: they import
97
- * vitest, which the project has no reason to have, and the shared folder's
98
- * boundary tests would run against whatever else is in the user's
99
- * component-lib.
100
- */
101
- const TESTS_DIR = "__tests__";
102
- const isTestPath = (filePath) => filePath.split(/[\\/]/).includes(TESTS_DIR);
103
-
104
- const normalizeComponent = (component, availableComponents) =>
105
- availableComponents.find(
106
- (available) => available.toLowerCase() === component.toLowerCase(),
107
- );
108
-
109
- const getAvailableComponents = (beta = false) =>
110
- beta ? CONFIG.betaComponents : CONFIG.components;
111
-
112
- const getSourcePath = (beta = false) => (beta ? BETA_SOURCE_PATH : SOURCE_PATH);
113
-
114
- /** The folder a component lives in, which is not always its lowercased name. */
115
- const folderFor = (component, beta = false) =>
116
- (beta && CONFIG.betaFolders[component]) || component.toLowerCase();
117
-
118
- const copyCommonFiles = async (destPath) => {
119
- const commonFiles = [
120
- { src: ["..", "utils.ts"], dest: "utils.ts" },
121
- { src: ["..", "globalStyle.ts"], dest: "globalStyle.ts" },
122
- { src: ["..", "theme.ts"], dest: "theme.ts" },
123
- { src: ["..", "icon"], dest: "icon" },
124
- ];
125
-
126
- for (const file of commonFiles) {
127
- const src = path.join(SOURCE_PATH, ...file.src);
128
- const dest = path.join(destPath, file.dest);
129
- await fs.copy(src, dest, { overwrite: true });
130
- console.log(`✓ ${file.dest} copied successfully`);
131
- }
132
- };
133
-
134
- /* ------------------------------------------------------------ shared folder */
135
-
136
- const sha256 = (filePath) =>
137
- crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
138
-
139
- /** Every file under a directory, relative to it, with "/" separators. */
140
- const listFiles = (dir, prefix = "") =>
141
- fs
142
- .readdirSync(path.join(dir, prefix), { withFileTypes: true })
143
- .flatMap((entry) => {
144
- const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
145
- if (entry.name === MANIFEST_FILE || entry.name === TESTS_DIR) return [];
146
- return entry.isDirectory() ? listFiles(dir, relative) : [relative];
147
- });
148
-
149
- /** -1, 0 or 1, comparing dotted numeric versions such as "1.2.0". */
150
- const compareVersions = (a, b) => {
151
- const left = String(a).split(".").map(Number);
152
- const right = String(b).split(".").map(Number);
153
- for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
154
- const diff = (left[i] || 0) - (right[i] || 0);
155
- if (diff !== 0) return diff > 0 ? 1 : -1;
156
- }
157
- return 0;
158
- };
159
-
160
- const readJson = (filePath, fallback = null) => {
161
- try {
162
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
163
- } catch {
164
- return fallback;
165
- }
166
- };
167
-
168
- /** Files the manifest says we wrote, whose contents have since changed. */
169
- const locallyEdited = (dest, manifest) =>
170
- !manifest
171
- ? []
172
- : Object.keys(manifest.files).filter((file) => {
173
- const target = path.join(dest, file);
174
- return fs.existsSync(target) && manifest.files[file] !== sha256(target);
175
- });
176
-
177
- /**
178
- * Installs or updates `shared/`.
179
- *
180
- * Because components are copied into a project one at a time, a project can end
181
- * up with components built against different versions of this folder. The rules
182
- * are that `shared/` only ever moves forward, and that a file the user has
183
- * edited since we wrote it is never replaced without `--force`.
184
- */
185
- const installShared = async (destPath, { force = false } = {}) => {
186
- const src = path.join(BETA_SOURCE_PATH, SHARED_DIR);
187
- const dest = path.join(destPath, SHARED_DIR);
188
-
189
- if (!fs.existsSync(src)) {
190
- throw new Error(
191
- `The shared folder is missing from ${BETA_SOURCE_PATH}. Beta components cannot be installed without it.`,
192
- );
193
- }
194
-
195
- const srcVersion =
196
- (readJson(path.join(src, "version.json"), {}) || {}).version || "0.0.0";
197
- const manifest = readJson(path.join(dest, MANIFEST_FILE));
198
- const installedVersion = manifest ? manifest.version : null;
199
- const edited = locallyEdited(dest, manifest);
200
-
201
- if (installedVersion && !force) {
202
- const order = compareVersions(installedVersion, srcVersion);
203
-
204
- if (order > 0) {
205
- console.error(
206
- `\n✗ This project has shared v${installedVersion}, newer than the v${srcVersion} in this installer.`,
207
- );
208
- console.error(
209
- " Installing it would downgrade the folder and break the components already here.",
210
- );
211
- console.error(
212
- " Update the CLI, or re-run with --force to overwrite it anyway.",
213
- );
214
- process.exit(1);
215
- }
216
-
217
- if (order === 0) {
218
- console.log(
219
- edited.length > 0
220
- ? `✓ shared v${installedVersion} is already installed (keeping your ${edited.length} edited file(s))`
221
- : `✓ shared v${installedVersion} is already installed`,
222
- );
223
- return;
224
- }
225
- }
226
-
227
- if (edited.length > 0 && !force) {
228
- console.error(
229
- `\n✗ These files in component-lib/${SHARED_DIR} have local changes:`,
230
- );
231
- edited.forEach((file) => console.error(` - ${file}`));
232
- console.error(
233
- "\n Updating shared/ would replace them. Re-run with --force to do that, or move",
234
- );
235
- console.error(
236
- " your changes into your own module first: components are yours to edit,",
237
- );
238
- console.error(" shared/ is replaced on every update.");
239
- process.exit(1);
240
- }
241
-
242
- const files = listFiles(src);
243
- for (const file of files) {
244
- await fs.copy(path.join(src, file), path.join(dest, file), {
245
- overwrite: true,
246
- });
247
- }
248
-
249
- // Remove what an older version left behind, but only files we installed and
250
- // the user has not since changed.
251
- if (manifest) {
252
- const removed = Object.keys(manifest.files).filter(
253
- (file) => !files.includes(file),
254
- );
255
- for (const file of removed) {
256
- const target = path.join(dest, file);
257
- if (
258
- fs.existsSync(target) &&
259
- (force || manifest.files[file] === sha256(target))
260
- ) {
261
- await fs.remove(target);
262
- }
263
- }
264
- }
265
-
266
- fs.writeJsonSync(
267
- path.join(dest, MANIFEST_FILE),
268
- {
269
- version: srcVersion,
270
- installedAt: new Date().toISOString(),
271
- files: Object.fromEntries(
272
- files.map((file) => [file, sha256(path.join(src, file))]),
273
- ),
274
- },
275
- { spaces: 2 },
276
- );
277
-
278
- console.log(
279
- installedVersion
280
- ? `✓ shared updated ${installedVersion} → ${srcVersion}`
281
- : `✓ shared v${srcVersion} installed`,
282
- );
283
- };
284
-
285
- /** The files a component needs beside it, which differ between the two sets. */
286
- const copySupportFiles = async (destPath, beta, options) => {
287
- if (beta) {
288
- await installShared(destPath, options);
289
- return;
290
- }
291
- // The legacy components' common files. Beta components use none of them.
292
- if (!fs.existsSync(path.join(destPath, "utils.ts"))) {
293
- await copyCommonFiles(destPath);
294
- }
295
- };
296
-
297
- /* -------------------------------------------------------------- components */
298
-
299
- const checkComponentExists = (component, destPath, beta = false) => {
300
- const componentPath = path.join(destPath, folderFor(component, beta));
301
- return fs.existsSync(componentPath);
302
- };
303
-
304
- const copyComponent = async (component, destPath, beta = false) => {
305
- try {
306
- const componentSrc = path.join(
307
- getSourcePath(beta),
308
- folderFor(component, beta),
309
- );
310
- const componentDest = path.join(destPath, folderFor(component, beta));
311
-
312
- if (!fs.existsSync(componentSrc)) {
313
- throw new Error(
314
- `Component ${component} not found in ${beta ? "beta " : ""}source directory.`,
315
- );
316
- }
317
-
318
- await fs.copy(componentSrc, componentDest, {
319
- overwrite: true,
320
- filter: (source) => !isTestPath(path.relative(componentSrc, source)),
321
- });
322
- console.log(
323
- `✓ Component ${component} installed successfully ${
324
- component === "Grid"
325
- ? "This Version of Grid will be deprecated soon. Please Install The New Data Grid Component"
326
- : ""
327
- }`,
328
- );
329
- } catch (error) {
330
- console.error(`Error installing component ${component}:`, error.message);
331
- process.exit(1);
332
- }
333
- };
334
-
335
- const installComponentWithDependencies = async (
336
- component,
337
- destPath,
338
- beta = false,
339
- ) => {
340
- // Get dependencies for the component
341
- const dependencies = CONFIG.dependencies[component] || [];
342
- const componentsToInstall = new Set([component, ...dependencies]);
343
-
344
- // Check which components need to be installed
345
- const pendingInstalls = beta
346
- ? Array.from(componentsToInstall)
347
- : Array.from(componentsToInstall).filter(
348
- (comp) => !checkComponentExists(comp, destPath, beta),
349
- );
350
-
351
- if (pendingInstalls.length === 0) {
352
- console.log(
353
- `✓ ${component} and all its dependencies are already installed.`,
354
- );
355
- return;
356
- }
357
-
358
- // Install all pending components
359
- for (const comp of pendingInstalls) {
360
- await copyComponent(comp, destPath, beta);
361
- }
362
-
363
- if (dependencies.length > 0) {
364
- console.log(`\nInstalled dependencies for ${component}:`);
365
- dependencies.forEach((dep) => {
366
- console.log(`- ${dep}`);
367
- });
368
- }
369
-
370
- console.log(`\nFor documentation visit: ${CONFIG.docs}`);
371
- };
372
-
373
- const installMultipleComponents = async (
374
- components,
375
- destPath,
376
- beta = false,
377
- ) => {
378
- const allComponentsToInstall = new Set();
379
-
380
- // Collect all components and their dependencies
381
- components.forEach((component) => {
382
- const dependencies = CONFIG.dependencies[component] || [];
383
- allComponentsToInstall.add(component);
384
- dependencies.forEach((dep) => allComponentsToInstall.add(dep));
385
- });
386
-
387
- // Filter out already installed components
388
- const pendingInstalls = beta
389
- ? Array.from(allComponentsToInstall)
390
- : Array.from(allComponentsToInstall).filter(
391
- (comp) => !checkComponentExists(comp, destPath, beta),
392
- );
393
-
394
- if (pendingInstalls.length === 0) {
395
- console.log(
396
- "✓ All selected components and their dependencies are already installed.",
397
- );
398
- return;
399
- }
400
-
401
- console.log(`\nInstalling ${pendingInstalls.length} components...`);
402
-
403
- // Install all pending components
404
- for (const comp of pendingInstalls) {
405
- await copyComponent(comp, destPath, beta);
406
- }
407
-
408
- // Show dependency information
409
- const allDependencies = new Set();
410
- components.forEach((component) => {
411
- const dependencies = CONFIG.dependencies[component] || [];
412
- dependencies.forEach((dep) => allDependencies.add(dep));
413
- });
414
-
415
- if (allDependencies.size > 0) {
416
- console.log(`\nDependencies installed:`);
417
- Array.from(allDependencies).forEach((dep) => {
418
- console.log(`- ${dep}`);
419
- });
420
- }
421
-
422
- console.log(`\nFor documentation visit: ${CONFIG.docs}`);
423
- };
424
-
425
- const interactiveComponentSelector = async (beta = false) => {
426
- return new Promise((resolve) => {
427
- const rl = readline.createInterface({
428
- input: process.stdin,
429
- output: process.stdout,
430
- });
431
-
432
- let selectedComponents = new Set();
433
- let currentIndex = 0;
434
-
435
- const renderMenu = () => {
436
- console.clear();
437
- console.log("🚀 Component Installer - Interactive Mode");
438
- console.log(
439
- "Use ↑/↓ arrow keys to navigate, SPACE to select/deselect, ENTER to install\n",
440
- );
441
-
442
- const availableComponents = getAvailableComponents(beta);
443
-
444
- availableComponents.forEach((component, index) => {
445
- const isSelected = selectedComponents.has(component);
446
- const isCurrentIndex = index === currentIndex;
447
- const deps = CONFIG.dependencies[component]
448
- ? ` (requires: ${CONFIG.dependencies[component].join(", ")})`
449
- : "";
450
-
451
- const prefix = isCurrentIndex ? ">" : " ";
452
- const checkbox = isSelected ? "☑" : "☐";
453
- const line = `${prefix} ${checkbox} ${component}${deps}`;
454
-
455
- if (isCurrentIndex) {
456
- console.log(`\x1b[36m${line}\x1b[0m`); // Cyan highlight
457
- } else {
458
- console.log(line);
459
- }
460
- });
461
-
462
- console.log(`\nSelected: ${selectedComponents.size} components`);
463
- console.log("Press ENTER to install selected components, or 'q' to quit");
464
- };
465
-
466
- const handleKeyPress = (key) => {
467
- switch (key) {
468
- case "": // Up arrow
469
- currentIndex = Math.max(0, currentIndex - 1);
470
- renderMenu();
471
- break;
472
- case "": // Down arrow
473
- currentIndex = Math.min(
474
- getAvailableComponents(beta).length - 1,
475
- currentIndex + 1,
476
- );
477
- renderMenu();
478
- break;
479
- case " ": // Space bar
480
- const component = getAvailableComponents(beta)[currentIndex];
481
- if (selectedComponents.has(component)) {
482
- selectedComponents.delete(component);
483
- } else {
484
- selectedComponents.add(component);
485
- }
486
- renderMenu();
487
- break;
488
- case "\r": // Enter
489
- if (selectedComponents.size > 0) {
490
- rl.close();
491
- resolve(Array.from(selectedComponents));
492
- }
493
- break;
494
- case "q":
495
- rl.close();
496
- resolve([]);
497
- break;
498
- }
499
- };
500
-
501
- // Enable raw mode to capture arrow keys
502
- process.stdin.setRawMode(true);
503
- process.stdin.resume();
504
- process.stdin.setEncoding("utf8");
505
- process.stdin.on("data", handleKeyPress);
506
-
507
- renderMenu();
508
-
509
- rl.on("close", () => {
510
- process.stdin.setRawMode(false);
511
- process.stdin.pause();
512
- });
513
- });
514
- };
515
-
516
- const parseMultipleComponents = (componentString) => {
517
- return componentString
518
- .split(",")
519
- .map((comp) => comp.trim())
520
- .filter((comp) => comp.length > 0);
521
- };
522
-
523
- const validateComponents = (components, beta = false) => {
524
- const availableComponents = getAvailableComponents(beta);
525
- const invalidComponents = components.filter(
526
- (comp) => !availableComponents.includes(comp),
527
- );
528
- if (invalidComponents.length > 0) {
529
- if (invalidComponents.some((comp) => comp.toLowerCase() === SHARED_DIR)) {
530
- console.error(
531
- "`shared` is not a component: it is installed automatically with any beta component.",
532
- );
533
- }
534
- console.error(`Invalid components: ${invalidComponents.join(", ")}`);
535
- console.log(`\nAvailable ${beta ? "beta " : ""}components:`);
536
- availableComponents.forEach((comp) => console.log(`- ${comp}`));
537
- if (!beta) {
538
- console.log("\nRedesigned beta components (install with -beta):");
539
- CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
540
- }
541
- return false;
542
- }
543
- return true;
544
- };
545
-
546
- const main = async () => {
547
- const args = hideBin(process.argv).map((arg) =>
548
- arg === "-beta" ? "--beta" : arg,
549
- );
550
- const argv = yargs(args)
551
- .option("add", {
552
- alias: "a",
553
- describe:
554
- "Component to install (single component or comma-separated list)",
555
- type: "string",
556
- })
557
- .option("interactive", {
558
- alias: "i",
559
- describe: "Interactive component selection mode",
560
- type: "boolean",
561
- })
562
- .option("list", {
563
- alias: "l",
564
- describe: "List available components",
565
- type: "boolean",
566
- })
567
- .option("beta", {
568
- describe: "Install redesigned beta components",
569
- type: "boolean",
570
- default: false,
571
- })
572
- .option("force", {
573
- describe: "Replace files in shared/ that you have edited locally",
574
- type: "boolean",
575
- default: false,
576
- })
577
- .example("$0 -a Button", "Install a single component")
578
- .example("$0 -a Button,Card,Modal", "Install multiple components")
579
- .example("$0 -a DataGrid -beta", "Install the redesigned beta DataGrid")
580
- .example("$0 -a Combobox -beta", "Install the redesigned beta Combobox")
581
- .example("$0 -i", "Interactive selection mode")
582
- .help().argv;
583
-
584
- // List components if requested
585
- if (argv.list) {
586
- const availableComponents = getAvailableComponents(argv.beta);
587
- console.log(`\nAvailable ${argv.beta ? "beta " : ""}components:`);
588
- availableComponents.forEach((comp) => {
589
- const deps = CONFIG.dependencies[comp]
590
- ? ` (requires: ${CONFIG.dependencies[comp].join(", ")})`
591
- : "";
592
- console.log(`- ${comp}${deps}`);
593
- });
594
- if (argv.beta) {
595
- console.log(
596
- `\nEvery beta component also installs component-lib/${SHARED_DIR}, which they all import.`,
597
- );
598
- } else {
599
- console.log("\nRedesigned beta components (install with -beta):");
600
- CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
601
- }
602
- return;
603
- }
604
-
605
- // Interactive mode
606
- if (argv.interactive) {
607
- console.log("Starting interactive component selector...\n");
608
- const selectedComponents = await interactiveComponentSelector(argv.beta);
609
-
610
- if (selectedComponents.length === 0) {
611
- console.log("No components selected. Exiting...");
612
- return;
613
- }
614
-
615
- // Create destination directory in project root
616
- const destPath = DEFAULT_DEST_PATH;
617
- await fs.ensureDir(destPath);
618
-
619
- await copySupportFiles(destPath, argv.beta, { force: argv.force });
620
-
621
- // Install selected components
622
- await installMultipleComponents(selectedComponents, destPath, argv.beta);
623
- return;
624
- }
625
-
626
- if (!argv.add) {
627
- console.error(
628
- "Please specify a component to install using -a/--add, use -i/--interactive for interactive mode, or -l/--list to see available components",
629
- );
630
- process.exit(1);
631
- }
632
-
633
- // Parse components (single or multiple)
634
- const componentInput = argv.add;
635
- const components = parseMultipleComponents(componentInput).map(
636
- (component) =>
637
- normalizeComponent(component, getAvailableComponents(argv.beta)) ||
638
- component,
639
- );
640
-
641
- // Validate all components
642
- if (!validateComponents(components, argv.beta)) {
643
- process.exit(1);
644
- }
645
-
646
- // Create destination directory in project root
647
- const destPath = DEFAULT_DEST_PATH;
648
- await fs.ensureDir(destPath);
649
-
650
- await copySupportFiles(destPath, argv.beta, { force: argv.force });
651
-
652
- // Install components
653
- if (components.length === 1) {
654
- // Single component installation (existing behavior)
655
- await installComponentWithDependencies(components[0], destPath, argv.beta);
656
- } else {
657
- // Multiple components installation
658
- await installMultipleComponents(components, destPath, argv.beta);
659
- }
660
- };
661
-
662
- main().catch((error) => {
663
- console.error("Error:", error.message);
664
- process.exit(1);
665
- });
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs-extra");
4
+ const path = require("path");
5
+ const crypto = require("crypto");
6
+ const yargs = require("yargs/yargs");
7
+ const { hideBin } = require("yargs/helpers");
8
+ const readline = require("readline");
9
+
10
+ // Configuration
11
+ const CONFIG = {
12
+ components: [
13
+ "Card",
14
+ "Select",
15
+ "SideBar",
16
+ "MultiSelect",
17
+ "Button",
18
+ "DatePicker",
19
+ "Checkbox",
20
+ "DarkMode",
21
+ "Dialog",
22
+ "Input",
23
+ "Modal",
24
+ "Spinner",
25
+ "Toast",
26
+ "Tabs",
27
+ "Uploader",
28
+ "FormRenderer",
29
+ "MaterialInput",
30
+ "TextArea",
31
+ "ContextMenu",
32
+ "Navbar",
33
+ "DataGrid",
34
+ "BreadCrumb",
35
+ "Bargraph",
36
+ "UsePaginatedData",
37
+ "UseUploader",
38
+ ],
39
+ betaComponents: [
40
+ "DataGrid",
41
+ "Combobox",
42
+ "DatePicker",
43
+ "Toaster",
44
+ "FileUploader",
45
+ "Dialog",
46
+ "Input",
47
+ "Modal",
48
+ "Textarea",
49
+ "Button",
50
+ "Breadcrumb",
51
+ "Checkbox",
52
+ "Tabs",
53
+ "Spinner",
54
+ "Menu",
55
+ "Tooltip",
56
+ "Popover",
57
+ "Card",
58
+ "Skeleton",
59
+ "NumberInput",
60
+ "RadioGroup",
61
+ "Switch",
62
+ "Accordion",
63
+ "Alert",
64
+ "Avatar",
65
+ "Badge",
66
+ "Progress",
67
+ ],
68
+ // Beta folder names that are not simply the lowercased component name.
69
+ betaFolders: {
70
+ DataGrid: "data-grid",
71
+ DatePicker: "date-picker",
72
+ FileUploader: "file-uploader",
73
+ NumberInput: "number-input",
74
+ RadioGroup: "radio-group",
75
+ },
76
+ // Define component dependencies
77
+ dependencies: {
78
+ FormRenderer: ["Select", "MultiSelect", "Input", "DatePicker"],
79
+ },
80
+ docs: "https://gramprokit.vercel.app/",
81
+ };
82
+
83
+ const { version: PACKAGE_VERSION } = require("./package.json");
84
+
85
+ const SOURCE_PATH = path.join(__dirname, "source", "components");
86
+ const BETA_SOURCE_PATH = path.join(__dirname, "source", "beta-components");
87
+ const DEFAULT_DEST_PATH = path.join(process.cwd(), "component-lib");
88
+
89
+ /**
90
+ * The agent skill. It teaches a coding agent how to use these components, so it
91
+ * belongs at the root of the project being worked on, not in component-lib.
92
+ */
93
+ const SKILL_SOURCE_PATH = path.join(__dirname, ".gbs");
94
+ const SKILL_DEST_DIR = ".gbs";
95
+ const SKILL_NAME = "gbs-components";
96
+
97
+ /** Where SKILL.md and references/ live inside the package. */
98
+ const SKILL_SRC_DIR = path.join(
99
+ SKILL_SOURCE_PATH,
100
+ "skills",
101
+ SKILL_NAME,
102
+ );
103
+
104
+ /**
105
+ * Agents we can write a copy of the skill for. Each looks in its own place and
106
+ * none of them reads `.gbs`, which stays the canonical copy either way.
107
+ */
108
+ const SKILL_TARGETS = ["claude", "codex", "antigravity"];
109
+
110
+ /** Codex reads one always-on file the project usually maintains by hand, so we
111
+ * own a marked block inside it and never touch a line outside. */
112
+ const AGENTS_FILE = "AGENTS.md";
113
+ const BLOCK_START = "<!-- gbs-add-block:start -->";
114
+ const BLOCK_END = "<!-- gbs-add-block:end -->";
115
+
116
+ /** Newline, named so long template literals stay readable. */
117
+ const NL = String.fromCharCode(10);
118
+
119
+ /**
120
+ * Every beta component imports its shared helpers from `../../shared`, so this
121
+ * folder is installed alongside whatever the user picked. It is not a component
122
+ * and never appears in the component list.
123
+ */
124
+ const SHARED_DIR = "shared";
125
+ const MANIFEST_FILE = ".install-manifest.json";
126
+
127
+ /**
128
+ * Tests belong to this repo, not to the projects we install into: they import
129
+ * vitest, which the project has no reason to have, and the shared folder's
130
+ * boundary tests would run against whatever else is in the user's
131
+ * component-lib.
132
+ */
133
+ const TESTS_DIR = "__tests__";
134
+ const isTestPath = (filePath) => filePath.split(/[\\/]/).includes(TESTS_DIR);
135
+
136
+ const normalizeComponent = (component, availableComponents) =>
137
+ availableComponents.find(
138
+ (available) => available.toLowerCase() === component.toLowerCase(),
139
+ );
140
+
141
+ const getAvailableComponents = (beta = false) =>
142
+ beta ? CONFIG.betaComponents : CONFIG.components;
143
+
144
+ const getSourcePath = (beta = false) => (beta ? BETA_SOURCE_PATH : SOURCE_PATH);
145
+
146
+ /** The folder a component lives in, which is not always its lowercased name. */
147
+ const folderFor = (component, beta = false) =>
148
+ (beta && CONFIG.betaFolders[component]) || component.toLowerCase();
149
+
150
+ const copyCommonFiles = async (destPath) => {
151
+ const commonFiles = [
152
+ { src: ["..", "utils.ts"], dest: "utils.ts" },
153
+ { src: ["..", "globalStyle.ts"], dest: "globalStyle.ts" },
154
+ { src: ["..", "theme.ts"], dest: "theme.ts" },
155
+ { src: ["..", "icon"], dest: "icon" },
156
+ ];
157
+
158
+ for (const file of commonFiles) {
159
+ const src = path.join(SOURCE_PATH, ...file.src);
160
+ const dest = path.join(destPath, file.dest);
161
+ await fs.copy(src, dest, { overwrite: true });
162
+ console.log(`✓ ${file.dest} copied successfully`);
163
+ }
164
+ };
165
+
166
+ /* ------------------------------------------------------------ shared folder */
167
+
168
+ const sha256 = (filePath) =>
169
+ crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
170
+
171
+ /** Every file under a directory, relative to it, with "/" separators. */
172
+ const listFiles = (dir, prefix = "") =>
173
+ fs
174
+ .readdirSync(path.join(dir, prefix), { withFileTypes: true })
175
+ .flatMap((entry) => {
176
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
177
+ if (entry.name === MANIFEST_FILE || entry.name === TESTS_DIR) return [];
178
+ return entry.isDirectory() ? listFiles(dir, relative) : [relative];
179
+ });
180
+
181
+ /** -1, 0 or 1, comparing dotted numeric versions such as "1.2.0". */
182
+ const compareVersions = (a, b) => {
183
+ const left = String(a).split(".").map(Number);
184
+ const right = String(b).split(".").map(Number);
185
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
186
+ const diff = (left[i] || 0) - (right[i] || 0);
187
+ if (diff !== 0) return diff > 0 ? 1 : -1;
188
+ }
189
+ return 0;
190
+ };
191
+
192
+ const readJson = (filePath, fallback = null) => {
193
+ try {
194
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
195
+ } catch {
196
+ return fallback;
197
+ }
198
+ };
199
+
200
+ /** Files the manifest says we wrote, whose contents have since changed. */
201
+ const locallyEdited = (dest, manifest) =>
202
+ !manifest
203
+ ? []
204
+ : Object.keys(manifest.files).filter((file) => {
205
+ const target = path.join(dest, file);
206
+ return fs.existsSync(target) && manifest.files[file] !== sha256(target);
207
+ });
208
+
209
+ /**
210
+ * Installs or updates `shared/`.
211
+ *
212
+ * Because components are copied into a project one at a time, a project can end
213
+ * up with components built against different versions of this folder. The rules
214
+ * are that `shared/` only ever moves forward, and that a file the user has
215
+ * edited since we wrote it is never replaced without `--force`.
216
+ */
217
+ const installShared = async (destPath, { force = false } = {}) => {
218
+ const src = path.join(BETA_SOURCE_PATH, SHARED_DIR);
219
+ const dest = path.join(destPath, SHARED_DIR);
220
+
221
+ if (!fs.existsSync(src)) {
222
+ throw new Error(
223
+ `The shared folder is missing from ${BETA_SOURCE_PATH}. Beta components cannot be installed without it.`,
224
+ );
225
+ }
226
+
227
+ const srcVersion =
228
+ (readJson(path.join(src, "version.json"), {}) || {}).version || "0.0.0";
229
+ const manifest = readJson(path.join(dest, MANIFEST_FILE));
230
+ const installedVersion = manifest ? manifest.version : null;
231
+ const edited = locallyEdited(dest, manifest);
232
+
233
+ if (installedVersion && !force) {
234
+ const order = compareVersions(installedVersion, srcVersion);
235
+
236
+ if (order > 0) {
237
+ console.error(
238
+ `\n✗ This project has shared v${installedVersion}, newer than the v${srcVersion} in this installer.`,
239
+ );
240
+ console.error(
241
+ " Installing it would downgrade the folder and break the components already here.",
242
+ );
243
+ console.error(
244
+ " Update the CLI, or re-run with --force to overwrite it anyway.",
245
+ );
246
+ process.exit(1);
247
+ }
248
+
249
+ if (order === 0) {
250
+ console.log(
251
+ edited.length > 0
252
+ ? `✓ shared v${installedVersion} is already installed (keeping your ${edited.length} edited file(s))`
253
+ : `✓ shared v${installedVersion} is already installed`,
254
+ );
255
+ return;
256
+ }
257
+ }
258
+
259
+ if (edited.length > 0 && !force) {
260
+ console.error(
261
+ `\n✗ These files in component-lib/${SHARED_DIR} have local changes:`,
262
+ );
263
+ edited.forEach((file) => console.error(` - ${file}`));
264
+ console.error(
265
+ "\n Updating shared/ would replace them. Re-run with --force to do that, or move",
266
+ );
267
+ console.error(
268
+ " your changes into your own module first: components are yours to edit,",
269
+ );
270
+ console.error(" shared/ is replaced on every update.");
271
+ process.exit(1);
272
+ }
273
+
274
+ const files = listFiles(src);
275
+ for (const file of files) {
276
+ await fs.copy(path.join(src, file), path.join(dest, file), {
277
+ overwrite: true,
278
+ });
279
+ }
280
+
281
+ // Remove what an older version left behind, but only files we installed and
282
+ // the user has not since changed.
283
+ if (manifest) {
284
+ const removed = Object.keys(manifest.files).filter(
285
+ (file) => !files.includes(file),
286
+ );
287
+ for (const file of removed) {
288
+ const target = path.join(dest, file);
289
+ if (
290
+ fs.existsSync(target) &&
291
+ (force || manifest.files[file] === sha256(target))
292
+ ) {
293
+ await fs.remove(target);
294
+ }
295
+ }
296
+ }
297
+
298
+ fs.writeJsonSync(
299
+ path.join(dest, MANIFEST_FILE),
300
+ {
301
+ version: srcVersion,
302
+ installedAt: new Date().toISOString(),
303
+ files: Object.fromEntries(
304
+ files.map((file) => [file, sha256(path.join(src, file))]),
305
+ ),
306
+ },
307
+ { spaces: 2 },
308
+ );
309
+
310
+ console.log(
311
+ installedVersion
312
+ ? `✓ shared updated ${installedVersion} → ${srcVersion}`
313
+ : `✓ shared v${srcVersion} installed`,
314
+ );
315
+ };
316
+
317
+ /* --------------------------------------------------------------- the skill */
318
+
319
+ /** Posix-style path, so manifest keys read the same on every platform. */
320
+ const rel = (...parts) => parts.join("/");
321
+
322
+ /**
323
+ * Removes directories a removal left empty, up to but not including `root`.
324
+ *
325
+ * Only ever removes an empty directory, so a `.claude` folder that also holds
326
+ * the project's own settings survives having our skill taken out of it.
327
+ */
328
+ const pruneEmptyDirs = async (root, from) => {
329
+ const stop = path.resolve(root);
330
+ let dir = path.dirname(path.resolve(root, from));
331
+
332
+ while (dir !== stop && dir.startsWith(stop)) {
333
+ if (!fs.existsSync(dir) || fs.readdirSync(dir).length > 0) return;
334
+ await fs.remove(dir);
335
+ dir = path.dirname(dir);
336
+ }
337
+ };
338
+
339
+ /** Splits `---` front matter from the body of a markdown document. */
340
+ const parseSkillDoc = (file) => {
341
+ const text = fs.readFileSync(file, "utf8").split("\r\n").join(NL);
342
+ const opener = "---" + NL;
343
+ if (!text.startsWith(opener)) return { frontMatter: "", body: text };
344
+
345
+ const marker = NL + "---" + NL;
346
+ const close = text.indexOf(marker, opener.length);
347
+ if (close === -1) return { frontMatter: "", body: text };
348
+
349
+ return {
350
+ frontMatter: text.slice(opener.length, close),
351
+ body: text.slice(close + marker.length),
352
+ };
353
+ };
354
+
355
+ /** One `key: value` line out of front matter we wrote ourselves. */
356
+ const frontMatterField = (frontMatter, key) => {
357
+ const prefix = key + ":";
358
+ const line = frontMatter.split(NL).find((item) => item.startsWith(prefix));
359
+ return line ? line.slice(prefix.length).trim() : "";
360
+ };
361
+
362
+ /**
363
+ * Claude Code: a full skill folder, references included, since it reads files
364
+ * bundled beside SKILL.md. Only the front matter changes — it spells the glob
365
+ * list `paths` where the GBS agent spells it `autoAttach`.
366
+ */
367
+ const writeClaudeSkill = async (root) => {
368
+ const base = rel(".claude", "skills", SKILL_NAME);
369
+ const written = [];
370
+
371
+ for (const file of listFiles(SKILL_SRC_DIR)) {
372
+ const from = path.join(SKILL_SRC_DIR, file);
373
+ const to = path.join(root, base, file);
374
+
375
+ if (file === "SKILL.md") {
376
+ const { frontMatter, body } = parseSkillDoc(from);
377
+ const globs = frontMatterField(frontMatter, "autoAttach");
378
+ const lines = [
379
+ "---",
380
+ "name: " + (frontMatterField(frontMatter, "name") || SKILL_NAME),
381
+ "description: " + frontMatterField(frontMatter, "description"),
382
+ ];
383
+ // Claude Code spells the glob list `paths`; the GBS agent spells it `autoAttach`.
384
+ if (globs) lines.push("paths: " + globs);
385
+ lines.push("---");
386
+
387
+ // `body` keeps the blank line that followed the original front matter.
388
+ await fs.outputFile(to, lines.join(NL) + NL + body);
389
+ } else {
390
+ await fs.copy(from, to, { overwrite: true });
391
+ }
392
+
393
+ written.push(rel(base, file));
394
+ }
395
+
396
+ return written;
397
+ };
398
+
399
+ /**
400
+ * Antigravity: one markdown rule, well inside its 12,000 character limit. Its
401
+ * glob activation is set on the rule in the IDE rather than in the file, so the
402
+ * globs are stated in the text for whoever configures it.
403
+ */
404
+ const writeAntigravityRule = async (root) => {
405
+ const { frontMatter, body } = parseSkillDoc(
406
+ path.join(SKILL_SRC_DIR, "SKILL.md"),
407
+ );
408
+ const target = rel(".agents", "rules", SKILL_NAME + ".md");
409
+
410
+ const preamble = [
411
+ "> Applies to React UI files: " +
412
+ (frontMatterField(frontMatter, "autoAttach") || "src/**/*.tsx") +
413
+ ".",
414
+ "> Set that as this rule's glob if you want it attached automatically.",
415
+ "> Deeper reference files live in " +
416
+ rel(SKILL_DEST_DIR, "skills", SKILL_NAME, "references") +
417
+ "/.",
418
+ "",
419
+ ].join(NL);
420
+
421
+ await fs.outputFile(path.join(root, target), preamble + body);
422
+ return [target];
423
+ };
424
+
425
+ /**
426
+ * Codex: a pointer in AGENTS.md rather than the skill itself, because that file
427
+ * is always in context and the skill is 5 KB. Everything outside our markers is
428
+ * left exactly as it was.
429
+ */
430
+ const writeAgentsPointer = async (root) => {
431
+ const file = path.join(root, AGENTS_FILE);
432
+ const skillPath = rel(SKILL_DEST_DIR, "skills", SKILL_NAME, "SKILL.md");
433
+
434
+ const block = [
435
+ BLOCK_START,
436
+ "",
437
+ "## GBS components",
438
+ "",
439
+ "This project uses the GBS headless component library. Before adding or",
440
+ "changing UI, read `" + skillPath + "` and follow it.",
441
+ "",
442
+ "It covers the import rule (components are copied into `component-lib/`, not",
443
+ "imported from a package), the change-handler name each component uses, and",
444
+ "the props that do not exist. Deeper detail is in the `references/` folder",
445
+ "beside it — open the one the task needs.",
446
+ "",
447
+ BLOCK_END,
448
+ ].join(NL);
449
+
450
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
451
+ let next;
452
+
453
+ if (existing.includes(BLOCK_START) && existing.includes(BLOCK_END)) {
454
+ next =
455
+ existing.slice(0, existing.indexOf(BLOCK_START)) +
456
+ block +
457
+ existing.slice(existing.indexOf(BLOCK_END) + BLOCK_END.length);
458
+ } else if (existing.trim()) {
459
+ next = existing.replace(/\s+$/, "") + NL + NL + block + NL;
460
+ } else {
461
+ next = "# AGENTS.md" + NL + NL + block + NL;
462
+ }
463
+
464
+ await fs.outputFile(file, next);
465
+ return existing.includes(BLOCK_START) ? "updated" : "added";
466
+ };
467
+
468
+ /**
469
+ * Installs or updates the skill at the project root.
470
+ *
471
+ * `.gbs/` is always written: it is the canonical copy and the one the GBS agent
472
+ * reads. The other agents each look somewhere else, so they get an adapter
473
+ * generated from the same source rather than a second copy to maintain.
474
+ *
475
+ * Everything we write is tracked by sha256 in one manifest, so a file the user
476
+ * has edited is never replaced without `--force`. AGENTS.md is the exception:
477
+ * the project owns that file, and we only rewrite our own marked block.
478
+ */
479
+ const installSkill = async (
480
+ destRoot,
481
+ { force = false, targets = SKILL_TARGETS } = {},
482
+ ) => {
483
+ const src = SKILL_SOURCE_PATH;
484
+
485
+ if (!fs.existsSync(src)) {
486
+ throw new Error(
487
+ `The skill is missing from ${src}. Update the CLI and try again.`,
488
+ );
489
+ }
490
+
491
+ const manifestPath = path.join(destRoot, SKILL_DEST_DIR, MANIFEST_FILE);
492
+ const manifest = readJson(manifestPath);
493
+ const installedVersion = manifest ? manifest.version : null;
494
+ const edited = locallyEdited(destRoot, manifest);
495
+
496
+ if (edited.length > 0 && !force) {
497
+ console.error(`${NL}✗ These skill files have local changes:`);
498
+ edited.forEach((file) => console.error(` - ${file}`));
499
+ console.error(
500
+ NL +
501
+ " Updating the skill would replace them. Re-run with --force to do that,",
502
+ );
503
+ console.error(
504
+ " or move your own guidance into a separate skill folder first.",
505
+ );
506
+ process.exit(1);
507
+ }
508
+
509
+ // The canonical copy.
510
+ const written = [];
511
+ for (const file of listFiles(src)) {
512
+ await fs.copy(path.join(src, file), path.join(destRoot, SKILL_DEST_DIR, file), {
513
+ overwrite: true,
514
+ });
515
+ written.push(rel(SKILL_DEST_DIR, file));
516
+ }
517
+
518
+ // One adapter per agent that looks somewhere else.
519
+ if (targets.includes("claude")) {
520
+ written.push(...(await writeClaudeSkill(destRoot)));
521
+ }
522
+ if (targets.includes("antigravity")) {
523
+ written.push(...(await writeAntigravityRule(destRoot)));
524
+ }
525
+ const agents = targets.includes("codex")
526
+ ? await writeAgentsPointer(destRoot)
527
+ : null;
528
+
529
+ // Drop what an older version left behind, but only files we installed and
530
+ // the user has not since changed.
531
+ if (manifest) {
532
+ const removed = Object.keys(manifest.files).filter(
533
+ (file) => !written.includes(file),
534
+ );
535
+ for (const file of removed) {
536
+ const target = path.join(destRoot, file);
537
+ if (
538
+ fs.existsSync(target) &&
539
+ (force || manifest.files[file] === sha256(target))
540
+ ) {
541
+ await fs.remove(target);
542
+ await pruneEmptyDirs(destRoot, file);
543
+ }
544
+ }
545
+ }
546
+
547
+ fs.writeJsonSync(
548
+ manifestPath,
549
+ {
550
+ version: PACKAGE_VERSION,
551
+ installedAt: new Date().toISOString(),
552
+ targets,
553
+ // Hashed after writing, because the adapters are generated rather than copied.
554
+ files: Object.fromEntries(
555
+ written.map((file) => [file, sha256(path.join(destRoot, file))]),
556
+ ),
557
+ },
558
+ { spaces: 2 },
559
+ );
560
+
561
+ console.log(
562
+ installedVersion
563
+ ? `✓ skill updated ${installedVersion} → ${PACKAGE_VERSION}`
564
+ : `✓ skill v${PACKAGE_VERSION} installed`,
565
+ );
566
+ console.log(` ${SKILL_DEST_DIR}/ (canonical, ${listFiles(src).length} files)`);
567
+ if (targets.includes("claude")) {
568
+ console.log(` .claude/skills/${SKILL_NAME}/ for Claude Code`);
569
+ }
570
+ if (targets.includes("antigravity")) {
571
+ console.log(` .agents/rules/${SKILL_NAME}.md for Antigravity`);
572
+ }
573
+ if (agents) {
574
+ console.log(` ${AGENTS_FILE} block ${agents}, for Codex`);
575
+ }
576
+ console.log(" Commit them so the whole team's agents pick this up.");
577
+ };
578
+
579
+ /** `--for claude,codex` picks the adapters; `none` writes only `.gbs/`. */
580
+ const parseSkillTargets = (value) => {
581
+ if (value === undefined || value === null || value === "") {
582
+ return SKILL_TARGETS;
583
+ }
584
+
585
+ const wanted = String(value)
586
+ .split(",")
587
+ .map((item) => item.trim().toLowerCase())
588
+ .filter(Boolean);
589
+
590
+ if (wanted.includes("none")) return [];
591
+
592
+ const unknown = wanted.filter((item) => !SKILL_TARGETS.includes(item));
593
+ if (unknown.length > 0) {
594
+ console.error(
595
+ `Unknown --for target(s): ${unknown.join(", ")}. Valid: ${SKILL_TARGETS.join(", ")}, none.`,
596
+ );
597
+ process.exit(1);
598
+ }
599
+
600
+ return wanted;
601
+ };
602
+
603
+ /** `-a skill` is accepted as well as `--skill`, since both read naturally. */
604
+ const isSkillWord = (value) =>
605
+ typeof value === "string" && value.trim().toLowerCase() === "skill";
606
+
607
+ /** The files a component needs beside it, which differ between the two sets. */
608
+ const copySupportFiles = async (destPath, beta, options) => {
609
+ if (beta) {
610
+ await installShared(destPath, options);
611
+ return;
612
+ }
613
+ // The legacy components' common files. Beta components use none of them.
614
+ if (!fs.existsSync(path.join(destPath, "utils.ts"))) {
615
+ await copyCommonFiles(destPath);
616
+ }
617
+ };
618
+
619
+ /* -------------------------------------------------------------- components */
620
+
621
+ const checkComponentExists = (component, destPath, beta = false) => {
622
+ const componentPath = path.join(destPath, folderFor(component, beta));
623
+ return fs.existsSync(componentPath);
624
+ };
625
+
626
+ const copyComponent = async (component, destPath, beta = false) => {
627
+ try {
628
+ const componentSrc = path.join(
629
+ getSourcePath(beta),
630
+ folderFor(component, beta),
631
+ );
632
+ const componentDest = path.join(destPath, folderFor(component, beta));
633
+
634
+ if (!fs.existsSync(componentSrc)) {
635
+ throw new Error(
636
+ `Component ${component} not found in ${beta ? "beta " : ""}source directory.`,
637
+ );
638
+ }
639
+
640
+ await fs.copy(componentSrc, componentDest, {
641
+ overwrite: true,
642
+ filter: (source) => !isTestPath(path.relative(componentSrc, source)),
643
+ });
644
+ console.log(
645
+ `✓ Component ${component} installed successfully ${
646
+ component === "Grid"
647
+ ? "This Version of Grid will be deprecated soon. Please Install The New Data Grid Component"
648
+ : ""
649
+ }`,
650
+ );
651
+ } catch (error) {
652
+ console.error(`Error installing component ${component}:`, error.message);
653
+ process.exit(1);
654
+ }
655
+ };
656
+
657
+ const installComponentWithDependencies = async (
658
+ component,
659
+ destPath,
660
+ beta = false,
661
+ ) => {
662
+ // Get dependencies for the component
663
+ const dependencies = CONFIG.dependencies[component] || [];
664
+ const componentsToInstall = new Set([component, ...dependencies]);
665
+
666
+ // Check which components need to be installed
667
+ const pendingInstalls = beta
668
+ ? Array.from(componentsToInstall)
669
+ : Array.from(componentsToInstall).filter(
670
+ (comp) => !checkComponentExists(comp, destPath, beta),
671
+ );
672
+
673
+ if (pendingInstalls.length === 0) {
674
+ console.log(
675
+ `✓ ${component} and all its dependencies are already installed.`,
676
+ );
677
+ return;
678
+ }
679
+
680
+ // Install all pending components
681
+ for (const comp of pendingInstalls) {
682
+ await copyComponent(comp, destPath, beta);
683
+ }
684
+
685
+ if (dependencies.length > 0) {
686
+ console.log(`\nInstalled dependencies for ${component}:`);
687
+ dependencies.forEach((dep) => {
688
+ console.log(`- ${dep}`);
689
+ });
690
+ }
691
+
692
+ console.log(`\nFor documentation visit: ${CONFIG.docs}`);
693
+ };
694
+
695
+ const installMultipleComponents = async (
696
+ components,
697
+ destPath,
698
+ beta = false,
699
+ ) => {
700
+ const allComponentsToInstall = new Set();
701
+
702
+ // Collect all components and their dependencies
703
+ components.forEach((component) => {
704
+ const dependencies = CONFIG.dependencies[component] || [];
705
+ allComponentsToInstall.add(component);
706
+ dependencies.forEach((dep) => allComponentsToInstall.add(dep));
707
+ });
708
+
709
+ // Filter out already installed components
710
+ const pendingInstalls = beta
711
+ ? Array.from(allComponentsToInstall)
712
+ : Array.from(allComponentsToInstall).filter(
713
+ (comp) => !checkComponentExists(comp, destPath, beta),
714
+ );
715
+
716
+ if (pendingInstalls.length === 0) {
717
+ console.log(
718
+ "✓ All selected components and their dependencies are already installed.",
719
+ );
720
+ return;
721
+ }
722
+
723
+ console.log(`\nInstalling ${pendingInstalls.length} components...`);
724
+
725
+ // Install all pending components
726
+ for (const comp of pendingInstalls) {
727
+ await copyComponent(comp, destPath, beta);
728
+ }
729
+
730
+ // Show dependency information
731
+ const allDependencies = new Set();
732
+ components.forEach((component) => {
733
+ const dependencies = CONFIG.dependencies[component] || [];
734
+ dependencies.forEach((dep) => allDependencies.add(dep));
735
+ });
736
+
737
+ if (allDependencies.size > 0) {
738
+ console.log(`\nDependencies installed:`);
739
+ Array.from(allDependencies).forEach((dep) => {
740
+ console.log(`- ${dep}`);
741
+ });
742
+ }
743
+
744
+ console.log(`\nFor documentation visit: ${CONFIG.docs}`);
745
+ };
746
+
747
+ const interactiveComponentSelector = async (beta = false) => {
748
+ return new Promise((resolve) => {
749
+ const rl = readline.createInterface({
750
+ input: process.stdin,
751
+ output: process.stdout,
752
+ });
753
+
754
+ let selectedComponents = new Set();
755
+ let currentIndex = 0;
756
+
757
+ const renderMenu = () => {
758
+ console.clear();
759
+ console.log("🚀 Component Installer - Interactive Mode");
760
+ console.log(
761
+ "Use ↑/↓ arrow keys to navigate, SPACE to select/deselect, ENTER to install\n",
762
+ );
763
+
764
+ const availableComponents = getAvailableComponents(beta);
765
+
766
+ availableComponents.forEach((component, index) => {
767
+ const isSelected = selectedComponents.has(component);
768
+ const isCurrentIndex = index === currentIndex;
769
+ const deps = CONFIG.dependencies[component]
770
+ ? ` (requires: ${CONFIG.dependencies[component].join(", ")})`
771
+ : "";
772
+
773
+ const prefix = isCurrentIndex ? ">" : " ";
774
+ const checkbox = isSelected ? "☑" : "☐";
775
+ const line = `${prefix} ${checkbox} ${component}${deps}`;
776
+
777
+ if (isCurrentIndex) {
778
+ console.log(`\x1b[36m${line}\x1b[0m`); // Cyan highlight
779
+ } else {
780
+ console.log(line);
781
+ }
782
+ });
783
+
784
+ console.log(`\nSelected: ${selectedComponents.size} components`);
785
+ console.log("Press ENTER to install selected components, or 'q' to quit");
786
+ };
787
+
788
+ const handleKeyPress = (key) => {
789
+ switch (key) {
790
+ case "": // Up arrow
791
+ currentIndex = Math.max(0, currentIndex - 1);
792
+ renderMenu();
793
+ break;
794
+ case "": // Down arrow
795
+ currentIndex = Math.min(
796
+ getAvailableComponents(beta).length - 1,
797
+ currentIndex + 1,
798
+ );
799
+ renderMenu();
800
+ break;
801
+ case " ": // Space bar
802
+ const component = getAvailableComponents(beta)[currentIndex];
803
+ if (selectedComponents.has(component)) {
804
+ selectedComponents.delete(component);
805
+ } else {
806
+ selectedComponents.add(component);
807
+ }
808
+ renderMenu();
809
+ break;
810
+ case "\r": // Enter
811
+ if (selectedComponents.size > 0) {
812
+ rl.close();
813
+ resolve(Array.from(selectedComponents));
814
+ }
815
+ break;
816
+ case "q":
817
+ rl.close();
818
+ resolve([]);
819
+ break;
820
+ }
821
+ };
822
+
823
+ // Enable raw mode to capture arrow keys
824
+ process.stdin.setRawMode(true);
825
+ process.stdin.resume();
826
+ process.stdin.setEncoding("utf8");
827
+ process.stdin.on("data", handleKeyPress);
828
+
829
+ renderMenu();
830
+
831
+ rl.on("close", () => {
832
+ process.stdin.setRawMode(false);
833
+ process.stdin.pause();
834
+ });
835
+ });
836
+ };
837
+
838
+ const parseMultipleComponents = (componentString) => {
839
+ return componentString
840
+ .split(",")
841
+ .map((comp) => comp.trim())
842
+ .filter((comp) => comp.length > 0);
843
+ };
844
+
845
+ const validateComponents = (components, beta = false) => {
846
+ const availableComponents = getAvailableComponents(beta);
847
+ const invalidComponents = components.filter(
848
+ (comp) => !availableComponents.includes(comp),
849
+ );
850
+ if (invalidComponents.length > 0) {
851
+ if (invalidComponents.some((comp) => comp.toLowerCase() === SHARED_DIR)) {
852
+ console.error(
853
+ "`shared` is not a component: it is installed automatically with any beta component.",
854
+ );
855
+ }
856
+ console.error(`Invalid components: ${invalidComponents.join(", ")}`);
857
+ console.log(`\nAvailable ${beta ? "beta " : ""}components:`);
858
+ availableComponents.forEach((comp) => console.log(`- ${comp}`));
859
+ if (!beta) {
860
+ console.log("\nRedesigned beta components (install with -beta):");
861
+ CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
862
+ }
863
+ return false;
864
+ }
865
+ return true;
866
+ };
867
+
868
+ const main = async () => {
869
+ const args = hideBin(process.argv).map((arg) => {
870
+ if (arg === "-beta") return "--beta";
871
+ if (arg === "-skill") return "--skill";
872
+ return arg;
873
+ });
874
+ const argv = yargs(args)
875
+ .option("add", {
876
+ alias: "a",
877
+ describe:
878
+ "Component to install (single component or comma-separated list)",
879
+ type: "string",
880
+ })
881
+ .option("interactive", {
882
+ alias: "i",
883
+ describe: "Interactive component selection mode",
884
+ type: "boolean",
885
+ })
886
+ .option("list", {
887
+ alias: "l",
888
+ describe: "List available components",
889
+ type: "boolean",
890
+ })
891
+ .option("beta", {
892
+ describe: "Install redesigned beta components",
893
+ type: "boolean",
894
+ default: false,
895
+ })
896
+ .option("skill", {
897
+ describe:
898
+ "Install the GBS agent skill at the project root, for every supported agent",
899
+ type: "boolean",
900
+ default: false,
901
+ })
902
+ .option("for", {
903
+ describe:
904
+ "With --skill: which agents to write adapters for (claude, codex, antigravity, or none)",
905
+ type: "string",
906
+ })
907
+ .option("force", {
908
+ describe: "Replace files you have edited locally in shared/ or the skill",
909
+ type: "boolean",
910
+ default: false,
911
+ })
912
+ .example("$0 -a Button", "Install a single component")
913
+ .example("$0 -a Button,Card,Modal", "Install multiple components")
914
+ .example("$0 -a DataGrid -beta", "Install the redesigned beta DataGrid")
915
+ .example("$0 -a Combobox -beta", "Install the redesigned beta Combobox")
916
+ .example("$0 -skill", "Install the agent skill for every supported agent")
917
+ .example("$0 -skill --for claude", "Only the Claude Code copy")
918
+ .example("$0 -skill --for none", "Only .gbs/, no adapters")
919
+ .example("$0 -a DataGrid -beta -skill", "Install a component and the skill")
920
+ .example("$0 -i", "Interactive selection mode")
921
+ .help().argv;
922
+
923
+ // List components if requested
924
+ if (argv.list) {
925
+ const availableComponents = getAvailableComponents(argv.beta);
926
+ console.log(`\nAvailable ${argv.beta ? "beta " : ""}components:`);
927
+ availableComponents.forEach((comp) => {
928
+ const deps = CONFIG.dependencies[comp]
929
+ ? ` (requires: ${CONFIG.dependencies[comp].join(", ")})`
930
+ : "";
931
+ console.log(`- ${comp}${deps}`);
932
+ });
933
+ if (argv.beta) {
934
+ console.log(
935
+ `\nEvery beta component also installs component-lib/${SHARED_DIR}, which they all import.`,
936
+ );
937
+ console.log(
938
+ `${NL}The agent skill installs separately, at the project root, for ${SKILL_TARGETS.join(", ")} and the GBS agent:${NL} npx gbs-add-block -skill`,
939
+ );
940
+ } else {
941
+ console.log("\nRedesigned beta components (install with -beta):");
942
+ CONFIG.betaComponents.forEach((comp) => console.log(`- ${comp}`));
943
+ }
944
+ return;
945
+ }
946
+
947
+ // Interactive mode
948
+ if (argv.interactive) {
949
+ console.log("Starting interactive component selector...\n");
950
+ const selectedComponents = await interactiveComponentSelector(argv.beta);
951
+
952
+ if (selectedComponents.length === 0) {
953
+ console.log("No components selected. Exiting...");
954
+ return;
955
+ }
956
+
957
+ // Create destination directory in project root
958
+ const destPath = DEFAULT_DEST_PATH;
959
+ await fs.ensureDir(destPath);
960
+
961
+ await copySupportFiles(destPath, argv.beta, { force: argv.force });
962
+
963
+ // Install selected components
964
+ await installMultipleComponents(selectedComponents, destPath, argv.beta);
965
+ return;
966
+ }
967
+
968
+ // The skill goes to the project root, not component-lib, so it is handled
969
+ // before anything reaches the component installer.
970
+ if (argv.skill || isSkillWord(argv.add)) {
971
+ await installSkill(process.cwd(), {
972
+ force: argv.force,
973
+ targets: parseSkillTargets(argv.for),
974
+ });
975
+
976
+ // `-a --skill` leaves `add` empty, and `-a skill` names no component.
977
+ if (!argv.add || isSkillWord(argv.add)) return;
978
+ }
979
+
980
+ if (!argv.add) {
981
+ console.error(
982
+ "Please specify a component to install using -a/--add, install the agent skill with --skill, use -i/--interactive for interactive mode, or -l/--list to see available components",
983
+ );
984
+ process.exit(1);
985
+ }
986
+
987
+ // Parse components (single or multiple)
988
+ const componentInput = argv.add;
989
+ const components = parseMultipleComponents(componentInput).map(
990
+ (component) =>
991
+ normalizeComponent(component, getAvailableComponents(argv.beta)) ||
992
+ component,
993
+ );
994
+
995
+ // Validate all components
996
+ if (!validateComponents(components, argv.beta)) {
997
+ process.exit(1);
998
+ }
999
+
1000
+ // Create destination directory in project root
1001
+ const destPath = DEFAULT_DEST_PATH;
1002
+ await fs.ensureDir(destPath);
1003
+
1004
+ await copySupportFiles(destPath, argv.beta, { force: argv.force });
1005
+
1006
+ // Install components
1007
+ if (components.length === 1) {
1008
+ // Single component installation (existing behavior)
1009
+ await installComponentWithDependencies(components[0], destPath, argv.beta);
1010
+ } else {
1011
+ // Multiple components installation
1012
+ await installMultipleComponents(components, destPath, argv.beta);
1013
+ }
1014
+ };
1015
+
1016
+ main().catch((error) => {
1017
+ console.error("Error:", error.message);
1018
+ process.exit(1);
1019
+ });