@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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +124 -54
  3. 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
- return neededDockerServices(layers).filter((name) => layers.docker.services[name]);
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
- return { enabled: true, services: enableDockerServices(needed) };
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 selected = flags.dockerServices.filter((name) => needed.includes(name));
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(input.isTTY && output.isTTY);
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 rl = createInterface({ input, output });
839
- const io = { input, output };
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 = (await rl.question(`${message}${suffix}: `)).trim();
896
+ const answer = await askLine(`${message}${suffix}: `);
844
897
  return answer || defaultValue || "";
845
898
  },
846
899
  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
- }
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 rl.question(`${message} (${hint}): `)).trim().toLowerCase();
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
- 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
- }
912
+ const current = stdio();
913
+ if (hasRawMode(current.input)) {
914
+ return promptSelect(message, choices, defaultValue, current);
872
915
  }
873
- console.log(message);
916
+ current.output.write(`${message}
917
+ `);
874
918
  for (const [index, choice] of choices.entries()) {
875
919
  const marker = choice.value === defaultValue ? "*" : " ";
876
- console.log(` ${index + 1}) ${marker} ${choice.label}`);
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 = (await rl.question(`Choose [${defaultIndex}]: `)).trim();
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
- if (canUseRawKeys()) {
893
- rl.pause();
894
- try {
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
- console.log(`${message} (yes/no each)`);
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 rl.question(` ${choice.label} (${hint}): `)).trim().toLowerCase();
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
- rl.close();
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 ph2 = layers.database === "postgres";
2808
- const notePlaceholder = ph2 ? "$1" : "?";
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 ? ph2 ? "$1, $2, $3, $4, $5), ($6, $7, $8, $9, $10" : "?, ?, ?, ?, ?), (?, ?, ?, ?, ?" : ph2 ? "$1, $2, $3, $4), ($5, $6, $7, $8" : "?, ?, ?, ?), (?, ?, ?, ?";
2812
- const adminFlag = ph2 ? "false" : "0";
2813
- const adminTrue = ph2 ? "true" : "1";
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 (${ph2 ? "$1, $2, $3" : "?, ?, ?"})",
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 = neededTools.length === 0 ? "none" : dockerOn.length > 0 ? dockerOn.join("+") : "local";
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/starter",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Interactive create-strata wizard. Choose each layer; one database engine.",
5
5
  "type": "module",
6
6
  "license": "MIT",