@actuate-media/cli 0.6.0 → 0.8.0
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +27 -25
- package/CHANGELOG.md +19 -0
- package/dist/__tests__/db-sync.test.js +32 -1
- package/dist/__tests__/db-sync.test.js.map +1 -1
- package/dist/__tests__/deployment-diagnostics.test.js +42 -1
- package/dist/__tests__/deployment-diagnostics.test.js.map +1 -1
- package/dist/__tests__/vercel-env-matrix.test.d.ts +2 -0
- package/dist/__tests__/vercel-env-matrix.test.d.ts.map +1 -0
- package/dist/__tests__/vercel-env-matrix.test.js +48 -0
- package/dist/__tests__/vercel-env-matrix.test.js.map +1 -0
- package/dist/commands/db-sync.d.ts +22 -0
- package/dist/commands/db-sync.d.ts.map +1 -1
- package/dist/commands/db-sync.js +94 -0
- package/dist/commands/db-sync.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +70 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/migrate-sections.d.ts +3 -0
- package/dist/commands/migrate-sections.d.ts.map +1 -0
- package/dist/commands/migrate-sections.js +56 -0
- package/dist/commands/migrate-sections.js.map +1 -0
- package/dist/commands/seed.d.ts.map +1 -1
- package/dist/commands/seed.js +2 -40
- package/dist/commands/seed.js.map +1 -1
- package/dist/commands/vercel-blob-link.d.ts +3 -0
- package/dist/commands/vercel-blob-link.d.ts.map +1 -0
- package/dist/commands/vercel-blob-link.js +82 -0
- package/dist/commands/vercel-blob-link.js.map +1 -0
- package/dist/deployment/diagnostics.d.ts +13 -0
- package/dist/deployment/diagnostics.d.ts.map +1 -1
- package/dist/deployment/diagnostics.js +55 -0
- package/dist/deployment/diagnostics.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/database.d.ts +19 -0
- package/dist/utils/database.d.ts.map +1 -0
- package/dist/utils/database.js +58 -0
- package/dist/utils/database.js.map +1 -0
- package/dist/vercel/client.d.ts +32 -0
- package/dist/vercel/client.d.ts.map +1 -0
- package/dist/vercel/client.js +74 -0
- package/dist/vercel/client.js.map +1 -0
- package/dist/vercel/env-matrix.d.ts +34 -0
- package/dist/vercel/env-matrix.d.ts.map +1 -0
- package/dist/vercel/env-matrix.js +57 -0
- package/dist/vercel/env-matrix.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/db-sync.test.ts +55 -1
- package/src/__tests__/deployment-diagnostics.test.ts +51 -0
- package/src/__tests__/vercel-env-matrix.test.ts +56 -0
- package/src/commands/db-sync.ts +116 -0
- package/src/commands/doctor.ts +118 -10
- package/src/commands/migrate-sections.ts +73 -0
- package/src/commands/seed.ts +2 -56
- package/src/commands/vercel-blob-link.ts +115 -0
- package/src/deployment/diagnostics.ts +70 -0
- package/src/index.ts +4 -0
- package/src/utils/database.ts +77 -0
- package/src/vercel/client.ts +112 -0
- package/src/vercel/env-matrix.ts +101 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { registerMigrateCommand } from './commands/migrate.js';
|
|
4
|
+
import { registerMigrateSectionsCommand } from './commands/migrate-sections.js';
|
|
4
5
|
import { registerGenerateCommand } from './commands/generate.js';
|
|
5
6
|
import { registerSeedCommand } from './commands/seed.js';
|
|
6
7
|
import { registerImportCommand } from './commands/import.js';
|
|
@@ -12,12 +13,14 @@ import { registerDbSyncCommand } from './commands/db-sync.js';
|
|
|
12
13
|
import { registerDbStatusCommand } from './commands/db-status.js';
|
|
13
14
|
import { registerInitCommand } from './commands/init.js';
|
|
14
15
|
import { registerDeployCheckCommand, registerDoctorCommand, registerVerifyCommand, } from './commands/doctor.js';
|
|
16
|
+
import { registerVercelBlobLinkCommand } from './commands/vercel-blob-link.js';
|
|
15
17
|
const program = new Command();
|
|
16
18
|
program
|
|
17
19
|
.name('actuate')
|
|
18
20
|
.description('Actuate CMS — CLI toolkit for migrations, codegen, and more')
|
|
19
21
|
.version('0.1.0');
|
|
20
22
|
registerMigrateCommand(program);
|
|
23
|
+
registerMigrateSectionsCommand(program);
|
|
21
24
|
registerGenerateCommand(program);
|
|
22
25
|
registerSeedCommand(program);
|
|
23
26
|
registerImportCommand(program);
|
|
@@ -31,5 +34,6 @@ registerInitCommand(program);
|
|
|
31
34
|
registerDoctorCommand(program);
|
|
32
35
|
registerDeployCheckCommand(program);
|
|
33
36
|
registerVerifyCommand(program);
|
|
37
|
+
registerVercelBlobLinkCommand(program);
|
|
34
38
|
program.parse();
|
|
35
39
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAA;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAE9E,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAE7B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAA;AAEnB,sBAAsB,CAAC,OAAO,CAAC,CAAA;AAC/B,8BAA8B,CAAC,OAAO,CAAC,CAAA;AACvC,uBAAuB,CAAC,OAAO,CAAC,CAAA;AAChC,mBAAmB,CAAC,OAAO,CAAC,CAAA;AAC5B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,sBAAsB,CAAC,OAAO,CAAC,CAAA;AAC/B,0BAA0B,CAAC,OAAO,CAAC,CAAA;AACnC,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,uBAAuB,CAAC,OAAO,CAAC,CAAA;AAChC,mBAAmB,CAAC,OAAO,CAAC,CAAA;AAC5B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,0BAA0B,CAAC,OAAO,CAAC,CAAA;AACnC,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,6BAA6B,CAAC,OAAO,CAAC,CAAA;AAEtC,OAAO,CAAC,KAAK,EAAE,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for CLI commands that need a live connection to the consumer
|
|
3
|
+
* project's database (seed, populate, data migrations…).
|
|
4
|
+
*
|
|
5
|
+
* Resolution order mirrors how a scaffolded Actuate project exposes Prisma:
|
|
6
|
+
* 1. If `cms-core` already has an initialized client (e.g. set up by the host
|
|
7
|
+
* app in-process), reuse it.
|
|
8
|
+
* 2. Otherwise build a client from the project's generated Prisma client
|
|
9
|
+
* (`generated/prisma/client.ts`, Prisma 7 + pg driver adapter).
|
|
10
|
+
* 3. Fall back to a plain `@prisma/client` resolved from the project.
|
|
11
|
+
*
|
|
12
|
+
* The returned `disconnect()` only tears down clients this helper created — a
|
|
13
|
+
* pre-initialized in-process client is left untouched.
|
|
14
|
+
*/
|
|
15
|
+
export declare function connectProjectDatabase(): Promise<{
|
|
16
|
+
db: any;
|
|
17
|
+
disconnect: () => Promise<void>;
|
|
18
|
+
}>;
|
|
19
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/utils/database.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC;IAEtD,EAAE,EAAE,GAAG,CAAA;IACP,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAChC,CAAC,CAiBD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
/**
|
|
6
|
+
* Shared helper for CLI commands that need a live connection to the consumer
|
|
7
|
+
* project's database (seed, populate, data migrations…).
|
|
8
|
+
*
|
|
9
|
+
* Resolution order mirrors how a scaffolded Actuate project exposes Prisma:
|
|
10
|
+
* 1. If `cms-core` already has an initialized client (e.g. set up by the host
|
|
11
|
+
* app in-process), reuse it.
|
|
12
|
+
* 2. Otherwise build a client from the project's generated Prisma client
|
|
13
|
+
* (`generated/prisma/client.ts`, Prisma 7 + pg driver adapter).
|
|
14
|
+
* 3. Fall back to a plain `@prisma/client` resolved from the project.
|
|
15
|
+
*
|
|
16
|
+
* The returned `disconnect()` only tears down clients this helper created — a
|
|
17
|
+
* pre-initialized in-process client is left untouched.
|
|
18
|
+
*/
|
|
19
|
+
export async function connectProjectDatabase() {
|
|
20
|
+
const { getDB, initDB, isDBInitialized } = await import('@actuate-media/cms-core');
|
|
21
|
+
if (isDBInitialized()) {
|
|
22
|
+
return { db: getDB(), disconnect: async () => { } };
|
|
23
|
+
}
|
|
24
|
+
const db = await createProjectPrismaClient();
|
|
25
|
+
initDB(db);
|
|
26
|
+
return {
|
|
27
|
+
db,
|
|
28
|
+
disconnect: async () => {
|
|
29
|
+
if (typeof db.$disconnect === 'function') {
|
|
30
|
+
await db.$disconnect();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- see above.
|
|
36
|
+
async function createProjectPrismaClient() {
|
|
37
|
+
if (!process.env.DATABASE_URL) {
|
|
38
|
+
throw new Error('DATABASE_URL is required to connect to the project database.');
|
|
39
|
+
}
|
|
40
|
+
const requireFromProject = createRequire(path.join(process.cwd(), 'package.json'));
|
|
41
|
+
const generatedClient = path.resolve('generated', 'prisma', 'client.ts');
|
|
42
|
+
if (existsSync(generatedClient)) {
|
|
43
|
+
const [{ tsImport }, adapterModule, pgModule] = await Promise.all([
|
|
44
|
+
import('tsx/esm/api'),
|
|
45
|
+
import(pathToFileURL(requireFromProject.resolve('@prisma/adapter-pg')).href),
|
|
46
|
+
import(pathToFileURL(requireFromProject.resolve('pg')).href),
|
|
47
|
+
]);
|
|
48
|
+
const { PrismaClient } = (await tsImport(pathToFileURL(generatedClient).href, import.meta.url));
|
|
49
|
+
const { PrismaPg } = adapterModule;
|
|
50
|
+
const pg = pgModule.default ?? pgModule;
|
|
51
|
+
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
|
|
52
|
+
const adapter = new PrismaPg(pool);
|
|
53
|
+
return new PrismaClient({ adapter });
|
|
54
|
+
}
|
|
55
|
+
const clientModule = (await import(pathToFileURL(requireFromProject.resolve('@prisma/client')).href));
|
|
56
|
+
return new clientModule.PrismaClient();
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/utils/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB;IAK1C,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAA;IAElF,IAAI,eAAe,EAAE,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAO,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC,EAAE,CAAA;IACzD,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,yBAAyB,EAAE,CAAA;IAC5C,MAAM,CAAC,EAAE,CAAC,CAAA;IACV,OAAO;QACL,EAAE;QACF,UAAU,EAAE,KAAK,IAAI,EAAE;YACrB,IAAI,OAAO,EAAE,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;gBACzC,MAAM,EAAE,CAAC,WAAW,EAAE,CAAA;YACxB,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,4EAA4E;AAC5E,KAAK,UAAU,yBAAyB;IACtC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAA;IACjF,CAAC;IAED,MAAM,kBAAkB,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,cAAc,CAAC,CAAC,CAAA;IAClF,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAA;IAExE,IAAI,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QAChC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,EAAE,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAChE,MAAM,CAAC,aAAa,CAAC;YACrB,MAAM,CAAC,aAAa,CAAC,kBAAkB,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC;YAC5E,MAAM,CAAC,aAAa,CAAC,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;SAC7D,CAAC,CAAA;QACF,MAAM,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,QAAQ,CACtC,aAAa,CAAC,eAAe,CAAC,CAAC,IAAI,EACnC,MAAM,CAAC,IAAI,CAAC,GAAG,CAChB,CAEA,CAAA;QACD,MAAM,EAAE,QAAQ,EAAE,GAAG,aAA6D,CAAA;QAClF,MAAM,EAAE,GAAI,QAA0C,CAAC,OAAO,IAAI,QAAQ,CAAA;QAC1E,MAAM,IAAI,GAAG,IAAK,EAAU,CAAC,IAAI,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAA;QACjF,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAA;QAClC,OAAO,IAAI,YAAY,CAAC,EAAE,OAAO,EAAS,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,YAAY,GAAG,CAAC,MAAM,MAAM,CAChC,aAAa,CAAC,kBAAkB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CACjE,CAEA,CAAA;IACD,OAAO,IAAI,YAAY,CAAC,YAAY,EAAE,CAAA;AACxC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const VERCEL_TARGETS: readonly ["production", "preview", "development"];
|
|
2
|
+
export type VercelTarget = (typeof VERCEL_TARGETS)[number];
|
|
3
|
+
export interface VercelLink {
|
|
4
|
+
projectId: string;
|
|
5
|
+
orgId?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface VercelEnvVar {
|
|
8
|
+
key: string;
|
|
9
|
+
/** Targets this var applies to, e.g. ['production', 'preview']. */
|
|
10
|
+
target: VercelTarget[];
|
|
11
|
+
type?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class VercelApiError extends Error {
|
|
14
|
+
readonly status: number;
|
|
15
|
+
constructor(message: string, status: number);
|
|
16
|
+
}
|
|
17
|
+
/** Read `.vercel/project.json` (written by `vercel link`). Returns null if absent. */
|
|
18
|
+
export declare function readVercelLink(cwd: string): Promise<VercelLink | null>;
|
|
19
|
+
/** Resolve a Vercel token from an explicit flag or the standard env vars. */
|
|
20
|
+
export declare function resolveVercelToken(explicit?: string): string | undefined;
|
|
21
|
+
export interface VercelClientOptions {
|
|
22
|
+
token: string;
|
|
23
|
+
/** Team / org id, appended as `teamId` when the project belongs to a team. */
|
|
24
|
+
teamId?: string;
|
|
25
|
+
fetchImpl?: typeof fetch;
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface VercelClient {
|
|
29
|
+
listProjectEnv(projectId: string): Promise<VercelEnvVar[]>;
|
|
30
|
+
}
|
|
31
|
+
export declare function createVercelClient(options: VercelClientOptions): VercelClient;
|
|
32
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/vercel/client.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,cAAc,mDAAoD,CAAA;AAC/E,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAE1D,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,mEAAmE;IACnE,MAAM,EAAE,YAAY,EAAE,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;gBAEX,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAK5C;AAED,sFAAsF;AACtF,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAS5E;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAA;CAC3D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAyC7E"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Vercel REST client for the CLI. Scope is deliberately small: read a
|
|
5
|
+
* project's environment variables so we can validate Blob wiring and print a
|
|
6
|
+
* per-environment readiness matrix. No write operations.
|
|
7
|
+
*
|
|
8
|
+
* Auth: a Vercel access token via `--token`, `VERCEL_TOKEN`, or
|
|
9
|
+
* `VERCEL_API_TOKEN`. Project/team identity from `.vercel/project.json` (written
|
|
10
|
+
* by `vercel link`) or explicit overrides. The client is fetch-injectable so the
|
|
11
|
+
* evaluation logic can be unit-tested without network access.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_BASE_URL = 'https://api.vercel.com';
|
|
14
|
+
export const VERCEL_TARGETS = ['production', 'preview', 'development'];
|
|
15
|
+
export class VercelApiError extends Error {
|
|
16
|
+
status;
|
|
17
|
+
constructor(message, status) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'VercelApiError';
|
|
20
|
+
this.status = status;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Read `.vercel/project.json` (written by `vercel link`). Returns null if absent. */
|
|
24
|
+
export async function readVercelLink(cwd) {
|
|
25
|
+
try {
|
|
26
|
+
const raw = await readFile(join(cwd, '.vercel', 'project.json'), 'utf-8');
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (!parsed.projectId)
|
|
29
|
+
return null;
|
|
30
|
+
return { projectId: parsed.projectId, orgId: parsed.orgId };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Resolve a Vercel token from an explicit flag or the standard env vars. */
|
|
37
|
+
export function resolveVercelToken(explicit) {
|
|
38
|
+
return explicit ?? process.env.VERCEL_TOKEN ?? process.env.VERCEL_API_TOKEN ?? undefined;
|
|
39
|
+
}
|
|
40
|
+
export function createVercelClient(options) {
|
|
41
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
42
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
43
|
+
async function request(path) {
|
|
44
|
+
const url = new URL(path, baseUrl);
|
|
45
|
+
if (options.teamId)
|
|
46
|
+
url.searchParams.set('teamId', options.teamId);
|
|
47
|
+
const response = await fetchImpl(url.toString(), {
|
|
48
|
+
headers: { Authorization: `Bearer ${options.token}` },
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
let detail = '';
|
|
52
|
+
try {
|
|
53
|
+
const body = (await response.json());
|
|
54
|
+
detail = body?.error?.message ? `: ${body.error.message}` : '';
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// non-JSON error body — status alone is enough context
|
|
58
|
+
}
|
|
59
|
+
throw new VercelApiError(`Vercel API request failed (HTTP ${response.status})${detail}`, response.status);
|
|
60
|
+
}
|
|
61
|
+
return (await response.json());
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
async listProjectEnv(projectId) {
|
|
65
|
+
const data = await request(`/v9/projects/${encodeURIComponent(projectId)}/env`);
|
|
66
|
+
return (data.envs ?? []).map((env) => ({
|
|
67
|
+
key: env.key,
|
|
68
|
+
target: Array.isArray(env.target) ? env.target : [],
|
|
69
|
+
type: env.type,
|
|
70
|
+
}));
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/vercel/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type VercelEnvVar, type VercelTarget } from './client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure evaluation helpers over a project's Vercel env vars. Kept free of I/O so
|
|
4
|
+
* the matrix / blob logic is unit-testable without hitting the Vercel API.
|
|
5
|
+
*/
|
|
6
|
+
export type TargetPresence = Record<VercelTarget, boolean>;
|
|
7
|
+
/** Which targets a given env var key is defined for. */
|
|
8
|
+
export declare function presenceForKey(envVars: VercelEnvVar[], key: string): TargetPresence;
|
|
9
|
+
export interface EnvMatrixRow {
|
|
10
|
+
key: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
presence: TargetPresence;
|
|
13
|
+
}
|
|
14
|
+
export interface EnvMatrix {
|
|
15
|
+
rows: EnvMatrixRow[];
|
|
16
|
+
/** Required vars missing on production or preview (deploy-blocking). */
|
|
17
|
+
missingCritical: {
|
|
18
|
+
key: string;
|
|
19
|
+
targets: VercelTarget[];
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
export declare function buildEnvMatrix(envVars: VercelEnvVar[], requiredKeys: readonly string[], optionalKeys?: readonly string[]): EnvMatrix;
|
|
23
|
+
export type BlobLinkStatus = 'ok' | 'partial' | 'none';
|
|
24
|
+
export interface BlobLinkReport {
|
|
25
|
+
/** True when any BLOB_* var indicates a store is connected to the project. */
|
|
26
|
+
linked: boolean;
|
|
27
|
+
tokenByTarget: TargetPresence;
|
|
28
|
+
/** Targets (any) where BLOB_READ_WRITE_TOKEN is absent while linked. */
|
|
29
|
+
missingTargets: VercelTarget[];
|
|
30
|
+
/** 'partial' when production/preview lack the token (deploy-blocking). */
|
|
31
|
+
status: BlobLinkStatus;
|
|
32
|
+
}
|
|
33
|
+
export declare function evaluateBlobLink(envVars: VercelEnvVar[]): BlobLinkReport;
|
|
34
|
+
//# sourceMappingURL=env-matrix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-matrix.d.ts","sourceRoot":"","sources":["../../src/vercel/env-matrix.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA;AAElF;;;GAGG;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;AAM1D,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,cAAc,CAWnF;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,cAAc,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,YAAY,EAAE,CAAA;IACpB,wEAAwE;IACxE,eAAe,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,EAAE,CAAA;KAAE,EAAE,CAAA;CAC5D;AAKD,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EAAE,EACvB,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,YAAY,GAAE,SAAS,MAAM,EAAO,GACnC,SAAS,CAoBX;AAED,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAA;AAEtD,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAA;IACf,aAAa,EAAE,cAAc,CAAA;IAC7B,wEAAwE;IACxE,cAAc,EAAE,YAAY,EAAE,CAAA;IAC9B,0EAA0E;IAC1E,MAAM,EAAE,cAAc,CAAA;CACvB;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,cAAc,CAkBxE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { VERCEL_TARGETS } from './client.js';
|
|
2
|
+
function emptyPresence() {
|
|
3
|
+
return { production: false, preview: false, development: false };
|
|
4
|
+
}
|
|
5
|
+
/** Which targets a given env var key is defined for. */
|
|
6
|
+
export function presenceForKey(envVars, key) {
|
|
7
|
+
const presence = emptyPresence();
|
|
8
|
+
for (const env of envVars) {
|
|
9
|
+
if (env.key !== key)
|
|
10
|
+
continue;
|
|
11
|
+
for (const target of env.target) {
|
|
12
|
+
if (VERCEL_TARGETS.includes(target)) {
|
|
13
|
+
presence[target] = true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return presence;
|
|
18
|
+
}
|
|
19
|
+
/** Production and preview are the deploy-blocking targets; development is local-only. */
|
|
20
|
+
const DEPLOY_TARGETS = ['production', 'preview'];
|
|
21
|
+
export function buildEnvMatrix(envVars, requiredKeys, optionalKeys = []) {
|
|
22
|
+
const rows = [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
const missingCritical = [];
|
|
25
|
+
for (const key of requiredKeys) {
|
|
26
|
+
seen.add(key);
|
|
27
|
+
const presence = presenceForKey(envVars, key);
|
|
28
|
+
rows.push({ key, required: true, presence });
|
|
29
|
+
const missing = DEPLOY_TARGETS.filter((t) => !presence[t]);
|
|
30
|
+
if (missing.length > 0)
|
|
31
|
+
missingCritical.push({ key, targets: missing });
|
|
32
|
+
}
|
|
33
|
+
for (const key of optionalKeys) {
|
|
34
|
+
if (seen.has(key))
|
|
35
|
+
continue;
|
|
36
|
+
seen.add(key);
|
|
37
|
+
rows.push({ key, required: false, presence: presenceForKey(envVars, key) });
|
|
38
|
+
}
|
|
39
|
+
return { rows, missingCritical };
|
|
40
|
+
}
|
|
41
|
+
const BLOB_LINK_SIGNALS = ['BLOB_STORE_ID', 'BLOB_WEBHOOK_PUBLIC_KEY', 'BLOB_READ_WRITE_TOKEN'];
|
|
42
|
+
export function evaluateBlobLink(envVars) {
|
|
43
|
+
const linked = envVars.some((env) => BLOB_LINK_SIGNALS.includes(env.key) || env.key.startsWith('BLOB_'));
|
|
44
|
+
const tokenByTarget = presenceForKey(envVars, 'BLOB_READ_WRITE_TOKEN');
|
|
45
|
+
if (!linked) {
|
|
46
|
+
return { linked: false, tokenByTarget, missingTargets: [], status: 'none' };
|
|
47
|
+
}
|
|
48
|
+
const missingTargets = VERCEL_TARGETS.filter((t) => !tokenByTarget[t]);
|
|
49
|
+
const criticalMissing = DEPLOY_TARGETS.some((t) => !tokenByTarget[t]);
|
|
50
|
+
return {
|
|
51
|
+
linked: true,
|
|
52
|
+
tokenByTarget,
|
|
53
|
+
missingTargets,
|
|
54
|
+
status: criticalMissing ? 'partial' : 'ok',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=env-matrix.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-matrix.js","sourceRoot":"","sources":["../../src/vercel/env-matrix.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAwC,MAAM,aAAa,CAAA;AASlF,SAAS,aAAa;IACpB,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;AAClE,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,cAAc,CAAC,OAAuB,EAAE,GAAW;IACjE,MAAM,QAAQ,GAAG,aAAa,EAAE,CAAA;IAChC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,IAAI,GAAG,CAAC,GAAG,KAAK,GAAG;YAAE,SAAQ;QAC7B,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YAChC,IAAK,cAAoC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3D,QAAQ,CAAC,MAAsB,CAAC,GAAG,IAAI,CAAA;YACzC,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAcD,yFAAyF;AACzF,MAAM,cAAc,GAAmB,CAAC,YAAY,EAAE,SAAS,CAAC,CAAA;AAEhE,MAAM,UAAU,cAAc,CAC5B,OAAuB,EACvB,YAA+B,EAC/B,eAAkC,EAAE;IAEpC,MAAM,IAAI,GAAmB,EAAE,CAAA;IAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,MAAM,eAAe,GAA+C,EAAE,CAAA;IAEtE,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;QAC7C,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC5C,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,eAAe,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAA;IACzE,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAQ;QAC3B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAA;IAC7E,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAA;AAClC,CAAC;AAcD,MAAM,iBAAiB,GAAG,CAAC,eAAe,EAAE,yBAAyB,EAAE,uBAAuB,CAAC,CAAA;AAE/F,MAAM,UAAU,gBAAgB,CAAC,OAAuB;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CACzB,CAAC,GAAG,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAC5E,CAAA;IACD,MAAM,aAAa,GAAG,cAAc,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAA;IAEtE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IAC7E,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACtE,MAAM,eAAe,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACrE,OAAO;QACL,MAAM,EAAE,IAAI;QACZ,aAAa;QACb,cAAc;QACd,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;KAC3C,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuate-media/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "CLI for Actuate CMS — migrations, codegen, imports, exports, and upgrades",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"typescript": "^5.7.0",
|
|
30
30
|
"vitest": "^4.1.0",
|
|
31
|
-
"@actuate-media/cms-core": "0.
|
|
31
|
+
"@actuate-media/cms-core": "0.55.0"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc",
|
|
@@ -3,7 +3,13 @@ import { tmpdir } from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildConsumerSchema,
|
|
8
|
+
checkSchemaDrift,
|
|
9
|
+
driftGuidance,
|
|
10
|
+
syncPrismaAssets,
|
|
11
|
+
type DriftRunner,
|
|
12
|
+
} from '../commands/db-sync.js'
|
|
7
13
|
|
|
8
14
|
const CORE_SCHEMA = `generator client {
|
|
9
15
|
provider = "prisma-client"
|
|
@@ -164,4 +170,52 @@ describe('db:sync (WS-D D2 — post-upgrade schema sync)', () => {
|
|
|
164
170
|
expect(second.migrationsAdded).toEqual([])
|
|
165
171
|
})
|
|
166
172
|
})
|
|
173
|
+
|
|
174
|
+
describe('checkSchemaDrift', () => {
|
|
175
|
+
const okRunner: DriftRunner = async () => ({ exitCode: 0, stdout: '', stderr: '' })
|
|
176
|
+
const driftRunner: DriftRunner = async () => ({
|
|
177
|
+
exitCode: 2,
|
|
178
|
+
stdout: 'ALTER TABLE "actuate_media_usages" RENAME COLUMN "fieldName" TO "fieldPath";',
|
|
179
|
+
stderr: '',
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('skips when no database URL is configured', async () => {
|
|
183
|
+
const result = await checkSchemaDrift('prisma/schema.prisma', {}, okRunner)
|
|
184
|
+
expect(result.status).toBe('skipped')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('reports no-drift on exit code 0', async () => {
|
|
188
|
+
const result = await checkSchemaDrift(
|
|
189
|
+
'prisma/schema.prisma',
|
|
190
|
+
{ DATABASE_URL: 'postgres://x' },
|
|
191
|
+
okRunner,
|
|
192
|
+
)
|
|
193
|
+
expect(result.status).toBe('no-drift')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('reports drift (with reconciling SQL) on exit code 2', async () => {
|
|
197
|
+
const result = await checkSchemaDrift(
|
|
198
|
+
'prisma/schema.prisma',
|
|
199
|
+
{ DATABASE_URL: 'postgres://x' },
|
|
200
|
+
driftRunner,
|
|
201
|
+
)
|
|
202
|
+
expect(result.status).toBe('drift')
|
|
203
|
+
expect(result.detail).toContain('RENAME COLUMN')
|
|
204
|
+
expect(driftGuidance(result).join('\n')).toContain('prisma migrate dev --name')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('prefers DIRECT_DATABASE_URL for the diff connection', async () => {
|
|
208
|
+
let usedUrl = ''
|
|
209
|
+
const spyRunner: DriftRunner = async ({ url }) => {
|
|
210
|
+
usedUrl = url
|
|
211
|
+
return { exitCode: 0, stdout: '', stderr: '' }
|
|
212
|
+
}
|
|
213
|
+
await checkSchemaDrift(
|
|
214
|
+
'prisma/schema.prisma',
|
|
215
|
+
{ DATABASE_URL: 'postgres://pooled', DIRECT_DATABASE_URL: 'postgres://direct' },
|
|
216
|
+
spyRunner,
|
|
217
|
+
)
|
|
218
|
+
expect(usedUrl).toBe('postgres://direct')
|
|
219
|
+
})
|
|
220
|
+
})
|
|
167
221
|
})
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import {
|
|
4
4
|
buildDeploymentManifest,
|
|
5
5
|
createDiagnosticReport,
|
|
6
|
+
detectBlobLinkState,
|
|
6
7
|
REQUIRED_CMS_MODELS,
|
|
7
8
|
REQUIRED_ENV_VARS,
|
|
8
9
|
} from '../deployment/diagnostics.js'
|
|
@@ -127,6 +128,56 @@ describe('deployment diagnostics', () => {
|
|
|
127
128
|
)
|
|
128
129
|
})
|
|
129
130
|
|
|
131
|
+
it('fails (not warns) when a Blob store is linked but the read-write token is missing', () => {
|
|
132
|
+
const report = createDiagnosticReport({
|
|
133
|
+
schemaModels: new Set(REQUIRED_CMS_MODELS),
|
|
134
|
+
schemaContent: '',
|
|
135
|
+
env: {
|
|
136
|
+
DATABASE_URL: 'postgresql://example',
|
|
137
|
+
CMS_SECRET: 'secret',
|
|
138
|
+
CMS_ENCRYPTION_KEY: 'a'.repeat(64),
|
|
139
|
+
NEXT_PUBLIC_SITE_URL: 'https://example.com',
|
|
140
|
+
BLOB_STORE_ID: 'store_abc123',
|
|
141
|
+
BLOB_WEBHOOK_PUBLIC_KEY: 'pk_abc',
|
|
142
|
+
// BLOB_READ_WRITE_TOKEN deliberately absent — the partial-link failure.
|
|
143
|
+
},
|
|
144
|
+
packageManager: 'pnpm',
|
|
145
|
+
schemaPath: 'prisma/schema.prisma',
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(report.status).toBe('fail')
|
|
149
|
+
expect(report.checks).toEqual(
|
|
150
|
+
expect.arrayContaining([expect.objectContaining({ id: 'blob-storage', status: 'fail' })]),
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('passes the blob check when no Blob storage is configured', () => {
|
|
155
|
+
const report = createDiagnosticReport({
|
|
156
|
+
schemaModels: new Set(REQUIRED_CMS_MODELS),
|
|
157
|
+
schemaContent: '',
|
|
158
|
+
env: {
|
|
159
|
+
DATABASE_URL: 'postgresql://example',
|
|
160
|
+
CMS_SECRET: 'secret',
|
|
161
|
+
CMS_ENCRYPTION_KEY: 'a'.repeat(64),
|
|
162
|
+
NEXT_PUBLIC_SITE_URL: 'https://example.com',
|
|
163
|
+
},
|
|
164
|
+
packageManager: 'pnpm',
|
|
165
|
+
schemaPath: 'prisma/schema.prisma',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(report.checks).toEqual(
|
|
169
|
+
expect.arrayContaining([expect.objectContaining({ id: 'blob-storage', status: 'pass' })]),
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('classifies blob link state from env and config signals', () => {
|
|
174
|
+
expect(detectBlobLinkState({ BLOB_READ_WRITE_TOKEN: 'vercel_blob_rw_x' })).toBe('ok')
|
|
175
|
+
expect(detectBlobLinkState({ BLOB_READ_WRITE_TOKEN: 'not-a-real-token' })).toBe('token-format')
|
|
176
|
+
expect(detectBlobLinkState({ BLOB_STORE_ID: 'store_x' })).toBe('partial')
|
|
177
|
+
expect(detectBlobLinkState({}, "storage: { provider: 'vercel-blob' }")).toBe('partial')
|
|
178
|
+
expect(detectBlobLinkState({})).toBe('none')
|
|
179
|
+
})
|
|
180
|
+
|
|
130
181
|
it('exposes machine-readable deployment metadata', () => {
|
|
131
182
|
const manifest = buildDeploymentManifest()
|
|
132
183
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { VercelEnvVar } from '../vercel/client.js'
|
|
4
|
+
import { buildEnvMatrix, evaluateBlobLink, presenceForKey } from '../vercel/env-matrix.js'
|
|
5
|
+
|
|
6
|
+
function env(key: string, target: VercelEnvVar['target']): VercelEnvVar {
|
|
7
|
+
return { key, target }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('presenceForKey', () => {
|
|
11
|
+
it('aggregates targets across multiple entries for the same key', () => {
|
|
12
|
+
const vars = [env('DATABASE_URL', ['production']), env('DATABASE_URL', ['preview'])]
|
|
13
|
+
expect(presenceForKey(vars, 'DATABASE_URL')).toEqual({
|
|
14
|
+
production: true,
|
|
15
|
+
preview: true,
|
|
16
|
+
development: false,
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('buildEnvMatrix', () => {
|
|
22
|
+
it('flags required vars missing on production or preview as critical', () => {
|
|
23
|
+
const vars = [
|
|
24
|
+
env('DATABASE_URL', ['production', 'preview', 'development']),
|
|
25
|
+
env('CMS_SECRET', ['production']),
|
|
26
|
+
]
|
|
27
|
+
const matrix = buildEnvMatrix(vars, ['DATABASE_URL', 'CMS_SECRET'], ['CRON_SECRET'])
|
|
28
|
+
|
|
29
|
+
expect(matrix.missingCritical).toEqual([{ key: 'CMS_SECRET', targets: ['preview'] }])
|
|
30
|
+
expect(matrix.rows.find((r) => r.key === 'CRON_SECRET')).toMatchObject({ required: false })
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('evaluateBlobLink', () => {
|
|
35
|
+
it('reports none when no blob vars exist', () => {
|
|
36
|
+
expect(evaluateBlobLink([env('DATABASE_URL', ['production'])]).status).toBe('none')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('reports partial when linked but token missing on production/preview', () => {
|
|
40
|
+
const report = evaluateBlobLink([
|
|
41
|
+
env('BLOB_STORE_ID', ['production', 'preview', 'development']),
|
|
42
|
+
])
|
|
43
|
+
expect(report.linked).toBe(true)
|
|
44
|
+
expect(report.status).toBe('partial')
|
|
45
|
+
expect(report.missingTargets).toContain('production')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('reports ok when token covers production and preview (dev optional)', () => {
|
|
49
|
+
const report = evaluateBlobLink([
|
|
50
|
+
env('BLOB_STORE_ID', ['production', 'preview']),
|
|
51
|
+
env('BLOB_READ_WRITE_TOKEN', ['production', 'preview']),
|
|
52
|
+
])
|
|
53
|
+
expect(report.status).toBe('ok')
|
|
54
|
+
expect(report.missingTargets).toEqual(['development'])
|
|
55
|
+
})
|
|
56
|
+
})
|