@beignet/cli 0.0.47 → 0.0.49
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/CHANGELOG.md +46 -0
- package/README.md +126 -42
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +10 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +66 -6
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.d.ts +8 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +55 -9
- package/dist/check.js.map +1 -1
- package/dist/db.d.ts +38 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +52 -0
- package/dist/db.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -6
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +454 -170
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +12 -5
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +3 -3
- package/dist/make/payments.js.map +1 -1
- package/dist/make/tenancy.js +1 -1
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +56 -19
- package/dist/mcp.js.map +1 -1
- package/dist/preflight.d.ts +11 -3
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +139 -16
- package/dist/preflight.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +149 -9
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts +13 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +122 -8
- package/dist/provider-audit.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +29 -7
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts +1 -0
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +20 -6
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/index.d.ts +1 -0
- package/dist/templates/db/index.d.ts.map +1 -1
- package/dist/templates/db/index.js.map +1 -1
- package/dist/templates/db/migration-status.d.ts +9 -0
- package/dist/templates/db/migration-status.d.ts.map +1 -0
- package/dist/templates/db/migration-status.js +117 -0
- package/dist/templates/db/migration-status.js.map +1 -0
- package/dist/templates/db/mysql.d.ts.map +1 -1
- package/dist/templates/db/mysql.js +23 -0
- package/dist/templates/db/mysql.js.map +1 -1
- package/dist/templates/db/postgres.d.ts.map +1 -1
- package/dist/templates/db/postgres.js +21 -0
- package/dist/templates/db/postgres.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +53 -0
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +5 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +4 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +56 -11
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +115 -6
- package/src/app-map.ts +418 -109
- package/src/check.ts +86 -10
- package/src/db.ts +97 -0
- package/src/explain.ts +4 -4
- package/src/index.ts +108 -7
- package/src/inspect.ts +659 -216
- package/src/lib.ts +9 -1
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +12 -5
- package/src/make/payments.ts +3 -3
- package/src/make/tenancy.ts +1 -1
- package/src/make.ts +225 -8
- package/src/mcp.ts +73 -21
- package/src/preflight.ts +195 -16
- package/src/provider-add.ts +193 -9
- package/src/provider-audit.ts +210 -18
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +29 -7
- package/src/templates/base.ts +21 -4
- package/src/templates/db/index.ts +1 -0
- package/src/templates/db/migration-status.ts +126 -0
- package/src/templates/db/mysql.ts +23 -0
- package/src/templates/db/postgres.ts +21 -0
- package/src/templates/db/sqlite.ts +53 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/server.ts +2 -0
- package/src/templates/shared.ts +4 -2
package/src/preflight.ts
CHANGED
|
@@ -2,12 +2,15 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
4
4
|
import { createJiti } from "jiti";
|
|
5
|
+
import { runDatabaseStatus } from "./db.js";
|
|
5
6
|
import {
|
|
6
7
|
type InspectDiagnostic,
|
|
7
8
|
inspectApp,
|
|
8
9
|
productionHardeningDiagnosticCodes,
|
|
9
10
|
} from "./inspect.js";
|
|
10
11
|
import {
|
|
12
|
+
detectedProviderVariantsForRules,
|
|
13
|
+
formatProviderEnvAlternatives,
|
|
11
14
|
installedPackageNames,
|
|
12
15
|
type ProviderPackageJson,
|
|
13
16
|
providerDoctorRulesForInstalledPackages,
|
|
@@ -46,8 +49,9 @@ export interface PreflightOptions {
|
|
|
46
49
|
*/
|
|
47
50
|
doctor?: boolean;
|
|
48
51
|
/**
|
|
49
|
-
*
|
|
50
|
-
* access and real credentials, so it
|
|
52
|
+
* Inspect database migration status, then boot the app server and run every
|
|
53
|
+
* port's `checkHealth()`. Needs network access and real credentials, so it
|
|
54
|
+
* is opt-in.
|
|
51
55
|
*/
|
|
52
56
|
connect?: boolean;
|
|
53
57
|
/**
|
|
@@ -56,9 +60,12 @@ export interface PreflightOptions {
|
|
|
56
60
|
*/
|
|
57
61
|
serverModule?: string;
|
|
58
62
|
/**
|
|
59
|
-
* Per-dependency health check timeout in milliseconds.
|
|
63
|
+
* Per migration-status or dependency health check timeout in milliseconds.
|
|
64
|
+
* Defaults to 5000.
|
|
60
65
|
*/
|
|
61
66
|
connectTimeoutMs?: number;
|
|
67
|
+
/** Cancel connected migration inspection and health checks on abort. */
|
|
68
|
+
signal?: AbortSignal;
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
export interface PreflightResult {
|
|
@@ -79,6 +86,15 @@ export interface PreflightResult {
|
|
|
79
86
|
* Ports whose `checkHealth()` ran during a `connect` pass.
|
|
80
87
|
*/
|
|
81
88
|
checkedDependencies: string[];
|
|
89
|
+
/**
|
|
90
|
+
* Database migration state observed before dependency checks.
|
|
91
|
+
*/
|
|
92
|
+
migrationStatus:
|
|
93
|
+
| "not-checked"
|
|
94
|
+
| "current"
|
|
95
|
+
| "pending"
|
|
96
|
+
| "failed"
|
|
97
|
+
| "unavailable";
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
/**
|
|
@@ -175,6 +191,46 @@ function applyEnvToProcess(
|
|
|
175
191
|
};
|
|
176
192
|
}
|
|
177
193
|
|
|
194
|
+
function appendProviderEnvFindings(options: {
|
|
195
|
+
owner: string;
|
|
196
|
+
required: readonly string[];
|
|
197
|
+
alternatives: readonly (readonly string[])[];
|
|
198
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
199
|
+
requiredEnvKeys: Set<string>;
|
|
200
|
+
findings: PreflightFinding[];
|
|
201
|
+
}): void {
|
|
202
|
+
for (const key of options.required) {
|
|
203
|
+
options.requiredEnvKeys.add(key);
|
|
204
|
+
const value = options.env[key];
|
|
205
|
+
if (value === undefined || value.trim() === "") {
|
|
206
|
+
options.findings.push({
|
|
207
|
+
severity: "error",
|
|
208
|
+
code: "BEIGNET_PREFLIGHT_ENV_MISSING",
|
|
209
|
+
message: `${key} is required by ${options.owner} but is not set.`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const key of options.alternatives.flat()) {
|
|
215
|
+
options.requiredEnvKeys.add(key);
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
options.alternatives.length > 0 &&
|
|
219
|
+
!options.alternatives.some((alternative) =>
|
|
220
|
+
alternative.every((key) => {
|
|
221
|
+
const value = options.env[key];
|
|
222
|
+
return value !== undefined && value.trim() !== "";
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
225
|
+
) {
|
|
226
|
+
options.findings.push({
|
|
227
|
+
severity: "error",
|
|
228
|
+
code: "BEIGNET_PREFLIGHT_ENV_MISSING",
|
|
229
|
+
message: `${options.owner} requires one complete environment configuration: ${formatProviderEnvAlternatives(options.alternatives)}.`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
178
234
|
/**
|
|
179
235
|
* Validate the deploy environment against installed provider manifests, the
|
|
180
236
|
* app's `.env.example` placeholders, and the app env schema module.
|
|
@@ -221,6 +277,7 @@ export async function runPreflight(
|
|
|
221
277
|
envModuleChecked: false,
|
|
222
278
|
doctorChecked: false,
|
|
223
279
|
checkedDependencies: [],
|
|
280
|
+
migrationStatus: "not-checked",
|
|
224
281
|
};
|
|
225
282
|
}
|
|
226
283
|
|
|
@@ -230,19 +287,34 @@ export async function runPreflight(
|
|
|
230
287
|
);
|
|
231
288
|
const checkedProviders = rules.map((rule) => rule.packageName);
|
|
232
289
|
const requiredEnvKeys = new Set<string>();
|
|
290
|
+
const detectedVariants = await detectedProviderVariantsForRules(
|
|
291
|
+
options.cwd,
|
|
292
|
+
rules,
|
|
293
|
+
);
|
|
233
294
|
|
|
234
295
|
for (const rule of rules) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
296
|
+
if (rule.variants !== undefined) {
|
|
297
|
+
for (const variant of detectedVariants.get(rule.packageName) ?? []) {
|
|
298
|
+
appendProviderEnvFindings({
|
|
299
|
+
owner: `${rule.packageName} (${variant.displayName})`,
|
|
300
|
+
required: variant.requiredEnv,
|
|
301
|
+
alternatives: variant.requiredEnvAlternatives,
|
|
302
|
+
env,
|
|
303
|
+
requiredEnvKeys,
|
|
304
|
+
findings,
|
|
243
305
|
});
|
|
244
306
|
}
|
|
307
|
+
continue;
|
|
245
308
|
}
|
|
309
|
+
|
|
310
|
+
appendProviderEnvFindings({
|
|
311
|
+
owner: rule.packageName,
|
|
312
|
+
required: rule.requiredEnv ?? [],
|
|
313
|
+
alternatives: rule.requiredEnvAlternatives ?? [],
|
|
314
|
+
env,
|
|
315
|
+
requiredEnvKeys,
|
|
316
|
+
findings,
|
|
317
|
+
});
|
|
246
318
|
}
|
|
247
319
|
|
|
248
320
|
const inngestDev = env.INNGEST_DEV?.trim().toLowerCase();
|
|
@@ -303,6 +375,7 @@ export async function runPreflight(
|
|
|
303
375
|
let envModuleChecked = false;
|
|
304
376
|
let doctorChecked = false;
|
|
305
377
|
const checkedDependencies: string[] = [];
|
|
378
|
+
let migrationStatus: PreflightResult["migrationStatus"] = "not-checked";
|
|
306
379
|
const restoreProcessEnv = applyEnvToProcess(env);
|
|
307
380
|
|
|
308
381
|
try {
|
|
@@ -344,7 +417,67 @@ export async function runPreflight(
|
|
|
344
417
|
}
|
|
345
418
|
|
|
346
419
|
if (options.connect) {
|
|
347
|
-
|
|
420
|
+
if (!packageJson.scripts?.["db:status"]) {
|
|
421
|
+
migrationStatus = "unavailable";
|
|
422
|
+
findings.push({
|
|
423
|
+
severity: "note",
|
|
424
|
+
code: "BEIGNET_PREFLIGHT_DATABASE_STATUS_UNAVAILABLE",
|
|
425
|
+
message:
|
|
426
|
+
'No app-owned "db:status" script is configured; skipping pending migration detection.',
|
|
427
|
+
});
|
|
428
|
+
} else {
|
|
429
|
+
try {
|
|
430
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? 5000;
|
|
431
|
+
const result = await runDatabaseStatus({
|
|
432
|
+
cwd: options.cwd,
|
|
433
|
+
captureOutput: true,
|
|
434
|
+
maxOutputBytes: 8 * 1024,
|
|
435
|
+
signal: options.signal,
|
|
436
|
+
timeoutMs: connectTimeoutMs,
|
|
437
|
+
});
|
|
438
|
+
migrationStatus = result.status;
|
|
439
|
+
|
|
440
|
+
if (result.status === "pending") {
|
|
441
|
+
findings.push({
|
|
442
|
+
severity: "error",
|
|
443
|
+
code: "BEIGNET_PREFLIGHT_DATABASE_MIGRATIONS_PENDING",
|
|
444
|
+
message: databaseStatusFindingMessage(
|
|
445
|
+
"Database migrations are pending. Run `beignet db migrate` in the serialized migration job before booting the app.",
|
|
446
|
+
result.stdout,
|
|
447
|
+
result.stderr,
|
|
448
|
+
result.outputTruncated,
|
|
449
|
+
),
|
|
450
|
+
});
|
|
451
|
+
} else if (result.status === "failed") {
|
|
452
|
+
findings.push({
|
|
453
|
+
severity: "error",
|
|
454
|
+
code: "BEIGNET_PREFLIGHT_DATABASE_STATUS_FAILED",
|
|
455
|
+
message: databaseStatusFindingMessage(
|
|
456
|
+
result.timedOut
|
|
457
|
+
? `Database migration status inspection timed out after ${connectTimeoutMs}ms.`
|
|
458
|
+
: "Database migration status inspection failed.",
|
|
459
|
+
result.stdout,
|
|
460
|
+
result.stderr,
|
|
461
|
+
result.outputTruncated,
|
|
462
|
+
),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
options.signal?.throwIfAborted();
|
|
467
|
+
migrationStatus = "failed";
|
|
468
|
+
findings.push({
|
|
469
|
+
severity: "error",
|
|
470
|
+
code: "BEIGNET_PREFLIGHT_DATABASE_STATUS_FAILED",
|
|
471
|
+
message: `Database migration status inspection failed: ${
|
|
472
|
+
error instanceof Error ? error.message : String(error)
|
|
473
|
+
}`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (migrationStatus !== "pending" && migrationStatus !== "failed") {
|
|
479
|
+
await runDependencyChecks(options, findings, checkedDependencies);
|
|
480
|
+
}
|
|
348
481
|
}
|
|
349
482
|
} finally {
|
|
350
483
|
restoreProcessEnv();
|
|
@@ -356,9 +489,26 @@ export async function runPreflight(
|
|
|
356
489
|
envModuleChecked,
|
|
357
490
|
doctorChecked,
|
|
358
491
|
checkedDependencies,
|
|
492
|
+
migrationStatus,
|
|
359
493
|
};
|
|
360
494
|
}
|
|
361
495
|
|
|
496
|
+
function databaseStatusFindingMessage(
|
|
497
|
+
summary: string,
|
|
498
|
+
stdout?: string,
|
|
499
|
+
stderr?: string,
|
|
500
|
+
outputTruncated?: boolean,
|
|
501
|
+
): string {
|
|
502
|
+
const details = [
|
|
503
|
+
stdout?.trim() ? `stdout:\n${stdout.trim()}` : undefined,
|
|
504
|
+
stderr?.trim() ? `stderr:\n${stderr.trim()}` : undefined,
|
|
505
|
+
].filter((value): value is string => value !== undefined);
|
|
506
|
+
if (outputTruncated) {
|
|
507
|
+
details.push("Output truncated; showing the retained tail.");
|
|
508
|
+
}
|
|
509
|
+
return [summary, ...details].join("\n");
|
|
510
|
+
}
|
|
511
|
+
|
|
362
512
|
interface HealthCheckable {
|
|
363
513
|
checkHealth(): Promise<unknown>;
|
|
364
514
|
}
|
|
@@ -381,10 +531,13 @@ async function withTimeout<T>(
|
|
|
381
531
|
work: Promise<T>,
|
|
382
532
|
ms: number,
|
|
383
533
|
label: string,
|
|
534
|
+
signal?: AbortSignal,
|
|
384
535
|
): Promise<T> {
|
|
536
|
+
signal?.throwIfAborted();
|
|
385
537
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
538
|
+
let onAbort: (() => void) | undefined;
|
|
386
539
|
try {
|
|
387
|
-
|
|
540
|
+
const contenders: Promise<T>[] = [
|
|
388
541
|
work,
|
|
389
542
|
new Promise<never>((_, reject) => {
|
|
390
543
|
timer = setTimeout(
|
|
@@ -392,9 +545,25 @@ async function withTimeout<T>(
|
|
|
392
545
|
ms,
|
|
393
546
|
);
|
|
394
547
|
}),
|
|
395
|
-
]
|
|
548
|
+
];
|
|
549
|
+
if (signal) {
|
|
550
|
+
contenders.push(
|
|
551
|
+
new Promise<never>((_, reject) => {
|
|
552
|
+
onAbort = () =>
|
|
553
|
+
reject(
|
|
554
|
+
signal.reason instanceof Error
|
|
555
|
+
? signal.reason
|
|
556
|
+
: new Error("Connected preflight cancelled."),
|
|
557
|
+
);
|
|
558
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
559
|
+
if (signal.aborted) onAbort();
|
|
560
|
+
}),
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
return await Promise.race(contenders);
|
|
396
564
|
} finally {
|
|
397
565
|
if (timer !== undefined) clearTimeout(timer);
|
|
566
|
+
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
398
567
|
}
|
|
399
568
|
}
|
|
400
569
|
|
|
@@ -407,6 +576,7 @@ async function runDependencyChecks(
|
|
|
407
576
|
findings: PreflightFinding[],
|
|
408
577
|
checkedDependencies: string[],
|
|
409
578
|
): Promise<void> {
|
|
579
|
+
options.signal?.throwIfAborted();
|
|
410
580
|
const serverModule = options.serverModule ?? "server/index.ts";
|
|
411
581
|
const serverModulePath = path.resolve(options.cwd, serverModule);
|
|
412
582
|
const timeoutMs = options.connectTimeoutMs ?? 5000;
|
|
@@ -424,6 +594,7 @@ async function runDependencyChecks(
|
|
|
424
594
|
| { ports: Record<string, unknown>; stop?: () => Promise<void> }
|
|
425
595
|
| undefined;
|
|
426
596
|
try {
|
|
597
|
+
options.signal?.throwIfAborted();
|
|
427
598
|
const jiti = createJiti(serverModulePath, {
|
|
428
599
|
interopDefault: true,
|
|
429
600
|
moduleCache: false,
|
|
@@ -452,6 +623,7 @@ async function runDependencyChecks(
|
|
|
452
623
|
stop?: () => Promise<void>;
|
|
453
624
|
};
|
|
454
625
|
} catch (error) {
|
|
626
|
+
options.signal?.throwIfAborted();
|
|
455
627
|
findings.push({
|
|
456
628
|
severity: "error",
|
|
457
629
|
code: "BEIGNET_PREFLIGHT_SERVER_BOOT_FAILED",
|
|
@@ -464,6 +636,7 @@ async function runDependencyChecks(
|
|
|
464
636
|
|
|
465
637
|
try {
|
|
466
638
|
for (const [name, port] of Object.entries(server.ports)) {
|
|
639
|
+
options.signal?.throwIfAborted();
|
|
467
640
|
if (!isHealthCheckable(port)) continue;
|
|
468
641
|
checkedDependencies.push(name);
|
|
469
642
|
try {
|
|
@@ -471,6 +644,7 @@ async function runDependencyChecks(
|
|
|
471
644
|
port.checkHealth(),
|
|
472
645
|
timeoutMs,
|
|
473
646
|
`${name}.checkHealth()`,
|
|
647
|
+
options.signal,
|
|
474
648
|
);
|
|
475
649
|
if (!healthResultOk(result)) {
|
|
476
650
|
const message = (result as { message?: unknown }).message;
|
|
@@ -483,6 +657,7 @@ async function runDependencyChecks(
|
|
|
483
657
|
});
|
|
484
658
|
}
|
|
485
659
|
} catch (error) {
|
|
660
|
+
options.signal?.throwIfAborted();
|
|
486
661
|
findings.push({
|
|
487
662
|
severity: "error",
|
|
488
663
|
code: "BEIGNET_PREFLIGHT_DEPENDENCY_UNHEALTHY",
|
|
@@ -534,14 +709,18 @@ export function formatPreflight(result: PreflightResult, cwd: string): string {
|
|
|
534
709
|
const dependencySummary = result.checkedDependencies.length
|
|
535
710
|
? `, ${result.checkedDependencies.length} dependency health check(s) run`
|
|
536
711
|
: "";
|
|
712
|
+
const migrationSummary =
|
|
713
|
+
result.migrationStatus === "not-checked"
|
|
714
|
+
? "migration status not checked"
|
|
715
|
+
: `migration status ${result.migrationStatus}`;
|
|
537
716
|
|
|
538
717
|
if (errors.length === 0) {
|
|
539
718
|
lines.push(
|
|
540
|
-
`Preflight passed in ${cwd}: ${providerSummary} checked, ${envSummary}, ${doctorSummary}${dependencySummary}.`,
|
|
719
|
+
`Preflight passed in ${cwd}: ${providerSummary} checked, ${envSummary}, ${doctorSummary}, ${migrationSummary}${dependencySummary}.`,
|
|
541
720
|
);
|
|
542
721
|
} else {
|
|
543
722
|
lines.push(
|
|
544
|
-
`Preflight failed in ${cwd}: ${errors.length} error(s) across ${providerSummary}, ${envSummary}, ${doctorSummary}${dependencySummary}.`,
|
|
723
|
+
`Preflight failed in ${cwd}: ${errors.length} error(s) across ${providerSummary}, ${envSummary}, ${doctorSummary}, ${migrationSummary}${dependencySummary}.`,
|
|
545
724
|
);
|
|
546
725
|
}
|
|
547
726
|
|
package/src/provider-add.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
3
4
|
import {
|
|
4
5
|
type ProviderPresetName,
|
|
5
6
|
providerPresetChoices,
|
|
@@ -164,7 +165,10 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
164
165
|
"Add a worker entrypoint that runs createBullMQJobWorker(...) in a worker process; dispatched jobs do not execute without one. See the jobs docs page.",
|
|
165
166
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
166
167
|
],
|
|
167
|
-
conflictingProviderEntries: [
|
|
168
|
+
conflictingProviderEntries: [
|
|
169
|
+
"createInngestJobsProvider",
|
|
170
|
+
"inlineJobsProvider",
|
|
171
|
+
],
|
|
168
172
|
},
|
|
169
173
|
"jobs-inngest": {
|
|
170
174
|
name: "jobs-inngest",
|
|
@@ -215,7 +219,10 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
215
219
|
"For non-Next runtimes, call createAppInngestFunctions(...) after creating the app server, then mount the result with Inngest's host adapter.",
|
|
216
220
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
217
221
|
],
|
|
218
|
-
conflictingProviderEntries: [
|
|
222
|
+
conflictingProviderEntries: [
|
|
223
|
+
"createBullMQJobsProvider",
|
|
224
|
+
"inlineJobsProvider",
|
|
225
|
+
],
|
|
219
226
|
},
|
|
220
227
|
"mail-resend": {
|
|
221
228
|
name: "mail-resend",
|
|
@@ -276,7 +283,14 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
276
283
|
importName: "MailerPort",
|
|
277
284
|
importFrom: "@beignet/core/mail",
|
|
278
285
|
},
|
|
279
|
-
envVars: [
|
|
286
|
+
envVars: [
|
|
287
|
+
"MAIL_HOST",
|
|
288
|
+
"MAIL_PORT",
|
|
289
|
+
"MAIL_USER",
|
|
290
|
+
"MAIL_PASS",
|
|
291
|
+
"MAIL_FROM",
|
|
292
|
+
"MAIL_REQUIRE_TLS",
|
|
293
|
+
],
|
|
280
294
|
envExample: [
|
|
281
295
|
"# SMTP mail provider",
|
|
282
296
|
"MAIL_HOST=smtp.example.com",
|
|
@@ -284,6 +298,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
284
298
|
"MAIL_USER=dev-placeholder-change-me",
|
|
285
299
|
"MAIL_PASS=dev-placeholder-change-me",
|
|
286
300
|
"MAIL_FROM=Beignet App <noreply@example.com>",
|
|
301
|
+
"MAIL_REQUIRE_TLS=true",
|
|
287
302
|
"",
|
|
288
303
|
].join("\n"),
|
|
289
304
|
docs: [
|
|
@@ -293,12 +308,13 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
293
308
|
"- Peer dependency: `nodemailer`",
|
|
294
309
|
"- The preset wires `createSmtpMailProvider()` in `server/providers.ts` and defers `mailer` in `infra/port-wiring.ts`.",
|
|
295
310
|
"- `.env.example` ships dev placeholders so the app boots; set real `MAIL_*` values before sending mail.",
|
|
311
|
+
"- Encrypted transport is required by default: port 465 uses implicit TLS and other ports must negotiate STARTTLS.",
|
|
296
312
|
"- Use `ctx.ports.mailer` in app code; `ctx.ports.smtp` is available as an escape hatch.",
|
|
297
313
|
"",
|
|
298
314
|
].join("\n"),
|
|
299
315
|
nextSteps: [
|
|
300
316
|
"Run your package manager install command.",
|
|
301
|
-
"Set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM in .env.local or your deployment environment.",
|
|
317
|
+
"Set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM in .env.local or your deployment environment; keep MAIL_REQUIRE_TLS=true outside trusted local capture services.",
|
|
302
318
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
303
319
|
],
|
|
304
320
|
conflictingProviderEntries: [
|
|
@@ -561,6 +577,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
561
577
|
dependencies: (packageJson) => ({
|
|
562
578
|
"@beignet/provider-storage-s3": beignetDependencyVersion(packageJson),
|
|
563
579
|
"@aws-sdk/client-s3": externalVersions.awsSdkS3,
|
|
580
|
+
"@aws-sdk/lib-storage": externalVersions.awsSdkLibStorage,
|
|
564
581
|
"@aws-sdk/s3-request-presigner": externalVersions.awsSdkS3,
|
|
565
582
|
}),
|
|
566
583
|
appPort: {
|
|
@@ -601,7 +618,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
601
618
|
"## S3-compatible storage",
|
|
602
619
|
"",
|
|
603
620
|
"- Package: `@beignet/provider-storage-s3`",
|
|
604
|
-
"- Peer dependencies: `@aws-sdk/client-s3
|
|
621
|
+
"- Peer dependencies: `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, and `@aws-sdk/s3-request-presigner`",
|
|
605
622
|
"- The preset wires `createS3StorageProvider()` in `server/providers.ts` and defers `storage` in `infra/port-wiring.ts`.",
|
|
606
623
|
"- Set `STORAGE_S3_BUCKET` before booting the app; set endpoint and credentials when using an S3-compatible host outside AWS defaults.",
|
|
607
624
|
"- Use `ctx.ports.storage` in app code; `ctx.ports.s3Storage` is available as an escape hatch.",
|
|
@@ -639,6 +656,8 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
639
656
|
"# Vercel Blob storage provider. Vercel sets BLOB_READ_WRITE_TOKEN",
|
|
640
657
|
"# automatically on deployments with a connected Blob store.",
|
|
641
658
|
"# BLOB_READ_WRITE_TOKEN=vercel_blob_rw_...",
|
|
659
|
+
"# OIDC alternative on Vercel: VERCEL_OIDC_TOKEN plus BLOB_STORE_ID.",
|
|
660
|
+
"# BLOB_STORE_ID=store_...",
|
|
642
661
|
"# BLOB_ACCESS=private",
|
|
643
662
|
"# BLOB_KEY_PREFIX=",
|
|
644
663
|
"",
|
|
@@ -649,14 +668,14 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
649
668
|
"- Package: `@beignet/provider-storage-vercel-blob`",
|
|
650
669
|
"- Peer dependency: `@vercel/blob`",
|
|
651
670
|
"- The preset wires `createVercelBlobStorageProvider()` in `server/providers.ts` and defers `storage` in `infra/port-wiring.ts`.",
|
|
652
|
-
"- On Vercel, connect a Blob store to the project
|
|
671
|
+
"- On Vercel, connect a Blob store to the project. Use its `BLOB_READ_WRITE_TOKEN`, or Vercel OIDC with `VERCEL_OIDC_TOKEN` plus `BLOB_STORE_ID`; set a read-write token manually outside Vercel.",
|
|
653
672
|
"- The store is uniform-visibility (`BLOB_ACCESS`, default `private`): Vercel Blob does not report per-object access back, so mixed visibility needs a second store.",
|
|
654
673
|
"- Use `ctx.ports.storage` in app code; `ctx.ports.vercelBlob` is available as an escape hatch.",
|
|
655
674
|
"",
|
|
656
675
|
].join("\n"),
|
|
657
676
|
nextSteps: [
|
|
658
677
|
"Run your package manager install command.",
|
|
659
|
-
"Connect a Vercel Blob store
|
|
678
|
+
"Connect a Vercel Blob store and configure either BLOB_READ_WRITE_TOKEN or Vercel OIDC with BLOB_STORE_ID plus VERCEL_OIDC_TOKEN.",
|
|
660
679
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
661
680
|
],
|
|
662
681
|
conflictingProviderEntries: [
|
|
@@ -689,6 +708,8 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
689
708
|
"UPSTASH_REDIS_REST_TOKEN=dev-placeholder-change-me",
|
|
690
709
|
"UPSTASH_PREFIX=beignet:ratelimit",
|
|
691
710
|
"# UPSTASH_ALGORITHM=fixed-window",
|
|
711
|
+
"# UPSTASH_TIMEOUT_MS=5000",
|
|
712
|
+
"# UPSTASH_TIMEOUT_POLICY=fail-closed",
|
|
692
713
|
"",
|
|
693
714
|
].join("\n"),
|
|
694
715
|
docs: [
|
|
@@ -698,6 +719,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
698
719
|
"- Peer dependencies: `@upstash/ratelimit` and `@upstash/redis`",
|
|
699
720
|
"- The preset wires `createUpstashRateLimitProvider()` in `server/providers.ts` and defers `rateLimit` in `infra/port-wiring.ts`.",
|
|
700
721
|
"- `.env.example` ships dev placeholders so the app boots; set real `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` values before rate limiting requests.",
|
|
722
|
+
"- Rate-limit checks time out after 5000ms and fail closed by default. Set `UPSTASH_TIMEOUT_POLICY=fail-open` only when availability is more important than enforcement during an Upstash outage.",
|
|
701
723
|
"- Use `ctx.ports.rateLimit` in app code; `ctx.ports.upstash` is available as an escape hatch.",
|
|
702
724
|
"",
|
|
703
725
|
].join("\n"),
|
|
@@ -1174,9 +1196,13 @@ function updatePackageJson(
|
|
|
1174
1196
|
}
|
|
1175
1197
|
|
|
1176
1198
|
function updateServerProviders(source: string, preset: ProviderPreset): string {
|
|
1177
|
-
|
|
1199
|
+
const withoutGeneratedFallback =
|
|
1200
|
+
preset.appPort.key === "jobs"
|
|
1201
|
+
? removeGeneratedInlineJobsProvider(source)
|
|
1202
|
+
: source;
|
|
1203
|
+
assertNoConflictingProviders(withoutGeneratedFallback, preset);
|
|
1178
1204
|
|
|
1179
|
-
let next =
|
|
1205
|
+
let next = withoutGeneratedFallback;
|
|
1180
1206
|
for (const importLine of preset.additionalImports ?? []) {
|
|
1181
1207
|
if (!next.includes(importLine)) next = insertAfterImports(next, importLine);
|
|
1182
1208
|
}
|
|
@@ -1200,6 +1226,164 @@ function updateServerProviders(source: string, preset: ProviderPreset): string {
|
|
|
1200
1226
|
return result.kind === "updated" ? result.source : next;
|
|
1201
1227
|
}
|
|
1202
1228
|
|
|
1229
|
+
function removeGeneratedInlineJobsProvider(source: string): string {
|
|
1230
|
+
const marker = "// beignet:generated-inline-jobs-provider";
|
|
1231
|
+
const sourceFile = ts.createSourceFile(
|
|
1232
|
+
"providers.ts",
|
|
1233
|
+
source,
|
|
1234
|
+
ts.ScriptTarget.Latest,
|
|
1235
|
+
true,
|
|
1236
|
+
ts.ScriptKind.TS,
|
|
1237
|
+
);
|
|
1238
|
+
const statement = sourceFile.statements.find((candidate) => {
|
|
1239
|
+
if (!ts.isVariableStatement(candidate)) return false;
|
|
1240
|
+
if (candidate.declarationList.declarations.length !== 1) return false;
|
|
1241
|
+
const statementStart = candidate.getStart(sourceFile);
|
|
1242
|
+
const markerStart = source.lastIndexOf(marker, statementStart);
|
|
1243
|
+
if (
|
|
1244
|
+
markerStart < 0 ||
|
|
1245
|
+
source.slice(markerStart + marker.length, statementStart).trim() !== ""
|
|
1246
|
+
) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
const declaration = candidate.declarationList.declarations.find(
|
|
1250
|
+
(item) =>
|
|
1251
|
+
ts.isIdentifier(item.name) && item.name.text === "inlineJobsProvider",
|
|
1252
|
+
);
|
|
1253
|
+
if (!declaration?.initializer) return false;
|
|
1254
|
+
const text = declaration.initializer.getText(sourceFile);
|
|
1255
|
+
return (
|
|
1256
|
+
/\bcreateProvider\b/.test(text) &&
|
|
1257
|
+
/\bcreateInlineJobDispatcher\b/.test(text) &&
|
|
1258
|
+
/\bname\s*:\s*["']inline-jobs["']/.test(text)
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
if (!statement) return source;
|
|
1262
|
+
|
|
1263
|
+
const statementStart = statement.getStart(sourceFile);
|
|
1264
|
+
const markerStart = source.lastIndexOf(marker, statementStart);
|
|
1265
|
+
const removeStart =
|
|
1266
|
+
markerStart >= 0 &&
|
|
1267
|
+
source.slice(markerStart + marker.length, statementStart).trim() === ""
|
|
1268
|
+
? markerStart
|
|
1269
|
+
: statementStart;
|
|
1270
|
+
let next = `${source.slice(0, removeStart)}${source.slice(statement.end)}`;
|
|
1271
|
+
const registry = arrayInitializerInfo(next, "providers");
|
|
1272
|
+
if (!registry) {
|
|
1273
|
+
throw new Error(
|
|
1274
|
+
"Could not remove the generated inline jobs provider because `export const providers = [...]` is missing.",
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
const lines = registry.text.split(/\r?\n/);
|
|
1278
|
+
const entryIndex = lines.findIndex(
|
|
1279
|
+
(line) =>
|
|
1280
|
+
line.trim() === "inlineJobsProvider," ||
|
|
1281
|
+
line.trim() === "inlineJobsProvider",
|
|
1282
|
+
);
|
|
1283
|
+
let nextRegistry: string | undefined;
|
|
1284
|
+
if (entryIndex >= 0) {
|
|
1285
|
+
lines.splice(entryIndex, 1);
|
|
1286
|
+
nextRegistry = lines.join(registry.text.includes("\r\n") ? "\r\n" : "\n");
|
|
1287
|
+
} else if (!registry.text.includes("\n")) {
|
|
1288
|
+
const beforeFollowingEntry = /\binlineJobsProvider\s*,\s*/;
|
|
1289
|
+
const afterPreviousEntry = /,\s*inlineJobsProvider\b/;
|
|
1290
|
+
const onlyEntry = /^\[\s*inlineJobsProvider\s*\]$/;
|
|
1291
|
+
if (onlyEntry.test(registry.text)) nextRegistry = "[]";
|
|
1292
|
+
else if (beforeFollowingEntry.test(registry.text)) {
|
|
1293
|
+
nextRegistry = registry.text.replace(beforeFollowingEntry, "");
|
|
1294
|
+
} else if (afterPreviousEntry.test(registry.text)) {
|
|
1295
|
+
nextRegistry = registry.text.replace(afterPreviousEntry, "");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (nextRegistry === undefined) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
"Could not remove the generated inline jobs provider from `export const providers = [...]`.",
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
next = `${next.slice(0, registry.start)}${nextRegistry}${next.slice(registry.end)}`;
|
|
1304
|
+
|
|
1305
|
+
return removeNamedProviderImport(
|
|
1306
|
+
next,
|
|
1307
|
+
"createInlineJobDispatcher",
|
|
1308
|
+
"@beignet/core/jobs",
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function removeNamedProviderImport(
|
|
1313
|
+
source: string,
|
|
1314
|
+
name: string,
|
|
1315
|
+
moduleName: string,
|
|
1316
|
+
): string {
|
|
1317
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1318
|
+
const pattern = new RegExp(
|
|
1319
|
+
`import \\{([^}]*)\\} from ["']${escapedModule}["'];`,
|
|
1320
|
+
);
|
|
1321
|
+
const match = pattern.exec(source);
|
|
1322
|
+
if (!match) return source;
|
|
1323
|
+
const names = match[1]
|
|
1324
|
+
.split(",")
|
|
1325
|
+
.map((entry) => entry.trim())
|
|
1326
|
+
.filter(Boolean);
|
|
1327
|
+
if (!names.includes(name)) return source;
|
|
1328
|
+
if (hasIdentifierReferenceOutsideImport(source, name, moduleName)) {
|
|
1329
|
+
return source;
|
|
1330
|
+
}
|
|
1331
|
+
const remaining = names.filter((entry) => entry !== name);
|
|
1332
|
+
if (remaining.length === 0) {
|
|
1333
|
+
const lineBreak = source.startsWith("\r\n", match.index + match[0].length)
|
|
1334
|
+
? "\r\n"
|
|
1335
|
+
: source[match.index + match[0].length] === "\n"
|
|
1336
|
+
? "\n"
|
|
1337
|
+
: "";
|
|
1338
|
+
return `${source.slice(0, match.index)}${source.slice(match.index + match[0].length + lineBreak.length)}`;
|
|
1339
|
+
}
|
|
1340
|
+
const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
|
|
1341
|
+
const replacement = match[1].includes("\n")
|
|
1342
|
+
? `import {\n${indent}${remaining.join(`,\n${indent}`)},\n} from "${moduleName}";`
|
|
1343
|
+
: `import { ${remaining.join(", ")} } from "${moduleName}";`;
|
|
1344
|
+
return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function hasIdentifierReferenceOutsideImport(
|
|
1348
|
+
source: string,
|
|
1349
|
+
name: string,
|
|
1350
|
+
moduleName: string,
|
|
1351
|
+
): boolean {
|
|
1352
|
+
const sourceFile = ts.createSourceFile(
|
|
1353
|
+
"providers.ts",
|
|
1354
|
+
source,
|
|
1355
|
+
ts.ScriptTarget.Latest,
|
|
1356
|
+
true,
|
|
1357
|
+
ts.ScriptKind.TS,
|
|
1358
|
+
);
|
|
1359
|
+
const importDeclaration = sourceFile.statements.find(
|
|
1360
|
+
(statement): statement is ts.ImportDeclaration =>
|
|
1361
|
+
ts.isImportDeclaration(statement) &&
|
|
1362
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
1363
|
+
statement.moduleSpecifier.text === moduleName &&
|
|
1364
|
+
Boolean(
|
|
1365
|
+
statement.importClause?.namedBindings &&
|
|
1366
|
+
ts.isNamedImports(statement.importClause.namedBindings) &&
|
|
1367
|
+
statement.importClause.namedBindings.elements.some(
|
|
1368
|
+
(element) => !element.propertyName && element.name.text === name,
|
|
1369
|
+
),
|
|
1370
|
+
),
|
|
1371
|
+
);
|
|
1372
|
+
if (!importDeclaration) return true;
|
|
1373
|
+
|
|
1374
|
+
let referenced = false;
|
|
1375
|
+
const visit = (node: ts.Node): void => {
|
|
1376
|
+
if (node === importDeclaration || referenced) return;
|
|
1377
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
1378
|
+
referenced = true;
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
ts.forEachChild(node, visit);
|
|
1382
|
+
};
|
|
1383
|
+
ts.forEachChild(sourceFile, visit);
|
|
1384
|
+
return referenced;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1203
1387
|
function assertNoConflictingProviders(
|
|
1204
1388
|
source: string,
|
|
1205
1389
|
preset: ProviderPreset,
|