storyshelf 0.6.1 → 0.6.2
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/commands/doctor.js +1 -1
- package/dist/commands/server/docker.js +55 -59
- package/dist/commands/server/init.js +141 -123
- package/dist/commands/server/prompts.js +9 -2
- package/dist/commands/server/serve.js +7 -4
- package/dist/commands/shared/detect-adapters.js +7 -2
- package/dist/commands/shared/scaffold.js +41 -0
- package/dist/commands/storybook-build.js +1 -1
- package/dist/commands/worker/init.js +22 -16
- package/dist/commands/worker/serve.js +6 -2
- package/dist/config.js +3 -3
- package/dist/index.js +31 -31
- package/package.json +6 -4
package/README.md
CHANGED
package/dist/commands/doctor.js
CHANGED
|
@@ -7,8 +7,8 @@ import { printLine } from "../output.js";
|
|
|
7
7
|
import { needsBuild, resolveBuildCommand } from "./storybook-build.js";
|
|
8
8
|
import { createClient } from "../client.js";
|
|
9
9
|
import { resolveConnection } from "./connection.js";
|
|
10
|
-
import { resolve } from "node:path";
|
|
11
10
|
import { access, readdir } from "node:fs/promises";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
12
|
import { gitRepoStatus } from "@storyshelf/affected";
|
|
13
13
|
//#region src/commands/doctor.ts
|
|
14
14
|
function mark(status) {
|
|
@@ -3,26 +3,29 @@ import "node:path";
|
|
|
3
3
|
import "node:url";
|
|
4
4
|
import.meta.url;
|
|
5
5
|
//#region src/commands/server/docker.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
6
|
+
/** Playwright base image: its bundled browsers must match the `playwright-core` pin. */
|
|
7
|
+
const PLAYWRIGHT_IMAGE = "mcr.microsoft.com/playwright:v1.63.0-noble";
|
|
8
|
+
/**
|
|
9
|
+
* Images run the TypeScript sources directly (Node strips types), exactly like
|
|
10
|
+
* `npm start`. Bundling is deliberately avoided: a single-file ESM bundle breaks
|
|
11
|
+
* CommonJS dependencies ("Dynamic require ... is not supported") and the vendored
|
|
12
|
+
* assets `@storyshelf/app` reads from disk next to its own modules.
|
|
13
|
+
*/
|
|
14
|
+
function imageLines(baseImage, entry, exposePort) {
|
|
15
|
+
return [
|
|
16
|
+
`FROM ${baseImage}`,
|
|
17
|
+
"WORKDIR /app",
|
|
18
|
+
"COPY package*.json ./",
|
|
19
|
+
"RUN npm install --omit=dev",
|
|
20
|
+
"COPY src/ ./src/",
|
|
21
|
+
...exposePort ? ["EXPOSE 3000", "ENV PORT=3000"] : [],
|
|
22
|
+
"ENV DATA_DIR=/data",
|
|
23
|
+
`CMD ["node", "${entry}"]`
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
const DOCKERFILE_LINES = imageLines(PLAYWRIGHT_IMAGE, "src/index.ts", true);
|
|
27
|
+
/** Server image without a browser: the capture worker runs in its own image. */
|
|
28
|
+
const SLIM_DOCKERFILE_LINES = imageLines("node:24-alpine", "src/index.ts", true);
|
|
26
29
|
const DOCKERIGNORE_LINES = [
|
|
27
30
|
"node_modules/",
|
|
28
31
|
".git/",
|
|
@@ -68,60 +71,53 @@ const COMPOSE_POSTGRES_VOLUMES_LINES = [" postgres-data:"];
|
|
|
68
71
|
function generateDockerfile() {
|
|
69
72
|
return DOCKERFILE_LINES.join("\n");
|
|
70
73
|
}
|
|
74
|
+
function generateSlimServerDockerfile() {
|
|
75
|
+
return SLIM_DOCKERFILE_LINES.join("\n");
|
|
76
|
+
}
|
|
71
77
|
function generateDockerignore() {
|
|
72
78
|
return DOCKERIGNORE_LINES.join("\n");
|
|
73
79
|
}
|
|
74
|
-
function
|
|
80
|
+
function composeLines(database, withWorker) {
|
|
75
81
|
return [
|
|
76
82
|
...COMPOSE_BASE_LINES,
|
|
77
83
|
" # Add your env vars here:",
|
|
78
84
|
" # - AUTH_PASSWORD=your-password",
|
|
79
85
|
...database === "turso" ? COMPOSE_TURSO_LINES : [],
|
|
80
86
|
...database === "postgres" ? COMPOSE_POSTGRES_LINES : [],
|
|
87
|
+
...withWorker ? workerComposeLines(database) : [],
|
|
81
88
|
...COMPOSE_VOLUMES_LINES,
|
|
82
89
|
...database === "postgres" ? COMPOSE_POSTGRES_VOLUMES_LINES : []
|
|
83
|
-
]
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
function generateComposeYaml(database = "sqlite") {
|
|
93
|
+
return composeLines(database, false).join("\n");
|
|
94
|
+
}
|
|
95
|
+
const WORKER_DOCKERFILE_LINES = imageLines(PLAYWRIGHT_IMAGE, "src/worker.ts", false);
|
|
96
|
+
function workerComposeLines(database) {
|
|
97
|
+
const postgres = database === "postgres";
|
|
98
|
+
return [
|
|
99
|
+
" worker:",
|
|
100
|
+
" build:",
|
|
101
|
+
" dockerfile: Dockerfile.worker",
|
|
102
|
+
" volumes:",
|
|
103
|
+
" - storyshelf-data:/data",
|
|
104
|
+
" environment:",
|
|
105
|
+
" - SECRET=change-me",
|
|
106
|
+
" - DATA_DIR=/data",
|
|
107
|
+
...postgres ? [" - DATABASE_URL=postgres://shelf:shelf@postgres:5432/shelf"] : [],
|
|
108
|
+
" - QUEUE_URL=${QUEUE_URL}",
|
|
109
|
+
" - WORKER_CONCURRENCY=2",
|
|
110
|
+
" depends_on:",
|
|
111
|
+
" storyshelf:",
|
|
112
|
+
" condition: service_started",
|
|
113
|
+
...postgres ? [" postgres:", " condition: service_healthy"] : []
|
|
114
|
+
];
|
|
84
115
|
}
|
|
85
|
-
const WORKER_DOCKERFILE_LINES = [
|
|
86
|
-
"FROM node:lts-alpine AS builder",
|
|
87
|
-
"WORKDIR /app",
|
|
88
|
-
"COPY package.json ./",
|
|
89
|
-
"RUN npm install",
|
|
90
|
-
"COPY worker.ts ./",
|
|
91
|
-
"RUN npx esbuild worker.ts --bundle --platform=node --format=esm \\",
|
|
92
|
-
" --external:playwright-core \\",
|
|
93
|
-
" --outfile=dist/worker.mjs",
|
|
94
|
-
"",
|
|
95
|
-
"FROM mcr.microsoft.com/playwright:v1.63.0-noble",
|
|
96
|
-
"WORKDIR /app",
|
|
97
|
-
"COPY --from=builder /app/dist/worker.mjs ./",
|
|
98
|
-
"COPY --from=builder /app/node_modules/playwright-core ./node_modules/playwright-core",
|
|
99
|
-
"ENV DATA_DIR=/data",
|
|
100
|
-
"CMD [\"node\", \"dist/worker.mjs\"]"
|
|
101
|
-
];
|
|
102
|
-
const WORKER_COMPOSE_LINES = [
|
|
103
|
-
" worker:",
|
|
104
|
-
" build:",
|
|
105
|
-
" dockerfile: Dockerfile.worker",
|
|
106
|
-
" volumes:",
|
|
107
|
-
" - storyshelf-data:/data",
|
|
108
|
-
" environment:",
|
|
109
|
-
" - SECRET=change-me",
|
|
110
|
-
" - DATA_DIR=/data",
|
|
111
|
-
" - QUEUE_URL=${QUEUE_URL}",
|
|
112
|
-
" - WORKER_CONCURRENCY=2",
|
|
113
|
-
" depends_on:",
|
|
114
|
-
" storyshelf:",
|
|
115
|
-
" condition: service_started"
|
|
116
|
-
];
|
|
117
116
|
function generateWorkerDockerfile() {
|
|
118
117
|
return WORKER_DOCKERFILE_LINES.join("\n");
|
|
119
118
|
}
|
|
120
|
-
function generateWorkerComposeSnippet() {
|
|
121
|
-
return WORKER_COMPOSE_LINES.join("\n");
|
|
122
|
-
}
|
|
123
119
|
function generateComposeYamlWithWorker(database = "sqlite") {
|
|
124
|
-
return
|
|
120
|
+
return composeLines(database, true).join("\n");
|
|
125
121
|
}
|
|
126
122
|
//#endregion
|
|
127
|
-
export { generateComposeYaml, generateComposeYamlWithWorker, generateDockerfile, generateDockerignore,
|
|
123
|
+
export { generateComposeYaml, generateComposeYamlWithWorker, generateDockerfile, generateDockerignore, generateSlimServerDockerfile, generateWorkerDockerfile };
|
|
@@ -5,13 +5,14 @@ import.meta.url;
|
|
|
5
5
|
import { detectPackageRunner, installCommand, startCommand } from "../../config.js";
|
|
6
6
|
import { printError, printLine } from "../../output.js";
|
|
7
7
|
import { detectInstalledAdapters } from "../shared/detect-adapters.js";
|
|
8
|
-
import {
|
|
8
|
+
import { SCAFFOLD_DEV_DEPENDENCIES, SERVER_ENTRY, WORKER_ENTRY, generateTsconfig, writeScaffoldFile } from "../shared/scaffold.js";
|
|
9
|
+
import { generateComposeYaml, generateComposeYamlWithWorker, generateDockerfile, generateDockerignore, generateSlimServerDockerfile, generateWorkerDockerfile } from "./docker.js";
|
|
9
10
|
import { ADVANCED_ADAPTERS_PROMPT, AWS_INFRA_PROMPTS, AZURE_INFRA_PROMPTS, DEPLOY_TARGET_PROMPT, GCP_INFRA_PROMPTS, PROJECT_PROMPTS, localInfraPrompts } from "./prompts.js";
|
|
10
11
|
import { generateAwsTerraformFiles } from "./terraform-aws.js";
|
|
11
12
|
import { generateAzureTerraformFiles } from "./terraform-azure.js";
|
|
12
13
|
import { generateGcpTerraformFiles } from "./terraform-gcp.js";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
14
14
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
15
16
|
import prompts from "prompts";
|
|
16
17
|
//#region src/commands/server/init.ts
|
|
17
18
|
/** Azure SDK pins (must match @storyshelf/queue-azure peerDependencies). */
|
|
@@ -143,20 +144,34 @@ const STORAGE_INIT = {
|
|
|
143
144
|
azure: `createAzureStorage({ container: "storybook", connectionString: process.env.AZURE_STORAGE_CONNECTION! })`,
|
|
144
145
|
gcs: `createGcsStorage({ bucket: process.env.GCS_BUCKET! })`
|
|
145
146
|
};
|
|
146
|
-
/** Whether the generated server wires notification providers. */
|
|
147
|
+
/** Whether the generated server wires notification providers (opt-in). */
|
|
147
148
|
function wantsNotifications(answers) {
|
|
148
|
-
return answers.notifications ??
|
|
149
|
+
return answers.notifications ?? false;
|
|
150
|
+
}
|
|
151
|
+
/** Whether the generated server/worker wire OpenTelemetry (opt-in). */
|
|
152
|
+
function wantsObservability(answers) {
|
|
153
|
+
return answers.observability ?? false;
|
|
154
|
+
}
|
|
155
|
+
const NAMED_IMPORT_RE = /^import \{ (?<names>[^}]+) \} from "(?<from>[^"]+)";$/u;
|
|
156
|
+
/** Merge named imports that share a module into one line (keeps first-seen order). */
|
|
157
|
+
function mergeImports(lines) {
|
|
158
|
+
const merged = /* @__PURE__ */ new Map();
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
const match = NAMED_IMPORT_RE.exec(line);
|
|
161
|
+
const key = match?.groups?.["from"] ?? line;
|
|
162
|
+
const names = match?.groups?.["names"]?.split(", ") ?? [];
|
|
163
|
+
merged.set(key, [...merged.get(key) ?? [], ...names]);
|
|
164
|
+
}
|
|
165
|
+
return [...merged].map(([from, names]) => names.length > 0 ? `import { ${names.join(", ")} } from "${from}";` : from);
|
|
149
166
|
}
|
|
150
167
|
function buildImports(answers) {
|
|
151
168
|
const imports = [
|
|
152
169
|
`import { serve } from "@hono/node-server";`,
|
|
153
170
|
`import { createShelfApp } from "@storyshelf/app";`,
|
|
154
|
-
`import { createShelfLogger } from "@storyshelf/core/logger";`,
|
|
155
|
-
`import { otelLogMixin } from "@storyshelf/observability";`,
|
|
156
|
-
`import { initObservabilityFromEnv } from "@storyshelf/observability/node";`,
|
|
157
171
|
DB_IMPORT[answers.database],
|
|
158
172
|
STORAGE_IMPORT[answers.storage]
|
|
159
173
|
];
|
|
174
|
+
if (wantsObservability(answers)) imports.push(`import { createShelfLogger } from "@storyshelf/core/logger";`, `import { otelLogMixin } from "@storyshelf/observability";`, `import { initObservabilityFromEnv } from "@storyshelf/observability/node";`);
|
|
160
175
|
if (answers.queue === "sqs") imports.push(`import { createSqsCaptureQueue } from "@storyshelf/queue-sqs";`);
|
|
161
176
|
if (answers.queue === "redis") imports.push(`import { createRedisCaptureQueue } from "@storyshelf/queue-redis";`);
|
|
162
177
|
if (answers.queue === "azure-storage-queues") imports.push(`import { createAzureStorageQueuesQueue } from "@storyshelf/queue-azure/storage-queues";`);
|
|
@@ -177,11 +192,10 @@ function buildImports(answers) {
|
|
|
177
192
|
if (answers.auth !== "none") imports.push(`import { createAuthSystemHook } from "@storyshelf/app";`);
|
|
178
193
|
}
|
|
179
194
|
if (answers.queue === "memory") imports.push(`import { createPlaywrightCaptureRunner } from "@storyshelf/runner-playwright";`);
|
|
180
|
-
return imports;
|
|
195
|
+
return mergeImports(imports);
|
|
181
196
|
}
|
|
182
197
|
function buildAdapterLines(answers) {
|
|
183
198
|
const lines = [
|
|
184
|
-
`// Adapters — swap these for your deployment`,
|
|
185
199
|
...dbPrelude(answers.database),
|
|
186
200
|
`const database = ${DB_INIT[answers.database]};`,
|
|
187
201
|
`const storage = ${STORAGE_INIT[answers.storage]};`
|
|
@@ -192,26 +206,25 @@ function buildAdapterLines(answers) {
|
|
|
192
206
|
if (answers.queue === "azure-service-bus") lines.push(`const captureQueue = createAzureServiceBusQueue({ queueName: "capture-jobs", connectionString: process.env.AZURE_SERVICE_BUS_CONNECTION! });`);
|
|
193
207
|
if (answers.queue === "gcp-pubsub") lines.push(`const captureQueue = createGcpPubSubQueue({ topic: "capture-jobs", subscription: "capture-jobs-worker", projectId: process.env.GOOGLE_CLOUD_PROJECT! });`);
|
|
194
208
|
if (answers.queue === "memory") lines.push(`const captureRunner = createPlaywrightCaptureRunner();`);
|
|
195
|
-
if (wantsNotifications(answers)) lines.push(`//
|
|
209
|
+
if (wantsNotifications(answers)) lines.push(`// SMTP sender from SMTP_* env (undefined keeps invites out-of-band).`, `const emailSender = smtpPresetFromEnv(process.env);`, `const emailNotifiers = emailSender`, ` ? [createEmailNotifier(emailSender, { from: process.env.SMTP_FROM })]`, ` : [];`);
|
|
196
210
|
return lines;
|
|
197
211
|
}
|
|
198
212
|
function buildRouterLines(answers) {
|
|
199
|
-
const lines = [
|
|
200
|
-
`//
|
|
213
|
+
const lines = wantsObservability(answers) ? [
|
|
214
|
+
`// Noop unless OTEL_EXPORTER_OTLP_ENDPOINT is set.`,
|
|
201
215
|
`const observability = await initObservabilityFromEnv();`,
|
|
202
216
|
`const shelfLogger = createShelfLogger({ mixin: otelLogMixin });`,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
` storage,`
|
|
207
|
-
];
|
|
217
|
+
``
|
|
218
|
+
] : [];
|
|
219
|
+
lines.push(`const app = createShelfApp({`, ` database,`, ` storage,`);
|
|
208
220
|
if (isRemoteQueue(answers.queue)) lines.push(` captureQueue,`);
|
|
209
221
|
else lines.push(` captureRunner,`);
|
|
210
222
|
if (wantsNotifications(answers)) lines.push(` notifiers: [...chatNotifiers, ...emailNotifiers],`);
|
|
211
|
-
if (answers.auth === "password") {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
if (answers.auth === "password") lines.push(` auth: createShelfAuth({`, ` db: database,`, ` secret: process.env.SECRET!,`, ` baseURL: process.env.PUBLIC_BASE_URL!,`, ...wantsNotifications(answers) ? [
|
|
224
|
+
` emailSender,`,
|
|
225
|
+
` fromEmail: process.env.SMTP_FROM,`,
|
|
226
|
+
` onAuthSystemEvent: createAuthSystemHook(),`
|
|
227
|
+
] : [], ` }).adapter,`);
|
|
215
228
|
if (answers.auth === "oauth" && resolveDeployTarget(answers) === "aws") lines.push(` auth: createShelfAuth({`, ` db: database,`, ` secret: process.env.SECRET!,`, ` baseURL: process.env.PUBLIC_BASE_URL!,`, ...wantsNotifications(answers) ? [
|
|
216
229
|
` emailSender,`,
|
|
217
230
|
` fromEmail: process.env.SMTP_FROM,`,
|
|
@@ -226,17 +239,20 @@ function buildRouterLines(answers) {
|
|
|
226
239
|
const host = answers.git === "github" ? "gitHubHost" : "gitLabHost";
|
|
227
240
|
lines.push(` gitHosts: [${host}],`);
|
|
228
241
|
}
|
|
229
|
-
if (answers
|
|
230
|
-
|
|
231
|
-
lines.push(`});`, ``, `await app.lifecycle.setup();`, `const logger = app.lifecycle.logger;`);
|
|
242
|
+
if (wantsObservability(answers)) lines.push(` logger: shelfLogger,`, ` observability,`);
|
|
243
|
+
lines.push(` config: {`, ` secret: process.env.SECRET,`, ` previousSecret: process.env.SECRET_PREVIOUS || undefined,`, ` migrateCredentialsOnBoot: process.env.SECRET_MIGRATE === "true",`, ...answers.queue === "memory" ? [` scratchDir: dataDir,`] : [], ` },`, `});`, ``, `await app.lifecycle.setup();`);
|
|
232
244
|
if (answers.auth === "password") lines.push(``, `// Provision the env-driven local admin (AUTH_PASSWORD, at least 12 chars).`, `await ensurePasswordAdmin(database, {`, ` email: process.env.AUTH_EMAIL ?? "admin@example.com",`, ` password: process.env.AUTH_PASSWORD!,`, `});`);
|
|
233
245
|
return lines;
|
|
234
246
|
}
|
|
247
|
+
/** Whether the generated server reads `dataDir` (local sqlite/storage or in-process scratch). */
|
|
248
|
+
function usesDataDir(answers) {
|
|
249
|
+
return answers.queue === "memory" || DB_INIT[answers.database].includes("dataDir") || STORAGE_INIT[answers.storage].includes("dataDir");
|
|
250
|
+
}
|
|
235
251
|
function generateServer(answers) {
|
|
236
252
|
return [
|
|
237
253
|
...buildImports(answers),
|
|
238
254
|
``,
|
|
239
|
-
`const dataDir = process.env.DATA_DIR || "./data"
|
|
255
|
+
...usesDataDir(answers) ? [`const dataDir = process.env.DATA_DIR || "./data";`] : [],
|
|
240
256
|
`const port = Number(process.env.PORT) || 3000;`,
|
|
241
257
|
``,
|
|
242
258
|
...buildAdapterLines(answers),
|
|
@@ -244,19 +260,14 @@ function generateServer(answers) {
|
|
|
244
260
|
...buildRouterLines(answers),
|
|
245
261
|
``,
|
|
246
262
|
`const server = serve({ fetch: app.fetch, port }, () => {`,
|
|
247
|
-
` logger.info({ port }, "StoryShelf server listening");`,
|
|
263
|
+
` app.lifecycle.logger.info({ port }, "StoryShelf server listening");`,
|
|
248
264
|
`});`,
|
|
249
265
|
``,
|
|
250
|
-
`const
|
|
251
|
-
`
|
|
252
|
-
`
|
|
253
|
-
`};`,
|
|
254
|
-
`
|
|
255
|
-
` shutdown().catch(() => {});`,
|
|
256
|
-
`});`,
|
|
257
|
-
`process.on("SIGINT", () => {`,
|
|
258
|
-
` shutdown().catch(() => {});`,
|
|
259
|
-
`});`,
|
|
266
|
+
`for (const signal of ["SIGINT", "SIGTERM"] as const) {`,
|
|
267
|
+
` process.once(signal, () => {`,
|
|
268
|
+
` app.lifecycle.teardown().finally(() => server.close());`,
|
|
269
|
+
` });`,
|
|
270
|
+
`}`,
|
|
260
271
|
``
|
|
261
272
|
].join("\n");
|
|
262
273
|
}
|
|
@@ -275,54 +286,53 @@ function workerQueueInit(queue) {
|
|
|
275
286
|
return `const queue = createSqsCaptureQueue({ queueUrl: process.env.QUEUE_URL! });`;
|
|
276
287
|
}
|
|
277
288
|
function generateWorkerFile(answers) {
|
|
278
|
-
const
|
|
279
|
-
const queueInit = workerQueueInit(answers.queue);
|
|
289
|
+
const observability = wantsObservability(answers);
|
|
280
290
|
return [
|
|
281
|
-
|
|
291
|
+
workerQueueImport(answers.queue),
|
|
282
292
|
`import { createCaptureWorker } from "@storyshelf/worker";`,
|
|
283
|
-
`import { initObservabilityFromEnv } from "@storyshelf/observability/node"
|
|
293
|
+
...observability ? [`import { initObservabilityFromEnv } from "@storyshelf/observability/node";`] : [],
|
|
284
294
|
`import { createPlaywrightCaptureRunner } from "@storyshelf/runner-playwright";`,
|
|
285
295
|
DB_IMPORT[answers.database],
|
|
286
296
|
STORAGE_IMPORT[answers.storage],
|
|
287
297
|
``,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
...observability ? [
|
|
299
|
+
`// Noop unless OTEL_EXPORTER_OTLP_ENDPOINT is set.`,
|
|
300
|
+
`const observability = await initObservabilityFromEnv();`,
|
|
301
|
+
``
|
|
302
|
+
] : [],
|
|
291
303
|
`const dataDir = process.env.DATA_DIR || "./data";`,
|
|
292
304
|
...dbPrelude(answers.database),
|
|
293
|
-
|
|
294
|
-
`const storage = ${STORAGE_INIT[answers.storage]};`,
|
|
295
|
-
queueInit,
|
|
296
|
-
`const runner = createPlaywrightCaptureRunner();`,
|
|
305
|
+
workerQueueInit(answers.queue),
|
|
297
306
|
``,
|
|
298
307
|
`const worker = createCaptureWorker({`,
|
|
299
308
|
` queue,`,
|
|
300
|
-
` db: database,`,
|
|
301
|
-
` storage,`,
|
|
302
|
-
` runner,`,
|
|
309
|
+
` db: ${DB_INIT[answers.database]},`,
|
|
310
|
+
` storage: ${STORAGE_INIT[answers.storage]},`,
|
|
311
|
+
` runner: createPlaywrightCaptureRunner(),`,
|
|
303
312
|
` scratchDir: dataDir,`,
|
|
304
313
|
` config: { concurrency: Number(process.env.WORKER_CONCURRENCY) || 2 },`,
|
|
305
314
|
`});`,
|
|
306
315
|
``,
|
|
307
316
|
`await worker.start();`,
|
|
308
317
|
``,
|
|
309
|
-
`const
|
|
310
|
-
`
|
|
311
|
-
`
|
|
312
|
-
`};`,
|
|
313
|
-
`
|
|
314
|
-
`process.on("SIGINT", () => { shutdown().catch(() => {}); });`,
|
|
318
|
+
`for (const signal of ["SIGINT", "SIGTERM"] as const) {`,
|
|
319
|
+
` process.once(signal, () => {`,
|
|
320
|
+
observability ? ` worker.stop().finally(() => observability.shutdown());` : ` void worker.stop();`,
|
|
321
|
+
` });`,
|
|
322
|
+
`}`,
|
|
315
323
|
``
|
|
316
324
|
].join("\n");
|
|
317
325
|
}
|
|
318
326
|
function buildDeps(answers) {
|
|
319
327
|
const deps = {
|
|
320
328
|
"@hono/node-server": "^1.17.0",
|
|
321
|
-
"@storyshelf/
|
|
322
|
-
|
|
323
|
-
"@storyshelf/observability": "0.6.1",
|
|
324
|
-
[DB_PACKAGE[answers.database]]: "0.6.1"
|
|
329
|
+
"@storyshelf/app": "0.6.2",
|
|
330
|
+
[DB_PACKAGE[answers.database]]: "0.6.2"
|
|
325
331
|
};
|
|
332
|
+
if (wantsObservability(answers)) {
|
|
333
|
+
deps["@storyshelf/core"] = "0.6.2";
|
|
334
|
+
deps["@storyshelf/observability"] = "0.6.2";
|
|
335
|
+
}
|
|
326
336
|
if (answers.database === "turso") deps["@libsql/client"] = LIBSQL_CLIENT_SDK;
|
|
327
337
|
if (answers.database === "better-sqlite3") deps["better-sqlite3"] = BETTER_SQLITE3_SDK;
|
|
328
338
|
if (answers.database === "pg") deps["pg"] = PG_SDK;
|
|
@@ -332,40 +342,40 @@ function buildDeps(answers) {
|
|
|
332
342
|
if (answers.database === "mysql") deps["mysql2"] = MYSQL2_SDK;
|
|
333
343
|
if (answers.database === "planetscale") deps["@planetscale/database"] = PLANETSCALE_SDK;
|
|
334
344
|
if (answers.database === "tidb") deps["@tidbcloud/serverless"] = TIDBCLOUD_SDK;
|
|
335
|
-
if (answers.storage !== "local") deps[STORAGE_PACKAGE[answers.storage]] = "0.6.
|
|
345
|
+
if (answers.storage !== "local") deps[STORAGE_PACKAGE[answers.storage]] = "0.6.2";
|
|
336
346
|
if (answers.queue === "sqs" || answers.queue === "redis") {
|
|
337
347
|
const queuePkg = answers.queue === "redis" ? "@storyshelf/queue-redis" : "@storyshelf/queue-sqs";
|
|
338
|
-
deps[queuePkg] = "0.6.
|
|
348
|
+
deps[queuePkg] = "0.6.2";
|
|
339
349
|
if (answers.includeWorker) {
|
|
340
|
-
deps["@storyshelf/worker"] = "0.6.
|
|
341
|
-
deps["@storyshelf/runner-playwright"] = "0.6.
|
|
350
|
+
deps["@storyshelf/worker"] = "0.6.2";
|
|
351
|
+
deps["@storyshelf/runner-playwright"] = "0.6.2";
|
|
342
352
|
}
|
|
343
353
|
} else if (answers.queue === "azure-storage-queues" || answers.queue === "azure-service-bus") {
|
|
344
|
-
deps["@storyshelf/queue-azure"] = "0.6.
|
|
354
|
+
deps["@storyshelf/queue-azure"] = "0.6.2";
|
|
345
355
|
const sdk = azureSdkDep(answers.queue);
|
|
346
356
|
deps[sdk.name] = sdk.version;
|
|
347
357
|
if (answers.includeWorker) {
|
|
348
|
-
deps["@storyshelf/worker"] = "0.6.
|
|
349
|
-
deps["@storyshelf/runner-playwright"] = "0.6.
|
|
358
|
+
deps["@storyshelf/worker"] = "0.6.2";
|
|
359
|
+
deps["@storyshelf/runner-playwright"] = "0.6.2";
|
|
350
360
|
}
|
|
351
361
|
} else if (answers.queue === "gcp-pubsub") {
|
|
352
|
-
deps["@storyshelf/queue-gcp"] = "0.6.
|
|
362
|
+
deps["@storyshelf/queue-gcp"] = "0.6.2";
|
|
353
363
|
deps["@google-cloud/pubsub"] = GCP_PUBSUB_SDK;
|
|
354
364
|
if (answers.includeWorker) {
|
|
355
|
-
deps["@storyshelf/worker"] = "0.6.
|
|
356
|
-
deps["@storyshelf/runner-playwright"] = "0.6.
|
|
365
|
+
deps["@storyshelf/worker"] = "0.6.2";
|
|
366
|
+
deps["@storyshelf/runner-playwright"] = "0.6.2";
|
|
357
367
|
}
|
|
358
|
-
} else deps["@storyshelf/runner-playwright"] = "0.6.
|
|
359
|
-
if (answers.auth !== "none") deps[AUTH_PACKAGE[answers.auth] ?? ""] = "0.6.
|
|
360
|
-
if (answers.git !== "none") deps[GIT_PACKAGE[answers.git] ?? ""] = "0.6.
|
|
368
|
+
} else deps["@storyshelf/runner-playwright"] = "0.6.2";
|
|
369
|
+
if (answers.auth !== "none") deps[AUTH_PACKAGE[answers.auth] ?? ""] = "0.6.2";
|
|
370
|
+
if (answers.git !== "none") deps[GIT_PACKAGE[answers.git] ?? ""] = "0.6.2";
|
|
361
371
|
if (wantsNotifications(answers)) {
|
|
362
|
-
deps["@storyshelf/notify-chat"] = "0.6.
|
|
363
|
-
deps["@storyshelf/notify-email"] = "0.6.
|
|
372
|
+
deps["@storyshelf/notify-chat"] = "0.6.2";
|
|
373
|
+
deps["@storyshelf/notify-email"] = "0.6.2";
|
|
364
374
|
deps["nodemailer"] = NODEMAILER_SDK;
|
|
365
375
|
}
|
|
366
376
|
if (answers.queue !== "memory") {
|
|
367
377
|
const queuePkg = QUEUE_PACKAGE[answers.queue];
|
|
368
|
-
if (queuePkg) deps[queuePkg] = "0.6.
|
|
378
|
+
if (queuePkg) deps[queuePkg] = "0.6.2";
|
|
369
379
|
}
|
|
370
380
|
return deps;
|
|
371
381
|
}
|
|
@@ -376,6 +386,23 @@ function addInfraScripts(scripts) {
|
|
|
376
386
|
scripts["infra:destroy"] = "terraform -chdir=terraform destroy";
|
|
377
387
|
scripts["infra:outputs"] = "terraform -chdir=terraform output -json";
|
|
378
388
|
}
|
|
389
|
+
/** Whether the Docker setup needs compose: more than one service (Postgres or a worker). */
|
|
390
|
+
function needsCompose(answers) {
|
|
391
|
+
return answers.database === "postgres" || isRemoteQueue(answers.queue) && Boolean(answers.includeWorker);
|
|
392
|
+
}
|
|
393
|
+
/** `docker:*` npm scripts: compose when several services, plain `docker` for one container. */
|
|
394
|
+
function dockerScripts(answers) {
|
|
395
|
+
if (needsCompose(answers)) return {
|
|
396
|
+
"docker:build": "docker compose build",
|
|
397
|
+
"docker:up": "docker compose up --build",
|
|
398
|
+
"docker:down": "docker compose down",
|
|
399
|
+
"docker:logs": "docker compose logs -f"
|
|
400
|
+
};
|
|
401
|
+
return {
|
|
402
|
+
"docker:build": `docker build -t ${answers.name} .`,
|
|
403
|
+
"docker:run": `docker run --rm -p 3000:3000 -v ${answers.name}-data:/data -e SECRET=change-me ${answers.name}`
|
|
404
|
+
};
|
|
405
|
+
}
|
|
379
406
|
function generatePackageJson(answers) {
|
|
380
407
|
const pkg = {
|
|
381
408
|
name: answers.name,
|
|
@@ -383,39 +410,33 @@ function generatePackageJson(answers) {
|
|
|
383
410
|
type: "module",
|
|
384
411
|
private: true,
|
|
385
412
|
description: "StoryShelf self-hosted visual testing server.",
|
|
413
|
+
engines: { node: ">=24" },
|
|
386
414
|
scripts: {
|
|
387
|
-
start:
|
|
388
|
-
dev:
|
|
415
|
+
start: `node ${SERVER_ENTRY}`,
|
|
416
|
+
dev: `node --watch ${SERVER_ENTRY}`,
|
|
417
|
+
typecheck: "tsc"
|
|
389
418
|
},
|
|
390
419
|
dependencies: buildDeps(answers),
|
|
391
|
-
devDependencies:
|
|
420
|
+
devDependencies: SCAFFOLD_DEV_DEPENDENCIES
|
|
392
421
|
};
|
|
393
422
|
if (answers.includeWorker) {
|
|
394
|
-
pkg.scripts["worker"] =
|
|
395
|
-
pkg.scripts["worker:dev"] =
|
|
396
|
-
}
|
|
397
|
-
if (resolveDeployTarget(answers) === "docker") {
|
|
398
|
-
const scripts = pkg.scripts;
|
|
399
|
-
scripts["docker:build"] = "docker compose build";
|
|
400
|
-
scripts["docker:up"] = "docker compose up --build";
|
|
401
|
-
scripts["docker:down"] = "docker compose down";
|
|
402
|
-
scripts["docker:logs"] = "docker compose logs -f";
|
|
423
|
+
pkg.scripts["worker"] = `node ${WORKER_ENTRY}`;
|
|
424
|
+
pkg.scripts["worker:dev"] = `node --watch ${WORKER_ENTRY}`;
|
|
403
425
|
}
|
|
426
|
+
if (resolveDeployTarget(answers) === "docker") Object.assign(pkg.scripts, dockerScripts(answers));
|
|
404
427
|
if (resolveDeployTarget(answers) === "aws" || resolveDeployTarget(answers) === "azure" || resolveDeployTarget(answers) === "gcp") addInfraScripts(pkg.scripts);
|
|
405
428
|
return JSON.stringify(pkg, null, 2);
|
|
406
429
|
}
|
|
407
430
|
async function writeFiles(outDir, answers) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
printLine(`Created server.ts`);
|
|
431
|
+
await writeScaffoldFile(outDir, SERVER_ENTRY, generateServer(answers));
|
|
432
|
+
printLine(`Created ${SERVER_ENTRY}`);
|
|
411
433
|
if (answers.includeWorker) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
printLine(`Created worker.ts`);
|
|
434
|
+
await writeScaffoldFile(outDir, WORKER_ENTRY, generateWorkerFile(answers));
|
|
435
|
+
printLine(`Created ${WORKER_ENTRY}`);
|
|
415
436
|
}
|
|
416
|
-
|
|
417
|
-
await writeFile(join(outDir, "
|
|
418
|
-
printLine(`Created package.json`);
|
|
437
|
+
await writeFile(join(outDir, "package.json"), generatePackageJson(answers));
|
|
438
|
+
await writeFile(join(outDir, "tsconfig.json"), generateTsconfig());
|
|
439
|
+
printLine(`Created package.json, tsconfig.json`);
|
|
419
440
|
if (resolveDeployTarget(answers) === "aws") await writeTerraformFiles(outDir, answers);
|
|
420
441
|
if (resolveDeployTarget(answers) === "azure") await writeAzureTerraformFiles(outDir, answers);
|
|
421
442
|
if (resolveDeployTarget(answers) === "gcp") await writeGcpTerraformFiles(outDir, answers);
|
|
@@ -462,28 +483,20 @@ async function writeTerraformFiles(outDir, answers) {
|
|
|
462
483
|
}
|
|
463
484
|
async function writeDockerFiles(outDir, answers) {
|
|
464
485
|
if (!answers.docker) return;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
"RUN npm install --omit=dev",
|
|
471
|
-
"COPY server.ts ./",
|
|
472
|
-
"RUN npx esbuild server.ts --bundle --platform=node --format=esm --outfile=dist/server.mjs",
|
|
473
|
-
"EXPOSE 3000",
|
|
474
|
-
"CMD [\"node\", \"dist/server.mjs\"]"
|
|
475
|
-
].join("\n");
|
|
476
|
-
await writeFile(join(outDir, "Dockerfile"), slimDockerfile);
|
|
486
|
+
const created = ["Dockerfile", ".dockerignore"];
|
|
487
|
+
const workerSplit = isRemoteQueue(answers.queue) && answers.includeWorker;
|
|
488
|
+
await writeFile(join(outDir, "Dockerfile"), workerSplit ? generateSlimServerDockerfile() : generateDockerfile());
|
|
489
|
+
await writeFile(join(outDir, ".dockerignore"), generateDockerignore());
|
|
490
|
+
if (workerSplit) {
|
|
477
491
|
await writeFile(join(outDir, "Dockerfile.worker"), generateWorkerDockerfile());
|
|
478
|
-
|
|
479
|
-
await writeFile(join(outDir, "compose.yaml"), generateComposeYamlWithWorker(answers.database));
|
|
480
|
-
printLine(`Created Dockerfile, Dockerfile.worker, .dockerignore, compose.yaml`);
|
|
481
|
-
return;
|
|
492
|
+
created.splice(1, 0, "Dockerfile.worker");
|
|
482
493
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
494
|
+
if (needsCompose(answers)) {
|
|
495
|
+
const compose = workerSplit ? generateComposeYamlWithWorker(answers.database) : generateComposeYaml(answers.database);
|
|
496
|
+
await writeFile(join(outDir, "compose.yaml"), compose);
|
|
497
|
+
created.push("compose.yaml");
|
|
498
|
+
}
|
|
499
|
+
printLine(`Created ${created.join(", ")}`);
|
|
487
500
|
}
|
|
488
501
|
function printNextSteps(answers, runner) {
|
|
489
502
|
printLine(`\nScaffolded ${answers.name} in ${resolve(answers.dir)}`);
|
|
@@ -523,12 +536,17 @@ function printNextSteps(answers, runner) {
|
|
|
523
536
|
return;
|
|
524
537
|
}
|
|
525
538
|
if (target === "docker") {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
539
|
+
if (needsCompose(answers)) {
|
|
540
|
+
printLine(` npm run docker:up # builds + starts app (+ worker)`);
|
|
541
|
+
printLine(` npm run docker:logs # follow logs`);
|
|
542
|
+
printLine(` npm run docker:down # tear down`);
|
|
543
|
+
if (answers.includeWorker) {
|
|
544
|
+
printLine(` # or separately:`);
|
|
545
|
+
printLine(` # docker compose up storyshelf worker`);
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
printLine(` npm run docker:build # builds the image`);
|
|
549
|
+
printLine(` npm run docker:run # runs it on :3000 (data in a named volume)`);
|
|
532
550
|
}
|
|
533
551
|
return;
|
|
534
552
|
}
|
|
@@ -212,7 +212,13 @@ const NOTIFICATIONS_CONFIRM = {
|
|
|
212
212
|
type: "confirm",
|
|
213
213
|
name: "notifications",
|
|
214
214
|
message: "Enable email + chat notifications (Slack/Teams/email channels)?",
|
|
215
|
-
initial:
|
|
215
|
+
initial: false
|
|
216
|
+
};
|
|
217
|
+
const OBSERVABILITY_CONFIRM = {
|
|
218
|
+
type: "confirm",
|
|
219
|
+
name: "observability",
|
|
220
|
+
message: "Add OpenTelemetry tracing + metrics wiring?",
|
|
221
|
+
initial: false
|
|
216
222
|
};
|
|
217
223
|
/**
|
|
218
224
|
* Curated prompts for the local/docker path — deploy target is asked first
|
|
@@ -242,7 +248,8 @@ function localInfraPrompts(advanced) {
|
|
|
242
248
|
choices: queueChoices(advanced)
|
|
243
249
|
},
|
|
244
250
|
DOCKER_CONFIRM,
|
|
245
|
-
NOTIFICATIONS_CONFIRM
|
|
251
|
+
NOTIFICATIONS_CONFIRM,
|
|
252
|
+
OBSERVABILITY_CONFIRM
|
|
246
253
|
];
|
|
247
254
|
}
|
|
248
255
|
const WORKER_INFRA_PROMPTS = [
|
|
@@ -4,12 +4,15 @@ import "node:url";
|
|
|
4
4
|
import.meta.url;
|
|
5
5
|
import { detectPackageRunner, installCommand } from "../../config.js";
|
|
6
6
|
import { printLine } from "../../output.js";
|
|
7
|
-
import { join, resolve } from "node:path";
|
|
8
7
|
import { spawn } from "node:child_process";
|
|
9
8
|
import { access } from "node:fs/promises";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
10
|
//#region src/commands/server/serve.ts
|
|
11
11
|
/** Server entry filenames tried in order. */
|
|
12
12
|
const SERVER_CANDIDATES = [
|
|
13
|
+
"src/index.ts",
|
|
14
|
+
"src/index.js",
|
|
15
|
+
"src/index.mjs",
|
|
13
16
|
"server.ts",
|
|
14
17
|
"server.js",
|
|
15
18
|
"server.mjs",
|
|
@@ -19,7 +22,7 @@ const SERVER_CANDIDATES = [
|
|
|
19
22
|
];
|
|
20
23
|
/**
|
|
21
24
|
* Locate the server entry file in a scaffolded project.
|
|
22
|
-
* Tries `
|
|
25
|
+
* Tries `src/index.*` first, then the legacy root `server.*` / `index.*` layouts.
|
|
23
26
|
*
|
|
24
27
|
* @param dir - Absolute path to the server project directory
|
|
25
28
|
* @returns Absolute path to the first entry that exists
|
|
@@ -37,9 +40,9 @@ async function resolveServerFile(dir) {
|
|
|
37
40
|
}
|
|
38
41
|
throw new Error(`No server entry found in ${dir} (looked for ${SERVER_CANDIDATES.join(", ")}). Run "storyshelf server init" first.`);
|
|
39
42
|
}
|
|
40
|
-
/** Node
|
|
43
|
+
/** Node runs the entry directly: Node 24+ strips TypeScript types without a flag. */
|
|
41
44
|
function nodeArgs(file) {
|
|
42
|
-
return
|
|
45
|
+
return [file];
|
|
43
46
|
}
|
|
44
47
|
/** Forward SIGINT/SIGTERM to the child; returns a handle to detach and inspect. */
|
|
45
48
|
function relaySignals(child) {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import "node:path";
|
|
3
3
|
import "node:url";
|
|
4
4
|
import.meta.url;
|
|
5
|
-
import { join } from "node:path";
|
|
6
5
|
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
7
|
//#region src/commands/shared/detect-adapters.ts
|
|
8
8
|
/**
|
|
9
9
|
* Detect installed adapter choices from a project's package.json.
|
|
@@ -95,7 +95,12 @@ function detectMysqlFlavor(dir, deps) {
|
|
|
95
95
|
}
|
|
96
96
|
function readGeneratedSources(dir) {
|
|
97
97
|
let sources = "";
|
|
98
|
-
for (const file of [
|
|
98
|
+
for (const file of [
|
|
99
|
+
"src/index.ts",
|
|
100
|
+
"src/worker.ts",
|
|
101
|
+
"server.ts",
|
|
102
|
+
"worker.ts"
|
|
103
|
+
]) try {
|
|
99
104
|
sources += readFileSync(join(dir, file), "utf8");
|
|
100
105
|
} catch {}
|
|
101
106
|
return sources;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "node:path";
|
|
3
|
+
import "node:url";
|
|
4
|
+
import.meta.url;
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
//#region src/commands/shared/scaffold.ts
|
|
8
|
+
/** Server entry in a scaffolded project (a `src/` dir leaves room for helper modules). */
|
|
9
|
+
const SERVER_ENTRY = "src/index.ts";
|
|
10
|
+
/** Worker entry in a scaffolded project. */
|
|
11
|
+
const WORKER_ENTRY = "src/worker.ts";
|
|
12
|
+
/** Dev dependencies every scaffolded project gets (editor + `tsc --noEmit`). */
|
|
13
|
+
const SCAFFOLD_DEV_DEPENDENCIES = {
|
|
14
|
+
"@types/node": "^24.0.0",
|
|
15
|
+
typescript: "^7.0.2"
|
|
16
|
+
};
|
|
17
|
+
/** `tsconfig.json` for a scaffolded project: type-check only (Node strips types at runtime). */
|
|
18
|
+
function generateTsconfig() {
|
|
19
|
+
return `${JSON.stringify({
|
|
20
|
+
compilerOptions: {
|
|
21
|
+
target: "es2023",
|
|
22
|
+
module: "nodenext",
|
|
23
|
+
moduleResolution: "nodenext",
|
|
24
|
+
strict: true,
|
|
25
|
+
noEmit: true,
|
|
26
|
+
skipLibCheck: true,
|
|
27
|
+
verbatimModuleSyntax: true,
|
|
28
|
+
allowImportingTsExtensions: true,
|
|
29
|
+
types: ["node"]
|
|
30
|
+
},
|
|
31
|
+
include: ["src"]
|
|
32
|
+
}, null, 2)}\n`;
|
|
33
|
+
}
|
|
34
|
+
/** Write `contents` to `outDir/relativePath`, creating parent directories. */
|
|
35
|
+
async function writeScaffoldFile(outDir, relativePath, contents) {
|
|
36
|
+
const target = join(outDir, relativePath);
|
|
37
|
+
await mkdir(dirname(target), { recursive: true });
|
|
38
|
+
await writeFile(target, contents);
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { SCAFFOLD_DEV_DEPENDENCIES, SERVER_ENTRY, WORKER_ENTRY, generateTsconfig, writeScaffoldFile };
|
|
@@ -4,9 +4,9 @@ import "node:url";
|
|
|
4
4
|
import.meta.url;
|
|
5
5
|
import { detectPackageRunner } from "../config.js";
|
|
6
6
|
import { printLine } from "../output.js";
|
|
7
|
-
import { resolve } from "node:path";
|
|
8
7
|
import { execSync } from "node:child_process";
|
|
9
8
|
import { access, readdir } from "node:fs/promises";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
10
|
//#region src/commands/storybook-build.ts
|
|
11
11
|
/**
|
|
12
12
|
* Script invocation per runner. npm needs `--` to forward args; pnpm, bun,
|
|
@@ -5,10 +5,11 @@ import.meta.url;
|
|
|
5
5
|
import { detectPackageRunner, installCommand } from "../../config.js";
|
|
6
6
|
import { printError, printLine } from "../../output.js";
|
|
7
7
|
import { detectInstalledAdapters } from "../shared/detect-adapters.js";
|
|
8
|
+
import { SCAFFOLD_DEV_DEPENDENCIES, WORKER_ENTRY, generateTsconfig, writeScaffoldFile } from "../shared/scaffold.js";
|
|
8
9
|
import { generateDockerignore, generateWorkerDockerfile } from "../server/docker.js";
|
|
9
10
|
import { PROJECT_PROMPTS, WORKER_INFRA_PROMPTS } from "../server/prompts.js";
|
|
10
|
-
import { join, resolve } from "node:path";
|
|
11
11
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
12
13
|
import prompts from "prompts";
|
|
13
14
|
//#region src/commands/worker/init.ts
|
|
14
15
|
const DB_PACKAGE = {
|
|
@@ -151,18 +152,20 @@ function generateWorker(answers) {
|
|
|
151
152
|
``,
|
|
152
153
|
`await worker.start();`,
|
|
153
154
|
``,
|
|
154
|
-
`const
|
|
155
|
-
`process.
|
|
156
|
-
`
|
|
155
|
+
`for (const signal of ["SIGINT", "SIGTERM"] as const) {`,
|
|
156
|
+
` process.once(signal, () => {`,
|
|
157
|
+
` void worker.stop();`,
|
|
158
|
+
` });`,
|
|
159
|
+
`}`,
|
|
157
160
|
``
|
|
158
161
|
].join("\n");
|
|
159
162
|
}
|
|
160
163
|
function buildDeps(answers) {
|
|
161
164
|
const deps = {
|
|
162
|
-
"@storyshelf/core": "0.6.
|
|
163
|
-
"@storyshelf/worker": "0.6.
|
|
164
|
-
"@storyshelf/runner-playwright": "0.6.
|
|
165
|
-
[DB_PACKAGE[answers.database]]: "0.6.
|
|
165
|
+
"@storyshelf/core": "0.6.2",
|
|
166
|
+
"@storyshelf/worker": "0.6.2",
|
|
167
|
+
"@storyshelf/runner-playwright": "0.6.2",
|
|
168
|
+
[DB_PACKAGE[answers.database]]: "0.6.2"
|
|
166
169
|
};
|
|
167
170
|
if (answers.database === "turso") deps["@libsql/client"] = LIBSQL_CLIENT_SDK;
|
|
168
171
|
if (answers.database === "better-sqlite3") deps["better-sqlite3"] = BETTER_SQLITE3_SDK;
|
|
@@ -173,9 +176,9 @@ function buildDeps(answers) {
|
|
|
173
176
|
if (answers.database === "mysql") deps["mysql2"] = MYSQL2_SDK;
|
|
174
177
|
if (answers.database === "planetscale") deps["@planetscale/database"] = PLANETSCALE_SDK;
|
|
175
178
|
if (answers.database === "tidb") deps["@tidbcloud/serverless"] = TIDBCLOUD_SDK;
|
|
176
|
-
if (answers.storage !== "local") deps[STORAGE_PACKAGE[answers.storage]] = "0.6.
|
|
179
|
+
if (answers.storage !== "local") deps[STORAGE_PACKAGE[answers.storage]] = "0.6.2";
|
|
177
180
|
const qp = QUEUE_PACKAGE[answers.queue];
|
|
178
|
-
if (qp) deps[qp] = "0.6.
|
|
181
|
+
if (qp) deps[qp] = "0.6.2";
|
|
179
182
|
if (answers.queue === "azure-storage-queues") deps["@azure/storage-queue"] = AZURE_STORAGE_QUEUE_SDK;
|
|
180
183
|
if (answers.queue === "azure-service-bus") deps["@azure/service-bus"] = AZURE_SERVICE_BUS_SDK;
|
|
181
184
|
if (answers.queue === "gcp-pubsub") deps["@google-cloud/pubsub"] = GCP_PUBSUB_SDK;
|
|
@@ -188,20 +191,23 @@ function generatePackageJson(answers) {
|
|
|
188
191
|
type: "module",
|
|
189
192
|
private: true,
|
|
190
193
|
description: "StoryShelf capture worker (remote queue).",
|
|
194
|
+
engines: { node: ">=24" },
|
|
191
195
|
scripts: {
|
|
192
|
-
start:
|
|
193
|
-
dev:
|
|
196
|
+
start: `node ${WORKER_ENTRY}`,
|
|
197
|
+
dev: `node --watch ${WORKER_ENTRY}`,
|
|
198
|
+
typecheck: "tsc"
|
|
194
199
|
},
|
|
195
200
|
dependencies: buildDeps(answers),
|
|
196
|
-
devDependencies:
|
|
201
|
+
devDependencies: SCAFFOLD_DEV_DEPENDENCIES
|
|
197
202
|
};
|
|
198
203
|
return JSON.stringify(pkg, null, 2);
|
|
199
204
|
}
|
|
200
205
|
async function writeFiles(outDir, answers) {
|
|
201
|
-
await
|
|
202
|
-
printLine(`Created
|
|
206
|
+
await writeScaffoldFile(outDir, WORKER_ENTRY, generateWorker(answers));
|
|
207
|
+
printLine(`Created ${WORKER_ENTRY}`);
|
|
203
208
|
await writeFile(join(outDir, "package.json"), generatePackageJson(answers));
|
|
204
|
-
|
|
209
|
+
await writeFile(join(outDir, "tsconfig.json"), generateTsconfig());
|
|
210
|
+
printLine(`Created package.json, tsconfig.json`);
|
|
205
211
|
if (answers.docker) {
|
|
206
212
|
await writeFile(join(outDir, "Dockerfile"), generateWorkerDockerfile());
|
|
207
213
|
await writeFile(join(outDir, ".dockerignore"), generateDockerignore());
|
|
@@ -4,11 +4,14 @@ import "node:url";
|
|
|
4
4
|
import.meta.url;
|
|
5
5
|
import { detectPackageRunner, installCommand } from "../../config.js";
|
|
6
6
|
import { printLine } from "../../output.js";
|
|
7
|
-
import { join, resolve } from "node:path";
|
|
8
7
|
import { spawn } from "node:child_process";
|
|
9
8
|
import { access } from "node:fs/promises";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
10
|
//#region src/commands/worker/serve.ts
|
|
11
11
|
const WORKER_CANDIDATES = [
|
|
12
|
+
"src/worker.ts",
|
|
13
|
+
"src/worker.js",
|
|
14
|
+
"src/worker.mjs",
|
|
12
15
|
"worker.ts",
|
|
13
16
|
"worker.js",
|
|
14
17
|
"worker.mjs",
|
|
@@ -28,8 +31,9 @@ async function resolveWorkerFile(dir) {
|
|
|
28
31
|
}
|
|
29
32
|
throw new Error(`No worker entry found in ${dir} (looked for ${WORKER_CANDIDATES.join(", ")}). Run "storyshelf worker init" first.`);
|
|
30
33
|
}
|
|
34
|
+
/** Node runs the entry directly: Node 24+ strips TypeScript types without a flag. */
|
|
31
35
|
function nodeArgs(file) {
|
|
32
|
-
return
|
|
36
|
+
return [file];
|
|
33
37
|
}
|
|
34
38
|
function relaySignals(child) {
|
|
35
39
|
let relayed = false;
|
package/dist/config.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import "node:path";
|
|
3
3
|
import "node:url";
|
|
4
4
|
import.meta.url;
|
|
5
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
5
|
import { execSync } from "node:child_process";
|
|
7
6
|
import { existsSync } from "node:fs";
|
|
8
7
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
//#region src/config.ts
|
|
11
11
|
/** Parse an unknown value into a `StorybookConfig`, or return the reason it is invalid. */
|
|
@@ -233,7 +233,7 @@ const SCHEMA_BASE_URL = "https://unpkg.com/storyshelf";
|
|
|
233
233
|
const SCHEMA_PACKAGE_PATH = "schema/storyshelf-config.json";
|
|
234
234
|
/** Running CLI version, or undefined in dev/test without the build define. */
|
|
235
235
|
function cliVersion() {
|
|
236
|
-
return "0.6.
|
|
236
|
+
return "0.6.2";
|
|
237
237
|
}
|
|
238
238
|
/**
|
|
239
239
|
* Resolve the `$schema` value for a config file: a path relative to the
|
|
@@ -523,4 +523,4 @@ async function runnerFromLockfiles(cwd) {
|
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
525
|
//#endregion
|
|
526
|
-
export { assertStorybookMain, detectGitDefaultBranch, detectGitRepository, detectPackageName, detectPackageRunner, detectStorybookMeta, findStorybookMain, installCommand, loadStorybookConfig, schemaReference, startCommand, writeStorybookConfig };
|
|
526
|
+
export { assertStorybookMain, cliVersion, detectGitDefaultBranch, detectGitRepository, detectPackageName, detectPackageRunner, detectStorybookMeta, findStorybookMain, installCommand, loadStorybookConfig, schemaReference, startCommand, writeStorybookConfig };
|
package/dist/index.js
CHANGED
|
@@ -2,19 +2,7 @@
|
|
|
2
2
|
import "node:path";
|
|
3
3
|
import "node:url";
|
|
4
4
|
import.meta.url;
|
|
5
|
-
import {
|
|
6
|
-
import { runCreate } from "./commands/create.js";
|
|
7
|
-
import { runUpload } from "./commands/upload.js";
|
|
8
|
-
import { handleError, runDefaultCommand } from "./commands/default.js";
|
|
9
|
-
import { runDoctor } from "./commands/doctor.js";
|
|
10
|
-
import { runInit } from "./commands/init.js";
|
|
11
|
-
import { runPurge } from "./commands/purge.js";
|
|
12
|
-
import { runRetry } from "./commands/retry.js";
|
|
13
|
-
import { runServerInit } from "./commands/server/init.js";
|
|
14
|
-
import { runServerServe } from "./commands/server/serve.js";
|
|
15
|
-
import { runWhoami } from "./commands/whoami.js";
|
|
16
|
-
import { runWorkerInit } from "./commands/worker/init.js";
|
|
17
|
-
import { runWorkerServe } from "./commands/worker/serve.js";
|
|
5
|
+
import { cliVersion } from "./config.js";
|
|
18
6
|
import { isMainModule } from "./is-main.js";
|
|
19
7
|
import { Command } from "commander";
|
|
20
8
|
//#region src/index.ts
|
|
@@ -25,7 +13,7 @@ import { Command } from "commander";
|
|
|
25
13
|
*/
|
|
26
14
|
function createProgram() {
|
|
27
15
|
const program = new Command();
|
|
28
|
-
program.name("storyshelf").description("Self-hosted visual testing for Storybook.").version("0.
|
|
16
|
+
program.name("storyshelf").description("Self-hosted visual testing for Storybook.").version(cliVersion() ?? "0.0.0-dev");
|
|
29
17
|
const commands = [
|
|
30
18
|
buildInitCommand(),
|
|
31
19
|
buildCreateCommand(),
|
|
@@ -41,54 +29,66 @@ function createProgram() {
|
|
|
41
29
|
for (const command of commands) program.addCommand(command);
|
|
42
30
|
return program;
|
|
43
31
|
}
|
|
44
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Wrap a command so its module (and heavy dependencies such as the zip library
|
|
34
|
+
* or prompts) loads only when that command runs. `storyshelf -h` and the other
|
|
35
|
+
* commands then skip ~40 ms of imports they never use.
|
|
36
|
+
*/
|
|
37
|
+
function run(load) {
|
|
45
38
|
return async (args) => {
|
|
46
|
-
|
|
39
|
+
try {
|
|
40
|
+
await (await load())(args);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const { handleError } = await import("./commands/default.js");
|
|
43
|
+
handleError(error);
|
|
44
|
+
}
|
|
47
45
|
};
|
|
48
46
|
}
|
|
49
47
|
function buildInitCommand() {
|
|
50
|
-
return new Command("init").description("Initialize Storybook project with .storybook/storyshelf.json (client config)").option("--url <url>", "server base URL").option("--slug <slug>", "project slug").option("--build-dir <dir>", "built Storybook directory (default storybook-static)").option("--build-command <cmd>", "build command (e.g. \"npm run build-storybook\")").option("--build-script-name <name>", "npm script to build Storybook (default build-storybook)").option("--skip <glob>", "skip upload for matching branch (glob)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").option("--token <token>", "auth token for sync (or STORYSHELF_TOKEN/STORYSHELF_ADMIN_TOKEN env)").action(run(runInit));
|
|
48
|
+
return new Command("init").description("Initialize Storybook project with .storybook/storyshelf.json (client config)").option("--url <url>", "server base URL").option("--slug <slug>", "project slug").option("--build-dir <dir>", "built Storybook directory (default storybook-static)").option("--build-command <cmd>", "build command (e.g. \"npm run build-storybook\")").option("--build-script-name <name>", "npm script to build Storybook (default build-storybook)").option("--skip <glob>", "skip upload for matching branch (glob)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").option("--token <token>", "auth token for sync (or STORYSHELF_TOKEN/STORYSHELF_ADMIN_TOKEN env)").action(run(async () => (await import("./commands/init.js")).runInit));
|
|
51
49
|
}
|
|
52
50
|
function buildCreateCommand() {
|
|
53
|
-
return new Command("create").description("Create a project on StoryShelf server (requires admin token)").option("--url <url>", "server base URL").option("--name <name>", "project name").option("--token <token>", "admin token (or STORYSHELF_ADMIN_TOKEN env)").action(run(runCreate));
|
|
51
|
+
return new Command("create").description("Create a project on StoryShelf server (requires admin token)").option("--url <url>", "server base URL").option("--name <name>", "project name").option("--token <token>", "admin token (or STORYSHELF_ADMIN_TOKEN env)").action(run(async () => (await import("./commands/create.js")).runCreate));
|
|
54
52
|
}
|
|
55
53
|
function buildServerCommand() {
|
|
56
54
|
const server = new Command("server").description("Server operations");
|
|
57
|
-
server.command("init").description("Scaffold a new StoryShelf server project").option("--dir <dir>", "output directory").action(run(runServerInit));
|
|
58
|
-
const serve = new Command("serve").description("Run a scaffolded StoryShelf server project").option("--dir <dir>", "server project directory (default cwd)").option("--port <port>", "port override (sets PORT)").action(run(runServerServe));
|
|
55
|
+
server.command("init").description("Scaffold a new StoryShelf server project").option("--dir <dir>", "output directory").action(run(async () => (await import("./commands/server/init.js")).runServerInit));
|
|
56
|
+
const serve = new Command("serve").description("Run a scaffolded StoryShelf server project").option("--dir <dir>", "server project directory (default cwd)").option("--port <port>", "port override (sets PORT)").action(run(async () => (await import("./commands/server/serve.js")).runServerServe));
|
|
59
57
|
server.addCommand(serve, { isDefault: true });
|
|
60
58
|
return server;
|
|
61
59
|
}
|
|
62
60
|
function buildWorkerCommand() {
|
|
63
61
|
const worker = new Command("worker").description("Worker operations");
|
|
64
|
-
worker.command("init").description("Scaffold a new StoryShelf worker project").option("--dir <dir>", "output directory").action(run(runWorkerInit));
|
|
65
|
-
worker.command("serve").description("Run a scaffolded StoryShelf worker project").option("--dir <dir>", "worker project directory (default cwd)").option("--queue-url <url>", "SQS queue URL override (sets QUEUE_URL)").option("--concurrency <n>", "concurrency override (sets WORKER_CONCURRENCY)").action(run(runWorkerServe));
|
|
66
|
-
const runCmd = new Command("run").description("Run a scaffolded worker (alias for worker serve)").option("--dir <dir>", "worker project directory (default cwd)").option("--queue-url <url>", "SQS queue URL override").option("--concurrency <n>", "concurrency override").action(run(runWorkerServe));
|
|
62
|
+
worker.command("init").description("Scaffold a new StoryShelf worker project").option("--dir <dir>", "output directory").action(run(async () => (await import("./commands/worker/init.js")).runWorkerInit));
|
|
63
|
+
worker.command("serve").description("Run a scaffolded StoryShelf worker project").option("--dir <dir>", "worker project directory (default cwd)").option("--queue-url <url>", "SQS queue URL override (sets QUEUE_URL)").option("--concurrency <n>", "concurrency override (sets WORKER_CONCURRENCY)").action(run(async () => (await import("./commands/worker/serve.js")).runWorkerServe));
|
|
64
|
+
const runCmd = new Command("run").description("Run a scaffolded worker (alias for worker serve)").option("--dir <dir>", "worker project directory (default cwd)").option("--queue-url <url>", "SQS queue URL override").option("--concurrency <n>", "concurrency override").action(run(async () => (await import("./commands/worker/serve.js")).runWorkerServe));
|
|
67
65
|
worker.addCommand(runCmd);
|
|
68
66
|
return worker;
|
|
69
67
|
}
|
|
70
68
|
function buildPurgeCommand() {
|
|
71
|
-
return new Command("purge").description("Purge expired builds on a StoryShelf server").requiredOption("--url <url>", "server base URL").option("--token <token>", "admin token (or STORYSHELF_ADMIN_TOKEN env)").action(run(runPurge));
|
|
69
|
+
return new Command("purge").description("Purge expired builds on a StoryShelf server").requiredOption("--url <url>", "server base URL").option("--token <token>", "admin token (or STORYSHELF_ADMIN_TOKEN env)").action(run(async () => (await import("./commands/purge.js")).runPurge));
|
|
72
70
|
}
|
|
73
71
|
function buildUploadCommand() {
|
|
74
|
-
return new Command("upload").description("Create a build record for a StoryShelf project").option("--url <url>", "server base URL (or .storybook/storyshelf.json)").option("--slug <slug>", "project slug (or .storybook/storyshelf.json)").option("--token <token>", "CI token (or STORYSHELF_TOKEN env)").option("--sha <sha>", "revision SHA (flag, CI env, local git HEAD, or synthesized local-*)").option("--branch <branch>", "baseline namespace (flag, CI env, local git branch, or local)").option("--build-dir <dir>", "built Storybook directory (default storybook-static)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").option("--build-command <cmd>", "build command to run if buildDir missing/empty").option("--build-script-name <name>", "npm script to build Storybook (default build-storybook)").option("--force-build", "force rebuild even if buildDir exists").option("--dry-run", "validate and build without sending any requests").option("--skip <glob>", "skip upload for matching branch (glob)").option("--message <message>", "build message").option("--author-email <email>", "author email").option("--author-name <name>", "author name").option("--full", "disable affected capture (render every story)").option("--untraced <glob>", "exclude matching files from affected tracing (repeatable)", (value, previous) => [...previous, value], []).option("--stats-file <path>", "bundler stats file (default <buildDir>/preview-stats.json)").option("--label <key=value>", "build label (repeatable: --label pr=123)", (value, previous) => [...previous, value], []).action(run(runUpload));
|
|
72
|
+
return new Command("upload").description("Create a build record for a StoryShelf project").option("--url <url>", "server base URL (or .storybook/storyshelf.json)").option("--slug <slug>", "project slug (or .storybook/storyshelf.json)").option("--token <token>", "CI token (or STORYSHELF_TOKEN env)").option("--sha <sha>", "revision SHA (flag, CI env, local git HEAD, or synthesized local-*)").option("--branch <branch>", "baseline namespace (flag, CI env, local git branch, or local)").option("--build-dir <dir>", "built Storybook directory (default storybook-static)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").option("--build-command <cmd>", "build command to run if buildDir missing/empty").option("--build-script-name <name>", "npm script to build Storybook (default build-storybook)").option("--force-build", "force rebuild even if buildDir exists").option("--dry-run", "validate and build without sending any requests").option("--skip <glob>", "skip upload for matching branch (glob)").option("--message <message>", "build message").option("--author-email <email>", "author email").option("--author-name <name>", "author name").option("--full", "disable affected capture (render every story)").option("--untraced <glob>", "exclude matching files from affected tracing (repeatable)", (value, previous) => [...previous, value], []).option("--stats-file <path>", "bundler stats file (default <buildDir>/preview-stats.json)").option("--label <key=value>", "build label (repeatable: --label pr=123)", (value, previous) => [...previous, value], []).action(run(async () => (await import("./commands/upload.js")).runUpload));
|
|
75
73
|
}
|
|
76
74
|
function buildBuildCommand() {
|
|
77
|
-
return new Command("build").description("Build Storybook for upload (no server contact)").option("--build-dir <dir>", "built Storybook directory (default storybook-static)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").option("--build-command <cmd>", "build command to run if buildDir missing/empty").option("--build-script-name <name>", "npm script to build Storybook (default build-storybook)").option("--force-build", "force rebuild even if buildDir exists").action(run(runBuild));
|
|
75
|
+
return new Command("build").description("Build Storybook for upload (no server contact)").option("--build-dir <dir>", "built Storybook directory (default storybook-static)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").option("--build-command <cmd>", "build command to run if buildDir missing/empty").option("--build-script-name <name>", "npm script to build Storybook (default build-storybook)").option("--force-build", "force rebuild even if buildDir exists").action(run(async () => (await import("./commands/build.js")).runBuild));
|
|
78
76
|
}
|
|
79
77
|
function buildDoctorCommand() {
|
|
80
|
-
return new Command("doctor").description("Diagnose whether an upload would succeed (no side effects)").option("--url <url>", "server base URL (or .storybook/storyshelf.json)").option("--slug <slug>", "project slug (or .storybook/storyshelf.json)").option("--token <token>", "CI token (or STORYSHELF_TOKEN env)").option("--build-dir <dir>", "built Storybook directory (default storybook-static)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").option("--build-command <cmd>", "build command to run if buildDir missing/empty").option("--build-script-name <name>", "npm script to build Storybook (default build-storybook)").action(run(runDoctor));
|
|
78
|
+
return new Command("doctor").description("Diagnose whether an upload would succeed (no side effects)").option("--url <url>", "server base URL (or .storybook/storyshelf.json)").option("--slug <slug>", "project slug (or .storybook/storyshelf.json)").option("--token <token>", "CI token (or STORYSHELF_TOKEN env)").option("--build-dir <dir>", "built Storybook directory (default storybook-static)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").option("--build-command <cmd>", "build command to run if buildDir missing/empty").option("--build-script-name <name>", "npm script to build Storybook (default build-storybook)").action(run(async () => (await import("./commands/doctor.js")).runDoctor));
|
|
81
79
|
}
|
|
82
80
|
function buildWhoamiCommand() {
|
|
83
|
-
return new Command("whoami").description("Verify the token against the server and print the project").option("--url <url>", "server base URL (or .storybook/storyshelf.json)").option("--slug <slug>", "project slug (or .storybook/storyshelf.json)").option("--token <token>", "CI token (or STORYSHELF_TOKEN env)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").action(run(runWhoami));
|
|
81
|
+
return new Command("whoami").description("Verify the token against the server and print the project").option("--url <url>", "server base URL (or .storybook/storyshelf.json)").option("--slug <slug>", "project slug (or .storybook/storyshelf.json)").option("--token <token>", "CI token (or STORYSHELF_TOKEN env)").option("-c, --config <path>", "config file path (default .storybook/storyshelf.json)").action(run(async () => (await import("./commands/whoami.js")).runWhoami));
|
|
84
82
|
}
|
|
85
83
|
function buildRetryCommand() {
|
|
86
|
-
return new Command("retry").description("Retry a failed StoryShelf build").requiredOption("--url <url>", "server base URL").requiredOption("--slug <slug>", "project slug").requiredOption("--build-id <id>", "build id").option("--token <token>", "CI token (or STORYSHELF_TOKEN env)").action(run(runRetry));
|
|
84
|
+
return new Command("retry").description("Retry a failed StoryShelf build").requiredOption("--url <url>", "server base URL").requiredOption("--slug <slug>", "project slug").requiredOption("--build-id <id>", "build id").option("--token <token>", "CI token (or STORYSHELF_TOKEN env)").action(run(async () => (await import("./commands/retry.js")).runRetry));
|
|
87
85
|
}
|
|
88
86
|
if (isMainModule(import.meta.url, process.argv[1])) {
|
|
89
87
|
const program = createProgram();
|
|
90
|
-
if (process.argv.length <= 2)
|
|
91
|
-
|
|
88
|
+
if (process.argv.length <= 2) {
|
|
89
|
+
const { runDefaultCommand } = await import("./commands/default.js");
|
|
90
|
+
await runDefaultCommand(program);
|
|
91
|
+
} else program.parse(process.argv);
|
|
92
92
|
}
|
|
93
93
|
//#endregion
|
|
94
94
|
export { createProgram };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyshelf",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "StoryShelf CLI: create projects, upload builds, purge, and retry from CI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ci",
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
"schema"
|
|
32
32
|
],
|
|
33
33
|
"type": "module",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=24"
|
|
36
|
+
},
|
|
34
37
|
"sideEffects": false,
|
|
35
38
|
"types": "./dist/index.d.mts",
|
|
36
39
|
"exports": {
|
|
@@ -50,15 +53,14 @@
|
|
|
50
53
|
"prepublishOnly": "nub run build"
|
|
51
54
|
},
|
|
52
55
|
"dependencies": {
|
|
53
|
-
"@storyshelf/affected": "0.6.
|
|
56
|
+
"@storyshelf/affected": "0.6.2",
|
|
54
57
|
"archiver": "8.0.0",
|
|
55
58
|
"commander": "^13.0.0",
|
|
56
|
-
"hono": "^4.11.0",
|
|
57
59
|
"picomatch": "^4.0.2",
|
|
58
60
|
"prompts": "^2.4.2"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
61
|
-
"@storyshelf/core": "0.6.
|
|
63
|
+
"@storyshelf/core": "0.6.2",
|
|
62
64
|
"@types/node": "^26.0.0",
|
|
63
65
|
"@types/picomatch": "^4.0.1",
|
|
64
66
|
"@types/prompts": "^2.4.9",
|