vectorvesper 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +658 -249
  2. package/package.json +8 -5
package/dist/index.js CHANGED
@@ -144,7 +144,17 @@ var RegistryComponentSchema = z.object({
144
144
  supportsReducedMotion: z.boolean(),
145
145
  usesWebGL: z.boolean(),
146
146
  usesPointer: z.boolean(),
147
- usesScroll: z.boolean()
147
+ usesScroll: z.boolean(),
148
+ // Optional metadata — kept in sync with the registry builder schema.
149
+ // Declared here (rather than relying on .passthrough()) so the CLI can
150
+ // read them with full type-safety for post-install guidance.
151
+ entry: z.string().optional(),
152
+ exportName: z.string().optional(),
153
+ usesTailwind: z.boolean().optional(),
154
+ fallbacks: z.array(z.string()).optional(),
155
+ recipes: z.array(z.string()).optional(),
156
+ docsUrl: z.string().optional(),
157
+ previewUrl: z.string().optional()
148
158
  }).passthrough();
149
159
  var IndexComponentSchema = z.object({
150
160
  slug: z.string(),
@@ -168,7 +178,10 @@ var IndexComponentSchema = z.object({
168
178
  supportsReducedMotion: z.boolean(),
169
179
  usesWebGL: z.boolean(),
170
180
  usesPointer: z.boolean(),
171
- usesScroll: z.boolean()
181
+ usesScroll: z.boolean(),
182
+ entry: z.string().optional(),
183
+ exportName: z.string().optional(),
184
+ usesTailwind: z.boolean().optional()
172
185
  }).passthrough();
173
186
  var RegistryIndexSchema = z.object({
174
187
  version: z.string(),
@@ -419,15 +432,15 @@ function handleHttpError(status, url) {
419
432
  );
420
433
  case 402:
421
434
  throw new Error(
422
- `This is a Pro component and requires an active subscription.
423
- \u{1F449} Upgrade at https://vectorvesper.dev/pricing
435
+ `This is a Pro component and requires a license.
436
+ \u{1F449} Get lifetime access at https://vectorvesper.dev/pricing
424
437
  \u{1F4A1} Run "vv list" to see all free components.`
425
438
  );
426
439
  case 403:
427
440
  throw new Error(
428
- `Access denied. Your license may have expired or doesn't include this component.
441
+ `Access denied. Your license doesn't include this component.
429
442
  \u{1F449} Check your account at https://vectorvesper.dev/account
430
- \u{1F4A1} Run "vv whoami" to see your current plan.`
443
+ \u{1F4A1} Run "vv whoami" to see your current access.`
431
444
  );
432
445
  case 404:
433
446
  throw new Error(
@@ -451,7 +464,8 @@ async function fetchWithRetry(url) {
451
464
  "User-Agent": "vv-cli",
452
465
  "Accept": "application/json"
453
466
  };
454
- if (token) {
467
+ const isPublicRegistry = url.includes("raw.githubusercontent.com");
468
+ if (token && !isPublicRegistry) {
455
469
  headers["Authorization"] = `Bearer ${token}`;
456
470
  logger.debug(`Using auth token for request`);
457
471
  }
@@ -598,43 +612,281 @@ async function listCommand() {
598
612
  }
599
613
 
600
614
  // src/commands/add.ts
615
+ import fs7 from "fs";
616
+ import path7 from "path";
617
+ import ora2 from "ora";
618
+ import pc6 from "picocolors";
619
+ import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
620
+
621
+ // src/utils/installer.ts
601
622
  import fs5 from "fs";
602
623
  import path5 from "path";
603
- import ora2 from "ora";
604
624
  import pc4 from "picocolors";
605
- import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
625
+ var MAX_DEPTH = 20;
606
626
  function toForwardSlash(p) {
607
627
  return p.replace(/\\/g, "/");
608
628
  }
609
- function reportMissingDependencies(resolvedComponents, projectRoot, projectInfo) {
610
- const pkgPath = path5.join(projectRoot, "package.json");
611
- if (!fs5.existsSync(pkgPath)) return;
629
+ async function resolveComponentTree(slug) {
630
+ const resolved = [];
631
+ const visited = /* @__PURE__ */ new Set();
632
+ async function walk(componentSlug, depth) {
633
+ if (visited.has(componentSlug)) return;
634
+ if (depth > MAX_DEPTH) {
635
+ throw new Error(
636
+ `Dependency resolution exceeded maximum depth (${MAX_DEPTH}). Possible circular dependency involving "${componentSlug}".`
637
+ );
638
+ }
639
+ visited.add(componentSlug);
640
+ const component = await fetchComponent(componentSlug);
641
+ if (component.tier === "pro" && !getAuthToken()) {
642
+ throw new Error(
643
+ `"${componentSlug}" is a Pro component and requires a license.
644
+ \u{1F449} Run ${pc4.cyan("vv login <your-license-key>")} to unlock pro components.
645
+ \u{1F511} Get a license at ${pc4.cyan("https://vectorvesper.dev/pricing")}`
646
+ );
647
+ }
648
+ for (const depSlug of component.registryDependencies ?? []) {
649
+ await walk(depSlug, depth + 1);
650
+ }
651
+ resolved.push(component);
652
+ }
653
+ await walk(slug, 0);
654
+ return resolved;
655
+ }
656
+ function planFiles(components, installDir, projectRoot) {
657
+ const planned = [];
658
+ const compDir = path5.join(projectRoot, installDir);
659
+ for (const component of components) {
660
+ for (const file of component.files) {
661
+ const absolutePath = path5.resolve(compDir, file.target);
662
+ const relativeFromRoot = path5.relative(projectRoot, absolutePath);
663
+ if (relativeFromRoot.startsWith("..") || path5.isAbsolute(relativeFromRoot)) {
664
+ throw new Error(
665
+ `Security Error: component target tries to escape project root: ${file.target}`
666
+ );
667
+ }
668
+ planned.push({
669
+ absolutePath,
670
+ relativePath: relativeFromRoot,
671
+ target: file.target,
672
+ content: file.content,
673
+ exists: fs5.existsSync(absolutePath)
674
+ });
675
+ }
676
+ }
677
+ return planned;
678
+ }
679
+ function writeFiles(files) {
680
+ for (const file of files) {
681
+ const folder = path5.dirname(file.absolutePath);
682
+ if (!fs5.existsSync(folder)) {
683
+ fs5.mkdirSync(folder, { recursive: true });
684
+ }
685
+ fs5.writeFileSync(file.absolutePath, file.content, "utf-8");
686
+ }
687
+ }
688
+ var MANIFEST_NAME = "vv-manifest.json";
689
+ function getManifestPath(projectRoot) {
690
+ return path5.join(projectRoot, MANIFEST_NAME);
691
+ }
692
+ function readManifest(projectRoot) {
693
+ const manifestPath = getManifestPath(projectRoot);
694
+ if (!fs5.existsSync(manifestPath)) {
695
+ return { components: {} };
696
+ }
697
+ const parsed = JSON.parse(fs5.readFileSync(manifestPath, "utf-8"));
698
+ if (!parsed.components) parsed.components = {};
699
+ return parsed;
700
+ }
701
+ function writeManifest(projectRoot, manifest) {
702
+ fs5.writeFileSync(getManifestPath(projectRoot), JSON.stringify(manifest, null, 2), "utf-8");
703
+ }
704
+ function recordInstall(components, installDir, projectRoot) {
705
+ const manifest = readManifest(projectRoot);
706
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
707
+ for (const component of components) {
708
+ manifest.components[component.slug] = {
709
+ version: component.version,
710
+ installedAt: toForwardSlash(path5.join(installDir, component.slug)),
711
+ installedOn: today,
712
+ files: component.files.map((f) => f.target)
713
+ };
714
+ }
715
+ writeManifest(projectRoot, manifest);
716
+ }
717
+
718
+ // src/utils/install.ts
719
+ import fs6 from "fs";
720
+ import path6 from "path";
721
+ import { spawnSync } from "child_process";
722
+ var NPM_NAME_REGEX = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i;
723
+ function isValidPackageName(name) {
724
+ return NPM_NAME_REGEX.test(name);
725
+ }
726
+ function getMissingDependencies(components, projectRoot) {
727
+ const pkgPath = path6.join(projectRoot, "package.json");
728
+ if (!fs6.existsSync(pkgPath)) return [];
729
+ let installed = {};
612
730
  try {
613
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
614
- const installedDeps = {
731
+ const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
732
+ installed = {
615
733
  ...pkg.dependencies,
616
734
  ...pkg.devDependencies,
617
735
  ...pkg.peerDependencies
618
736
  };
619
- const missingDeps = /* @__PURE__ */ new Set();
620
- for (const component of resolvedComponents) {
621
- if (component.dependencies) {
622
- for (const dep of component.dependencies) {
623
- if (!installedDeps[dep]) {
624
- missingDeps.add(dep);
625
- }
626
- }
627
- }
737
+ } catch {
738
+ return [];
739
+ }
740
+ const missing = /* @__PURE__ */ new Set();
741
+ for (const component of components) {
742
+ for (const dep of component.dependencies ?? []) {
743
+ if (!installed[dep]) missing.add(dep);
744
+ }
745
+ }
746
+ return Array.from(missing);
747
+ }
748
+ function getInstallCommand(pm, deps) {
749
+ const depsStr = deps.join(" ");
750
+ switch (pm) {
751
+ case "pnpm":
752
+ return `pnpm add ${depsStr}`;
753
+ case "yarn":
754
+ return `yarn add ${depsStr}`;
755
+ case "bun":
756
+ return `bun add ${depsStr}`;
757
+ default:
758
+ return `npm install ${depsStr}`;
759
+ }
760
+ }
761
+ function installDependencies(deps, pm, projectRoot) {
762
+ const invalid = deps.filter((d) => !isValidPackageName(d));
763
+ if (invalid.length > 0) {
764
+ throw new Error(`Refusing to install \u2014 invalid package name(s): ${invalid.join(", ")}`);
765
+ }
766
+ const command = getInstallCommand(pm, deps);
767
+ const result = spawnSync(command, {
768
+ cwd: projectRoot,
769
+ stdio: "inherit",
770
+ shell: true
771
+ });
772
+ return result.status === 0;
773
+ }
774
+
775
+ // src/utils/guidance.ts
776
+ import pc5 from "picocolors";
777
+ function toPascalCase(input) {
778
+ return input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
779
+ }
780
+ function toImportPath(alias, entryTarget) {
781
+ let p = entryTarget.replace(/\.(tsx|ts|jsx|js)$/i, "");
782
+ p = p.replace(/\/index$/i, "");
783
+ const base = alias.replace(/\/+$/, "");
784
+ return `${base}/${p}`;
785
+ }
786
+ function buildGuidance(component, ctx) {
787
+ const entryTarget = component.entry ?? component.files[0]?.target ?? `${component.slug}/index.ts`;
788
+ const importPath = toImportPath(ctx.alias, entryTarget);
789
+ const exportName = component.exportName ?? "default";
790
+ const isDefault = exportName === "default";
791
+ const varName = isDefault ? toPascalCase(component.name || component.slug) : exportName;
792
+ const importSnippet = isDefault ? `import ${varName} from "${importPath}";` : `import { ${exportName} } from "${importPath}";`;
793
+ const guidance = {
794
+ importSnippet,
795
+ notes: [],
796
+ warnings: [],
797
+ docsUrl: component.docsUrl
798
+ };
799
+ if (component.usesWebGL) {
800
+ const resolver = isDefault ? `() => import("${importPath}")` : `() => import("${importPath}").then((m) => m.${exportName})`;
801
+ if (ctx.framework === "next") {
802
+ guidance.ssrSnippet = `"use client";
803
+ import dynamic from "next/dynamic";
804
+
805
+ const ${varName} = dynamic(
806
+ ${resolver},
807
+ { ssr: false }
808
+ );`;
809
+ } else if (ctx.framework === "unknown") {
810
+ guidance.notes.push(
811
+ "This is a WebGL component. If you server-render (e.g. Next.js), load it with `next/dynamic` and `{ ssr: false }` to avoid hydration errors."
812
+ );
628
813
  }
629
- if (missingDeps.size > 0) {
630
- console.log(pc4.yellow(`
631
- \u26A0\uFE0F Missing peer dependencies in your project:`));
632
- const pm = projectInfo.packageManager;
633
- const depsStr = Array.from(missingDeps).join(" ");
634
- const installCmd = pm === "pnpm" ? `pnpm add ${depsStr}` : pm === "yarn" ? `yarn add ${depsStr}` : pm === "bun" ? `bun add ${depsStr}` : `npm install ${depsStr}`;
635
- console.log(` Run: ${pc4.bold(pc4.cyan(installCmd))}`);
814
+ }
815
+ if (component.supportsReducedMotion) {
816
+ guidance.notes.push("Honors `prefers-reduced-motion` automatically \u2014 no extra setup needed.");
817
+ }
818
+ if (component.usesTailwind && !ctx.hasTailwind) {
819
+ guidance.warnings.push(
820
+ "Styled with Tailwind CSS, which wasn't detected in this project. Install Tailwind or swap the classNames for your own styles."
821
+ );
822
+ }
823
+ return guidance;
824
+ }
825
+ function printGuidance(component, ctx) {
826
+ const g = buildGuidance(component, ctx);
827
+ console.log(pc5.bold(pc5.white(`
828
+ Using ${component.title || component.name}:`)));
829
+ if (g.ssrSnippet) {
830
+ console.log(pc5.dim(" // SSR-safe (Next.js) \u2014 disables server rendering for WebGL:"));
831
+ for (const line of g.ssrSnippet.split("\n")) {
832
+ console.log(line ? ` ${pc5.cyan(line)}` : "");
636
833
  }
637
- } catch {
834
+ } else {
835
+ console.log(` ${pc5.cyan(g.importSnippet)}`);
836
+ }
837
+ for (const note of g.notes) {
838
+ console.log(` ${pc5.green("\u2022")} ${pc5.dim(note)}`);
839
+ }
840
+ for (const warning of g.warnings) {
841
+ console.log(` ${pc5.yellow("\u26A0")} ${pc5.yellow(warning)}`);
842
+ }
843
+ if (g.docsUrl) {
844
+ console.log(` ${pc5.dim("Docs:")} ${pc5.cyan(g.docsUrl)}`);
845
+ }
846
+ }
847
+
848
+ // src/commands/add.ts
849
+ async function handleDependencies(components, projectRoot, projectInfo, options) {
850
+ const missing = getMissingDependencies(components, projectRoot);
851
+ if (missing.length === 0) return;
852
+ const installCmd = getInstallCommand(projectInfo.packageManager, missing);
853
+ const depsLabel = missing.map((d) => pc6.cyan(d)).join(", ");
854
+ if (options.install === false) {
855
+ console.log(pc6.yellow(`
856
+ \u26A0\uFE0F Missing dependencies: ${depsLabel}`));
857
+ console.log(` Run: ${pc6.bold(pc6.cyan(installCmd))}`);
858
+ return;
859
+ }
860
+ let shouldInstall = options.yes === true;
861
+ if (!shouldInstall) {
862
+ console.log("");
863
+ const answer = await confirm2({
864
+ message: `Install ${missing.length} missing ${missing.length === 1 ? "dependency" : "dependencies"} (${missing.join(", ")}) with ${projectInfo.packageManager}?`,
865
+ initialValue: true
866
+ });
867
+ if (isCancel2(answer)) {
868
+ console.log(pc6.dim(` Skipped. Run: ${pc6.cyan(installCmd)}`));
869
+ return;
870
+ }
871
+ shouldInstall = answer;
872
+ }
873
+ if (!shouldInstall) {
874
+ console.log(pc6.dim(` Skipped. Run: ${pc6.cyan(installCmd)} when ready.`));
875
+ return;
876
+ }
877
+ console.log(pc6.dim(`
878
+ Installing dependencies with ${projectInfo.packageManager}...`));
879
+ try {
880
+ const ok = installDependencies(missing, projectInfo.packageManager, projectRoot);
881
+ if (ok) {
882
+ console.log(pc6.green(`\u2714 Dependencies installed.`));
883
+ } else {
884
+ console.log(pc6.yellow(`\u26A0\uFE0F Dependency install did not complete. Run manually: ${pc6.cyan(installCmd)}`));
885
+ }
886
+ } catch (error) {
887
+ const message = error instanceof Error ? error.message : String(error);
888
+ console.log(pc6.yellow(`\u26A0\uFE0F ${message}`));
889
+ console.log(` Run manually: ${pc6.bold(pc6.cyan(installCmd))}`);
638
890
  }
639
891
  }
640
892
  async function addCommand(slug, options = {}) {
@@ -642,7 +894,7 @@ async function addCommand(slug, options = {}) {
642
894
  validateSlug(slug);
643
895
  } catch (error) {
644
896
  const message = error instanceof Error ? error.message : String(error);
645
- console.error(`${pc4.red(pc4.bold("Error:"))} ${message}`);
897
+ console.error(`${pc6.red(pc6.bold("Error:"))} ${message}`);
646
898
  process.exit(1);
647
899
  }
648
900
  let config;
@@ -650,173 +902,324 @@ async function addCommand(slug, options = {}) {
650
902
  config = loadConfig();
651
903
  } catch (error) {
652
904
  const message = error instanceof Error ? error.message : String(error);
653
- console.error(`${pc4.red(pc4.bold("Error:"))} ${message}`);
905
+ console.error(`${pc6.red(pc6.bold("Error:"))} ${message}`);
654
906
  process.exit(1);
655
907
  }
656
908
  if (!config) {
657
909
  console.error(
658
- `${pc4.red(pc4.bold("Error:"))} No ${pc4.cyan("vv.config.json")} found in this directory.
659
- \u{1F449} Run ${pc4.bold(pc4.cyan("vv init"))} first to set up your project.`
910
+ `${pc6.red(pc6.bold("Error:"))} No ${pc6.cyan("vv.config.json")} found in this directory.
911
+ \u{1F449} Run ${pc6.bold(pc6.cyan("vv init"))} first to set up your project.`
660
912
  );
661
913
  process.exit(1);
662
914
  return;
663
915
  }
664
916
  const projectInfo = detectProject();
665
917
  const componentInstallDir = resolveAlias(config.aliases.vv, projectInfo.hasSrcDir);
666
- const spinner = ora2({
667
- text: `Resolving component ${pc4.cyan(slug)}...`,
668
- color: "cyan"
669
- }).start();
670
- const resolvedComponents = [];
671
- const visited = /* @__PURE__ */ new Set();
672
- async function resolveComponentTree(componentSlug, depth = 0) {
673
- if (visited.has(componentSlug)) return;
674
- if (depth > 20) {
675
- throw new Error(`Dependency resolution exceeded maximum depth (20). Possible circular dependency involving "${componentSlug}".`);
676
- }
677
- visited.add(componentSlug);
678
- const component = await fetchComponent(componentSlug);
679
- if (component.tier === "pro" && !getAuthToken()) {
680
- throw new Error(
681
- `"${componentSlug}" is a Pro component and requires authentication.
682
- \u{1F449} Run ${pc4.cyan("vv login <your-license-key>")} to unlock pro components.
683
- \u{1F511} Get a key at ${pc4.cyan("https://vectorvesper.dev/pricing")}`
684
- );
685
- }
686
- if (component.registryDependencies && component.registryDependencies.length > 0) {
687
- for (const depSlug of component.registryDependencies) {
688
- await resolveComponentTree(depSlug, depth + 1);
689
- }
690
- }
691
- resolvedComponents.push(component);
692
- }
918
+ const projectRoot = process.cwd();
919
+ const spinner = ora2({ text: `Resolving component ${pc6.cyan(slug)}...`, color: "cyan" }).start();
920
+ let resolvedComponents;
693
921
  try {
694
- await resolveComponentTree(slug);
922
+ resolvedComponents = await resolveComponentTree(slug);
695
923
  } catch (error) {
696
924
  const message = error instanceof Error ? error.message : String(error);
697
- spinner.fail(pc4.red(`Failed to resolve component "${slug}": ${message}`));
925
+ spinner.fail(pc6.red(`Failed to resolve component "${slug}": ${message}`));
698
926
  process.exit(1);
927
+ return;
699
928
  }
700
929
  spinner.text = "Checking file conflicts...";
930
+ let planned;
931
+ try {
932
+ planned = planFiles(resolvedComponents, componentInstallDir, projectRoot);
933
+ } catch (error) {
934
+ const message = error instanceof Error ? error.message : String(error);
935
+ spinner.fail(pc6.red(message));
936
+ process.exit(1);
937
+ return;
938
+ }
701
939
  const filesToWrite = [];
702
- const projectRoot = process.cwd();
703
- for (const component of resolvedComponents) {
704
- const compDir = path5.join(projectRoot, componentInstallDir);
705
- for (const file of component.files) {
706
- const fileWritePath = path5.resolve(compDir, file.target);
707
- const relativeFromRoot = path5.relative(projectRoot, fileWritePath);
708
- if (relativeFromRoot.startsWith("..") || path5.isAbsolute(relativeFromRoot)) {
709
- spinner.fail(
710
- `${pc4.red(pc4.bold("Security Error:"))} Component target file path tries to escape project root: ${file.target}`
711
- );
712
- process.exit(1);
713
- }
714
- if (fs5.existsSync(fileWritePath)) {
715
- if (options.dryRun) {
716
- } else if (!options.overwrite && !options.yes) {
717
- spinner.stop();
718
- const relativeDisplayPath = path5.relative(projectRoot, fileWritePath);
719
- const shouldOverwrite = await confirm2({
720
- message: pc4.yellow(`File already exists: "${relativeDisplayPath}". Overwrite?`),
721
- initialValue: false
722
- });
723
- if (isCancel2(shouldOverwrite) || !shouldOverwrite) {
724
- console.log(pc4.dim(` Skipping existing file: ${relativeDisplayPath}`));
725
- spinner.start("Continuing installation...");
726
- continue;
727
- }
728
- spinner.start("Continuing installation...");
729
- }
730
- }
731
- filesToWrite.push({
732
- absolutePath: fileWritePath,
733
- relativePath: relativeFromRoot,
734
- content: file.content
940
+ for (const file of planned) {
941
+ if (file.exists && !options.dryRun && !options.overwrite && !options.yes) {
942
+ spinner.stop();
943
+ const answer = await confirm2({
944
+ message: pc6.yellow(`File already exists: "${file.relativePath}". Overwrite?`),
945
+ initialValue: false
735
946
  });
947
+ if (isCancel2(answer) || !answer) {
948
+ console.log(pc6.dim(` Skipping existing file: ${file.relativePath}`));
949
+ spinner.start("Continuing installation...");
950
+ continue;
951
+ }
952
+ spinner.start("Continuing installation...");
736
953
  }
954
+ filesToWrite.push({
955
+ absolutePath: file.absolutePath,
956
+ relativePath: file.relativePath,
957
+ content: file.content
958
+ });
737
959
  }
960
+ const targetComponent = resolvedComponents[resolvedComponents.length - 1];
961
+ const installPath = toForwardSlash(path7.join(componentInstallDir, targetComponent.slug));
738
962
  if (options.dryRun) {
739
- spinner.succeed(pc4.yellow("Dry-run: Simulation completed. No files were written."));
740
- console.log(pc4.bold(pc4.yellow("\n[Dry Run] Files that would be created/modified:")));
963
+ spinner.succeed(pc6.yellow("Dry-run: simulation completed. No files were written."));
964
+ console.log(pc6.bold(pc6.yellow("\n[Dry Run] Files that would be created/modified:")));
741
965
  for (const file of filesToWrite) {
742
- const status = fs5.existsSync(file.absolutePath) ? pc4.yellow("(exists, would overwrite)") : pc4.green("(new)");
743
- console.log(` ${pc4.cyan(file.relativePath)} ${status}`);
966
+ const status = fs7.existsSync(file.absolutePath) ? pc6.yellow("(exists, would overwrite)") : pc6.green("(new)");
967
+ console.log(` ${pc6.cyan(file.relativePath)} ${status}`);
968
+ }
969
+ const missing = getMissingDependencies(resolvedComponents, projectRoot);
970
+ if (missing.length > 0) {
971
+ console.log(pc6.yellow(`
972
+ \u26A0\uFE0F Would need dependencies: ${missing.map((d) => pc6.cyan(d)).join(", ")}`));
973
+ console.log(` ${pc6.dim(getInstallCommand(projectInfo.packageManager, missing))}`);
744
974
  }
745
- reportMissingDependencies(resolvedComponents, projectRoot, projectInfo);
746
- const targetComponent2 = resolvedComponents[resolvedComponents.length - 1];
747
- console.log("");
748
- console.log(
749
- pc4.bold(pc4.yellow("\u2714 [Dry Run] Would add ")) + pc4.bold(pc4.cyan(targetComponent2.slug)) + pc4.bold(pc4.yellow(` to ${toForwardSlash(path5.join(componentInstallDir, targetComponent2.slug))}`))
750
- );
751
975
  console.log("");
752
- console.log(pc4.gray("\u{1F449} Loving Vector Vesper? Star the repo: https://github.com/vectorvesper/vv-components"));
753
- console.log(pc4.gray("\u{1F680} Need premium WebGL effects? Get the Pixel Library: https://vectorvesper.dev/library"));
976
+ console.log(pc6.bold(pc6.yellow(`\u2714 [Dry Run] Would add ${pc6.cyan(targetComponent.slug)} to ${installPath}`)));
754
977
  console.log("");
755
978
  return;
756
979
  }
757
980
  if (filesToWrite.length === 0) {
758
- spinner.succeed(pc4.green("All files skipped (already up to date)."));
981
+ spinner.succeed(pc6.green("All files skipped (already up to date)."));
759
982
  return;
760
983
  }
761
984
  spinner.text = "Writing component files...";
762
985
  try {
763
- for (const file of filesToWrite) {
764
- const folder = path5.dirname(file.absolutePath);
765
- if (!fs5.existsSync(folder)) {
766
- fs5.mkdirSync(folder, { recursive: true });
767
- }
768
- fs5.writeFileSync(file.absolutePath, file.content, "utf-8");
769
- }
986
+ writeFiles(filesToWrite);
770
987
  } catch (error) {
771
988
  const message = error instanceof Error ? error.message : String(error);
772
- spinner.fail(pc4.red(`Failed to write files: ${message}`));
989
+ spinner.fail(pc6.red(`Failed to write files: ${message}`));
773
990
  process.exit(1);
991
+ return;
774
992
  }
775
993
  try {
776
- const manifestPath = path5.join(projectRoot, "vv-manifest.json");
777
- let manifest = { components: {} };
778
- if (fs5.existsSync(manifestPath)) {
779
- manifest = JSON.parse(fs5.readFileSync(manifestPath, "utf-8"));
780
- }
781
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
782
- for (const component of resolvedComponents) {
783
- manifest.components[component.slug] = {
784
- version: component.version,
785
- installedAt: toForwardSlash(path5.join(componentInstallDir, component.slug)),
786
- installedOn: today,
787
- files: component.files.map((f) => f.target)
788
- };
789
- }
790
- fs5.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
994
+ recordInstall(resolvedComponents, componentInstallDir, projectRoot);
791
995
  } catch (error) {
792
996
  const message = error instanceof Error ? error.message : String(error);
793
997
  logger.warn(`Failed to update vv-manifest.json: ${message}`);
794
998
  logger.warn("Component was installed but may not appear in 'vv info' or 'vv diff'.");
795
999
  }
796
- spinner.succeed(pc4.green("Files written successfully!"));
797
- console.log(pc4.dim("\nCreated files:"));
1000
+ spinner.succeed(pc6.green("Files written successfully!"));
1001
+ console.log(pc6.dim("\nCreated files:"));
798
1002
  for (const file of filesToWrite) {
799
- console.log(` ${pc4.green("\u2714")} ${pc4.cyan(file.relativePath)}`);
1003
+ console.log(` ${pc6.green("\u2714")} ${pc6.cyan(file.relativePath)}`);
800
1004
  }
801
- reportMissingDependencies(resolvedComponents, projectRoot, projectInfo);
802
- const targetComponent = resolvedComponents[resolvedComponents.length - 1];
1005
+ await handleDependencies(resolvedComponents, projectRoot, projectInfo, options);
1006
+ printGuidance(targetComponent, {
1007
+ alias: config.aliases.vv,
1008
+ framework: projectInfo.framework,
1009
+ hasTailwind: projectInfo.hasTailwind
1010
+ });
803
1011
  console.log("");
804
- console.log(
805
- pc4.bold(pc4.green("\u2714 Added ")) + pc4.bold(pc4.cyan(targetComponent.slug)) + pc4.bold(pc4.green(` to ${toForwardSlash(path5.join(componentInstallDir, targetComponent.slug))}`))
806
- );
1012
+ console.log(pc6.bold(pc6.green(`\u2714 Added ${pc6.cyan(targetComponent.slug)} to ${installPath}`)));
1013
+ console.log("");
1014
+ console.log(pc6.gray("\u{1F449} Loving Vector Vesper? Star the repo: https://github.com/vectorvesper/vv-components"));
1015
+ console.log(pc6.gray("\u{1F680} Premium shader & pixel systems: https://vectorvesper.dev/pricing"));
1016
+ console.log("");
1017
+ }
1018
+
1019
+ // src/commands/update.ts
1020
+ import pc7 from "picocolors";
1021
+ import ora3 from "ora";
1022
+ async function updateCommand(slug, options = {}) {
1023
+ const projectRoot = process.cwd();
1024
+ let config;
1025
+ try {
1026
+ config = loadConfig();
1027
+ } catch (error) {
1028
+ const message = error instanceof Error ? error.message : String(error);
1029
+ console.error(`${pc7.red(pc7.bold("Error:"))} ${message}`);
1030
+ process.exit(1);
1031
+ return;
1032
+ }
1033
+ if (!config) {
1034
+ console.error(
1035
+ `${pc7.red(pc7.bold("Error:"))} No ${pc7.cyan("vv.config.json")} found.
1036
+ \u{1F449} Run ${pc7.bold(pc7.cyan("vv init"))} first.`
1037
+ );
1038
+ process.exit(1);
1039
+ return;
1040
+ }
1041
+ const projectInfo = detectProject();
1042
+ const installDir = resolveAlias(config.aliases.vv, projectInfo.hasSrcDir);
1043
+ const manifest = readManifest(projectRoot);
1044
+ const installedSlugs = Object.keys(manifest.components);
1045
+ if (installedSlugs.length === 0) {
1046
+ console.log(pc7.yellow(`
1047
+ \u26A0\uFE0F No components are installed in this project.`));
1048
+ console.log(`\u{1F449} Add one with ${pc7.bold(pc7.cyan("vv add <slug>"))}
1049
+ `);
1050
+ return;
1051
+ }
1052
+ let targets;
1053
+ if (slug) {
1054
+ try {
1055
+ validateSlug(slug);
1056
+ } catch (error) {
1057
+ const message = error instanceof Error ? error.message : String(error);
1058
+ console.error(pc7.red(`
1059
+ \u274C ${message}
1060
+ `));
1061
+ process.exit(1);
1062
+ return;
1063
+ }
1064
+ if (!manifest.components[slug]) {
1065
+ console.log(pc7.red(`
1066
+ \u274C Component "${slug}" is not installed in this project.`));
1067
+ console.log(`\u{1F449} Run ${pc7.cyan(`vv add ${slug}`)} to install it.
1068
+ `);
1069
+ return;
1070
+ }
1071
+ targets = [slug];
1072
+ } else {
1073
+ targets = installedSlugs;
1074
+ }
1075
+ const spinner = ora3({ text: "Checking for updates...", color: "cyan" }).start();
1076
+ const updated = [];
1077
+ const upToDate = [];
1078
+ const failed = [];
1079
+ for (const target of targets) {
1080
+ spinner.text = `Updating ${pc7.cyan(target)}...`;
1081
+ try {
1082
+ const components = await resolveComponentTree(target);
1083
+ const remote = components[components.length - 1];
1084
+ const localVersion = manifest.components[target]?.version;
1085
+ if (localVersion === remote.version && !options.force) {
1086
+ upToDate.push(target);
1087
+ continue;
1088
+ }
1089
+ const planned = planFiles(components, installDir, projectRoot);
1090
+ writeFiles(planned.map((f) => ({ absolutePath: f.absolutePath, content: f.content })));
1091
+ recordInstall(components, installDir, projectRoot);
1092
+ updated.push(`${target} \u2192 ${remote.version}`);
1093
+ } catch (error) {
1094
+ const message = error instanceof Error ? error.message : String(error);
1095
+ failed.push({ slug: target, reason: message });
1096
+ }
1097
+ }
1098
+ spinner.stop();
1099
+ console.log(pc7.bold(pc7.cyan("\nVector Vesper Update\n")));
1100
+ if (updated.length > 0) {
1101
+ console.log(pc7.bold(pc7.green(` Updated (${updated.length}):`)));
1102
+ for (const u of updated) console.log(` ${pc7.green("\u2714")} ${u}`);
1103
+ console.log(pc7.dim(" Note: updating overwrites local edits to these files."));
1104
+ }
1105
+ if (upToDate.length > 0) {
1106
+ console.log(pc7.bold(pc7.gray(`
1107
+ Already up to date (${upToDate.length}):`)));
1108
+ console.log(` ${pc7.dim(upToDate.join(", "))}`);
1109
+ }
1110
+ if (failed.length > 0) {
1111
+ console.log(pc7.bold(pc7.red(`
1112
+ Failed (${failed.length}):`)));
1113
+ for (const f of failed) console.log(` ${pc7.red("\u2716")} ${f.slug}: ${pc7.dim(f.reason)}`);
1114
+ }
1115
+ console.log("");
1116
+ if (failed.length > 0) process.exit(1);
1117
+ }
1118
+
1119
+ // src/commands/remove.ts
1120
+ import fs8 from "fs";
1121
+ import path8 from "path";
1122
+ import pc8 from "picocolors";
1123
+ import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
1124
+ async function removeCommand(slug, options = {}) {
1125
+ try {
1126
+ validateSlug(slug);
1127
+ } catch (error) {
1128
+ const message = error instanceof Error ? error.message : String(error);
1129
+ console.error(`${pc8.red(pc8.bold("Error:"))} ${message}`);
1130
+ process.exit(1);
1131
+ return;
1132
+ }
1133
+ const projectRoot = process.cwd();
1134
+ let config;
1135
+ try {
1136
+ config = loadConfig();
1137
+ } catch (error) {
1138
+ const message = error instanceof Error ? error.message : String(error);
1139
+ console.error(`${pc8.red(pc8.bold("Error:"))} ${message}`);
1140
+ process.exit(1);
1141
+ return;
1142
+ }
1143
+ if (!config) {
1144
+ console.error(
1145
+ `${pc8.red(pc8.bold("Error:"))} No ${pc8.cyan("vv.config.json")} found.
1146
+ \u{1F449} Run ${pc8.bold(pc8.cyan("vv init"))} first.`
1147
+ );
1148
+ process.exit(1);
1149
+ return;
1150
+ }
1151
+ const manifest = readManifest(projectRoot);
1152
+ const entry = manifest.components[slug];
1153
+ if (!entry) {
1154
+ console.log(pc8.yellow(`
1155
+ \u26A0\uFE0F Component "${slug}" is not installed (not in vv-manifest.json).`));
1156
+ console.log(`\u{1F449} See installed components with ${pc8.bold(pc8.cyan("vv info"))}
1157
+ `);
1158
+ return;
1159
+ }
1160
+ const projectInfo = detectProject();
1161
+ const installDir = resolveAlias(config.aliases.vv, projectInfo.hasSrcDir);
1162
+ const baseDir = path8.join(projectRoot, installDir);
1163
+ const targets = entry.files ?? [];
1164
+ const absoluteFiles = targets.map((t) => path8.resolve(baseDir, t));
1165
+ console.log(pc8.bold(pc8.cyan(`
1166
+ Remove ${slug}
1167
+ `)));
1168
+ console.log(pc8.dim(" The following files will be deleted:"));
1169
+ for (const f of absoluteFiles) {
1170
+ console.log(` ${fs8.existsSync(f) ? pc8.red("\u2212") : pc8.dim("\xB7")} ${pc8.cyan(path8.relative(projectRoot, f))}`);
1171
+ }
1172
+ if (!options.yes) {
1173
+ const answer = await confirm3({
1174
+ message: pc8.yellow(`Delete ${absoluteFiles.length} file(s) for "${slug}"?`),
1175
+ initialValue: false
1176
+ });
1177
+ if (isCancel3(answer) || !answer) {
1178
+ console.log(pc8.dim("\n Cancelled. Nothing was removed.\n"));
1179
+ return;
1180
+ }
1181
+ }
1182
+ let deleted = 0;
1183
+ for (const f of absoluteFiles) {
1184
+ try {
1185
+ if (fs8.existsSync(f)) {
1186
+ fs8.rmSync(f);
1187
+ deleted++;
1188
+ }
1189
+ } catch (error) {
1190
+ const message = error instanceof Error ? error.message : String(error);
1191
+ console.log(pc8.yellow(` \u26A0\uFE0F Could not delete ${path8.relative(projectRoot, f)}: ${message}`));
1192
+ }
1193
+ }
1194
+ const componentDir = path8.resolve(baseDir, slug);
1195
+ try {
1196
+ if (fs8.existsSync(componentDir) && fs8.readdirSync(componentDir).length === 0) {
1197
+ fs8.rmdirSync(componentDir);
1198
+ }
1199
+ } catch {
1200
+ }
1201
+ delete manifest.components[slug];
1202
+ try {
1203
+ writeManifest(projectRoot, manifest);
1204
+ } catch (error) {
1205
+ const message = error instanceof Error ? error.message : String(error);
1206
+ console.log(pc8.yellow(` \u26A0\uFE0F Removed files but failed to update vv-manifest.json: ${message}`));
1207
+ }
807
1208
  console.log("");
808
- console.log(pc4.gray("\u{1F449} Loving Vector Vesper? Star the repo: https://github.com/vectorvesper/vv-components"));
809
- console.log(pc4.gray("\u{1F680} Need premium WebGL effects? Get the Pixel Library: https://vectorvesper.dev/library"));
1209
+ console.log(pc8.bold(pc8.green(`\u2714 Removed ${pc8.cyan(slug)} (${deleted} file(s) deleted).`)));
1210
+ if ((entry.files ?? []).length === 0) {
1211
+ console.log(pc8.dim(" No files were tracked for this component; manifest entry cleared."));
1212
+ }
810
1213
  console.log("");
811
1214
  }
812
1215
 
813
1216
  // src/commands/info.ts
814
- import fs6 from "fs";
815
- import path6 from "path";
816
- import pc5 from "picocolors";
817
- var CLI_VERSION = true ? "0.1.0" : "0.0.0-dev";
1217
+ import fs9 from "fs";
1218
+ import path9 from "path";
1219
+ import pc9 from "picocolors";
1220
+ var CLI_VERSION = true ? "0.2.0" : "0.0.0-dev";
818
1221
  async function infoCommand() {
819
- console.log(pc5.bold(pc5.cyan("\nVector Vesper Diagnostics\n")));
1222
+ console.log(pc9.bold(pc9.cyan("\nVector Vesper Diagnostics\n")));
820
1223
  const projectInfo = detectProject();
821
1224
  let config;
822
1225
  try {
@@ -824,95 +1227,95 @@ async function infoCommand() {
824
1227
  } catch {
825
1228
  config = null;
826
1229
  }
827
- console.log(pc5.bold(pc5.white("Environment:")));
828
- console.log(` \u2022 CLI Version: ${pc5.cyan(CLI_VERSION)}`);
829
- console.log(` \u2022 Node Version: ${pc5.cyan(process.version)}`);
830
- console.log(` \u2022 OS: ${pc5.cyan(`${process.platform} ${process.arch}`)}`);
831
- console.log(` \u2022 Package Manager: ${pc5.cyan(projectInfo.packageManager)}`);
832
- console.log(` \u2022 Framework: ${pc5.cyan(projectInfo.framework)}`);
833
- console.log(` \u2022 TypeScript: ${pc5.cyan(projectInfo.isTypeScript ? "Yes" : "No")}`);
834
- console.log(` \u2022 Has src/ folder: ${pc5.cyan(projectInfo.hasSrcDir ? "Yes" : "No")}`);
835
- console.log(` \u2022 Tailwind CSS: ${pc5.cyan(projectInfo.hasTailwind ? "Yes" : "No")}`);
1230
+ console.log(pc9.bold(pc9.white("Environment:")));
1231
+ console.log(` \u2022 CLI Version: ${pc9.cyan(CLI_VERSION)}`);
1232
+ console.log(` \u2022 Node Version: ${pc9.cyan(process.version)}`);
1233
+ console.log(` \u2022 OS: ${pc9.cyan(`${process.platform} ${process.arch}`)}`);
1234
+ console.log(` \u2022 Package Manager: ${pc9.cyan(projectInfo.packageManager)}`);
1235
+ console.log(` \u2022 Framework: ${pc9.cyan(projectInfo.framework)}`);
1236
+ console.log(` \u2022 TypeScript: ${pc9.cyan(projectInfo.isTypeScript ? "Yes" : "No")}`);
1237
+ console.log(` \u2022 Has src/ folder: ${pc9.cyan(projectInfo.hasSrcDir ? "Yes" : "No")}`);
1238
+ console.log(` \u2022 Tailwind CSS: ${pc9.cyan(projectInfo.hasTailwind ? "Yes" : "No")}`);
836
1239
  console.log("");
837
- console.log(pc5.bold(pc5.white("Authentication:")));
1240
+ console.log(pc9.bold(pc9.white("Authentication:")));
838
1241
  const token = getAuthToken();
839
1242
  if (token) {
840
1243
  const maskedKey = token.slice(0, 6) + "\u2026" + token.slice(-4);
841
- console.log(` \u2022 Status: ${pc5.green("Authenticated")}`);
842
- console.log(` \u2022 Key: ${pc5.cyan(maskedKey)}`);
1244
+ console.log(` \u2022 Status: ${pc9.green("Authenticated")}`);
1245
+ console.log(` \u2022 Key: ${pc9.cyan(maskedKey)}`);
843
1246
  } else {
844
- console.log(` \u2022 Status: ${pc5.yellow("Anonymous (free tier)")}`);
845
- console.log(` ${pc5.dim(" Run")} ${pc5.cyan("vv login <key>")} ${pc5.dim("to unlock pro components")}`);
1247
+ console.log(` \u2022 Status: ${pc9.yellow("Anonymous (free tier)")}`);
1248
+ console.log(` ${pc9.dim(" Run")} ${pc9.cyan("vv login <key>")} ${pc9.dim("to unlock pro components")}`);
846
1249
  }
847
1250
  console.log("");
848
- console.log(pc5.bold(pc5.white("Configuration (vv.config.json):")));
1251
+ console.log(pc9.bold(pc9.white("Configuration (vv.config.json):")));
849
1252
  if (config) {
850
- console.log(` \u2022 Framework Set: ${pc5.cyan(config.framework)}`);
851
- console.log(` \u2022 TypeScript Set: ${pc5.cyan(config.tsx ? "Yes" : "No")}`);
852
- console.log(` \u2022 Component Alias: ${pc5.cyan(config.aliases.vv)}`);
1253
+ console.log(` \u2022 Framework Set: ${pc9.cyan(config.framework)}`);
1254
+ console.log(` \u2022 TypeScript Set: ${pc9.cyan(config.tsx ? "Yes" : "No")}`);
1255
+ console.log(` \u2022 Component Alias: ${pc9.cyan(config.aliases.vv)}`);
853
1256
  } else {
854
- console.log(` ${pc5.yellow("\u26A0\uFE0F No vv.config.json found in this directory.")}`);
1257
+ console.log(` ${pc9.yellow("\u26A0\uFE0F No vv.config.json found in this directory.")}`);
855
1258
  }
856
1259
  console.log("");
857
- console.log(pc5.bold(pc5.white("Installed Components (vv-manifest.json):")));
858
- const manifestPath = path6.join(process.cwd(), "vv-manifest.json");
859
- if (fs6.existsSync(manifestPath)) {
1260
+ console.log(pc9.bold(pc9.white("Installed Components (vv-manifest.json):")));
1261
+ const manifestPath = path9.join(process.cwd(), "vv-manifest.json");
1262
+ if (fs9.existsSync(manifestPath)) {
860
1263
  try {
861
- const manifest = JSON.parse(fs6.readFileSync(manifestPath, "utf-8"));
1264
+ const manifest = JSON.parse(fs9.readFileSync(manifestPath, "utf-8"));
862
1265
  const components = manifest.components || {};
863
1266
  const slugs = Object.keys(components);
864
1267
  if (slugs.length === 0) {
865
- console.log(` ${pc5.dim("No components installed yet.")}`);
1268
+ console.log(` ${pc9.dim("No components installed yet.")}`);
866
1269
  } else {
867
- console.log(pc5.dim(" ------------------------------------------------------------"));
868
- console.log(` ${pc5.bold(pc5.white("Component".padEnd(25)))} ${pc5.bold(pc5.white("Version".padEnd(10)))} ${pc5.bold(pc5.white("Installed At"))}`);
869
- console.log(pc5.dim(" ------------------------------------------------------------"));
1270
+ console.log(pc9.dim(" ------------------------------------------------------------"));
1271
+ console.log(` ${pc9.bold(pc9.white("Component".padEnd(25)))} ${pc9.bold(pc9.white("Version".padEnd(10)))} ${pc9.bold(pc9.white("Installed At"))}`);
1272
+ console.log(pc9.dim(" ------------------------------------------------------------"));
870
1273
  for (const slug of slugs) {
871
1274
  const info = components[slug];
872
1275
  const version2 = info?.version ?? "unknown";
873
1276
  const installedAt = info?.installedAt ?? "unknown";
874
- console.log(` ${pc5.cyan(slug.padEnd(25))} ${pc5.green(version2.padEnd(10))} ${pc5.gray(installedAt)}`);
1277
+ console.log(` ${pc9.cyan(slug.padEnd(25))} ${pc9.green(version2.padEnd(10))} ${pc9.gray(installedAt)}`);
875
1278
  }
876
- console.log(pc5.dim(" ------------------------------------------------------------"));
1279
+ console.log(pc9.dim(" ------------------------------------------------------------"));
877
1280
  }
878
1281
  } catch {
879
- console.log(` ${pc5.red("\u274C Error reading vv-manifest.json: Invalid JSON format.")}`);
1282
+ console.log(` ${pc9.red("\u274C Error reading vv-manifest.json: Invalid JSON format.")}`);
880
1283
  }
881
1284
  } else {
882
- console.log(` ${pc5.dim("No components installed yet (vv-manifest.json not found).")}`);
1285
+ console.log(` ${pc9.dim("No components installed yet (vv-manifest.json not found).")}`);
883
1286
  }
884
1287
  console.log("");
885
1288
  }
886
1289
 
887
1290
  // src/commands/diff.ts
888
- import fs7 from "fs";
889
- import path7 from "path";
890
- import pc6 from "picocolors";
891
- import ora3 from "ora";
1291
+ import fs10 from "fs";
1292
+ import path10 from "path";
1293
+ import pc10 from "picocolors";
1294
+ import ora4 from "ora";
892
1295
  async function diffCommand(slug) {
893
1296
  const projectRoot = process.cwd();
894
- const manifestPath = path7.join(projectRoot, "vv-manifest.json");
895
- if (!fs7.existsSync(manifestPath)) {
896
- console.log(pc6.yellow(`
1297
+ const manifestPath = path10.join(projectRoot, "vv-manifest.json");
1298
+ if (!fs10.existsSync(manifestPath)) {
1299
+ console.log(pc10.yellow(`
897
1300
  \u26A0\uFE0F No vv-manifest.json found in this directory.`));
898
- console.log(`\u{1F449} Add components first using ${pc6.bold(pc6.cyan("vv add <slug>"))}
1301
+ console.log(`\u{1F449} Add components first using ${pc10.bold(pc10.cyan("vv add <slug>"))}
899
1302
  `);
900
1303
  return;
901
1304
  }
902
1305
  let manifest;
903
1306
  try {
904
- manifest = JSON.parse(fs7.readFileSync(manifestPath, "utf-8"));
1307
+ manifest = JSON.parse(fs10.readFileSync(manifestPath, "utf-8"));
905
1308
  } catch {
906
- console.error(pc6.red(`\u274C Error reading vv-manifest.json: Invalid JSON format.`));
1309
+ console.error(pc10.red(`\u274C Error reading vv-manifest.json: Invalid JSON format.`));
907
1310
  process.exit(1);
908
1311
  return;
909
1312
  }
910
1313
  const installedComponents = manifest.components || {};
911
1314
  const installedSlugs = Object.keys(installedComponents);
912
1315
  if (installedSlugs.length === 0) {
913
- console.log(pc6.yellow(`
1316
+ console.log(pc10.yellow(`
914
1317
  \u26A0\uFE0F No components are registered as installed in vv-manifest.json.`));
915
- console.log(`\u{1F449} Add components using ${pc6.bold(pc6.cyan("vv add <slug>"))}
1318
+ console.log(`\u{1F449} Add components using ${pc10.bold(pc10.cyan("vv add <slug>"))}
916
1319
  `);
917
1320
  return;
918
1321
  }
@@ -921,21 +1324,21 @@ async function diffCommand(slug) {
921
1324
  validateSlug(slug);
922
1325
  } catch (error) {
923
1326
  const message = error instanceof Error ? error.message : String(error);
924
- console.error(pc6.red(`
1327
+ console.error(pc10.red(`
925
1328
  \u274C ${message}
926
1329
  `));
927
1330
  process.exit(1);
928
1331
  }
929
1332
  if (!installedComponents[slug]) {
930
- console.log(pc6.red(`
1333
+ console.log(pc10.red(`
931
1334
  \u274C Component "${slug}" is not installed in this project.`));
932
- console.log(`\u{1F449} Run ${pc6.cyan(`vv add ${slug}`)} to install it.
1335
+ console.log(`\u{1F449} Run ${pc10.cyan(`vv add ${slug}`)} to install it.
933
1336
  `);
934
1337
  return;
935
1338
  }
936
1339
  const localInfo = installedComponents[slug];
937
- const spinner = ora3({
938
- text: `Checking updates for ${pc6.cyan(slug)}...`,
1340
+ const spinner = ora4({
1341
+ text: `Checking updates for ${pc10.cyan(slug)}...`,
939
1342
  color: "cyan"
940
1343
  }).start();
941
1344
  try {
@@ -943,39 +1346,39 @@ async function diffCommand(slug) {
943
1346
  spinner.stop();
944
1347
  const localVersion = localInfo.version ?? "unknown";
945
1348
  const remoteVersion = remoteComponent.version;
946
- console.log(pc6.bold(pc6.cyan(`
1349
+ console.log(pc10.bold(pc10.cyan(`
947
1350
  Component Diff: ${slug}`)));
948
- console.log(` \u2022 Local version: ${pc6.yellow(localVersion)}`);
949
- console.log(` \u2022 Latest version: ${pc6.green(remoteVersion)}`);
1351
+ console.log(` \u2022 Local version: ${pc10.yellow(localVersion)}`);
1352
+ console.log(` \u2022 Latest version: ${pc10.green(remoteVersion)}`);
950
1353
  if (localVersion === remoteVersion) {
951
- console.log(` \u2022 Status: ${pc6.green("\u2714 Up to date")}
1354
+ console.log(` \u2022 Status: ${pc10.green("\u2714 Up to date")}
952
1355
  `);
953
1356
  } else {
954
- console.log(` \u2022 Status: ${pc6.bold(pc6.yellow("\u26A0\uFE0F Update available!"))}`);
955
- console.log(` \u2022 Run ${pc6.bold(pc6.cyan(`vv add ${slug} --overwrite`))} to update.
1357
+ console.log(` \u2022 Status: ${pc10.bold(pc10.yellow("\u26A0\uFE0F Update available!"))}`);
1358
+ console.log(` \u2022 Run ${pc10.bold(pc10.cyan(`vv add ${slug} --overwrite`))} to update.
956
1359
  `);
957
1360
  }
958
1361
  } catch (error) {
959
1362
  const message = error instanceof Error ? error.message : String(error);
960
- spinner.fail(pc6.red(`Failed to fetch remote component details: ${message}`));
1363
+ spinner.fail(pc10.red(`Failed to fetch remote component details: ${message}`));
961
1364
  process.exit(1);
962
1365
  }
963
1366
  } else {
964
- const spinner = ora3({
1367
+ const spinner = ora4({
965
1368
  text: "Comparing local components with registry index...",
966
1369
  color: "cyan"
967
1370
  }).start();
968
1371
  try {
969
1372
  const registryIndex = await fetchRegistryIndex();
970
1373
  spinner.stop();
971
- console.log(pc6.bold(pc6.cyan("\nVector Vesper Component Version Comparison\n")));
972
- console.log(pc6.dim(" ----------------------------------------------------------------------"));
1374
+ console.log(pc10.bold(pc10.cyan("\nVector Vesper Component Version Comparison\n")));
1375
+ console.log(pc10.dim(" ----------------------------------------------------------------------"));
973
1376
  console.log(
974
- ` ${pc6.bold(pc6.white("Component".padEnd(25)))} ${pc6.bold(
975
- pc6.white("Local".padEnd(10))
976
- )} ${pc6.bold(pc6.white("Latest".padEnd(10)))} ${pc6.bold(pc6.white("Status"))}`
1377
+ ` ${pc10.bold(pc10.white("Component".padEnd(25)))} ${pc10.bold(
1378
+ pc10.white("Local".padEnd(10))
1379
+ )} ${pc10.bold(pc10.white("Latest".padEnd(10)))} ${pc10.bold(pc10.white("Status"))}`
977
1380
  );
978
- console.log(pc6.dim(" ----------------------------------------------------------------------"));
1381
+ console.log(pc10.dim(" ----------------------------------------------------------------------"));
979
1382
  let updatesAvailableCount = 0;
980
1383
  for (const instSlug of installedSlugs) {
981
1384
  const localInfo = installedComponents[instSlug];
@@ -983,59 +1386,59 @@ Component Diff: ${slug}`)));
983
1386
  const remoteInfo = registryIndex.components.find((c) => c.slug === instSlug);
984
1387
  if (!remoteInfo) {
985
1388
  console.log(
986
- ` ${pc6.cyan(instSlug.padEnd(25))} ${pc6.yellow(
1389
+ ` ${pc10.cyan(instSlug.padEnd(25))} ${pc10.yellow(
987
1390
  localVersion.padEnd(10)
988
- )} ${pc6.red("unknown".padEnd(10))} ${pc6.red("Not found in registry")}`
1391
+ )} ${pc10.red("unknown".padEnd(10))} ${pc10.red("Not found in registry")}`
989
1392
  );
990
1393
  } else {
991
1394
  const remoteVersion = remoteInfo.version;
992
1395
  if (localVersion === remoteVersion) {
993
1396
  console.log(
994
- ` ${pc6.cyan(instSlug.padEnd(25))} ${pc6.gray(
1397
+ ` ${pc10.cyan(instSlug.padEnd(25))} ${pc10.gray(
995
1398
  localVersion.padEnd(10)
996
- )} ${pc6.gray(remoteVersion.padEnd(10))} ${pc6.green("\u2714 Up to date")}`
1399
+ )} ${pc10.gray(remoteVersion.padEnd(10))} ${pc10.green("\u2714 Up to date")}`
997
1400
  );
998
1401
  } else {
999
1402
  updatesAvailableCount++;
1000
1403
  console.log(
1001
- ` ${pc6.cyan(instSlug.padEnd(25))} ${pc6.yellow(
1404
+ ` ${pc10.cyan(instSlug.padEnd(25))} ${pc10.yellow(
1002
1405
  localVersion.padEnd(10)
1003
- )} ${pc6.green(remoteVersion.padEnd(10))} ${pc6.bold(
1004
- pc6.yellow("\u26A0\uFE0F Update available")
1406
+ )} ${pc10.green(remoteVersion.padEnd(10))} ${pc10.bold(
1407
+ pc10.yellow("\u26A0\uFE0F Update available")
1005
1408
  )}`
1006
1409
  );
1007
1410
  }
1008
1411
  }
1009
1412
  }
1010
- console.log(pc6.dim(" ----------------------------------------------------------------------"));
1413
+ console.log(pc10.dim(" ----------------------------------------------------------------------"));
1011
1414
  if (updatesAvailableCount > 0) {
1012
1415
  console.log(
1013
1416
  `
1014
- \u{1F4E2} ${pc6.bold(pc6.yellow(String(updatesAvailableCount)))} component(s) have updates available.`
1417
+ \u{1F4E2} ${pc10.bold(pc10.yellow(String(updatesAvailableCount)))} component(s) have updates available.`
1015
1418
  );
1016
- console.log(`\u{1F449} Run ${pc6.bold(pc6.cyan("vv add <slug> --overwrite"))} to update a component.
1419
+ console.log(`\u{1F449} Run ${pc10.bold(pc10.cyan("vv add <slug> --overwrite"))} to update a component.
1017
1420
  `);
1018
1421
  } else {
1019
1422
  console.log(`
1020
- ${pc6.green("\u2714")} All components are up to date!
1423
+ ${pc10.green("\u2714")} All components are up to date!
1021
1424
  `);
1022
1425
  }
1023
1426
  } catch (error) {
1024
1427
  const message = error instanceof Error ? error.message : String(error);
1025
- spinner.fail(pc6.red(`Failed to check updates: ${message}`));
1428
+ spinner.fail(pc10.red(`Failed to check updates: ${message}`));
1026
1429
  process.exit(1);
1027
1430
  }
1028
1431
  }
1029
1432
  }
1030
1433
 
1031
1434
  // src/commands/auth.ts
1032
- import pc7 from "picocolors";
1435
+ import pc11 from "picocolors";
1033
1436
  async function loginCommand(key) {
1034
1437
  if (!key || key.trim().length === 0) {
1035
1438
  logger.error("Please provide a license key.");
1036
1439
  console.log(`
1037
- Usage: ${pc7.cyan("vv login <your-license-key>")}`);
1038
- console.log(` \u{1F511} Get a key at ${pc7.cyan("https://vectorvesper.dev/pricing")}
1440
+ Usage: ${pc11.cyan("vv login <your-license-key>")}`);
1441
+ console.log(` \u{1F511} Get a key at ${pc11.cyan("https://vectorvesper.dev/pricing")}
1039
1442
  `);
1040
1443
  process.exit(1);
1041
1444
  }
@@ -1043,7 +1446,7 @@ async function loginCommand(key) {
1043
1446
  if (trimmedKey.length < 8) {
1044
1447
  logger.error("That doesn't look like a valid license key.");
1045
1448
  console.log(`
1046
- \u{1F511} License keys are available at ${pc7.cyan("https://vectorvesper.dev/pricing")}
1449
+ \u{1F511} License keys are available at ${pc11.cyan("https://vectorvesper.dev/pricing")}
1047
1450
  `);
1048
1451
  process.exit(1);
1049
1452
  }
@@ -1060,18 +1463,18 @@ async function loginCommand(key) {
1060
1463
  process.exit(1);
1061
1464
  }
1062
1465
  console.log("");
1063
- console.log(` ${pc7.green(pc7.bold("\u2714"))} License key saved successfully!`);
1064
- console.log(` ${pc7.dim("Stored at:")} ${pc7.cyan(getGlobalConfigPath())}`);
1466
+ console.log(` ${pc11.green(pc11.bold("\u2714"))} License key saved successfully!`);
1467
+ console.log(` ${pc11.dim("Stored at:")} ${pc11.cyan(getGlobalConfigPath())}`);
1065
1468
  console.log("");
1066
- console.log(` ${pc7.dim("Pro components are now available when you run")} ${pc7.cyan("vv add <slug>")}`);
1067
- console.log(` ${pc7.dim("To verify your account:")} ${pc7.cyan("vv whoami")}`);
1469
+ console.log(` ${pc11.dim("Pro components are now available when you run")} ${pc11.cyan("vv add <slug>")}`);
1470
+ console.log(` ${pc11.dim("To verify your account:")} ${pc11.cyan("vv whoami")}`);
1068
1471
  console.log("");
1069
1472
  }
1070
1473
  async function logoutCommand() {
1071
1474
  const config = loadGlobalConfig();
1072
1475
  if (!config.auth?.token) {
1073
1476
  console.log(`
1074
- ${pc7.dim("You're not logged in. Nothing to do.")}
1477
+ ${pc11.dim("You're not logged in. Nothing to do.")}
1075
1478
  `);
1076
1479
  return;
1077
1480
  }
@@ -1084,46 +1487,46 @@ async function logoutCommand() {
1084
1487
  process.exit(1);
1085
1488
  }
1086
1489
  console.log("");
1087
- console.log(` ${pc7.green(pc7.bold("\u2714"))} Logged out successfully.`);
1088
- console.log(` ${pc7.dim("Your license key has been removed from")} ${pc7.cyan(getGlobalConfigPath())}`);
1089
- console.log(` ${pc7.dim("Free components remain available.")}`);
1490
+ console.log(` ${pc11.green(pc11.bold("\u2714"))} Logged out successfully.`);
1491
+ console.log(` ${pc11.dim("Your license key has been removed from")} ${pc11.cyan(getGlobalConfigPath())}`);
1492
+ console.log(` ${pc11.dim("Free components remain available.")}`);
1090
1493
  console.log("");
1091
1494
  }
1092
1495
  async function whoamiCommand() {
1093
1496
  const config = loadGlobalConfig();
1094
1497
  console.log("");
1095
- console.log(pc7.bold(pc7.cyan(" Vector Vesper Account Status\n")));
1498
+ console.log(pc11.bold(pc11.cyan(" Vector Vesper Account Status\n")));
1096
1499
  if (config.auth?.token) {
1097
1500
  const maskedKey = config.auth.token.slice(0, 6) + "\u2026" + config.auth.token.slice(-4);
1098
1501
  const tier = config.auth.tier || "pro";
1099
- console.log(` ${pc7.bold("Status:")} ${pc7.green("Authenticated")}`);
1100
- console.log(` ${pc7.bold("Key:")} ${pc7.cyan(maskedKey)}`);
1101
- console.log(` ${pc7.bold("Tier:")} ${pc7.magenta(tier)}`);
1502
+ console.log(` ${pc11.bold("Status:")} ${pc11.green("Authenticated")}`);
1503
+ console.log(` ${pc11.bold("Key:")} ${pc11.cyan(maskedKey)}`);
1504
+ console.log(` ${pc11.bold("Tier:")} ${pc11.magenta(tier)}`);
1102
1505
  if (config.auth.email) {
1103
- console.log(` ${pc7.bold("Email:")} ${pc7.cyan(config.auth.email)}`);
1506
+ console.log(` ${pc11.bold("Email:")} ${pc11.cyan(config.auth.email)}`);
1104
1507
  }
1105
1508
  if (config.auth.expiresAt) {
1106
1509
  const expiry = new Date(config.auth.expiresAt);
1107
1510
  const now = /* @__PURE__ */ new Date();
1108
1511
  const isExpired = expiry < now;
1109
- console.log(` ${pc7.bold("Expires:")} ${isExpired ? pc7.red("EXPIRED") : pc7.green(expiry.toLocaleDateString())}`);
1512
+ console.log(` ${pc11.bold("Expires:")} ${isExpired ? pc11.red("EXPIRED") : pc11.green(expiry.toLocaleDateString())}`);
1110
1513
  }
1111
1514
  console.log("");
1112
- console.log(` ${pc7.dim("Config:")} ${pc7.cyan(getGlobalConfigPath())}`);
1113
- console.log(` ${pc7.dim("Manage your account at")} ${pc7.cyan("https://vectorvesper.dev/account")}`);
1515
+ console.log(` ${pc11.dim("Config:")} ${pc11.cyan(getGlobalConfigPath())}`);
1516
+ console.log(` ${pc11.dim("Manage your account at")} ${pc11.cyan("https://vectorvesper.dev/account")}`);
1114
1517
  } else {
1115
- console.log(` ${pc7.bold("Status:")} ${pc7.yellow("Anonymous (free tier)")}`);
1116
- console.log(` ${pc7.bold("Access:")} ${pc7.dim("Free components only")}`);
1518
+ console.log(` ${pc11.bold("Status:")} ${pc11.yellow("Anonymous (free tier)")}`);
1519
+ console.log(` ${pc11.bold("Access:")} ${pc11.dim("Free components only")}`);
1117
1520
  console.log("");
1118
- console.log(` ${pc7.dim("To unlock pro components:")}`);
1119
- console.log(` ${pc7.cyan("vv login <your-license-key>")}`);
1120
- console.log(` \u{1F511} ${pc7.dim("Get a key at")} ${pc7.cyan("https://vectorvesper.dev/pricing")}`);
1521
+ console.log(` ${pc11.dim("To unlock pro components:")}`);
1522
+ console.log(` ${pc11.cyan("vv login <your-license-key>")}`);
1523
+ console.log(` \u{1F511} ${pc11.dim("Get a key at")} ${pc11.cyan("https://vectorvesper.dev/pricing")}`);
1121
1524
  }
1122
1525
  console.log("");
1123
1526
  }
1124
1527
 
1125
1528
  // src/index.ts
1126
- var version = true ? "0.1.0" : "0.0.0-dev";
1529
+ var version = true ? "0.2.0" : "0.0.0-dev";
1127
1530
  var program = new Command();
1128
1531
  program.name("vv").description("Vector Vesper CLI \u2014 add visual components to your React project").version(version).option("--verbose", "Show detailed debug output").hook("preAction", (thisCommand) => {
1129
1532
  const opts = thisCommand.opts();
@@ -1137,9 +1540,15 @@ program.command("init").description("Initialize Vector Vesper configuration in y
1137
1540
  program.command("list").description("List all available components in the registry").action(async () => {
1138
1541
  await listCommand();
1139
1542
  });
1140
- program.command("add <slug>").description("Add a component to your project").option("-o, --overwrite", "Overwrite existing files without prompting").option("-y, --yes", "Accept all default prompts").option("-d, --dry-run", "Simulate the installation without writing files").action(async (slug, options) => {
1543
+ program.command("add <slug>").description("Add a component to your project").option("-o, --overwrite", "Overwrite existing files without prompting").option("-y, --yes", "Accept all default prompts").option("-d, --dry-run", "Simulate the installation without writing files").option("--no-install", "Skip auto-installing missing dependencies").action(async (slug, options) => {
1141
1544
  await addCommand(slug, options);
1142
1545
  });
1546
+ program.command("update [slug]").description("Update installed components to the latest registry version").option("-f, --force", "Re-write files even if the version already matches").action(async (slug, options) => {
1547
+ await updateCommand(slug, options);
1548
+ });
1549
+ program.command("remove <slug>").alias("rm").description("Remove an installed component and its files").option("-y, --yes", "Skip the confirmation prompt").action(async (slug, options) => {
1550
+ await removeCommand(slug, options);
1551
+ });
1143
1552
  program.command("info").description("Show project diagnostics and list installed components").action(async () => {
1144
1553
  await infoCommand();
1145
1554
  });
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "vectorvesper",
3
- "version": "0.1.0",
4
- "description": "Add premium WebGL and React visual components to your project via CLI",
3
+ "version": "0.2.0",
4
+ "description": "Add WebGL, React Three Fiber & advanced motion components to your project via CLI — motion, engineered.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "./dist/index.js",
8
8
  "bin": {
9
- "vv": "./dist/index.js"
9
+ "vv": "dist/index.js"
10
10
  },
11
11
  "files": [
12
12
  "dist",
@@ -41,7 +41,9 @@
41
41
  "scripts": {
42
42
  "build": "tsup",
43
43
  "dev": "tsup --watch",
44
- "typecheck": "tsc --noEmit"
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest"
45
47
  },
46
48
  "dependencies": {
47
49
  "@clack/prompts": "^0.7.0",
@@ -53,6 +55,7 @@
53
55
  "devDependencies": {
54
56
  "@types/node": "^25.9.2",
55
57
  "tsup": "^8.0.2",
56
- "typescript": "^6.0.3"
58
+ "typescript": "^6.0.3",
59
+ "vitest": "^2.1.8"
57
60
  }
58
61
  }