storyshelf 0.3.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,66 +14,348 @@ const PROJECT_PROMPTS = [{
14
14
  message: "Directory?",
15
15
  initial: (prev) => `./${prev}`
16
16
  }];
17
- const INFRA_PROMPTS = [
17
+ const DEPLOY_TARGET_PROMPT = {
18
+ type: "select",
19
+ name: "deployTarget",
20
+ message: "Deploy target?",
21
+ choices: [
22
+ {
23
+ title: "Local (bare node)",
24
+ value: "local"
25
+ },
26
+ {
27
+ title: "Docker (compose)",
28
+ value: "docker"
29
+ },
30
+ {
31
+ title: "AWS (ECS + S3 + SQS + Postgres + Cognito)",
32
+ value: "aws"
33
+ },
34
+ {
35
+ title: "Azure (Container Apps + Blob + Queues/Service Bus + Postgres + Entra)",
36
+ value: "azure"
37
+ },
38
+ {
39
+ title: "Google Cloud (Cloud Run + GCS + Pub/Sub + Postgres + Identity Platform)",
40
+ value: "gcp"
41
+ }
42
+ ]
43
+ };
44
+ /**
45
+ * Curated shortlists for the local/docker path — one per engine plus the
46
+ * zero-config default first. Everything else lives behind the advanced
47
+ * toggle so newcomers pick from 4 databases, not 14.
48
+ */
49
+ const CURATED_DATABASE_CHOICES = [
50
+ {
51
+ title: "SQLite (node:sqlite, local)",
52
+ value: "sqlite"
53
+ },
54
+ {
55
+ title: "Postgres (postgres.js, default)",
56
+ value: "postgres"
57
+ },
58
+ {
59
+ title: "MySQL / MariaDB (mysql2)",
60
+ value: "mysql"
61
+ },
62
+ {
63
+ title: "PGlite (embedded WASM Postgres)",
64
+ value: "pglite"
65
+ }
66
+ ];
67
+ const ADVANCED_DATABASE_CHOICES = [
68
+ {
69
+ title: "Turso (libSQL, serverless)",
70
+ value: "turso"
71
+ },
72
+ {
73
+ title: "better-sqlite3 (native, local)",
74
+ value: "better-sqlite3"
75
+ },
76
+ {
77
+ title: "Bun SQLite (bun:sqlite)",
78
+ value: "bun-sqlite"
79
+ },
80
+ {
81
+ title: "Cloudflare D1 (serverless edge)",
82
+ value: "d1"
83
+ },
84
+ {
85
+ title: "Postgres pg (node-postgres, pooler-safe)",
86
+ value: "pg"
87
+ },
88
+ {
89
+ title: "Neon (serverless Postgres)",
90
+ value: "neon"
91
+ },
92
+ {
93
+ title: "Neon HTTP (fetch, edge)",
94
+ value: "neon-http"
95
+ },
96
+ {
97
+ title: "Vercel Postgres",
98
+ value: "vercel"
99
+ },
100
+ {
101
+ title: "PlanetScale (serverless MySQL)",
102
+ value: "planetscale"
103
+ },
104
+ {
105
+ title: "TiDB Serverless (MySQL)",
106
+ value: "tidb"
107
+ }
108
+ ];
109
+ const CURATED_STORAGE_CHOICES = [{
110
+ title: "Local filesystem",
111
+ value: "local"
112
+ }, {
113
+ title: "S3-compatible",
114
+ value: "s3"
115
+ }];
116
+ const ADVANCED_STORAGE_CHOICES = [{
117
+ title: "Azure Blob Storage",
118
+ value: "azure"
119
+ }, {
120
+ title: "GCS (Google Cloud Storage)",
121
+ value: "gcs"
122
+ }];
123
+ const CURATED_QUEUE_CHOICES = [{
124
+ title: "In-memory (single server)",
125
+ value: "memory"
126
+ }, {
127
+ title: "Redis (self-hosted, Docker Compose)",
128
+ value: "redis"
129
+ }];
130
+ const ADVANCED_QUEUE_CHOICES = [
131
+ {
132
+ title: "SQS (AWS remote worker)",
133
+ value: "sqs"
134
+ },
135
+ {
136
+ title: "Azure Storage Queues (Azurite-local, remote worker)",
137
+ value: "azure-storage-queues"
138
+ },
139
+ {
140
+ title: "Azure Service Bus (native dead-lettering, remote worker)",
141
+ value: "azure-service-bus"
142
+ },
143
+ {
144
+ title: "GCP Pub/Sub (emulator-local, remote worker)",
145
+ value: "gcp-pubsub"
146
+ }
147
+ ];
148
+ /** Opt-in to the full adapter matrix on the local/docker path. */
149
+ const ADVANCED_ADAPTERS_PROMPT = {
150
+ type: "confirm",
151
+ name: "advancedAdapters",
152
+ message: "Show all adapter options (serverless/edge presets)?",
153
+ initial: false
154
+ };
155
+ /** Database choices for the local/docker path. */
156
+ function databaseChoices(advanced) {
157
+ return advanced ? [...CURATED_DATABASE_CHOICES, ...ADVANCED_DATABASE_CHOICES] : [...CURATED_DATABASE_CHOICES];
158
+ }
159
+ /** Storage choices for the local/docker path. */
160
+ function storageChoices(advanced) {
161
+ return advanced ? [...CURATED_STORAGE_CHOICES, ...ADVANCED_STORAGE_CHOICES] : [...CURATED_STORAGE_CHOICES];
162
+ }
163
+ /** Capture-queue choices for the local/docker path. */
164
+ function queueChoices(advanced) {
165
+ return advanced ? [...CURATED_QUEUE_CHOICES, ...ADVANCED_QUEUE_CHOICES] : [...CURATED_QUEUE_CHOICES];
166
+ }
167
+ const AUTH_SELECT = {
168
+ type: "select",
169
+ name: "auth",
170
+ message: "Which auth?",
171
+ choices: [
172
+ {
173
+ title: "None",
174
+ value: "none"
175
+ },
176
+ {
177
+ title: "Local accounts",
178
+ value: "password"
179
+ },
180
+ {
181
+ title: "OAuth/OIDC",
182
+ value: "oauth"
183
+ }
184
+ ]
185
+ };
186
+ const GIT_SELECT = {
187
+ type: "select",
188
+ name: "git",
189
+ message: "Which git provider?",
190
+ choices: [
191
+ {
192
+ title: "None",
193
+ value: "none"
194
+ },
195
+ {
196
+ title: "GitHub",
197
+ value: "github"
198
+ },
199
+ {
200
+ title: "GitLab",
201
+ value: "gitlab"
202
+ }
203
+ ]
204
+ };
205
+ const DOCKER_CONFIRM = {
206
+ type: "confirm",
207
+ name: "docker",
208
+ message: "Generate Docker files?",
209
+ initial: true
210
+ };
211
+ const NOTIFICATIONS_CONFIRM = {
212
+ type: "confirm",
213
+ name: "notifications",
214
+ message: "Enable email + chat notifications (Slack/Teams/email channels)?",
215
+ initial: true
216
+ };
217
+ /**
218
+ * Curated prompts for the local/docker path — deploy target is asked first
219
+ * (phase 1), then these (phase 2) with the shortlist or full matrix behind
220
+ * the advanced toggle.
221
+ */
222
+ function localInfraPrompts(advanced) {
223
+ return [
224
+ {
225
+ type: "select",
226
+ name: "database",
227
+ message: "Which database?",
228
+ choices: databaseChoices(advanced)
229
+ },
230
+ {
231
+ type: "select",
232
+ name: "storage",
233
+ message: "Which storage?",
234
+ choices: storageChoices(advanced)
235
+ },
236
+ AUTH_SELECT,
237
+ GIT_SELECT,
238
+ {
239
+ type: "select",
240
+ name: "queue",
241
+ message: "Which capture queue?",
242
+ choices: queueChoices(advanced)
243
+ },
244
+ DOCKER_CONFIRM,
245
+ NOTIFICATIONS_CONFIRM
246
+ ];
247
+ }
248
+ const WORKER_INFRA_PROMPTS = [
18
249
  {
19
250
  type: "select",
20
251
  name: "database",
21
252
  message: "Which database?",
22
- choices: [{
23
- title: "SQLite (local)",
24
- value: "sqlite"
25
- }, {
26
- title: "Turso (serverless)",
27
- value: "turso"
28
- }]
253
+ choices: [
254
+ {
255
+ title: "SQLite (node:sqlite, local)",
256
+ value: "sqlite"
257
+ },
258
+ {
259
+ title: "Turso (libSQL, serverless)",
260
+ value: "turso"
261
+ },
262
+ {
263
+ title: "better-sqlite3 (native, local)",
264
+ value: "better-sqlite3"
265
+ },
266
+ {
267
+ title: "Bun SQLite (bun:sqlite)",
268
+ value: "bun-sqlite"
269
+ },
270
+ {
271
+ title: "Cloudflare D1 (serverless edge)",
272
+ value: "d1"
273
+ },
274
+ {
275
+ title: "Postgres (postgres.js, default)",
276
+ value: "postgres"
277
+ },
278
+ {
279
+ title: "Postgres pg (node-postgres)",
280
+ value: "pg"
281
+ },
282
+ {
283
+ title: "Neon (serverless Postgres)",
284
+ value: "neon"
285
+ },
286
+ {
287
+ title: "Neon HTTP (fetch)",
288
+ value: "neon-http"
289
+ },
290
+ {
291
+ title: "Vercel Postgres",
292
+ value: "vercel"
293
+ },
294
+ {
295
+ title: "PGlite (embedded)",
296
+ value: "pglite"
297
+ },
298
+ {
299
+ title: "MySQL / MariaDB",
300
+ value: "mysql"
301
+ },
302
+ {
303
+ title: "PlanetScale",
304
+ value: "planetscale"
305
+ },
306
+ {
307
+ title: "TiDB Serverless",
308
+ value: "tidb"
309
+ }
310
+ ]
29
311
  },
30
312
  {
31
313
  type: "select",
32
314
  name: "storage",
33
315
  message: "Which storage?",
34
- choices: [{
35
- title: "Local filesystem",
36
- value: "local"
37
- }, {
38
- title: "S3-compatible",
39
- value: "s3"
40
- }]
41
- },
42
- {
43
- type: "select",
44
- name: "auth",
45
- message: "Which auth?",
46
316
  choices: [
47
317
  {
48
- title: "None",
49
- value: "none"
318
+ title: "Local filesystem",
319
+ value: "local"
50
320
  },
51
321
  {
52
- title: "Shared password",
53
- value: "password"
322
+ title: "S3-compatible",
323
+ value: "s3"
54
324
  },
55
325
  {
56
- title: "OAuth/OIDC",
57
- value: "oauth"
326
+ title: "GCS (Google Cloud Storage)",
327
+ value: "gcs"
58
328
  }
59
329
  ]
60
330
  },
61
331
  {
62
332
  type: "select",
63
- name: "git",
64
- message: "Which git provider?",
333
+ name: "queue",
334
+ message: "Which queue?",
65
335
  choices: [
66
336
  {
67
- title: "None",
68
- value: "none"
337
+ title: "SQS (AWS)",
338
+ value: "sqs"
339
+ },
340
+ {
341
+ title: "Redis (self-hosted)",
342
+ value: "redis"
69
343
  },
70
344
  {
71
- title: "GitHub",
72
- value: "github"
345
+ title: "Azure Storage Queues",
346
+ value: "azure-storage-queues"
73
347
  },
74
348
  {
75
- title: "GitLab",
76
- value: "gitlab"
349
+ title: "Azure Service Bus",
350
+ value: "azure-service-bus"
351
+ },
352
+ {
353
+ title: "GCP Pub/Sub",
354
+ value: "gcp-pubsub"
355
+ },
356
+ {
357
+ title: "In-memory (local dev only)",
358
+ value: "memory"
77
359
  }
78
360
  ]
79
361
  },
@@ -84,5 +366,98 @@ const INFRA_PROMPTS = [
84
366
  initial: true
85
367
  }
86
368
  ];
369
+ /** Follow-up prompts for the GCP deploy target (single prompts call). */
370
+ const GCP_INFRA_PROMPTS = [
371
+ {
372
+ type: "text",
373
+ name: "gcpProjectId",
374
+ message: "GCP project ID?",
375
+ initial: ""
376
+ },
377
+ {
378
+ type: "text",
379
+ name: "gcpLocation",
380
+ message: "GCP region?",
381
+ initial: "us-central1"
382
+ },
383
+ {
384
+ type: "text",
385
+ name: "domainName",
386
+ message: "Public domain for the app? (empty skips DNS)",
387
+ initial: ""
388
+ },
389
+ {
390
+ type: "text",
391
+ name: "identityTenant",
392
+ message: "Identity Platform tenant display name? (empty skips it)",
393
+ initial: ""
394
+ }
395
+ ];
396
+ /** Follow-up prompts for the Azure deploy target (single prompts call). */
397
+ const AZURE_INFRA_PROMPTS = [
398
+ {
399
+ type: "text",
400
+ name: "azureLocation",
401
+ message: "Azure region?",
402
+ initial: "eastus"
403
+ },
404
+ {
405
+ type: "select",
406
+ name: "azureQueueBackend",
407
+ message: "Azure capture queue backend?",
408
+ choices: [{
409
+ title: "Storage Queues (Azurite-testable, visibility-timeout)",
410
+ value: "storage-queues"
411
+ }, {
412
+ title: "Service Bus (native dead-lettering, SQS + DLQ parity)",
413
+ value: "service-bus"
414
+ }]
415
+ },
416
+ {
417
+ type: "text",
418
+ name: "domainName",
419
+ message: "Public domain for the app? (empty skips DNS)",
420
+ initial: ""
421
+ },
422
+ {
423
+ type: "text",
424
+ name: "entraTenantId",
425
+ message: "Entra tenant ID for the app registration? (empty skips it)",
426
+ initial: ""
427
+ }
428
+ ];
429
+ /** Follow-up prompts for the AWS deploy target (single prompts call). */
430
+ const AWS_INFRA_PROMPTS = [
431
+ {
432
+ type: "text",
433
+ name: "awsRegion",
434
+ message: "AWS region?",
435
+ initial: "us-east-1"
436
+ },
437
+ {
438
+ type: "select",
439
+ name: "dbEngine",
440
+ message: "Postgres engine?",
441
+ choices: [{
442
+ title: "RDS (VPC-isolated, battle-tested)",
443
+ value: "rds"
444
+ }, {
445
+ title: "Aurora DSQL (serverless)",
446
+ value: "dsql"
447
+ }]
448
+ },
449
+ {
450
+ type: "text",
451
+ name: "domainName",
452
+ message: "Public domain for the ALB? (empty leaves it HTTP-only)",
453
+ initial: ""
454
+ },
455
+ {
456
+ type: "text",
457
+ name: "samlMetadataUrl",
458
+ message: "SAML metadata URL for Cognito federation? (empty skips it)",
459
+ initial: ""
460
+ }
461
+ ];
87
462
  //#endregion
88
- export { INFRA_PROMPTS, PROJECT_PROMPTS };
463
+ export { ADVANCED_ADAPTERS_PROMPT, ADVANCED_DATABASE_CHOICES, ADVANCED_QUEUE_CHOICES, ADVANCED_STORAGE_CHOICES, AWS_INFRA_PROMPTS, AZURE_INFRA_PROMPTS, CURATED_DATABASE_CHOICES, CURATED_QUEUE_CHOICES, CURATED_STORAGE_CHOICES, DEPLOY_TARGET_PROMPT, GCP_INFRA_PROMPTS, PROJECT_PROMPTS, WORKER_INFRA_PROMPTS, databaseChoices, localInfraPrompts, queueChoices, storageChoices };
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ import "node:path";
3
+ import "node:url";
4
+ import.meta.url;
5
+ import { detectPackageRunner, installCommand } from "../../config.js";
6
+ import { printLine } from "../../output.js";
7
+ import { join, resolve } from "node:path";
8
+ import { spawn } from "node:child_process";
9
+ import { access } from "node:fs/promises";
10
+ //#region src/commands/server/serve.ts
11
+ /** Server entry filenames tried in order. */
12
+ const SERVER_CANDIDATES = [
13
+ "server.ts",
14
+ "server.js",
15
+ "server.mjs",
16
+ "index.ts",
17
+ "index.js",
18
+ "index.mjs"
19
+ ];
20
+ /**
21
+ * Locate the server entry file in a scaffolded project.
22
+ * Tries `server.ts`, `server.js`, `server.mjs`, then `index.*` fallbacks.
23
+ *
24
+ * @param dir - Absolute path to the server project directory
25
+ * @returns Absolute path to the first entry that exists
26
+ * @throws If no candidate is found (suggests running `storyshelf server init`)
27
+ */
28
+ async function resolveServerFile(dir) {
29
+ for (const candidate of SERVER_CANDIDATES) {
30
+ const full = join(dir, candidate);
31
+ try {
32
+ await access(full);
33
+ return full;
34
+ } catch {
35
+ continue;
36
+ }
37
+ }
38
+ throw new Error(`No server entry found in ${dir} (looked for ${SERVER_CANDIDATES.join(", ")}). Run "storyshelf server init" first.`);
39
+ }
40
+ /** Node flags for an entry file (TypeScript needs the transform flag). */
41
+ function nodeArgs(file) {
42
+ return file.endsWith(".ts") ? ["--experimental-transform-types", file] : [file];
43
+ }
44
+ /** Forward SIGINT/SIGTERM to the child; returns a handle to detach and inspect. */
45
+ function relaySignals(child) {
46
+ let relayed = false;
47
+ const relay = (signal) => {
48
+ relayed = true;
49
+ child.kill(signal);
50
+ };
51
+ const onSigint = () => {
52
+ relay("SIGINT");
53
+ };
54
+ const onSigterm = () => {
55
+ relay("SIGTERM");
56
+ };
57
+ process.on("SIGINT", onSigint);
58
+ process.on("SIGTERM", onSigterm);
59
+ return {
60
+ stop: () => {
61
+ process.off("SIGINT", onSigint);
62
+ process.off("SIGTERM", onSigterm);
63
+ },
64
+ relayed: () => relayed
65
+ };
66
+ }
67
+ /** Resolve the child exit code (relay-terminated exits count as clean). */
68
+ function waitForExit(child, relay) {
69
+ return new Promise((resolvePromise, rejectPromise) => {
70
+ child.on("error", rejectPromise);
71
+ child.on("exit", (code, signal) => {
72
+ if (signal && relay.relayed()) resolvePromise(0);
73
+ else resolvePromise(code ?? 1);
74
+ });
75
+ });
76
+ }
77
+ /** Spawn a child server, forwarding signals and resolving its exit code. */
78
+ async function defaultSpawnProcess(command, args, options) {
79
+ const child = spawn(command, [...args], {
80
+ stdio: "inherit",
81
+ cwd: options.cwd,
82
+ env: options.env
83
+ });
84
+ const relay = relaySignals(child);
85
+ try {
86
+ return await waitForExit(child, relay);
87
+ } finally {
88
+ relay.stop();
89
+ }
90
+ }
91
+ async function warnIfUninstalled(dir) {
92
+ try {
93
+ await access(join(dir, "node_modules"));
94
+ } catch {
95
+ const runner = await detectPackageRunner(dir);
96
+ printLine(`Warning: no node_modules in ${dir} — run "${installCommand(runner)}" there first if the server fails to start.`);
97
+ }
98
+ }
99
+ /**
100
+ * Run an existing scaffolded server project.
101
+ *
102
+ * @param options - Directory and port override.
103
+ * @param deps - Injectable process spawner (test seam).
104
+ */
105
+ async function runServerServe(options, deps = {}) {
106
+ const dir = resolve(options.dir ?? process.cwd());
107
+ const file = await resolveServerFile(dir);
108
+ await warnIfUninstalled(dir);
109
+ printLine(`Starting server: ${file}`);
110
+ const code = await (deps.spawnProcess ?? defaultSpawnProcess)(process.execPath, nodeArgs(file), {
111
+ cwd: dir,
112
+ env: childEnv(options)
113
+ });
114
+ if (code !== 0) throw new Error(`Server exited with code ${code}`);
115
+ }
116
+ /** Child environment: inherit the parent, applying the port override. */
117
+ function childEnv(options) {
118
+ const env = { ...process.env };
119
+ if (options.port !== void 0) env["PORT"] = options.port;
120
+ return env;
121
+ }
122
+ //#endregion
123
+ export { resolveServerFile, runServerServe };