@getstrata/starter 0.1.10 → 0.1.11
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/README.md +1 -1
- package/dist/cli.js +434 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# create-strata
|
|
2
2
|
|
|
3
|
-
Interactive starter for [Strata](https://github.com/EyK-26/strata). The wizard always asks each layer. SQLite APIs and Postgres HTML apps use the same generator.
|
|
3
|
+
Interactive starter for [Strata](https://github.com/EyK-26/strata). The wizard always asks each layer. Extras (MFA, email verification, SCIM, metrics) are toggled one by one. SQLite APIs and Postgres HTML apps use the same generator.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
package/dist/cli.js
CHANGED
|
@@ -109,6 +109,15 @@ function usesTenantTable(tenancy) {
|
|
|
109
109
|
function htmlAuthKit(auth) {
|
|
110
110
|
return authUsesCookie(auth);
|
|
111
111
|
}
|
|
112
|
+
function extraApplies(extra, auth) {
|
|
113
|
+
if (extra === "metrics") {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (extra === "mfa") {
|
|
117
|
+
return htmlAuthKit(auth);
|
|
118
|
+
}
|
|
119
|
+
return authNeedsUsers(auth);
|
|
120
|
+
}
|
|
112
121
|
function needsRedis(layers) {
|
|
113
122
|
return layers.cache === "redis" || layers.queue === "redis";
|
|
114
123
|
}
|
|
@@ -240,7 +249,7 @@ Options:
|
|
|
240
249
|
--email-verification / --no-email-verification
|
|
241
250
|
--scim / --no-scim
|
|
242
251
|
--metrics / --no-metrics
|
|
243
|
-
--extras
|
|
252
|
+
--extras Interactive extras list (MFA, email verification, SCIM, metrics)
|
|
244
253
|
--docker Write Docker Compose for every selected tool that needs a service
|
|
245
254
|
--no-docker Skip docker-compose.yml; use installs already on this machine
|
|
246
255
|
--docker-services Subset: postgres, mysql, redis, mailpit (comma-separated)
|
|
@@ -479,14 +488,355 @@ function layersFromFlags(flags) {
|
|
|
479
488
|
// src/prompt.ts
|
|
480
489
|
import { stdin as input, stdout as output } from "process";
|
|
481
490
|
import { createInterface } from "readline/promises";
|
|
491
|
+
|
|
492
|
+
// src/selectPrompt.ts
|
|
493
|
+
class PromptCancelledError extends Error {
|
|
494
|
+
constructor() {
|
|
495
|
+
super("Cancelled");
|
|
496
|
+
this.name = "PromptCancelledError";
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function consumeSelectKeys(buffer) {
|
|
500
|
+
const events = [];
|
|
501
|
+
let rest = buffer;
|
|
502
|
+
while (rest.length > 0) {
|
|
503
|
+
if (rest[0] === "\x1B") {
|
|
504
|
+
if (rest.length === 1) {
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (rest.startsWith("\x1B[")) {
|
|
508
|
+
const end = rest.search(/[A-Za-z]/);
|
|
509
|
+
if (end < 2) {
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
const command = rest[end];
|
|
513
|
+
rest = rest.slice(end + 1);
|
|
514
|
+
if (command === "A") {
|
|
515
|
+
events.push({ type: "up" });
|
|
516
|
+
} else if (command === "B") {
|
|
517
|
+
events.push({ type: "down" });
|
|
518
|
+
} else if (command === "C") {
|
|
519
|
+
events.push({ type: "right" });
|
|
520
|
+
} else if (command === "D") {
|
|
521
|
+
events.push({ type: "left" });
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (rest.startsWith("\x1BO")) {
|
|
526
|
+
if (rest.length < 3) {
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
const command = rest[2];
|
|
530
|
+
rest = rest.slice(3);
|
|
531
|
+
if (command === "A") {
|
|
532
|
+
events.push({ type: "up" });
|
|
533
|
+
} else if (command === "B") {
|
|
534
|
+
events.push({ type: "down" });
|
|
535
|
+
} else if (command === "C") {
|
|
536
|
+
events.push({ type: "right" });
|
|
537
|
+
} else if (command === "D") {
|
|
538
|
+
events.push({ type: "left" });
|
|
539
|
+
}
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
rest = rest.slice(1);
|
|
543
|
+
events.push({ type: "abort" });
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
const next = rest[0] ?? "";
|
|
547
|
+
rest = rest.slice(1);
|
|
548
|
+
if (next === "\x03") {
|
|
549
|
+
events.push({ type: "abort" });
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (next === "\r" || next === `
|
|
553
|
+
`) {
|
|
554
|
+
events.push({ type: "submit" });
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (next === " ") {
|
|
558
|
+
events.push({ type: "toggle" });
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (next === "y" || next === "Y") {
|
|
562
|
+
events.push({ type: "yes" });
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (next === "n" || next === "N") {
|
|
566
|
+
events.push({ type: "no" });
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (next >= "1" && next <= "9") {
|
|
570
|
+
events.push({ type: "digit", value: Number(next) });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return { events, rest };
|
|
574
|
+
}
|
|
575
|
+
function moveSelectIndex(index, delta, length) {
|
|
576
|
+
if (length <= 0) {
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
579
|
+
return (index + delta + length) % length;
|
|
580
|
+
}
|
|
581
|
+
function highlight(line, on) {
|
|
582
|
+
return on ? `\x1B[7m${line}\x1B[0m` : line;
|
|
583
|
+
}
|
|
584
|
+
function renderSelectLines(message, choices, index) {
|
|
585
|
+
return [
|
|
586
|
+
message,
|
|
587
|
+
...choices.map((choice, choiceIndex) => {
|
|
588
|
+
const selected = choiceIndex === index;
|
|
589
|
+
const marker = selected ? ">" : " ";
|
|
590
|
+
return highlight(` ${marker} ${choiceIndex + 1}) ${choice.label}`, selected);
|
|
591
|
+
}),
|
|
592
|
+
` \u2191/\u2193 and Enter, or 1-${choices.length}`
|
|
593
|
+
];
|
|
594
|
+
}
|
|
595
|
+
function renderConfirmLines(message, yes) {
|
|
596
|
+
return [
|
|
597
|
+
message,
|
|
598
|
+
highlight(` ${yes ? ">" : " "} yes`, yes),
|
|
599
|
+
highlight(` ${yes ? " " : ">"} no`, !yes),
|
|
600
|
+
" \u2191/\u2193 and Enter, or y / n"
|
|
601
|
+
];
|
|
602
|
+
}
|
|
603
|
+
function renderMultiSelectLines(message, choices, index) {
|
|
604
|
+
return [
|
|
605
|
+
message,
|
|
606
|
+
...choices.map((choice, choiceIndex) => {
|
|
607
|
+
const focused = choiceIndex === index;
|
|
608
|
+
const box = choice.enabled ? "[x]" : "[ ]";
|
|
609
|
+
const marker = focused ? ">" : " ";
|
|
610
|
+
return highlight(` ${marker} ${box} ${choiceIndex + 1}) ${choice.label}`, focused);
|
|
611
|
+
}),
|
|
612
|
+
" \u2191/\u2193 move, Space or 1-9 toggle, Enter to continue"
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
function writeLines(output, lines) {
|
|
616
|
+
for (const line of lines) {
|
|
617
|
+
output.write(`\x1B[2K${line}
|
|
618
|
+
`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function clearDrawnLines(output, lineCount) {
|
|
622
|
+
if (lineCount <= 0) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
output.write(`\x1B[${lineCount}F`);
|
|
626
|
+
for (let i = 0;i < lineCount; i += 1) {
|
|
627
|
+
output.write(`\x1B[2K
|
|
628
|
+
`);
|
|
629
|
+
}
|
|
630
|
+
output.write(`\x1B[${lineCount}F`);
|
|
631
|
+
}
|
|
632
|
+
function runRawPrompt(io, render, onEvent, summary) {
|
|
633
|
+
const { input, output } = io;
|
|
634
|
+
const wasRaw = Boolean(input.isTTY && typeof input.setRawMode === "function");
|
|
635
|
+
let buffer = "";
|
|
636
|
+
let lineCount = 0;
|
|
637
|
+
let settled = false;
|
|
638
|
+
const restore = () => {
|
|
639
|
+
output.write("\x1B[?25h");
|
|
640
|
+
if (wasRaw) {
|
|
641
|
+
input.setRawMode?.(false);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
const paint = () => {
|
|
645
|
+
const lines = render();
|
|
646
|
+
if (lineCount > 0) {
|
|
647
|
+
output.write(`\x1B[${lineCount}F`);
|
|
648
|
+
}
|
|
649
|
+
writeLines(output, lines);
|
|
650
|
+
lineCount = lines.length;
|
|
651
|
+
};
|
|
652
|
+
input.setEncoding("utf8");
|
|
653
|
+
if (wasRaw) {
|
|
654
|
+
input.setRawMode?.(true);
|
|
655
|
+
}
|
|
656
|
+
if (typeof input.resume === "function") {
|
|
657
|
+
input.resume();
|
|
658
|
+
}
|
|
659
|
+
output.write("\x1B[?25l");
|
|
660
|
+
paint();
|
|
661
|
+
return new Promise((resolve, reject) => {
|
|
662
|
+
const cleanup = () => {
|
|
663
|
+
input.off("data", onData);
|
|
664
|
+
restore();
|
|
665
|
+
};
|
|
666
|
+
const succeed = (result) => {
|
|
667
|
+
if (settled) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
settled = true;
|
|
671
|
+
cleanup();
|
|
672
|
+
clearDrawnLines(output, lineCount);
|
|
673
|
+
output.write(`${summary(result)}
|
|
674
|
+
`);
|
|
675
|
+
resolve(result);
|
|
676
|
+
};
|
|
677
|
+
const fail = (error) => {
|
|
678
|
+
if (settled) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
settled = true;
|
|
682
|
+
cleanup();
|
|
683
|
+
clearDrawnLines(output, lineCount);
|
|
684
|
+
reject(error);
|
|
685
|
+
};
|
|
686
|
+
function onData(chunk) {
|
|
687
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
688
|
+
const consumed = consumeSelectKeys(buffer);
|
|
689
|
+
buffer = consumed.rest;
|
|
690
|
+
for (const event of consumed.events) {
|
|
691
|
+
const outcome = onEvent(event);
|
|
692
|
+
if (outcome === "abort") {
|
|
693
|
+
fail(new PromptCancelledError);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (outcome === "continue") {
|
|
697
|
+
paint();
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
succeed(outcome.done);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
input.on("data", onData);
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
async function promptSelect(message, choices, defaultValue, io) {
|
|
708
|
+
if (choices.length === 0) {
|
|
709
|
+
throw new Error(`No choices for "${message}".`);
|
|
710
|
+
}
|
|
711
|
+
const found = choices.findIndex((choice) => choice.value === defaultValue);
|
|
712
|
+
let index = found >= 0 ? found : 0;
|
|
713
|
+
return runRawPrompt(io, () => renderSelectLines(message, choices, index), (event) => {
|
|
714
|
+
if (event.type === "abort") {
|
|
715
|
+
return "abort";
|
|
716
|
+
}
|
|
717
|
+
if (event.type === "up") {
|
|
718
|
+
index = moveSelectIndex(index, -1, choices.length);
|
|
719
|
+
return "continue";
|
|
720
|
+
}
|
|
721
|
+
if (event.type === "down") {
|
|
722
|
+
index = moveSelectIndex(index, 1, choices.length);
|
|
723
|
+
return "continue";
|
|
724
|
+
}
|
|
725
|
+
if (event.type === "digit") {
|
|
726
|
+
if (event.value >= 1 && event.value <= choices.length) {
|
|
727
|
+
const choice = choices[event.value - 1];
|
|
728
|
+
if (choice) {
|
|
729
|
+
return { done: choice.value };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return "continue";
|
|
733
|
+
}
|
|
734
|
+
if (event.type === "submit") {
|
|
735
|
+
const choice = choices[index];
|
|
736
|
+
if (choice) {
|
|
737
|
+
return { done: choice.value };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return "continue";
|
|
741
|
+
}, (value) => `${message} ${choices.find((choice) => choice.value === value)?.label ?? value}`);
|
|
742
|
+
}
|
|
743
|
+
async function promptConfirm(message, defaultValue, io) {
|
|
744
|
+
let yes = defaultValue;
|
|
745
|
+
return runRawPrompt(io, () => renderConfirmLines(message, yes), (event) => {
|
|
746
|
+
if (event.type === "abort") {
|
|
747
|
+
return "abort";
|
|
748
|
+
}
|
|
749
|
+
if (event.type === "up" || event.type === "right" || event.type === "yes") {
|
|
750
|
+
if (event.type === "yes") {
|
|
751
|
+
return { done: true };
|
|
752
|
+
}
|
|
753
|
+
yes = true;
|
|
754
|
+
return "continue";
|
|
755
|
+
}
|
|
756
|
+
if (event.type === "down" || event.type === "left" || event.type === "no") {
|
|
757
|
+
if (event.type === "no") {
|
|
758
|
+
return { done: false };
|
|
759
|
+
}
|
|
760
|
+
yes = false;
|
|
761
|
+
return "continue";
|
|
762
|
+
}
|
|
763
|
+
if (event.type === "digit") {
|
|
764
|
+
if (event.value === 1) {
|
|
765
|
+
return { done: true };
|
|
766
|
+
}
|
|
767
|
+
if (event.value === 2) {
|
|
768
|
+
return { done: false };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (event.type === "submit") {
|
|
772
|
+
return { done: yes };
|
|
773
|
+
}
|
|
774
|
+
return "continue";
|
|
775
|
+
}, (value) => `${message} ${value ? "yes" : "no"}`);
|
|
776
|
+
}
|
|
777
|
+
async function promptMultiSelect(message, choices, io) {
|
|
778
|
+
if (choices.length === 0) {
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
const selected = choices.map((choice) => choice.enabled);
|
|
782
|
+
let index = 0;
|
|
783
|
+
const enabledValues = () => choices.filter((_, choiceIndex) => selected[choiceIndex]).map((choice) => choice.value);
|
|
784
|
+
return runRawPrompt(io, () => renderMultiSelectLines(message, choices.map((choice, choiceIndex) => ({
|
|
785
|
+
...choice,
|
|
786
|
+
enabled: Boolean(selected[choiceIndex])
|
|
787
|
+
})), index), (event) => {
|
|
788
|
+
if (event.type === "abort") {
|
|
789
|
+
return "abort";
|
|
790
|
+
}
|
|
791
|
+
if (event.type === "up") {
|
|
792
|
+
index = moveSelectIndex(index, -1, choices.length);
|
|
793
|
+
return "continue";
|
|
794
|
+
}
|
|
795
|
+
if (event.type === "down") {
|
|
796
|
+
index = moveSelectIndex(index, 1, choices.length);
|
|
797
|
+
return "continue";
|
|
798
|
+
}
|
|
799
|
+
if (event.type === "toggle") {
|
|
800
|
+
selected[index] = !selected[index];
|
|
801
|
+
return "continue";
|
|
802
|
+
}
|
|
803
|
+
if (event.type === "digit") {
|
|
804
|
+
if (event.value >= 1 && event.value <= choices.length) {
|
|
805
|
+
const target = event.value - 1;
|
|
806
|
+
selected[target] = !selected[target];
|
|
807
|
+
index = target;
|
|
808
|
+
}
|
|
809
|
+
return "continue";
|
|
810
|
+
}
|
|
811
|
+
if (event.type === "submit") {
|
|
812
|
+
return { done: enabledValues() };
|
|
813
|
+
}
|
|
814
|
+
return "continue";
|
|
815
|
+
}, (values) => {
|
|
816
|
+
const labels = choices.filter((choice) => values.includes(choice.value)).map((choice) => choice.value);
|
|
817
|
+
return `${message} ${labels.length > 0 ? labels.join(", ") : "none"}`;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/prompt.ts
|
|
822
|
+
var EXTRA_CHOICES = [
|
|
823
|
+
{ value: "mfa", label: "mfa: authenticator challenge + setup pages" },
|
|
824
|
+
{ value: "emailVerification", label: "email-verification: signed links + /email/verify" },
|
|
825
|
+
{ value: "scim", label: "scim: /Users adapter" },
|
|
826
|
+
{ value: "metrics", label: "metrics: Prometheus token" }
|
|
827
|
+
];
|
|
482
828
|
function isInteractive(flags) {
|
|
483
829
|
if (flags.yes || flags.noInteractive) {
|
|
484
830
|
return false;
|
|
485
831
|
}
|
|
486
832
|
return Boolean(input.isTTY && output.isTTY);
|
|
487
833
|
}
|
|
834
|
+
function canUseRawKeys() {
|
|
835
|
+
return Boolean(input.isTTY && typeof input.setRawMode === "function");
|
|
836
|
+
}
|
|
488
837
|
function createReadlinePrompter() {
|
|
489
838
|
const rl = createInterface({ input, output });
|
|
839
|
+
const io = { input, output };
|
|
490
840
|
return {
|
|
491
841
|
async question(message, defaultValue) {
|
|
492
842
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
@@ -494,6 +844,15 @@ function createReadlinePrompter() {
|
|
|
494
844
|
return answer || defaultValue || "";
|
|
495
845
|
},
|
|
496
846
|
async confirm(message, defaultValue = false) {
|
|
847
|
+
if (canUseRawKeys()) {
|
|
848
|
+
rl.pause();
|
|
849
|
+
try {
|
|
850
|
+
return await promptConfirm(message, defaultValue, io);
|
|
851
|
+
} finally {
|
|
852
|
+
input.setRawMode?.(false);
|
|
853
|
+
rl.resume();
|
|
854
|
+
}
|
|
855
|
+
}
|
|
497
856
|
const hint = defaultValue ? "Y/n" : "y/N";
|
|
498
857
|
const answer = (await rl.question(`${message} (${hint}): `)).trim().toLowerCase();
|
|
499
858
|
if (!answer) {
|
|
@@ -502,6 +861,15 @@ function createReadlinePrompter() {
|
|
|
502
861
|
return answer === "y" || answer === "yes";
|
|
503
862
|
},
|
|
504
863
|
async select(message, choices, defaultValue) {
|
|
864
|
+
if (canUseRawKeys()) {
|
|
865
|
+
rl.pause();
|
|
866
|
+
try {
|
|
867
|
+
return await promptSelect(message, choices, defaultValue, io);
|
|
868
|
+
} finally {
|
|
869
|
+
input.setRawMode?.(false);
|
|
870
|
+
rl.resume();
|
|
871
|
+
}
|
|
872
|
+
}
|
|
505
873
|
console.log(message);
|
|
506
874
|
for (const [index, choice] of choices.entries()) {
|
|
507
875
|
const marker = choice.value === defaultValue ? "*" : " ";
|
|
@@ -520,11 +888,66 @@ function createReadlinePrompter() {
|
|
|
520
888
|
const match = choices.find((choice) => choice.value === answer || choice.label === answer);
|
|
521
889
|
return match?.value ?? defaultValue;
|
|
522
890
|
},
|
|
891
|
+
async multiSelect(message, choices) {
|
|
892
|
+
if (canUseRawKeys()) {
|
|
893
|
+
rl.pause();
|
|
894
|
+
try {
|
|
895
|
+
return await promptMultiSelect(message, choices, io);
|
|
896
|
+
} finally {
|
|
897
|
+
input.setRawMode?.(false);
|
|
898
|
+
rl.resume();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const enabled = new Set(choices.filter((choice) => choice.enabled).map((choice) => choice.value));
|
|
902
|
+
console.log(`${message} (yes/no each)`);
|
|
903
|
+
for (const choice of choices) {
|
|
904
|
+
const hint = enabled.has(choice.value) ? "Y/n" : "y/N";
|
|
905
|
+
const answer = (await rl.question(` ${choice.label} (${hint}): `)).trim().toLowerCase();
|
|
906
|
+
if (!answer) {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (answer === "y" || answer === "yes") {
|
|
910
|
+
enabled.add(choice.value);
|
|
911
|
+
} else if (answer === "n" || answer === "no") {
|
|
912
|
+
enabled.delete(choice.value);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return [...enabled];
|
|
916
|
+
},
|
|
523
917
|
close() {
|
|
524
918
|
rl.close();
|
|
525
919
|
}
|
|
526
920
|
};
|
|
527
921
|
}
|
|
922
|
+
function extrasStillToAsk(layers, flags) {
|
|
923
|
+
return EXTRA_CHOICES.filter((choice) => {
|
|
924
|
+
if (flags.extras[choice.value] !== undefined) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
return extraApplies(choice.value, layers.auth);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
async function promptExtras(prompter, extras, layers, flags) {
|
|
931
|
+
const choices = extrasStillToAsk(layers, flags);
|
|
932
|
+
if (choices.length === 0) {
|
|
933
|
+
return extras;
|
|
934
|
+
}
|
|
935
|
+
const picked = new Set(await prompter.multiSelect("Extras", choices.map((choice) => ({
|
|
936
|
+
value: choice.value,
|
|
937
|
+
label: choice.label,
|
|
938
|
+
enabled: extras[choice.value]
|
|
939
|
+
}))));
|
|
940
|
+
const next = { ...extras };
|
|
941
|
+
for (const choice of choices) {
|
|
942
|
+
next[choice.value] = picked.has(choice.value);
|
|
943
|
+
}
|
|
944
|
+
for (const choice of EXTRA_CHOICES) {
|
|
945
|
+
if (!extraApplies(choice.value, layers.auth) && flags.extras[choice.value] === undefined) {
|
|
946
|
+
next[choice.value] = false;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return next;
|
|
950
|
+
}
|
|
528
951
|
async function promptLayers(flags, prompter) {
|
|
529
952
|
const layers = applyFlagOverrides(defaultLayers(), flags);
|
|
530
953
|
layers.frontend = await prompter.select("Frontend", [
|
|
@@ -572,16 +995,10 @@ async function promptLayers(flags, prompter) {
|
|
|
572
995
|
{ value: "log", label: "log: print messages" },
|
|
573
996
|
{ value: "smtp", label: "smtp" }
|
|
574
997
|
], layers.mail);
|
|
575
|
-
if (layers.frontend === "spa-react" || layers.frontend === "hybrid") {
|
|
998
|
+
if (flags.spaPrefix === undefined && (layers.frontend === "spa-react" || layers.frontend === "hybrid")) {
|
|
576
999
|
layers.spaPrefix = await prompter.question("SPA prefix", layers.spaPrefix);
|
|
577
1000
|
}
|
|
578
|
-
|
|
579
|
-
if (askExtras) {
|
|
580
|
-
layers.extras.mfa = await prompter.confirm("Authenticator MFA (cookie challenge + setup pages)?", layers.extras.mfa);
|
|
581
|
-
layers.extras.emailVerification = await prompter.confirm("Email verification (signed links + /email/verify)?", layers.extras.emailVerification);
|
|
582
|
-
layers.extras.scim = await prompter.confirm("SCIM /Users adapter?", layers.extras.scim);
|
|
583
|
-
layers.extras.metrics = await prompter.confirm("Metrics token?", layers.extras.metrics);
|
|
584
|
-
}
|
|
1001
|
+
layers.extras = await promptExtras(prompter, layers.extras, layers, flags);
|
|
585
1002
|
if (!dockerFlagsProvided(flags)) {
|
|
586
1003
|
layers.docker = await promptDockerLayer(prompter, layers);
|
|
587
1004
|
}
|
|
@@ -2102,6 +2519,8 @@ Cookie name is \`strata_session\`. Forms send CSRF as \`_token\`.
|
|
|
2102
2519
|
Opaque token login: \`POST /api/v1/auth/login\` with \`{ "email", "password" }\`. Register: \`POST /api/v1/auth/register\`. Forgot/reset: \`POST /api/v1/auth/forgot-password\` and signed \`POST /api/v1/auth/reset-password\`. Send \`Authorization: Bearer\` after login.
|
|
2103
2520
|
` : ""}${authUsesJwt(layers.auth) ? `
|
|
2104
2521
|
JWT mint: \`POST /api/auth/token\` with email and password. Short-lived. Not a portal session.
|
|
2522
|
+
` : ""}${layers.extras.metrics ? `
|
|
2523
|
+
Prometheus scrape: \`GET /metrics\`. Production requires \`Authorization: Bearer <METRICS_TOKEN>\`.
|
|
2105
2524
|
` : ""}
|
|
2106
2525
|
## Production
|
|
2107
2526
|
|
|
@@ -2781,6 +3200,10 @@ export { starterProviders };
|
|
|
2781
3200
|
function renderCreateAppTs(layers) {
|
|
2782
3201
|
const ensureLine = needsEnsure(layers) ? `import { ensureAppDatabase } from "./ensureDatabase.ts";
|
|
2783
3202
|
` : "";
|
|
3203
|
+
const metricsImport = layers.extras.metrics ? `import { createMetricsRoutes } from "@getstrata/bootstrap/metricsRoutes";
|
|
3204
|
+
` : "";
|
|
3205
|
+
const metricsSpread = layers.extras.metrics ? `
|
|
3206
|
+
...createMetricsRoutes(),` : "";
|
|
2784
3207
|
return `import { join } from "node:path";
|
|
2785
3208
|
import "./preload.ts";
|
|
2786
3209
|
import { runProviderPhase } from "@getstrata/bootstrap/context";
|
|
@@ -2800,8 +3223,7 @@ import {
|
|
|
2800
3223
|
ensureModulesLoaded,
|
|
2801
3224
|
} from "@getstrata/bootstrap/discoverModules";
|
|
2802
3225
|
import { createHealthRoutes } from "@getstrata/bootstrap/health";
|
|
2803
|
-
import {
|
|
2804
|
-
import { assertProductionSecrets } from "@getstrata/bootstrap/secretsGuard";
|
|
3226
|
+
${metricsImport}import { assertProductionSecrets } from "@getstrata/bootstrap/secretsGuard";
|
|
2805
3227
|
import { createWebServer } from "@getstrata/bootstrap/web/server";
|
|
2806
3228
|
import { setActiveApplicationContext } from "@getstrata/core/runtime/applicationRegistry";
|
|
2807
3229
|
import { migrate } from "../db/migrate.ts";
|
|
@@ -2881,8 +3303,7 @@ ${needsEnsure(layers) ? ` await ensureAppDatabase();
|
|
|
2881
3303
|
|
|
2882
3304
|
const routes = mergeSpaRoutes(context.dependencies, {
|
|
2883
3305
|
...createHealthRoutes(context.dependencies),
|
|
2884
|
-
...buildRoutes(context.dependencies)
|
|
2885
|
-
...createMetricsRoutes(),
|
|
3306
|
+
...buildRoutes(context.dependencies),${metricsSpread}
|
|
2886
3307
|
}, {
|
|
2887
3308
|
distDirectory: join(import.meta.dir, "../../frontend/dist"),
|
|
2888
3309
|
});
|