@getstrata/starter 0.1.11 → 0.1.13
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 +2 -2
- package/dist/cli.js +124 -54
- 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. Extras (MFA, email verification, SCIM, metrics) are toggled one by one. 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. In a terminal, lists use ↑/↓ and Enter (or a number). 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
|
|
|
@@ -12,7 +12,7 @@ bunx create-strata html --frontend server-htmx --database postgres --auth cookie
|
|
|
12
12
|
|
|
13
13
|
Layer flags: `--frontend`, `--database`, `--auth`, `--tenancy`, `--cache`, `--queue`, `--mail`, `--spa-prefix`, plus extras (`--mfa`, `--scim`, `--metrics`, ...).
|
|
14
14
|
|
|
15
|
-
Pick **one** database engine. Docker Compose is optional and only includes services for the tools you selected (`--docker`, `--no-docker`, `--docker-services=postgres,redis`).
|
|
15
|
+
Pick **one** database engine. Docker Compose is optional and only includes services for the tools you selected (`--docker`, `--no-docker`, `--docker-services=postgres,redis`). `--docker` with Postgres or MySQL also writes Adminer.
|
|
16
16
|
|
|
17
17
|
## What you get
|
|
18
18
|
|
package/dist/cli.js
CHANGED
|
@@ -84,12 +84,13 @@ var TENANCY_DRIVERS = ["none", "column", "rls"];
|
|
|
84
84
|
var CACHE_DRIVERS = ["array", "redis"];
|
|
85
85
|
var QUEUE_DRIVERS = ["sync", "redis"];
|
|
86
86
|
var MAIL_DRIVERS = ["log", "smtp"];
|
|
87
|
-
var DOCKER_SERVICE_NAMES = ["postgres", "mysql", "redis", "mailpit"];
|
|
87
|
+
var DOCKER_SERVICE_NAMES = ["postgres", "mysql", "redis", "mailpit", "adminer"];
|
|
88
88
|
var DOCKER_SERVICE_LABELS = {
|
|
89
89
|
postgres: "Postgres",
|
|
90
90
|
mysql: "MySQL",
|
|
91
91
|
redis: "Redis",
|
|
92
|
-
mailpit: "SMTP (Mailpit)"
|
|
92
|
+
mailpit: "SMTP (Mailpit)",
|
|
93
|
+
adminer: "Adminer (database UI)"
|
|
93
94
|
};
|
|
94
95
|
function authUsesCookie(auth) {
|
|
95
96
|
return auth === "cookie" || auth.startsWith("cookie-");
|
|
@@ -122,7 +123,13 @@ function needsRedis(layers) {
|
|
|
122
123
|
return layers.cache === "redis" || layers.queue === "redis";
|
|
123
124
|
}
|
|
124
125
|
function emptyDockerServices() {
|
|
125
|
-
return { postgres: false, mysql: false, redis: false, mailpit: false };
|
|
126
|
+
return { postgres: false, mysql: false, redis: false, mailpit: false, adminer: false };
|
|
127
|
+
}
|
|
128
|
+
function dockerDatabaseService(database) {
|
|
129
|
+
if (database === "postgres" || database === "mysql") {
|
|
130
|
+
return database;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
126
133
|
}
|
|
127
134
|
function enableDockerServices(names) {
|
|
128
135
|
const services = emptyDockerServices();
|
|
@@ -151,7 +158,12 @@ function selectedDockerServices(layers) {
|
|
|
151
158
|
if (!layers.docker.enabled) {
|
|
152
159
|
return [];
|
|
153
160
|
}
|
|
154
|
-
|
|
161
|
+
const selected = neededDockerServices(layers).filter((name) => layers.docker.services[name]);
|
|
162
|
+
const databaseService = dockerDatabaseService(layers.database);
|
|
163
|
+
if (databaseService && selected.includes(databaseService) && layers.docker.services.adminer) {
|
|
164
|
+
selected.push("adminer");
|
|
165
|
+
}
|
|
166
|
+
return selected;
|
|
155
167
|
}
|
|
156
168
|
function reconcileDocker(layers) {
|
|
157
169
|
const selected = selectedDockerServices(layers);
|
|
@@ -168,7 +180,11 @@ function dockerLayerForNeeded(layers, enabled) {
|
|
|
168
180
|
if (!enabled || needed.length === 0) {
|
|
169
181
|
return { enabled: false, services: emptyDockerServices() };
|
|
170
182
|
}
|
|
171
|
-
|
|
183
|
+
const names = [...needed];
|
|
184
|
+
if (dockerDatabaseService(layers.database)) {
|
|
185
|
+
names.push("adminer");
|
|
186
|
+
}
|
|
187
|
+
return { enabled: true, services: enableDockerServices(names) };
|
|
172
188
|
}
|
|
173
189
|
|
|
174
190
|
// src/presets.ts
|
|
@@ -252,7 +268,7 @@ Options:
|
|
|
252
268
|
--extras Interactive extras list (MFA, email verification, SCIM, metrics)
|
|
253
269
|
--docker Write Docker Compose for every selected tool that needs a service
|
|
254
270
|
--no-docker Skip docker-compose.yml; use installs already on this machine
|
|
255
|
-
--docker-services Subset: postgres, mysql, redis, mailpit (comma-separated)
|
|
271
|
+
--docker-services Subset: postgres, mysql, redis, mailpit, adminer (comma-separated)
|
|
256
272
|
--force Replace an existing directory
|
|
257
273
|
--yes, --no-interactive
|
|
258
274
|
-h, --help
|
|
@@ -446,7 +462,13 @@ function applyDockerFlags(layers, flags) {
|
|
|
446
462
|
};
|
|
447
463
|
}
|
|
448
464
|
if (flags.dockerServices) {
|
|
449
|
-
const
|
|
465
|
+
const databaseService = dockerDatabaseService(layers.database);
|
|
466
|
+
const selected = flags.dockerServices.filter((name) => {
|
|
467
|
+
if (needed.includes(name)) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
return name === "adminer" && databaseService !== null && flags.dockerServices?.includes(databaseService) === true;
|
|
471
|
+
});
|
|
450
472
|
return {
|
|
451
473
|
...layers,
|
|
452
474
|
docker: {
|
|
@@ -486,10 +508,36 @@ function layersFromFlags(flags) {
|
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
// src/prompt.ts
|
|
489
|
-
import { stdin as input, stdout as output } from "process";
|
|
490
511
|
import { createInterface } from "readline/promises";
|
|
512
|
+
import tty2 from "tty";
|
|
491
513
|
|
|
492
514
|
// src/selectPrompt.ts
|
|
515
|
+
import tty from "tty";
|
|
516
|
+
function hasRawMode(input) {
|
|
517
|
+
return Boolean(input.isTTY && typeof input.setRawMode === "function");
|
|
518
|
+
}
|
|
519
|
+
var cachedFdIo;
|
|
520
|
+
function resolveSelectIo(preferred) {
|
|
521
|
+
if (preferred) {
|
|
522
|
+
return preferred;
|
|
523
|
+
}
|
|
524
|
+
const live = { input: process.stdin, output: process.stdout };
|
|
525
|
+
if (hasRawMode(live.input)) {
|
|
526
|
+
return live;
|
|
527
|
+
}
|
|
528
|
+
if (cachedFdIo && hasRawMode(cachedFdIo.input)) {
|
|
529
|
+
return cachedFdIo;
|
|
530
|
+
}
|
|
531
|
+
if (tty.isatty(0)) {
|
|
532
|
+
cachedFdIo = {
|
|
533
|
+
input: new tty.ReadStream(0),
|
|
534
|
+
output: tty.isatty(1) ? new tty.WriteStream(1) : process.stdout
|
|
535
|
+
};
|
|
536
|
+
return cachedFdIo;
|
|
537
|
+
}
|
|
538
|
+
return live;
|
|
539
|
+
}
|
|
540
|
+
|
|
493
541
|
class PromptCancelledError extends Error {
|
|
494
542
|
constructor() {
|
|
495
543
|
super("Cancelled");
|
|
@@ -829,54 +877,51 @@ function isInteractive(flags) {
|
|
|
829
877
|
if (flags.yes || flags.noInteractive) {
|
|
830
878
|
return false;
|
|
831
879
|
}
|
|
832
|
-
return Boolean(
|
|
833
|
-
}
|
|
834
|
-
function canUseRawKeys() {
|
|
835
|
-
return Boolean(input.isTTY && typeof input.setRawMode === "function");
|
|
880
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY || tty2.isatty(0) && tty2.isatty(1));
|
|
836
881
|
}
|
|
837
|
-
function createReadlinePrompter() {
|
|
838
|
-
const
|
|
839
|
-
|
|
882
|
+
function createReadlinePrompter(io) {
|
|
883
|
+
const stdio = () => resolveSelectIo(io);
|
|
884
|
+
async function askLine(message) {
|
|
885
|
+
const current = stdio();
|
|
886
|
+
const rl = createInterface({ input: current.input, output: current.output });
|
|
887
|
+
try {
|
|
888
|
+
return (await rl.question(message)).trim();
|
|
889
|
+
} finally {
|
|
890
|
+
rl.close();
|
|
891
|
+
}
|
|
892
|
+
}
|
|
840
893
|
return {
|
|
841
894
|
async question(message, defaultValue) {
|
|
842
895
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
843
|
-
const answer =
|
|
896
|
+
const answer = await askLine(`${message}${suffix}: `);
|
|
844
897
|
return answer || defaultValue || "";
|
|
845
898
|
},
|
|
846
899
|
async confirm(message, defaultValue = false) {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
return await promptConfirm(message, defaultValue, io);
|
|
851
|
-
} finally {
|
|
852
|
-
input.setRawMode?.(false);
|
|
853
|
-
rl.resume();
|
|
854
|
-
}
|
|
900
|
+
const current = stdio();
|
|
901
|
+
if (hasRawMode(current.input)) {
|
|
902
|
+
return promptConfirm(message, defaultValue, current);
|
|
855
903
|
}
|
|
856
904
|
const hint = defaultValue ? "Y/n" : "y/N";
|
|
857
|
-
const answer = (await
|
|
905
|
+
const answer = (await askLine(`${message} (${hint}): `)).toLowerCase();
|
|
858
906
|
if (!answer) {
|
|
859
907
|
return defaultValue;
|
|
860
908
|
}
|
|
861
909
|
return answer === "y" || answer === "yes";
|
|
862
910
|
},
|
|
863
911
|
async select(message, choices, defaultValue) {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
return await promptSelect(message, choices, defaultValue, io);
|
|
868
|
-
} finally {
|
|
869
|
-
input.setRawMode?.(false);
|
|
870
|
-
rl.resume();
|
|
871
|
-
}
|
|
912
|
+
const current = stdio();
|
|
913
|
+
if (hasRawMode(current.input)) {
|
|
914
|
+
return promptSelect(message, choices, defaultValue, current);
|
|
872
915
|
}
|
|
873
|
-
|
|
916
|
+
current.output.write(`${message}
|
|
917
|
+
`);
|
|
874
918
|
for (const [index, choice] of choices.entries()) {
|
|
875
919
|
const marker = choice.value === defaultValue ? "*" : " ";
|
|
876
|
-
|
|
920
|
+
current.output.write(` ${index + 1}) ${marker} ${choice.label}
|
|
921
|
+
`);
|
|
877
922
|
}
|
|
878
923
|
const defaultIndex = choices.findIndex((choice) => choice.value === defaultValue) + 1;
|
|
879
|
-
const answer =
|
|
924
|
+
const answer = await askLine(`Choose [${defaultIndex}]: `);
|
|
880
925
|
if (!answer) {
|
|
881
926
|
return defaultValue;
|
|
882
927
|
}
|
|
@@ -889,20 +934,16 @@ function createReadlinePrompter() {
|
|
|
889
934
|
return match?.value ?? defaultValue;
|
|
890
935
|
},
|
|
891
936
|
async multiSelect(message, choices) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
return await promptMultiSelect(message, choices, io);
|
|
896
|
-
} finally {
|
|
897
|
-
input.setRawMode?.(false);
|
|
898
|
-
rl.resume();
|
|
899
|
-
}
|
|
937
|
+
const current = stdio();
|
|
938
|
+
if (hasRawMode(current.input)) {
|
|
939
|
+
return promptMultiSelect(message, choices, current);
|
|
900
940
|
}
|
|
901
941
|
const enabled = new Set(choices.filter((choice) => choice.enabled).map((choice) => choice.value));
|
|
902
|
-
|
|
942
|
+
current.output.write(`${message} (yes/no each)
|
|
943
|
+
`);
|
|
903
944
|
for (const choice of choices) {
|
|
904
945
|
const hint = enabled.has(choice.value) ? "Y/n" : "y/N";
|
|
905
|
-
const answer = (await
|
|
946
|
+
const answer = (await askLine(` ${choice.label} (${hint}): `)).toLowerCase();
|
|
906
947
|
if (!answer) {
|
|
907
948
|
continue;
|
|
908
949
|
}
|
|
@@ -915,7 +956,7 @@ function createReadlinePrompter() {
|
|
|
915
956
|
return [...enabled];
|
|
916
957
|
},
|
|
917
958
|
close() {
|
|
918
|
-
|
|
959
|
+
return;
|
|
919
960
|
}
|
|
920
961
|
};
|
|
921
962
|
}
|
|
@@ -1025,7 +1066,14 @@ async function promptDockerLayer(prompter, layers) {
|
|
|
1025
1066
|
for (const name of needed) {
|
|
1026
1067
|
services[name] = await prompter.confirm(`Docker Compose for ${DOCKER_SERVICE_LABELS[name]}?`, true);
|
|
1027
1068
|
}
|
|
1069
|
+
const databaseService = dockerDatabaseService(layers.database);
|
|
1070
|
+
if (databaseService && services[databaseService]) {
|
|
1071
|
+
services.adminer = await prompter.confirm(`Docker Compose for ${DOCKER_SERVICE_LABELS.adminer}?`, true);
|
|
1072
|
+
}
|
|
1028
1073
|
const selected = needed.filter((name) => services[name]);
|
|
1074
|
+
if (services.adminer) {
|
|
1075
|
+
selected.push("adminer");
|
|
1076
|
+
}
|
|
1029
1077
|
return {
|
|
1030
1078
|
enabled: selected.length > 0,
|
|
1031
1079
|
services
|
|
@@ -2327,6 +2375,17 @@ function renderDockerCompose(projectName, layers) {
|
|
|
2327
2375
|
volumes:
|
|
2328
2376
|
- mysqldata:/var/lib/mysql`);
|
|
2329
2377
|
}
|
|
2378
|
+
if (selectedSet.has("adminer")) {
|
|
2379
|
+
const server = selectedSet.has("mysql") ? "mysql" : "postgres";
|
|
2380
|
+
services.push(` adminer:
|
|
2381
|
+
image: adminer:5.4.2
|
|
2382
|
+
environment:
|
|
2383
|
+
ADMINER_DEFAULT_SERVER: ${server}
|
|
2384
|
+
depends_on:
|
|
2385
|
+
- ${server}
|
|
2386
|
+
ports:
|
|
2387
|
+
- "8080:8080"`);
|
|
2388
|
+
}
|
|
2330
2389
|
if (selectedSet.has("redis")) {
|
|
2331
2390
|
services.push(` redis:
|
|
2332
2391
|
image: redis:7-alpine
|
|
@@ -2447,6 +2506,14 @@ function renderSupportingToolsReadme(layers) {
|
|
|
2447
2506
|
if (dockerOn.length > 0) {
|
|
2448
2507
|
const names = dockerOn.map((name) => DOCKER_SERVICE_LABELS[name]).join(", ");
|
|
2449
2508
|
lines.push(`Docker Compose includes ${names}.`, "", "```bash", "docker compose up -d", "```", "");
|
|
2509
|
+
if (dockerSet.has("adminer")) {
|
|
2510
|
+
const mysql = layers.database === "mysql";
|
|
2511
|
+
const system = mysql ? "MySQL" : "PostgreSQL";
|
|
2512
|
+
const server = mysql ? "mysql" : "postgres";
|
|
2513
|
+
const username = mysql ? "root" : "postgres";
|
|
2514
|
+
const password = mysql ? "root" : "postgres";
|
|
2515
|
+
lines.push(`Adminer: http://localhost:8080 (${system}, server \`${server}\`, username \`${username}\`, password \`${password}\`).`, "");
|
|
2516
|
+
}
|
|
2450
2517
|
}
|
|
2451
2518
|
if (localOn.length > 0) {
|
|
2452
2519
|
const names = localOn.map((name) => DOCKER_SERVICE_LABELS[name]).join(", ");
|
|
@@ -2804,13 +2871,13 @@ function renderMigrateTs(layers) {
|
|
|
2804
2871
|
}
|
|
2805
2872
|
const list = statements.map((sql) => ` \`${sql}\`,`).join(`
|
|
2806
2873
|
`);
|
|
2807
|
-
const
|
|
2808
|
-
const notePlaceholder =
|
|
2874
|
+
const ph = layers.database === "postgres";
|
|
2875
|
+
const notePlaceholder = ph ? "$1" : "?";
|
|
2809
2876
|
const verifyOn = layers.extras.emailVerification && authNeedsUsers(layers.auth);
|
|
2810
2877
|
const userColumns = verifyOn ? "name, email, password, is_admin, email_verified_at" : "name, email, password, is_admin";
|
|
2811
|
-
const userPlaceholders = verifyOn ?
|
|
2812
|
-
const adminFlag =
|
|
2813
|
-
const adminTrue =
|
|
2878
|
+
const userPlaceholders = verifyOn ? ph ? "$1, $2, $3, $4, $5), ($6, $7, $8, $9, $10" : "?, ?, ?, ?, ?), (?, ?, ?, ?, ?" : ph ? "$1, $2, $3, $4), ($5, $6, $7, $8" : "?, ?, ?, ?), (?, ?, ?, ?";
|
|
2879
|
+
const adminFlag = ph ? "false" : "0";
|
|
2880
|
+
const adminTrue = ph ? "true" : "1";
|
|
2814
2881
|
const verifiedNow = "new Date().toISOString()";
|
|
2815
2882
|
const userValues = verifyOn ? `["Demo User", "demo@example.com", password, ${adminFlag}, ${verifiedNow}, "Admin User", "admin@example.test", password, ${adminTrue}, ${verifiedNow}]` : `["Demo User", "demo@example.com", password, ${adminFlag}, "Admin User", "admin@example.test", password, ${adminTrue}]`;
|
|
2816
2883
|
const seedTenant = tenancyOn ? `
|
|
@@ -2819,7 +2886,7 @@ function renderMigrateTs(layers) {
|
|
|
2819
2886
|
);
|
|
2820
2887
|
if (Number(tenantCount) === 0) {
|
|
2821
2888
|
await sql.unsafe(
|
|
2822
|
-
"INSERT INTO tenant (slug, plan, region) VALUES (${
|
|
2889
|
+
"INSERT INTO tenant (slug, plan, region) VALUES (${ph ? "$1, $2, $3" : "?, ?, ?"})",
|
|
2823
2890
|
["default", "enterprise", "eu"],
|
|
2824
2891
|
);
|
|
2825
2892
|
}` : "";
|
|
@@ -3825,7 +3892,7 @@ function printNextSteps(projectName, layers, compose) {
|
|
|
3825
3892
|
const dockerOn = selectedDockerServices(layers);
|
|
3826
3893
|
const neededTools = neededDockerServices(layers);
|
|
3827
3894
|
const localOn = neededTools.filter((name) => !dockerOn.includes(name));
|
|
3828
|
-
const dockerSummary =
|
|
3895
|
+
const dockerSummary = dockerOn.length > 0 ? dockerOn.join("+") : neededTools.length === 0 ? "none" : "local";
|
|
3829
3896
|
console.log(`
|
|
3830
3897
|
Created Strata app in ${projectName}/
|
|
3831
3898
|
`);
|
|
@@ -3837,6 +3904,9 @@ Next steps:`);
|
|
|
3837
3904
|
if (compose) {
|
|
3838
3905
|
console.log(" docker compose up -d");
|
|
3839
3906
|
}
|
|
3907
|
+
if (dockerOn.includes("adminer")) {
|
|
3908
|
+
console.log(" Adminer: http://localhost:8080");
|
|
3909
|
+
}
|
|
3840
3910
|
if (localOn.length > 0) {
|
|
3841
3911
|
console.log(` Point env at local ${localOn.join(", ")} (see README).`);
|
|
3842
3912
|
}
|