@lunora/cli 1.0.0-alpha.8 → 1.0.0-alpha.9

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/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './packem_shared/COMMANDS-Bn8luojF.mjs';
2
+ import { runCli } from './packem_shared/COMMANDS-Dh0bSERi.mjs';
3
3
 
4
4
  try {
5
5
  const code = await runCli();
package/dist/index.d.mts CHANGED
@@ -508,16 +508,123 @@ type PackageManagerProbe = (manager: PackageManager) => boolean;
508
508
  /** A registry item a feature can install. */
509
509
  type FeatureItem = "auth" | "auth-auth0" | "auth-clerk" | "mail";
510
510
  /** The auth-provider choices offered for `add auth` / the init auth prompt. Each value is a registry item name. */
511
-
511
+ /** A single file the item scaffolds into the project. */
512
+ interface RegistryFile {
513
+ /** Source path inside the item dir (e.g. `schema.ts`). */
514
+ from: string;
515
+ /** Merge strategy. `create-or-skip` writes whole files; `schema-extension` AST-merges schema.ts. */
516
+ merge: "create-or-skip" | "schema-extension";
517
+ /** Destination relative to the project root (e.g. `lunora/ratelimit/index.ts`). */
518
+ to: string;
519
+ }
520
+ /** A wrangler.jsonc binding addition. `path` is the jsonc key path; `value` the value to set. */
521
+ interface RegistryBinding {
522
+ path: ReadonlyArray<string>;
523
+ value: unknown;
524
+ }
525
+ /**
526
+ * An environment variable an item needs. Scaffolded into `.dev.vars` (Workers'
527
+ * local-secrets file) on add — non-secrets get their `value`; secrets get an
528
+ * empty placeholder and a reminder to run `wrangler secret put` for production.
529
+ */
530
+ interface RegistryEnvVariable {
531
+ /** Human note on what the variable is for. */
532
+ description?: string;
533
+ /** The variable name (e.g. `RESEND_API_KEY`). */
534
+ name: string;
535
+ /** Mark as a secret: never write a value, only a placeholder, and remind about prod. Defaults to `true` when no `value` is given. */
536
+ secret?: boolean;
537
+ /** A default/example value for non-secret vars. */
538
+ value?: string;
539
+ }
540
+ /** The `registry.json` manifest shape. */
541
+ interface RegistryManifest {
542
+ /** wrangler.jsonc additions (best-effort structural edits). */
543
+ bindings?: ReadonlyArray<RegistryBinding>;
544
+ /** npm deps to add to the project package.json (name → version range). */
545
+ deps?: Readonly<Record<string, string>>;
546
+ description?: string;
547
+ /** npm devDependencies to add to the project package.json. */
548
+ devDependencies?: Readonly<Record<string, string>>;
549
+ /** Post-install guidance printed after the item is added (per-item next steps). */
550
+ docs?: string;
551
+ /** Environment variables the item needs; scaffolded into `.dev.vars`. */
552
+ envVars?: ReadonlyArray<RegistryEnvVariable>;
553
+ files: ReadonlyArray<RegistryFile>;
554
+ name: string;
555
+ /** Other registry items this one depends on (resolved transitively, deps first). */
556
+ requires?: ReadonlyArray<string>;
557
+ /** Short human-readable label (distinct from the longer `description`). */
558
+ title?: string;
559
+ }
560
+ interface AddCommandOptions {
561
+ /** Bypass the `--source` safety gate (matches init). */
562
+ allowUnsafeSource?: boolean;
563
+ /** `registry build --check`: verify the index is current instead of rewriting it. */
564
+ check?: boolean;
565
+ /** Inject a confirmer for non-interactive callers / tests. */
566
+ confirm?: (prompt: string) => Promise<boolean>;
567
+ cwd?: string;
568
+ /** Preview the file-level changes (a content diff) and write nothing. */
569
+ diff?: boolean;
570
+ /** Print the plan and stop without writing anything. */
571
+ dryRun?: boolean;
572
+ /** Local registry root (offline / tests). Expects per-item subdirs, each with a `registry.json`. */
573
+ from?: string;
574
+ /** Emit a JSON snapshot of the plan/result. */
575
+ json?: boolean;
576
+ /** `--list`: enumerate available items instead of adding. */
577
+ list?: boolean;
578
+ logger: Logger;
579
+ /** Item names to add (positional args). */
580
+ names: ReadonlyArray<string>;
581
+ /** `registry build` output path for the generated catalog (defaults to the root's `index.json`). */
582
+ out?: string;
583
+ /** Force-overwrite existing files (take the incoming copy) instead of skipping/conflicting. */
584
+ overwrite?: boolean;
585
+ /** Override the git ref (branch, tag, or commit) items are fetched from (default: version-derived); appended to the `source` base when that is set. Ignored when `from` is set. */
586
+ ref?: string;
587
+ /** Override the remote registry source base (default gh:anolilab/lunora/registry). */
588
+ source?: string;
589
+ /**
590
+ * Customize each resolved manifest after it is loaded but before the plan is
591
+ * printed / reconciled — used to inject user-chosen values into otherwise
592
+ * static manifests (e.g. the R2 `bucket_name` the init storage prompt asks
593
+ * for). Applied to every item; return the manifest unchanged to leave it as-is.
594
+ */
595
+ transformManifest?: (manifest: RegistryManifest) => RegistryManifest;
596
+ /** Skip the package.json mutation confirmation prompt. */
597
+ yes?: boolean;
598
+ }
599
+ interface AddCommandResult {
600
+ /** Bindings written to wrangler.jsonc. */
601
+ bindings: ReadonlyArray<string>;
602
+ code: number;
603
+ /** Deps added to package.json. */
604
+ deps: ReadonlyArray<string>;
605
+ /** Files skipped because they already existed. */
606
+ skipped: ReadonlyArray<string>;
607
+ /** Files written (absolute paths). */
608
+ written: ReadonlyArray<string>;
609
+ }
610
+ /** One resolved item: its parsed manifest plus the (possibly staged) directory it lives in. */
512
611
  /**
513
612
  * A feature offered in the post-scaffold multi-select. `auth`/`email` carry a
514
613
  * sub-prompt or alias; every other value IS the registry item name applied
515
614
  * directly (`storage` → the `storage` registry item, etc.).
516
615
  */
517
616
  type StackFeature = "auth" | "backup" | "crons" | "email" | "presence" | "ratelimit" | "storage";
617
+ /** Customize a resolved manifest before it is written (e.g. inject the chosen R2 bucket name). */
618
+ type OfferTransformManifest = (manifest: RegistryManifest) => RegistryManifest;
518
619
  interface OfferDeps {
519
- /** Apply one or more registry items into the new project; resolves `true` on success. */
520
- apply: (names: ReadonlyArray<string>) => Promise<boolean>;
620
+ /**
621
+ * Apply one or more registry items into the new project; resolves `true` on
622
+ * success. `options.transformManifest` customizes each item's manifest before
623
+ * it is written (used to inject the user-chosen R2 bucket name for storage).
624
+ */
625
+ apply: (names: ReadonlyArray<string>, options?: {
626
+ transformManifest?: OfferTransformManifest;
627
+ }) => Promise<boolean>;
521
628
  /** When `false`, skip all prompts and print the later-setup hint. */
522
629
  interactive: boolean;
523
630
  logger: Logger;
@@ -529,6 +636,8 @@ interface OfferDeps {
529
636
  }>, settings?: {
530
637
  defaults?: ReadonlyArray<StackFeature>;
531
638
  }) => Promise<StackFeature[]>;
639
+ /** The new project's name — seeds smart defaults like the `project-uploads` bucket name. */
640
+ projectName: string;
532
641
  /** Single-select among the auth providers (TTY-backed in production). */
533
642
  select: (message: string, options: ReadonlyArray<{
534
643
  description?: string;
@@ -537,14 +646,19 @@ interface OfferDeps {
537
646
  }>, settings?: {
538
647
  default?: FeatureItem;
539
648
  }) => Promise<FeatureItem | undefined>;
649
+ /** Single-line text input (TTY-backed in production) — used for the storage bucket-name prompt. */
650
+ text: (message: string, settings?: {
651
+ default?: string;
652
+ placeholder?: string;
653
+ }) => Promise<string>;
540
654
  }
541
655
  /**
542
656
  * Offer the stack features (auth, email, storage, rate limiting, crons,
543
- * presence, backups) in ONE multi-select after a successful scaffold. When auth
544
- * is picked, a follow-up single-select chooses the provider (email+password /
545
- * Clerk / Auth0); email maps to the `mail` item; every other feature value is
546
- * applied as its registry item directly. Picked items are applied in selection
547
- * order. Non-interactive: prints how to add them later and changes nothing.
657
+ * presence, backups) in ONE multi-select after a successful scaffold. Auth,
658
+ * email, and storage run a follow-up prompt (provider / destination / bucket
659
+ * name); every other feature value is applied as its registry item directly.
660
+ * Picked items are applied in selection order. Non-interactive: prints how to
661
+ * add them later and changes nothing.
548
662
  */
549
663
  type Template = "analog" | "astro" | "next" | "nuxt" | "react-router" | "standalone" | "sveltekit" | "tanstack-start-react" | "tanstack-start-solid";
550
664
  interface InitCommandOptions {
@@ -599,10 +713,10 @@ interface InitCommandOptions {
599
713
  packageManagerProbe?: PackageManagerProbe;
600
714
  /**
601
715
  * Inject the offer's prompts (tests). When set, the offer is treated as
602
- * interactive regardless of TTY, and these drive the feature multi-select
603
- * and the auth-provider sub-select.
716
+ * interactive regardless of TTY, and these drive the feature multi-select,
717
+ * the auth-provider sub-select, and the storage bucket-name text input.
604
718
  */
605
- prompt?: Pick<OfferDeps, "multiSelect" | "select">;
719
+ prompt?: Pick<OfferDeps, "multiSelect" | "select" | "text">;
606
720
  /**
607
721
  * Override the git ref (branch, tag, or commit) the default template source
608
722
  * is fetched from. Takes precedence over the version-derived ref. Ignored
@@ -681,99 +795,6 @@ interface IndexItem extends CatalogItem {
681
795
  declare const buildRegistryIndex: (root: string) => {
682
796
  items: IndexItem[];
683
797
  };
684
- /** A single file the item scaffolds into the project. */
685
- interface RegistryFile {
686
- /** Source path inside the item dir (e.g. `schema.ts`). */
687
- from: string;
688
- /** Merge strategy. `create-or-skip` writes whole files; `schema-extension` AST-merges schema.ts. */
689
- merge: "create-or-skip" | "schema-extension";
690
- /** Destination relative to the project root (e.g. `lunora/ratelimit/index.ts`). */
691
- to: string;
692
- }
693
- /** A wrangler.jsonc binding addition. `path` is the jsonc key path; `value` the value to set. */
694
- interface RegistryBinding {
695
- path: ReadonlyArray<string>;
696
- value: unknown;
697
- }
698
- /**
699
- * An environment variable an item needs. Scaffolded into `.dev.vars` (Workers'
700
- * local-secrets file) on add — non-secrets get their `value`; secrets get an
701
- * empty placeholder and a reminder to run `wrangler secret put` for production.
702
- */
703
- interface RegistryEnvVariable {
704
- /** Human note on what the variable is for. */
705
- description?: string;
706
- /** The variable name (e.g. `RESEND_API_KEY`). */
707
- name: string;
708
- /** Mark as a secret: never write a value, only a placeholder, and remind about prod. Defaults to `true` when no `value` is given. */
709
- secret?: boolean;
710
- /** A default/example value for non-secret vars. */
711
- value?: string;
712
- }
713
- /** The `registry.json` manifest shape. */
714
- interface RegistryManifest {
715
- /** wrangler.jsonc additions (best-effort structural edits). */
716
- bindings?: ReadonlyArray<RegistryBinding>;
717
- /** npm deps to add to the project package.json (name → version range). */
718
- deps?: Readonly<Record<string, string>>;
719
- description?: string;
720
- /** npm devDependencies to add to the project package.json. */
721
- devDependencies?: Readonly<Record<string, string>>;
722
- /** Post-install guidance printed after the item is added (per-item next steps). */
723
- docs?: string;
724
- /** Environment variables the item needs; scaffolded into `.dev.vars`. */
725
- envVars?: ReadonlyArray<RegistryEnvVariable>;
726
- files: ReadonlyArray<RegistryFile>;
727
- name: string;
728
- /** Other registry items this one depends on (resolved transitively, deps first). */
729
- requires?: ReadonlyArray<string>;
730
- /** Short human-readable label (distinct from the longer `description`). */
731
- title?: string;
732
- }
733
- interface AddCommandOptions {
734
- /** Bypass the `--source` safety gate (matches init). */
735
- allowUnsafeSource?: boolean;
736
- /** `registry build --check`: verify the index is current instead of rewriting it. */
737
- check?: boolean;
738
- /** Inject a confirmer for non-interactive callers / tests. */
739
- confirm?: (prompt: string) => Promise<boolean>;
740
- cwd?: string;
741
- /** Preview the file-level changes (a content diff) and write nothing. */
742
- diff?: boolean;
743
- /** Print the plan and stop without writing anything. */
744
- dryRun?: boolean;
745
- /** Local registry root (offline / tests). Expects per-item subdirs, each with a `registry.json`. */
746
- from?: string;
747
- /** Emit a JSON snapshot of the plan/result. */
748
- json?: boolean;
749
- /** `--list`: enumerate available items instead of adding. */
750
- list?: boolean;
751
- logger: Logger;
752
- /** Item names to add (positional args). */
753
- names: ReadonlyArray<string>;
754
- /** `registry build` output path for the generated catalog (defaults to the root's `index.json`). */
755
- out?: string;
756
- /** Force-overwrite existing files (take the incoming copy) instead of skipping/conflicting. */
757
- overwrite?: boolean;
758
- /** Override the git ref (branch, tag, or commit) items are fetched from (default: version-derived); appended to the `source` base when that is set. Ignored when `from` is set. */
759
- ref?: string;
760
- /** Override the remote registry source base (default gh:anolilab/lunora/registry). */
761
- source?: string;
762
- /** Skip the package.json mutation confirmation prompt. */
763
- yes?: boolean;
764
- }
765
- interface AddCommandResult {
766
- /** Bindings written to wrangler.jsonc. */
767
- bindings: ReadonlyArray<string>;
768
- code: number;
769
- /** Deps added to package.json. */
770
- deps: ReadonlyArray<string>;
771
- /** Files skipped because they already existed. */
772
- skipped: ReadonlyArray<string>;
773
- /** Files written (absolute paths). */
774
- written: ReadonlyArray<string>;
775
- }
776
- /** One resolved item: its parsed manifest plus the (possibly staged) directory it lives in. */
777
798
  /** `lunora registry add` (one or more item names): scaffold items into the project. */
778
799
  declare const runAddCommand: (options: AddCommandOptions) => Promise<AddCommandResult>;
779
800
  /**
package/dist/index.d.ts CHANGED
@@ -508,16 +508,123 @@ type PackageManagerProbe = (manager: PackageManager) => boolean;
508
508
  /** A registry item a feature can install. */
509
509
  type FeatureItem = "auth" | "auth-auth0" | "auth-clerk" | "mail";
510
510
  /** The auth-provider choices offered for `add auth` / the init auth prompt. Each value is a registry item name. */
511
-
511
+ /** A single file the item scaffolds into the project. */
512
+ interface RegistryFile {
513
+ /** Source path inside the item dir (e.g. `schema.ts`). */
514
+ from: string;
515
+ /** Merge strategy. `create-or-skip` writes whole files; `schema-extension` AST-merges schema.ts. */
516
+ merge: "create-or-skip" | "schema-extension";
517
+ /** Destination relative to the project root (e.g. `lunora/ratelimit/index.ts`). */
518
+ to: string;
519
+ }
520
+ /** A wrangler.jsonc binding addition. `path` is the jsonc key path; `value` the value to set. */
521
+ interface RegistryBinding {
522
+ path: ReadonlyArray<string>;
523
+ value: unknown;
524
+ }
525
+ /**
526
+ * An environment variable an item needs. Scaffolded into `.dev.vars` (Workers'
527
+ * local-secrets file) on add — non-secrets get their `value`; secrets get an
528
+ * empty placeholder and a reminder to run `wrangler secret put` for production.
529
+ */
530
+ interface RegistryEnvVariable {
531
+ /** Human note on what the variable is for. */
532
+ description?: string;
533
+ /** The variable name (e.g. `RESEND_API_KEY`). */
534
+ name: string;
535
+ /** Mark as a secret: never write a value, only a placeholder, and remind about prod. Defaults to `true` when no `value` is given. */
536
+ secret?: boolean;
537
+ /** A default/example value for non-secret vars. */
538
+ value?: string;
539
+ }
540
+ /** The `registry.json` manifest shape. */
541
+ interface RegistryManifest {
542
+ /** wrangler.jsonc additions (best-effort structural edits). */
543
+ bindings?: ReadonlyArray<RegistryBinding>;
544
+ /** npm deps to add to the project package.json (name → version range). */
545
+ deps?: Readonly<Record<string, string>>;
546
+ description?: string;
547
+ /** npm devDependencies to add to the project package.json. */
548
+ devDependencies?: Readonly<Record<string, string>>;
549
+ /** Post-install guidance printed after the item is added (per-item next steps). */
550
+ docs?: string;
551
+ /** Environment variables the item needs; scaffolded into `.dev.vars`. */
552
+ envVars?: ReadonlyArray<RegistryEnvVariable>;
553
+ files: ReadonlyArray<RegistryFile>;
554
+ name: string;
555
+ /** Other registry items this one depends on (resolved transitively, deps first). */
556
+ requires?: ReadonlyArray<string>;
557
+ /** Short human-readable label (distinct from the longer `description`). */
558
+ title?: string;
559
+ }
560
+ interface AddCommandOptions {
561
+ /** Bypass the `--source` safety gate (matches init). */
562
+ allowUnsafeSource?: boolean;
563
+ /** `registry build --check`: verify the index is current instead of rewriting it. */
564
+ check?: boolean;
565
+ /** Inject a confirmer for non-interactive callers / tests. */
566
+ confirm?: (prompt: string) => Promise<boolean>;
567
+ cwd?: string;
568
+ /** Preview the file-level changes (a content diff) and write nothing. */
569
+ diff?: boolean;
570
+ /** Print the plan and stop without writing anything. */
571
+ dryRun?: boolean;
572
+ /** Local registry root (offline / tests). Expects per-item subdirs, each with a `registry.json`. */
573
+ from?: string;
574
+ /** Emit a JSON snapshot of the plan/result. */
575
+ json?: boolean;
576
+ /** `--list`: enumerate available items instead of adding. */
577
+ list?: boolean;
578
+ logger: Logger;
579
+ /** Item names to add (positional args). */
580
+ names: ReadonlyArray<string>;
581
+ /** `registry build` output path for the generated catalog (defaults to the root's `index.json`). */
582
+ out?: string;
583
+ /** Force-overwrite existing files (take the incoming copy) instead of skipping/conflicting. */
584
+ overwrite?: boolean;
585
+ /** Override the git ref (branch, tag, or commit) items are fetched from (default: version-derived); appended to the `source` base when that is set. Ignored when `from` is set. */
586
+ ref?: string;
587
+ /** Override the remote registry source base (default gh:anolilab/lunora/registry). */
588
+ source?: string;
589
+ /**
590
+ * Customize each resolved manifest after it is loaded but before the plan is
591
+ * printed / reconciled — used to inject user-chosen values into otherwise
592
+ * static manifests (e.g. the R2 `bucket_name` the init storage prompt asks
593
+ * for). Applied to every item; return the manifest unchanged to leave it as-is.
594
+ */
595
+ transformManifest?: (manifest: RegistryManifest) => RegistryManifest;
596
+ /** Skip the package.json mutation confirmation prompt. */
597
+ yes?: boolean;
598
+ }
599
+ interface AddCommandResult {
600
+ /** Bindings written to wrangler.jsonc. */
601
+ bindings: ReadonlyArray<string>;
602
+ code: number;
603
+ /** Deps added to package.json. */
604
+ deps: ReadonlyArray<string>;
605
+ /** Files skipped because they already existed. */
606
+ skipped: ReadonlyArray<string>;
607
+ /** Files written (absolute paths). */
608
+ written: ReadonlyArray<string>;
609
+ }
610
+ /** One resolved item: its parsed manifest plus the (possibly staged) directory it lives in. */
512
611
  /**
513
612
  * A feature offered in the post-scaffold multi-select. `auth`/`email` carry a
514
613
  * sub-prompt or alias; every other value IS the registry item name applied
515
614
  * directly (`storage` → the `storage` registry item, etc.).
516
615
  */
517
616
  type StackFeature = "auth" | "backup" | "crons" | "email" | "presence" | "ratelimit" | "storage";
617
+ /** Customize a resolved manifest before it is written (e.g. inject the chosen R2 bucket name). */
618
+ type OfferTransformManifest = (manifest: RegistryManifest) => RegistryManifest;
518
619
  interface OfferDeps {
519
- /** Apply one or more registry items into the new project; resolves `true` on success. */
520
- apply: (names: ReadonlyArray<string>) => Promise<boolean>;
620
+ /**
621
+ * Apply one or more registry items into the new project; resolves `true` on
622
+ * success. `options.transformManifest` customizes each item's manifest before
623
+ * it is written (used to inject the user-chosen R2 bucket name for storage).
624
+ */
625
+ apply: (names: ReadonlyArray<string>, options?: {
626
+ transformManifest?: OfferTransformManifest;
627
+ }) => Promise<boolean>;
521
628
  /** When `false`, skip all prompts and print the later-setup hint. */
522
629
  interactive: boolean;
523
630
  logger: Logger;
@@ -529,6 +636,8 @@ interface OfferDeps {
529
636
  }>, settings?: {
530
637
  defaults?: ReadonlyArray<StackFeature>;
531
638
  }) => Promise<StackFeature[]>;
639
+ /** The new project's name — seeds smart defaults like the `project-uploads` bucket name. */
640
+ projectName: string;
532
641
  /** Single-select among the auth providers (TTY-backed in production). */
533
642
  select: (message: string, options: ReadonlyArray<{
534
643
  description?: string;
@@ -537,14 +646,19 @@ interface OfferDeps {
537
646
  }>, settings?: {
538
647
  default?: FeatureItem;
539
648
  }) => Promise<FeatureItem | undefined>;
649
+ /** Single-line text input (TTY-backed in production) — used for the storage bucket-name prompt. */
650
+ text: (message: string, settings?: {
651
+ default?: string;
652
+ placeholder?: string;
653
+ }) => Promise<string>;
540
654
  }
541
655
  /**
542
656
  * Offer the stack features (auth, email, storage, rate limiting, crons,
543
- * presence, backups) in ONE multi-select after a successful scaffold. When auth
544
- * is picked, a follow-up single-select chooses the provider (email+password /
545
- * Clerk / Auth0); email maps to the `mail` item; every other feature value is
546
- * applied as its registry item directly. Picked items are applied in selection
547
- * order. Non-interactive: prints how to add them later and changes nothing.
657
+ * presence, backups) in ONE multi-select after a successful scaffold. Auth,
658
+ * email, and storage run a follow-up prompt (provider / destination / bucket
659
+ * name); every other feature value is applied as its registry item directly.
660
+ * Picked items are applied in selection order. Non-interactive: prints how to
661
+ * add them later and changes nothing.
548
662
  */
549
663
  type Template = "analog" | "astro" | "next" | "nuxt" | "react-router" | "standalone" | "sveltekit" | "tanstack-start-react" | "tanstack-start-solid";
550
664
  interface InitCommandOptions {
@@ -599,10 +713,10 @@ interface InitCommandOptions {
599
713
  packageManagerProbe?: PackageManagerProbe;
600
714
  /**
601
715
  * Inject the offer's prompts (tests). When set, the offer is treated as
602
- * interactive regardless of TTY, and these drive the feature multi-select
603
- * and the auth-provider sub-select.
716
+ * interactive regardless of TTY, and these drive the feature multi-select,
717
+ * the auth-provider sub-select, and the storage bucket-name text input.
604
718
  */
605
- prompt?: Pick<OfferDeps, "multiSelect" | "select">;
719
+ prompt?: Pick<OfferDeps, "multiSelect" | "select" | "text">;
606
720
  /**
607
721
  * Override the git ref (branch, tag, or commit) the default template source
608
722
  * is fetched from. Takes precedence over the version-derived ref. Ignored
@@ -681,99 +795,6 @@ interface IndexItem extends CatalogItem {
681
795
  declare const buildRegistryIndex: (root: string) => {
682
796
  items: IndexItem[];
683
797
  };
684
- /** A single file the item scaffolds into the project. */
685
- interface RegistryFile {
686
- /** Source path inside the item dir (e.g. `schema.ts`). */
687
- from: string;
688
- /** Merge strategy. `create-or-skip` writes whole files; `schema-extension` AST-merges schema.ts. */
689
- merge: "create-or-skip" | "schema-extension";
690
- /** Destination relative to the project root (e.g. `lunora/ratelimit/index.ts`). */
691
- to: string;
692
- }
693
- /** A wrangler.jsonc binding addition. `path` is the jsonc key path; `value` the value to set. */
694
- interface RegistryBinding {
695
- path: ReadonlyArray<string>;
696
- value: unknown;
697
- }
698
- /**
699
- * An environment variable an item needs. Scaffolded into `.dev.vars` (Workers'
700
- * local-secrets file) on add — non-secrets get their `value`; secrets get an
701
- * empty placeholder and a reminder to run `wrangler secret put` for production.
702
- */
703
- interface RegistryEnvVariable {
704
- /** Human note on what the variable is for. */
705
- description?: string;
706
- /** The variable name (e.g. `RESEND_API_KEY`). */
707
- name: string;
708
- /** Mark as a secret: never write a value, only a placeholder, and remind about prod. Defaults to `true` when no `value` is given. */
709
- secret?: boolean;
710
- /** A default/example value for non-secret vars. */
711
- value?: string;
712
- }
713
- /** The `registry.json` manifest shape. */
714
- interface RegistryManifest {
715
- /** wrangler.jsonc additions (best-effort structural edits). */
716
- bindings?: ReadonlyArray<RegistryBinding>;
717
- /** npm deps to add to the project package.json (name → version range). */
718
- deps?: Readonly<Record<string, string>>;
719
- description?: string;
720
- /** npm devDependencies to add to the project package.json. */
721
- devDependencies?: Readonly<Record<string, string>>;
722
- /** Post-install guidance printed after the item is added (per-item next steps). */
723
- docs?: string;
724
- /** Environment variables the item needs; scaffolded into `.dev.vars`. */
725
- envVars?: ReadonlyArray<RegistryEnvVariable>;
726
- files: ReadonlyArray<RegistryFile>;
727
- name: string;
728
- /** Other registry items this one depends on (resolved transitively, deps first). */
729
- requires?: ReadonlyArray<string>;
730
- /** Short human-readable label (distinct from the longer `description`). */
731
- title?: string;
732
- }
733
- interface AddCommandOptions {
734
- /** Bypass the `--source` safety gate (matches init). */
735
- allowUnsafeSource?: boolean;
736
- /** `registry build --check`: verify the index is current instead of rewriting it. */
737
- check?: boolean;
738
- /** Inject a confirmer for non-interactive callers / tests. */
739
- confirm?: (prompt: string) => Promise<boolean>;
740
- cwd?: string;
741
- /** Preview the file-level changes (a content diff) and write nothing. */
742
- diff?: boolean;
743
- /** Print the plan and stop without writing anything. */
744
- dryRun?: boolean;
745
- /** Local registry root (offline / tests). Expects per-item subdirs, each with a `registry.json`. */
746
- from?: string;
747
- /** Emit a JSON snapshot of the plan/result. */
748
- json?: boolean;
749
- /** `--list`: enumerate available items instead of adding. */
750
- list?: boolean;
751
- logger: Logger;
752
- /** Item names to add (positional args). */
753
- names: ReadonlyArray<string>;
754
- /** `registry build` output path for the generated catalog (defaults to the root's `index.json`). */
755
- out?: string;
756
- /** Force-overwrite existing files (take the incoming copy) instead of skipping/conflicting. */
757
- overwrite?: boolean;
758
- /** Override the git ref (branch, tag, or commit) items are fetched from (default: version-derived); appended to the `source` base when that is set. Ignored when `from` is set. */
759
- ref?: string;
760
- /** Override the remote registry source base (default gh:anolilab/lunora/registry). */
761
- source?: string;
762
- /** Skip the package.json mutation confirmation prompt. */
763
- yes?: boolean;
764
- }
765
- interface AddCommandResult {
766
- /** Bindings written to wrangler.jsonc. */
767
- bindings: ReadonlyArray<string>;
768
- code: number;
769
- /** Deps added to package.json. */
770
- deps: ReadonlyArray<string>;
771
- /** Files skipped because they already existed. */
772
- skipped: ReadonlyArray<string>;
773
- /** Files written (absolute paths). */
774
- written: ReadonlyArray<string>;
775
- }
776
- /** One resolved item: its parsed manifest plus the (possibly staged) directory it lives in. */
777
798
  /** `lunora registry add` (one or more item names): scaffold items into the project. */
778
799
  declare const runAddCommand: (options: AddCommandOptions) => Promise<AddCommandResult>;
779
800
  /**
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { COMMANDS, VERSION, runCli } from './packem_shared/COMMANDS-Bn8luojF.mjs';
1
+ export { COMMANDS, VERSION, runCli } from './packem_shared/COMMANDS-Dh0bSERi.mjs';
2
2
  export { runCodegenCommand } from './packem_chunks/runCodegenCommand.mjs';
3
3
  export { DEFAULT_IMPORT_BATCH_SIZE, runExportCommand, runImportCommand } from './packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
4
4
  export { runDeployCommand } from './packem_chunks/runDeployCommand.mjs';
@@ -16,4 +16,4 @@ export { createRecordingSpawner, defaultSpawner } from './packem_shared/createRe
16
16
  export { default as parseManifest } from './packem_shared/parseManifest--vZf2FY1.mjs';
17
17
  export { REQUIRED_COMPATIBILITY_DATE, REQUIRED_FLAG, validateWranglerProject as validateWrangler, validateWranglerConfig } from '@lunora/config';
18
18
  export { buildRegistryIndex } from './packem_shared/buildRegistryIndex-BcYe607_.mjs';
19
- export { r as runAddCommand, a as runBuildIndexCommand, b as runRegistryViewCommand } from './packem_shared/commands-DqsEzojt.mjs';
19
+ export { r as runAddCommand, a as runBuildIndexCommand, b as runRegistryViewCommand } from './packem_shared/commands-CkkATMMx.mjs';
@@ -1,10 +1,10 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { findWranglerFile } from '@lunora/config';
3
- import { join } from '@visulima/path';
3
+ import { join, basename } from '@visulima/path';
4
4
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
5
- import { t as tuiSelect } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
6
- import { n as normalizeFeature, E as EMAIL_ITEM, D as DEFAULT_AUTH_ITEM, p as promptAuthProvider, A as AUTH_PROVIDER_OPTIONS } from '../packem_shared/features-ocSSpZtS.mjs';
7
- import { r as runAddCommand } from '../packem_shared/commands-DqsEzojt.mjs';
5
+ import { t as tuiText, a as tuiSelect } from '../packem_shared/tui-prompts-CA9lngSS.mjs';
6
+ import { n as normalizeFeature, E as EMAIL_ITEM, s as sanitizeBucketName, d as deriveBucketName, p as promptBucketName, r as resolveTypedDestination, M as MAIL_DESTINATION_PROMPT, a as sanitizeDatabaseName, b as deriveDatabaseName, c as promptDatabaseName, D as DEFAULT_AUTH_ITEM, e as promptAuthProvider, A as AUTH_PROVIDER_OPTIONS, w as withStorageBucketName, f as withMailDestination, g as withAuthDatabaseName } from '../packem_shared/storage-Bjo35hPa.mjs';
7
+ import { r as runAddCommand } from '../packem_shared/commands-CkkATMMx.mjs';
8
8
 
9
9
  const providerToItem = (provider) => {
10
10
  const value = provider.trim().toLowerCase();
@@ -28,6 +28,51 @@ const resolveAuthItem = async (options) => {
28
28
  const select = options.promptSelect ?? ((message, choices, settings) => tuiSelect(message, choices, settings));
29
29
  return promptAuthProvider(select);
30
30
  };
31
+ const textPrompt = (options) => options.promptText ?? ((message, settings) => tuiText(message, settings));
32
+ const resolveStorageBucketName = async (options) => {
33
+ const projectName = basename(options.cwd ?? process.cwd());
34
+ if (options.bucket !== void 0 && options.bucket !== "") {
35
+ const sanitized = sanitizeBucketName(options.bucket);
36
+ if (sanitized !== void 0) {
37
+ return sanitized;
38
+ }
39
+ const fallback = deriveBucketName(projectName);
40
+ options.logger.warn(`add: "${options.bucket}" isn't a valid R2 bucket name (lowercase alphanumeric + hyphens, 3–63 chars) — using "${fallback}".`);
41
+ return fallback;
42
+ }
43
+ if (options.yes === true) {
44
+ return deriveBucketName(projectName);
45
+ }
46
+ return promptBucketName(textPrompt(options), projectName);
47
+ };
48
+ const resolveMailDestination = async (options) => {
49
+ const warn = (message) => {
50
+ options.logger.warn(`add: ${message}`);
51
+ };
52
+ if (options.mailTo !== void 0 && options.mailTo !== "") {
53
+ return resolveTypedDestination(options.mailTo, warn);
54
+ }
55
+ if (options.yes === true) {
56
+ return void 0;
57
+ }
58
+ return resolveTypedDestination(await textPrompt(options)(MAIL_DESTINATION_PROMPT, { placeholder: "you@yourdomain.com" }), warn);
59
+ };
60
+ const resolveAuthDatabaseName = async (options) => {
61
+ const projectName = basename(options.cwd ?? process.cwd());
62
+ if (options.db !== void 0 && options.db !== "") {
63
+ const sanitized = sanitizeDatabaseName(options.db);
64
+ if (sanitized !== void 0) {
65
+ return sanitized;
66
+ }
67
+ const fallback = deriveDatabaseName(projectName);
68
+ options.logger.warn(`add: "${options.db}" isn't a usable D1 database name — using "${fallback}".`);
69
+ return fallback;
70
+ }
71
+ if (options.yes === true) {
72
+ return deriveDatabaseName(projectName);
73
+ }
74
+ return promptDatabaseName(textPrompt(options), projectName);
75
+ };
31
76
  const resolveFeatureItems = async (feature, options) => {
32
77
  if (feature.kind === "auth") {
33
78
  return [await resolveAuthItem(options)];
@@ -49,6 +94,28 @@ const runAddFeature = async (options) => {
49
94
  return { code: 1, items: [] };
50
95
  }
51
96
  const items = await resolveFeatureItems(feature, options);
97
+ const transforms = [];
98
+ if (items.includes("storage")) {
99
+ const bucketName = await resolveStorageBucketName(options);
100
+ transforms.push((manifest) => withStorageBucketName(manifest, bucketName));
101
+ }
102
+ if (items.includes("mail")) {
103
+ const destination = await resolveMailDestination(options);
104
+ if (destination !== void 0) {
105
+ transforms.push((manifest) => withMailDestination(manifest, destination));
106
+ }
107
+ }
108
+ if (items.some((name) => name === "auth" || name.startsWith("auth-"))) {
109
+ const databaseName = await resolveAuthDatabaseName(options);
110
+ transforms.push((manifest) => withAuthDatabaseName(manifest, databaseName));
111
+ }
112
+ const transformManifest = transforms.length > 0 ? (manifest) => {
113
+ let result2 = manifest;
114
+ for (const transform of transforms) {
115
+ result2 = transform(result2);
116
+ }
117
+ return result2;
118
+ } : void 0;
52
119
  const result = await runAddCommand({
53
120
  allowUnsafeSource: options.allowUnsafeSource,
54
121
  cwd,
@@ -57,6 +124,7 @@ const runAddFeature = async (options) => {
57
124
  names: [...items],
58
125
  ref: options.ref,
59
126
  source: options.source,
127
+ transformManifest,
60
128
  yes: true
61
129
  });
62
130
  return { code: result.code, items };
@@ -64,10 +132,13 @@ const runAddFeature = async (options) => {
64
132
  const execute = defineHandler(async ({ argument, cwd, logger, options }) => {
65
133
  const result = await runAddFeature({
66
134
  allowUnsafeSource: options.allowUnsafeSource === true,
135
+ bucket: options.bucket,
67
136
  cwd,
137
+ db: options.db,
68
138
  feature: argument[0],
69
139
  from: options.from,
70
140
  logger,
141
+ mailTo: options.mailTo,
71
142
  provider: options.provider,
72
143
  ref: options.ref,
73
144
  source: options.source,
@@ -1,5 +1,5 @@
1
1
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
2
- import { r as runAddCommand, b as runRegistryViewCommand, a as runBuildIndexCommand } from '../packem_shared/commands-DqsEzojt.mjs';
2
+ import { r as runAddCommand, b as runRegistryViewCommand, a as runBuildIndexCommand } from '../packem_shared/commands-CkkATMMx.mjs';
3
3
 
4
4
  const execute = defineHandler(({ argument, cwd, logger, options }) => {
5
5
  const subcommand = argument[0];
@@ -7,7 +7,7 @@ import { join } from '@visulima/path';
7
7
  import { Project } from 'ts-morph';
8
8
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
9
9
  import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
10
- import { a as tuiConfirm } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
10
+ import { b as tuiConfirm } from '../packem_shared/tui-prompts-CA9lngSS.mjs';
11
11
  import { runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
12
12
  import { runResetCommand } from './runResetCommand.mjs';
13
13
 
@@ -9,7 +9,7 @@ import { d as detectPackageManager, e as execArgsFor } from '../packem_shared/de
9
9
  import { createServer, request } from 'node:http';
10
10
  import { connect } from 'node:net';
11
11
  import { loadStudioAssets, studioAssetsStamp, renderStudioHtml, resolveAdminToken, SCHEMA_EDIT_ENDPOINT, POLICY_SCAFFOLD_ENDPOINT, SEED_ENDPOINT, serveJsonHandler, handleSchemaEditRequest, handlePolicyScaffoldRequest, handleSeedRequest } from '@lunora/config/studio-host';
12
- import { c as createTuiConfirm } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
12
+ import { c as createTuiConfirm } from '../packem_shared/tui-prompts-CA9lngSS.mjs';
13
13
 
14
14
  const DEFAULT_DEBOUNCE_MS = 100;
15
15
  const PATH_SEGMENT_SEPARATOR = /[/\\]/u;
@@ -10,10 +10,10 @@ import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
10
10
  import { a as detectInstalledManagers, i as installArgsFor } from '../packem_shared/detect-package-manager-DYp7n3mJ.mjs';
11
11
  import MagicString from 'magic-string';
12
12
  import { Project, SyntaxKind } from 'ts-morph';
13
- import { c as resolveDistTag, d as resolveSourceRef, r as runAddCommand } from '../packem_shared/commands-DqsEzojt.mjs';
13
+ import { c as resolveDistTag, d as resolveSourceRef, r as runAddCommand } from '../packem_shared/commands-CkkATMMx.mjs';
14
14
  import { defaultSpawner } from '../packem_shared/createRecordingSpawner-DxI3mebw.mjs';
15
- import { b as tuiOutro, d as tuiBanner, e as tuiText, f as tuiIntro, t as tuiSelect, w as withTuiSpinner, a as tuiConfirm, g as tuiMultiSelect } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
16
- import { p as promptAuthProvider, E as EMAIL_ITEM } from '../packem_shared/features-ocSSpZtS.mjs';
15
+ import { d as tuiOutro, e as tuiBanner, t as tuiText, f as tuiIntro, a as tuiSelect, w as withTuiSpinner, b as tuiConfirm, g as tuiMultiSelect } from '../packem_shared/tui-prompts-CA9lngSS.mjs';
16
+ import { p as promptBucketName, w as withStorageBucketName, M as MAIL_DESTINATION_PROMPT, r as resolveTypedDestination, E as EMAIL_ITEM, f as withMailDestination, e as promptAuthProvider, c as promptDatabaseName, g as withAuthDatabaseName } from '../packem_shared/storage-Bjo35hPa.mjs';
17
17
 
18
18
  const GITHUB_CONTENT = `name: Deploy
19
19
 
@@ -294,6 +294,27 @@ const STACK_FEATURE_OPTIONS = [
294
294
  { description: "Live presence / who's-online over hibernated WebSockets", label: "Presence", value: "presence" },
295
295
  { description: "Snapshot + restore your Durable Object data", label: "Backups", value: "backup" }
296
296
  ];
297
+ const applyAuthFeature = async (deps) => {
298
+ const provider = await promptAuthProvider(deps.select);
299
+ const databaseName = await promptDatabaseName(deps.text, deps.projectName);
300
+ await deps.apply([provider], { transformManifest: (manifest) => withAuthDatabaseName(manifest, databaseName) });
301
+ };
302
+ const applyEmailFeature = async (deps) => {
303
+ const answer = await deps.text(MAIL_DESTINATION_PROMPT, { placeholder: "you@yourdomain.com" });
304
+ const destination = resolveTypedDestination(answer, (message) => {
305
+ deps.logger.warn(message);
306
+ });
307
+ await deps.apply([EMAIL_ITEM], destination === void 0 ? void 0 : { transformManifest: (manifest) => withMailDestination(manifest, destination) });
308
+ };
309
+ const applyStorageFeature = async (deps) => {
310
+ const bucketName = await promptBucketName(deps.text, deps.projectName);
311
+ await deps.apply(["storage"], { transformManifest: (manifest) => withStorageBucketName(manifest, bucketName) });
312
+ };
313
+ const FEATURE_HANDLERS = {
314
+ auth: applyAuthFeature,
315
+ email: applyEmailFeature,
316
+ storage: applyStorageFeature
317
+ };
297
318
  const offerRegistryExtras = async (deps) => {
298
319
  if (!deps.interactive) {
299
320
  deps.logger.info("tip: add features later with `lunora add <auth|email|storage|ratelimit|crons|presence|backup>`.");
@@ -301,12 +322,8 @@ const offerRegistryExtras = async (deps) => {
301
322
  }
302
323
  const picked = await deps.multiSelect("Which features do you want to add?", STACK_FEATURE_OPTIONS, { defaults: [] });
303
324
  for (const feature of picked) {
304
- if (feature === "auth") {
305
- const provider = await promptAuthProvider(deps.select);
306
- await deps.apply([provider]);
307
- } else {
308
- await deps.apply([feature === "email" ? EMAIL_ITEM : feature]);
309
- }
325
+ const handler = FEATURE_HANDLERS[feature];
326
+ await (handler ? handler(deps) : deps.apply([feature]));
310
327
  }
311
328
  };
312
329
 
@@ -936,7 +953,7 @@ const runInPlaceInit = (cwd, logger) => {
936
953
  const offerIsInteractive = (options) => options.yes !== true && (options.prompt !== void 0 || (options.interactive ?? isInteractive()));
937
954
  const maybeOfferExtras = async (options, projectDirectory) => {
938
955
  const interactive = offerIsInteractive(options);
939
- const apply = async (names) => {
956
+ const apply = async (names, applyOptions) => {
940
957
  const applyLogger = isInteractive() ? {
941
958
  error: (message) => {
942
959
  options.logger.error(message);
@@ -959,6 +976,7 @@ const maybeOfferExtras = async (options, projectDirectory) => {
959
976
  names: [...names],
960
977
  ref: options.ref,
961
978
  source: options.registrySource,
979
+ transformManifest: applyOptions?.transformManifest,
962
980
  yes: true
963
981
  })
964
982
  );
@@ -970,7 +988,9 @@ const maybeOfferExtras = async (options, projectDirectory) => {
970
988
  interactive,
971
989
  logger: options.logger,
972
990
  multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, settings)),
973
- select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, settings))
991
+ projectName: basename(projectDirectory),
992
+ select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, settings)),
993
+ text: options.prompt?.text ?? ((message, settings) => tuiText(message, settings))
974
994
  });
975
995
  };
976
996
  const DEFAULT_FRAMEWORK = "react";
@@ -1,7 +1,7 @@
1
1
  import { existsSync, rmSync } from 'node:fs';
2
2
  import { join } from '@visulima/path';
3
3
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
4
- import { a as tuiConfirm } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
4
+ import { b as tuiConfirm } from '../packem_shared/tui-prompts-CA9lngSS.mjs';
5
5
 
6
6
  const runResetCommand = async (options) => {
7
7
  const cwd = options.cwd ?? process.cwd();
@@ -14,7 +14,8 @@ const addCommand = {
14
14
  ["lunora add auth", "Add authentication (asks which provider)"],
15
15
  ["lunora add auth --provider clerk", "Add Clerk auth without prompting"],
16
16
  ["lunora add email", "Add transactional email (Cloudflare Email Workers + dev mail catcher)"],
17
- ["lunora add storage", "Add the R2 storage registry item"],
17
+ ["lunora add storage", "Add the R2 storage registry item (asks for the bucket name)"],
18
+ ["lunora add storage --bucket my-app-uploads", "Add storage with a bucket name, no prompt"],
18
19
  ["lunora add crons", "Add the scheduled-jobs registry item"],
19
20
  ["lunora add storage --ref alpha", "Add an item from the alpha branch's registry"]
20
21
  ],
@@ -25,7 +26,10 @@ const addCommand = {
25
26
  name: "add",
26
27
  options: [
27
28
  { description: "auth: provider to use without prompting (auth | clerk | auth0)", name: "provider", type: String },
28
- { description: "Skip the provider prompt and use the default (email & password)", name: "yes", type: Boolean },
29
+ { description: "auth: D1 database name to use without prompting (lowercase alphanumeric + hyphens)", name: "db", type: String },
30
+ { description: "storage: R2 bucket name to use without prompting (lowercase alphanumeric + hyphens)", name: "bucket", type: String },
31
+ { description: "email: verified destination address to use without prompting", name: "mail-to", type: String },
32
+ { description: "Skip prompts (auth provider, DB name, bucket name, mail destination) and use the defaults", name: "yes", type: Boolean },
29
33
  { description: "Local registry root (offline; expects <name>/ subdirs)", name: "from", type: String },
30
34
  { description: "Override the remote registry source base (e.g. gh:owner/repo/registry)", name: "source", type: String },
31
35
  {
@@ -3,7 +3,7 @@ import { dirname, join } from '@visulima/path';
3
3
  import { DEV_VARS_FILE, parseDevVariableEntries } from '@lunora/config';
4
4
  import { modify, applyEdits, parse } from 'jsonc-parser';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { a as tuiConfirm } from './tui-prompts-XHFxlOg5.mjs';
6
+ import { b as tuiConfirm } from './tui-prompts-CA9lngSS.mjs';
7
7
  import { collectCatalog, buildRegistryIndex } from './buildRegistryIndex-BcYe607_.mjs';
8
8
  import { insertSchemaExtension } from './insertSchemaExtension-BuzF6-t2.mjs';
9
9
  import { createHash } from 'node:crypto';
@@ -583,6 +583,26 @@ const resolvePlan = async (names, options) => {
583
583
  const emptyResult = () => {
584
584
  return { bindings: [], code: 0, deps: [], skipped: [], written: [] };
585
585
  };
586
+ const setBindingField = (manifest, section, match, field, fieldValue) => {
587
+ if (!manifest.bindings) {
588
+ return manifest;
589
+ }
590
+ return {
591
+ ...manifest,
592
+ bindings: manifest.bindings.map((binding) => {
593
+ if (binding.path[0] !== section || !Array.isArray(binding.value)) {
594
+ return binding;
595
+ }
596
+ const entries = binding.value;
597
+ return {
598
+ ...binding,
599
+ value: entries.map(
600
+ (entry) => typeof entry === "object" && entry !== null && entry[match.key] === match.value ? { ...entry, [field]: fieldValue } : entry
601
+ )
602
+ };
603
+ })
604
+ };
605
+ };
586
606
 
587
607
  const printPlan = (logger, manifest) => {
588
608
  const label = manifest.title ?? manifest.description;
@@ -688,8 +708,12 @@ const runAddCommand = async (options) => {
688
708
  }
689
709
  let cleanups = [];
690
710
  try {
691
- const { cleanups: planCleanups, items } = await resolvePlan(options.names, options);
711
+ const { cleanups: planCleanups, items: resolvedItems } = await resolvePlan(options.names, options);
692
712
  cleanups = planCleanups;
713
+ const { transformManifest } = options;
714
+ const items = transformManifest ? resolvedItems.map((item) => {
715
+ return { ...item, manifest: transformManifest(item.manifest) };
716
+ }) : resolvedItems;
693
717
  for (const { manifest } of items) {
694
718
  printPlan(options.logger, manifest);
695
719
  }
@@ -785,4 +809,4 @@ const runBuildIndexCommand = async (options) => {
785
809
  return empty;
786
810
  };
787
811
 
788
- export { runBuildIndexCommand as a, runRegistryViewCommand as b, resolveDistTag as c, resolveSourceRef as d, runListCommand as e, runAddCommand as r };
812
+ export { runBuildIndexCommand as a, runRegistryViewCommand as b, resolveDistTag as c, resolveSourceRef as d, runListCommand as e, runAddCommand as r, setBindingField as s };
@@ -1,4 +1,4 @@
1
1
  import 'node:fs';
2
2
  import '@visulima/path';
3
- export { r as runAddCommand, a as runBuildIndexCommand, e as runListCommand, b as runRegistryViewCommand } from './commands-DqsEzojt.mjs';
3
+ export { r as runAddCommand, a as runBuildIndexCommand, e as runListCommand, b as runRegistryViewCommand } from './commands-CkkATMMx.mjs';
4
4
  import './buildRegistryIndex-BcYe607_.mjs';
@@ -0,0 +1,84 @@
1
+ import { s as setBindingField } from './commands-CkkATMMx.mjs';
2
+
3
+ const INVALID_SLUG_CHARS = /[^a-z0-9]+/u;
4
+ const toKebabSlug = (input, min, max) => {
5
+ let slug = input.toLowerCase().split(INVALID_SLUG_CHARS).filter(Boolean).join("-").slice(0, max);
6
+ if (slug.endsWith("-")) {
7
+ slug = slug.slice(0, -1);
8
+ }
9
+ return slug.length >= min ? slug : void 0;
10
+ };
11
+
12
+ const DB_BINDING = "DB";
13
+ const AUTH_DB_PROMPT = "Name your D1 database (run `wrangler d1 create` to get its id, then put it in wrangler.jsonc)";
14
+ const sanitizeDatabaseName = (input) => toKebabSlug(input, 1, 64);
15
+ const FALLBACK_DATABASE_NAME = "lunora-db";
16
+ const deriveDatabaseName = (projectName) => sanitizeDatabaseName(`${projectName}-db`) ?? FALLBACK_DATABASE_NAME;
17
+ const promptDatabaseName = async (text, projectName) => {
18
+ const fallback = deriveDatabaseName(projectName);
19
+ return sanitizeDatabaseName(await text(AUTH_DB_PROMPT, { default: fallback, placeholder: fallback })) ?? fallback;
20
+ };
21
+ const withAuthDatabaseName = (manifest, name) => setBindingField(manifest, "d1_databases", { key: "binding", value: DB_BINDING }, "database_name", name);
22
+
23
+ const AUTH_PROVIDER_OPTIONS = [
24
+ { description: "Email + password on better-auth (default)", label: "Email & password", value: "auth" },
25
+ { description: "Clerk-hosted auth", label: "Clerk", value: "auth-clerk" },
26
+ { description: "Auth0 (OIDC)", label: "Auth0", value: "auth-auth0" }
27
+ ];
28
+ const DEFAULT_AUTH_ITEM = "auth";
29
+ const promptAuthProvider = async (select) => await select("Which auth provider?", AUTH_PROVIDER_OPTIONS, { default: DEFAULT_AUTH_ITEM }) ?? DEFAULT_AUTH_ITEM;
30
+ const EMAIL_ITEM = "mail";
31
+ const normalizeFeature = (raw) => {
32
+ const value = raw.trim();
33
+ if (value === "") {
34
+ return void 0;
35
+ }
36
+ const lower = value.toLowerCase();
37
+ if (lower === "auth") {
38
+ return { kind: "auth" };
39
+ }
40
+ if (lower === "email" || lower === "mail") {
41
+ return { kind: "email" };
42
+ }
43
+ return { item: lower, kind: "item" };
44
+ };
45
+
46
+ const SEND_EMAIL_BINDING = "SEND_EMAIL";
47
+ const MAIL_DESTINATION_PROMPT = "Verified destination email for production delivery (blank = set it later in wrangler.jsonc)";
48
+ const isValidEmail = (value) => {
49
+ const trimmed = value.trim();
50
+ if (trimmed.length === 0 || trimmed.includes(" ")) {
51
+ return false;
52
+ }
53
+ const at = trimmed.indexOf("@");
54
+ if (at <= 0 || at !== trimmed.lastIndexOf("@")) {
55
+ return false;
56
+ }
57
+ const domain = trimmed.slice(at + 1);
58
+ return domain.length >= 3 && domain.includes(".") && !domain.startsWith(".") && !domain.endsWith(".");
59
+ };
60
+ const resolveTypedDestination = (entered, warn) => {
61
+ const trimmed = entered.trim();
62
+ if (trimmed === "") {
63
+ return void 0;
64
+ }
65
+ if (!isValidEmail(trimmed)) {
66
+ warn(`"${trimmed}" doesn't look like an email — leaving the placeholder; set destination_address in wrangler.jsonc.`);
67
+ return void 0;
68
+ }
69
+ return trimmed;
70
+ };
71
+ const withMailDestination = (manifest, address) => setBindingField(manifest, "send_email", { key: "name", value: SEND_EMAIL_BINDING }, "destination_address", address);
72
+
73
+ const UPLOADS_BINDING = "UPLOADS";
74
+ const STORAGE_BUCKET_PROMPT = "Name your R2 bucket (you can rename it in wrangler.jsonc later)";
75
+ const sanitizeBucketName = (input) => toKebabSlug(input, 3, 63);
76
+ const FALLBACK_BUCKET_NAME = "lunora-uploads";
77
+ const deriveBucketName = (projectName) => sanitizeBucketName(`${projectName}-uploads`) ?? FALLBACK_BUCKET_NAME;
78
+ const promptBucketName = async (text, projectName) => {
79
+ const fallback = deriveBucketName(projectName);
80
+ return sanitizeBucketName(await text(STORAGE_BUCKET_PROMPT, { default: fallback, placeholder: fallback })) ?? fallback;
81
+ };
82
+ const withStorageBucketName = (manifest, bucketName) => setBindingField(manifest, "r2_buckets", { key: "binding", value: UPLOADS_BINDING }, "bucket_name", bucketName);
83
+
84
+ export { AUTH_PROVIDER_OPTIONS as A, DEFAULT_AUTH_ITEM as D, EMAIL_ITEM as E, MAIL_DESTINATION_PROMPT as M, sanitizeDatabaseName as a, deriveDatabaseName as b, promptDatabaseName as c, deriveBucketName as d, promptAuthProvider as e, withMailDestination as f, withAuthDatabaseName as g, normalizeFeature as n, promptBucketName as p, resolveTypedDestination as r, sanitizeBucketName as s, withStorageBucketName as w };
@@ -266,4 +266,4 @@ const withTuiSpinner = async (label, task) => {
266
266
  }
267
267
  };
268
268
 
269
- export { tuiConfirm as a, tuiOutro as b, createTuiConfirm as c, tuiBanner as d, tuiText as e, tuiIntro as f, tuiMultiSelect as g, tuiSelect as t, withTuiSpinner as w };
269
+ export { tuiSelect as a, tuiConfirm as b, createTuiConfirm as c, tuiOutro as d, tuiBanner as e, tuiIntro as f, tuiMultiSelect as g, tuiText as t, withTuiSpinner as w };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/cli",
3
- "version": "1.0.0-alpha.8",
3
+ "version": "1.0.0-alpha.9",
4
4
  "description": "The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -41,7 +41,7 @@ This:
41
41
  1. Adds `@lunora/storage` and `@lunora/server` to `package.json` (run
42
42
  `pnpm install` afterwards).
43
43
  2. Adds an R2 bucket binding to `wrangler.jsonc` (`r2_buckets`, binding
44
- **`UPLOADS`**, `bucket_name: "REPLACE_ME-uploads"` — rename it to a real
44
+ **`UPLOADS`**, `bucket_name: "replace-me-uploads"` — rename it to a real
45
45
  bucket). It **merges** into any existing `r2_buckets`.
46
46
  3. Scaffolds `STORAGE_SIGNING_SECRET` (a secret) and `STORAGE_PUBLIC_BASE_URL`
47
47
  into `.dev.vars`.
@@ -137,8 +137,12 @@ persist that, and pass the bare key back in — the component re-scopes it.
137
137
 
138
138
  1. **Skipping `verifySignedUrl` on the download route.** Without it, anyone can
139
139
  read any key. Always verify before streaming.
140
- 2. **Placeholder bucket name.** `bucket_name: "REPLACE_ME-uploads"` ships as a
141
- placeholder rename it to a real R2 bucket.
140
+ 2. **Placeholder bucket name.** `lunora init` and `lunora add storage` prompt for
141
+ the bucket name (or take `--bucket <name>`), but the low-level
142
+ `lunora registry add storage` writes the placeholder
143
+ `bucket_name: "replace-me-uploads"` — rename it to a real R2 bucket. (R2 names
144
+ are lowercase alphanumeric + hyphens, 3–63 chars; wrangler rejects anything
145
+ else on `dev`/`deploy`.)
142
146
  3. **Short / shared signing secret.** Use ≥32 chars and a distinct secret per
143
147
  bucket; reusing it lets one bucket's URLs sign for another.
144
148
  4. **Proxying bytes through the Worker.** The design uploads/downloads directly
@@ -1,24 +0,0 @@
1
- const AUTH_PROVIDER_OPTIONS = [
2
- { description: "Email + password on better-auth (default)", label: "Email & password", value: "auth" },
3
- { description: "Clerk-hosted auth", label: "Clerk", value: "auth-clerk" },
4
- { description: "Auth0 (OIDC)", label: "Auth0", value: "auth-auth0" }
5
- ];
6
- const DEFAULT_AUTH_ITEM = "auth";
7
- const promptAuthProvider = async (select) => await select("Which auth provider?", AUTH_PROVIDER_OPTIONS, { default: DEFAULT_AUTH_ITEM }) ?? DEFAULT_AUTH_ITEM;
8
- const EMAIL_ITEM = "mail";
9
- const normalizeFeature = (raw) => {
10
- const value = raw.trim();
11
- if (value === "") {
12
- return void 0;
13
- }
14
- const lower = value.toLowerCase();
15
- if (lower === "auth") {
16
- return { kind: "auth" };
17
- }
18
- if (lower === "email" || lower === "mail") {
19
- return { kind: "email" };
20
- }
21
- return { item: lower, kind: "item" };
22
- };
23
-
24
- export { AUTH_PROVIDER_OPTIONS as A, DEFAULT_AUTH_ITEM as D, EMAIL_ITEM as E, normalizeFeature as n, promptAuthProvider as p };