hatchkit 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -418,6 +418,13 @@ function flagValue(name) {
418
418
  }
419
419
  return undefined;
420
420
  }
421
+ function provisionSurfaceModeFromManifest(manifest) {
422
+ if (manifest?.surfaces === "server-only")
423
+ return "server-only";
424
+ if (manifest?.surfaces === "client-only")
425
+ return "client-only";
426
+ return "shared";
427
+ }
421
428
  /** Resolve the GitHub `owner/repo` slug from the cwd's git origin.
422
429
  * Returns undefined when no remote is set or it's not a GitHub URL. */
423
430
  async function detectRepoSlug() {
@@ -537,7 +544,6 @@ function servicesImpossibleForProject(manifest) {
537
544
  return blocked;
538
545
  if (!manifest.domain) {
539
546
  blocked.add("email");
540
- blocked.add("search-console");
541
547
  }
542
548
  if (manifest.surfaces === "server-only")
543
549
  blocked.add("plausible");
@@ -549,6 +555,28 @@ function servicesImpossibleForProject(manifest) {
549
555
  blocked.add("s3");
550
556
  return blocked;
551
557
  }
558
+ function addPositionals(rawArgs) {
559
+ const valueFlags = new Set([
560
+ "--server-dir",
561
+ "--client-dir",
562
+ "--project-dir",
563
+ "--domain",
564
+ "--name",
565
+ "--surfaces",
566
+ ]);
567
+ const positional = [];
568
+ for (let i = 0; i < rawArgs.length; i++) {
569
+ const arg = rawArgs[i];
570
+ if (valueFlags.has(arg)) {
571
+ i += 1;
572
+ continue;
573
+ }
574
+ if (arg.startsWith("--"))
575
+ continue;
576
+ positional.push(arg);
577
+ }
578
+ return positional;
579
+ }
552
580
  function recordProvisionedEvent(ledger, event) {
553
581
  if (event.service === "glitchtip")
554
582
  ledger.record({ kind: "glitchtip", project: event.project });
@@ -631,15 +659,32 @@ async function handleAdd() {
631
659
  .map((s) => s.trim().toLowerCase())
632
660
  .every((s) => allServices.includes(s));
633
661
  };
634
- const positional = args.slice(1).filter((a) => !a.startsWith("--"));
635
- const inferredProjectDir = inferProjectDir(process.cwd());
636
- const inferredManifest = inferredProjectDir ? readManifest(inferredProjectDir) : null;
662
+ const nameFlag = flagValue("--name");
663
+ const domainFlag = flagValue("--domain");
664
+ const serverDirFlag = flagValue("--server-dir");
665
+ const clientDirFlag = flagValue("--client-dir");
666
+ const projectDirFlag = flagValue("--project-dir");
667
+ const surfaceFlag = flagValue("--surfaces");
668
+ const projectDirFromFlag = projectDirFlag ? resolve(projectDirFlag) : undefined;
669
+ if (projectDirFromFlag && !existsSync(projectDirFromFlag)) {
670
+ console.log(chalk.red(` --project-dir does not exist: ${projectDirFromFlag}`));
671
+ process.exit(1);
672
+ }
673
+ const positional = addPositionals(args.slice(1));
637
674
  const firstArgIsService = isServiceExpr(positional[0]);
638
- let baseName = firstArgIsService
639
- ? inferredManifest?.name
640
- : (positional[0] ?? inferredManifest?.name);
675
+ const positionalProjectName = firstArgIsService ? undefined : positional[0];
676
+ let inferredProjectDir = projectDirFromFlag ??
677
+ (positionalProjectName && existsSync(resolve(positionalProjectName))
678
+ ? resolve(positionalProjectName)
679
+ : inferProjectDir(process.cwd()));
680
+ let inferredManifest = inferredProjectDir ? readManifest(inferredProjectDir) : null;
681
+ let baseName = positionalProjectName ?? nameFlag ?? inferredManifest?.name;
641
682
  const rawService = firstArgIsService ? positional[0] : positional[1];
642
683
  if (!baseName) {
684
+ if (!process.stdin.isTTY) {
685
+ console.log(chalk.red(" Project name is required. Pass <project-name> or --name <name>."));
686
+ process.exit(1);
687
+ }
643
688
  const { input } = await import("@inquirer/prompts");
644
689
  const { validateProjectName } = await import("./utils/validate.js");
645
690
  baseName = await input({
@@ -647,14 +692,21 @@ async function handleAdd() {
647
692
  validate: validateProjectName,
648
693
  });
649
694
  }
695
+ if (!inferredProjectDir) {
696
+ const namedProjectDir = resolve(baseName);
697
+ if (existsSync(namedProjectDir)) {
698
+ inferredProjectDir = namedProjectDir;
699
+ inferredManifest = readManifest(namedProjectDir);
700
+ }
701
+ }
650
702
  const alreadyAdded = servicesAlreadyAdded({
651
703
  baseName,
652
704
  projectDir: inferredProjectDir,
653
705
  manifest: inferredManifest,
654
706
  });
655
707
  const impossible = servicesImpossibleForProject(inferredManifest);
656
- const hiddenServices = new Set([...alreadyAdded, ...impossible]);
657
- const addableServices = allServices.filter((service) => !hiddenServices.has(service));
708
+ const rerunnableServices = new Set(["search-console"]);
709
+ const addableServices = allServices.filter((service) => !alreadyAdded.has(service) && !impossible.has(service));
658
710
  let services;
659
711
  if (!rawService) {
660
712
  if (addableServices.length === 0) {
@@ -704,13 +756,19 @@ async function handleAdd() {
704
756
  console.log(chalk.dim(` Valid: ${allServices.join(", ")}, or 'all'`));
705
757
  process.exit(1);
706
758
  }
707
- const skipped = requested.filter((service) => hiddenServices.has(service));
759
+ const skipped = requested.filter((service) => {
760
+ const svc = service;
761
+ return impossible.has(svc) || (alreadyAdded.has(svc) && !rerunnableServices.has(svc));
762
+ });
708
763
  if (skipped.length > 0) {
709
764
  console.log(chalk.red(` Refusing to add already-present/unavailable service(s): ${skipped.join(", ")}`));
710
765
  console.log(chalk.dim(" Run `hatchkit remove` first if you want Hatchkit to recreate them."));
711
766
  process.exit(1);
712
767
  }
713
- services = requested.filter((service) => !hiddenServices.has(service));
768
+ services = requested.filter((service) => {
769
+ const svc = service;
770
+ return !impossible.has(svc) && (!alreadyAdded.has(svc) || rerunnableServices.has(svc));
771
+ });
714
772
  if (services.length === 0) {
715
773
  console.log(chalk.green(` Nothing to add — requested service(s) are already present or unavailable.`));
716
774
  return;
@@ -722,22 +780,29 @@ async function handleAdd() {
722
780
  // --surfaces=<shared|separate|server-only|client-only>
723
781
  // --server-dir <path> → absolute or project-relative env dir for the server
724
782
  // --client-dir <path> → same for the client
783
+ // --domain <domain> → site/domain-scoped services (Plausible/Search Console)
784
+ // --name <name> → project name when no positional/manifest name exists
725
785
  // (no surface flags) → prompt interactively
726
786
  const noWrite = args.includes("--no-write");
727
787
  const enableDevObs = args.includes("--enable-dev-obs");
728
- const surfaceFlag = args.find((a) => a.startsWith("--surfaces="))?.slice("--surfaces=".length);
729
- const serverDirIdx = args.indexOf("--server-dir");
730
- const clientDirIdx = args.indexOf("--client-dir");
731
- const projectDirIdx = args.indexOf("--project-dir");
732
- const serverDirFlag = serverDirIdx >= 0 ? args[serverDirIdx + 1] : undefined;
733
- const clientDirFlag = clientDirIdx >= 0 ? args[clientDirIdx + 1] : undefined;
734
- const projectDirFlag = projectDirIdx >= 0 ? args[projectDirIdx + 1] : undefined;
735
- const { resolve: resolvePath } = await import("node:path");
736
788
  const validSurfaceModes = ["shared", "separate", "server-only", "client-only"];
789
+ const noEnvServices = new Set(["email", "search-console"]);
790
+ const onlyNoEnvServices = services.every((service) => noEnvServices.has(service));
737
791
  let surfaces = undefined;
738
792
  if (noWrite) {
739
793
  surfaces = false;
740
794
  }
795
+ else if (onlyNoEnvServices) {
796
+ const projectDir = inferredProjectDir;
797
+ if (!projectDir) {
798
+ console.log(chalk.red(" A project directory is required for no-env services. Pass --project-dir <path>."));
799
+ process.exit(1);
800
+ }
801
+ surfaces = {
802
+ mode: provisionSurfaceModeFromManifest(inferredManifest),
803
+ projectDir,
804
+ };
805
+ }
741
806
  else if (surfaceFlag || serverDirFlag || clientDirFlag || projectDirFlag) {
742
807
  // Non-interactive surface config: require every field we need.
743
808
  if (!surfaceFlag || !validSurfaceModes.includes(surfaceFlag)) {
@@ -757,16 +822,16 @@ async function handleAdd() {
757
822
  }
758
823
  surfaces = {
759
824
  mode,
760
- serverEnvDir: needsServer ? resolvePath(serverDirFlag) : undefined,
761
- clientEnvDir: needsClient ? resolvePath(clientDirFlag) : undefined,
762
- // --project-dir is optional in the flag path. When provided, it
763
- // points at the directory holding `.hatchkit.json` (needed for
764
- // the `s3` service to read s3Buckets). When absent and the
765
- // serverEnvDir is a `packages/server` style subdir, we infer it
766
- // by walking up two segments.
825
+ serverEnvDir: needsServer ? resolve(serverDirFlag) : undefined,
826
+ clientEnvDir: needsClient ? resolve(clientDirFlag) : undefined,
827
+ // --project-dir is optional in the flag path. It points at the
828
+ // project root used for manifest/package/CNAME inference (and for
829
+ // s3Buckets). When absent and the serverEnvDir is a
830
+ // `packages/server` style subdir, we infer it by walking up two
831
+ // segments.
767
832
  projectDir: projectDirFlag
768
- ? resolvePath(projectDirFlag)
769
- : inferProjectDir(needsServer ? resolvePath(serverDirFlag) : undefined),
833
+ ? resolve(projectDirFlag)
834
+ : inferProjectDir(needsServer ? resolve(serverDirFlag) : undefined),
770
835
  };
771
836
  }
772
837
  const ledger = RunLedger.resumeOrStart(baseName);
@@ -775,6 +840,7 @@ async function handleAdd() {
775
840
  services,
776
841
  surfaces,
777
842
  enableDevObs,
843
+ domain: domainFlag,
778
844
  failIfExists: true,
779
845
  onProvisioned: (event) => recordProvisionedEvent(ledger, event),
780
846
  });
@@ -2354,6 +2420,7 @@ function printHelp(topic) {
2354
2420
 
2355
2421
  ${chalk.bold("Usage:")}
2356
2422
  hatchkit add [<project-name>] [<services>] [flags]
2423
+ hatchkit add [<services>] --name <project-name> [flags]
2357
2424
  hatchkit add [<services>] [flags] ${chalk.dim("(inside a project with .hatchkit.json)")}
2358
2425
 
2359
2426
  ${chalk.bold("What it does:")}
@@ -2407,14 +2474,21 @@ function printHelp(topic) {
2407
2474
  --surfaces=<mode> shared | server-only | client-only | separate
2408
2475
  --server-dir <path> Server env directory (skips prompt when set).
2409
2476
  --client-dir <path> Client env directory (skips prompt when set).
2410
- --project-dir <path> Project root holding .hatchkit.json (s3 only;
2477
+ --project-dir <path> Project root for manifest/package/CNAME inference
2478
+ (needed for s3 and non-Hatchkit Search Console onboarding;
2411
2479
  inferred from --server-dir if omitted).
2480
+ --name <name> Project name when no positional or manifest name exists.
2481
+ --domain <domain> Site/domain-scoped services (Plausible, Search Console).
2412
2482
 
2413
2483
  ${chalk.bold("Examples:")}
2414
2484
  hatchkit add
2415
2485
  hatchkit add search-console
2416
2486
  hatchkit add raptor-runner
2417
2487
  hatchkit add raptor-runner all --enable-dev-obs
2488
+ hatchkit add search-console --name asteroids --domain asteroids.trebeljahr.com \\
2489
+ --project-dir /Users/rico/projects/asteroid-game
2490
+ hatchkit add my-app search-console --domain app.example.com --project-dir ./my-app
2491
+ hatchkit add fractal-garden search-console --domain fractal.garden
2418
2492
  hatchkit add raptor-runner glitchtip,resend --no-write
2419
2493
  hatchkit add raptor-runner all --surfaces=shared \\
2420
2494
  --server-dir ./raptor-runner/packages/server \\