gencow 0.1.188 → 0.1.190
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/core/index.js +34 -12
- package/lib/app-command.mjs +19 -5
- package/lib/app-response-contract.mjs +11 -7
- package/lib/canonical-drizzle-generator.config.mjs +61 -0
- package/lib/cli-artifact-guard.mjs +29 -0
- package/lib/cli-project-runtime.mjs +19 -4
- package/lib/db-migration-command-steps.mjs +16 -3
- package/lib/db-push-command.mjs +20 -1
- package/lib/deploy-auditor.mjs +1 -1
- package/lib/deploy-dependency-compat.mjs +1 -1
- package/lib/deploy-failure-diagnostic.mjs +46 -9
- package/lib/deploy-lockfile-selection.mjs +47 -11
- package/lib/deploy-package-claim.mjs +33 -3
- package/lib/deploy-package-runtime.mjs +26 -48
- package/lib/deployment-operation-poll.mjs +86 -37
- package/lib/dev-cloud-bundle.mjs +9 -21
- package/lib/dev-cloud-command.mjs +17 -2
- package/lib/doctor-command.mjs +49 -1
- package/lib/drizzle-generate.mjs +92 -8
- package/lib/drizzle-generator-invocation.mjs +23 -0
- package/lib/init-command.mjs +4 -3
- package/lib/install-features.mjs +4 -5
- package/lib/migration-bundle-manifest.mjs +6 -14
- package/lib/migration-diagnostic.mjs +13 -1
- package/lib/platform-client.mjs +3 -1
- package/lib/project-migration-manifest.mjs +0 -1
- package/lib/readme-codegen.mjs +2 -2
- package/lib/runtime-command.mjs +4 -11
- package/lib/static-deploy-command.mjs +1 -0
- package/package.json +3 -2
- package/runtime/server.mjs +62819 -16245
- package/runtime/tooling.mjs +4 -0
- package/scripts/bundle-server.mjs +52 -12
- package/scripts/pre-publish-check.mjs +20 -0
- package/templates/ai-chat/prompt.md +1 -1
- package/templates/auth.ts +1 -1
- package/templates/fullstack/prompt.md +1 -1
- package/templates/task-app/prompt.md +1 -1
- package/runtime/server.mjs.map +0 -7
package/core/index.js
CHANGED
|
@@ -8,10 +8,12 @@ function hasStandardSchema(schema) {
|
|
|
8
8
|
return !!schema && typeof schema === "object" && "~standard" in schema && !!schema["~standard"] && typeof schema["~standard"].validate === "function";
|
|
9
9
|
}
|
|
10
10
|
var HttpRouteInputValidationError = class extends Error {
|
|
11
|
-
constructor(
|
|
12
|
-
super(message);
|
|
11
|
+
constructor(issues) {
|
|
12
|
+
super(issues[0]?.message ?? "Invalid input");
|
|
13
|
+
this.issues = issues;
|
|
13
14
|
this.name = "HttpRouteInputValidationError";
|
|
14
15
|
}
|
|
16
|
+
issues;
|
|
15
17
|
};
|
|
16
18
|
var HttpRouteOutputValidationError = class extends Error {
|
|
17
19
|
constructor(message) {
|
|
@@ -26,10 +28,16 @@ async function validateInput(schema, value) {
|
|
|
26
28
|
}
|
|
27
29
|
const result = await schema["~standard"].validate(value);
|
|
28
30
|
if ("issues" in result && result.issues.length > 0) {
|
|
29
|
-
throw new HttpRouteInputValidationError(
|
|
31
|
+
throw new HttpRouteInputValidationError(
|
|
32
|
+
result.issues.map((issue) => ({
|
|
33
|
+
message: issue.message,
|
|
34
|
+
path: issue.path?.map(String),
|
|
35
|
+
code: issue.code === void 0 ? void 0 : String(issue.code)
|
|
36
|
+
}))
|
|
37
|
+
);
|
|
30
38
|
}
|
|
31
39
|
if ("value" in result) return result.value;
|
|
32
|
-
throw new HttpRouteInputValidationError("Invalid input");
|
|
40
|
+
throw new HttpRouteInputValidationError([{ message: "Invalid input" }]);
|
|
33
41
|
}
|
|
34
42
|
async function validateOutput(schema, value) {
|
|
35
43
|
if (!schema) return value;
|
|
@@ -50,7 +58,7 @@ async function buildInputFromRequest(method, req) {
|
|
|
50
58
|
return await req.json();
|
|
51
59
|
} catch (err) {
|
|
52
60
|
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
-
throw new HttpRouteInputValidationError(`Invalid JSON body: ${message}`);
|
|
61
|
+
throw new HttpRouteInputValidationError([{ message: `Invalid JSON body: ${message}` }]);
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
return { ...req.query };
|
|
@@ -105,7 +113,7 @@ function composeRouteMiddlewares(middlewares, handler, inputSchema, outputSchema
|
|
|
105
113
|
};
|
|
106
114
|
}
|
|
107
115
|
var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
108
|
-
constructor(method, routePath, middlewares = [], inputSchema, outputSchema, inputValidationIndex = -1, outputValidationIndex = -1, anonymousAllowed = false) {
|
|
116
|
+
constructor(method, routePath, middlewares = [], inputSchema, outputSchema, inputValidationIndex = -1, outputValidationIndex = -1, anonymousAllowed = false, inputErrorMapper) {
|
|
109
117
|
this.method = method;
|
|
110
118
|
this.routePath = routePath;
|
|
111
119
|
this.middlewares = middlewares;
|
|
@@ -114,6 +122,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
114
122
|
this.inputValidationIndex = inputValidationIndex;
|
|
115
123
|
this.outputValidationIndex = outputValidationIndex;
|
|
116
124
|
this.anonymousAllowed = anonymousAllowed;
|
|
125
|
+
this.inputErrorMapper = inputErrorMapper;
|
|
117
126
|
}
|
|
118
127
|
method;
|
|
119
128
|
routePath;
|
|
@@ -123,6 +132,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
123
132
|
inputValidationIndex;
|
|
124
133
|
outputValidationIndex;
|
|
125
134
|
anonymousAllowed;
|
|
135
|
+
inputErrorMapper;
|
|
126
136
|
path(path) {
|
|
127
137
|
return new _HttpRouteBuilderImpl(
|
|
128
138
|
this.method,
|
|
@@ -132,7 +142,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
132
142
|
this.outputSchema,
|
|
133
143
|
this.inputValidationIndex,
|
|
134
144
|
this.outputValidationIndex,
|
|
135
|
-
this.anonymousAllowed
|
|
145
|
+
this.anonymousAllowed,
|
|
146
|
+
this.inputErrorMapper
|
|
136
147
|
);
|
|
137
148
|
}
|
|
138
149
|
allowAnonymous() {
|
|
@@ -144,7 +155,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
144
155
|
this.outputSchema,
|
|
145
156
|
this.inputValidationIndex,
|
|
146
157
|
this.outputValidationIndex,
|
|
147
|
-
true
|
|
158
|
+
true,
|
|
159
|
+
this.inputErrorMapper
|
|
148
160
|
);
|
|
149
161
|
}
|
|
150
162
|
use(middleware) {
|
|
@@ -156,10 +168,11 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
156
168
|
this.outputSchema,
|
|
157
169
|
this.inputValidationIndex,
|
|
158
170
|
this.outputValidationIndex,
|
|
159
|
-
this.anonymousAllowed
|
|
171
|
+
this.anonymousAllowed,
|
|
172
|
+
this.inputErrorMapper
|
|
160
173
|
);
|
|
161
174
|
}
|
|
162
|
-
input(schema) {
|
|
175
|
+
input(schema, options) {
|
|
163
176
|
const nextInputValidationIndex = this.middlewares.length;
|
|
164
177
|
return new _HttpRouteBuilderImpl(
|
|
165
178
|
this.method,
|
|
@@ -169,7 +182,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
169
182
|
this.outputSchema,
|
|
170
183
|
nextInputValidationIndex,
|
|
171
184
|
this.outputValidationIndex,
|
|
172
|
-
this.anonymousAllowed
|
|
185
|
+
this.anonymousAllowed,
|
|
186
|
+
options?.onError
|
|
173
187
|
);
|
|
174
188
|
}
|
|
175
189
|
output(schema) {
|
|
@@ -182,7 +196,8 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
182
196
|
schema,
|
|
183
197
|
this.inputValidationIndex,
|
|
184
198
|
nextOutputValidationIndex,
|
|
185
|
-
this.anonymousAllowed
|
|
199
|
+
this.anonymousAllowed,
|
|
200
|
+
this.inputErrorMapper
|
|
186
201
|
);
|
|
187
202
|
}
|
|
188
203
|
handler(handler) {
|
|
@@ -194,6 +209,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
194
209
|
const isPublic = this.anonymousAllowed;
|
|
195
210
|
const inputSchema = this.inputSchema;
|
|
196
211
|
const outputSchema = this.outputSchema;
|
|
212
|
+
const inputErrorMapper = this.inputErrorMapper;
|
|
197
213
|
const runRoute = composeRouteMiddlewares(
|
|
198
214
|
this.middlewares,
|
|
199
215
|
handler,
|
|
@@ -208,6 +224,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
208
224
|
normalizedInput = inputSchema ? await buildInputFromRequest(method, req) : void 0;
|
|
209
225
|
} catch (err) {
|
|
210
226
|
if (err instanceof HttpRouteInputValidationError) {
|
|
227
|
+
if (inputErrorMapper) return await inputErrorMapper(err.issues);
|
|
211
228
|
return { status: 400, body: { error: err.message } };
|
|
212
229
|
}
|
|
213
230
|
throw err;
|
|
@@ -217,6 +234,7 @@ var HttpRouteBuilderImpl = class _HttpRouteBuilderImpl {
|
|
|
217
234
|
result = await runRoute(ctx, normalizedInput, req);
|
|
218
235
|
} catch (err) {
|
|
219
236
|
if (err instanceof HttpRouteInputValidationError) {
|
|
237
|
+
if (inputErrorMapper) return await inputErrorMapper(err.issues);
|
|
220
238
|
return { status: 400, body: { error: err.message } };
|
|
221
239
|
}
|
|
222
240
|
if (err instanceof HttpRouteOutputValidationError) {
|
|
@@ -3006,6 +3024,10 @@ var RESERVED_TENANT_RUNTIME_ENV_KEYS = [
|
|
|
3006
3024
|
"PGBOUNCER_PORT",
|
|
3007
3025
|
"DIRECT_PG_PORT",
|
|
3008
3026
|
"GENCOW_DIRECT_DATABASE_URL",
|
|
3027
|
+
"GENCOW_BACKUP_DATABASE_URL",
|
|
3028
|
+
"GENCOW_BACKUP_EXPECTED_ROLE",
|
|
3029
|
+
"GENCOW_BACKUP_RLS_BYPASS_APPROVED",
|
|
3030
|
+
"GENCOW_RESTORE_DATABASE_URL",
|
|
3009
3031
|
"GENCOW_CATALOG_READ_DATABASE_URL",
|
|
3010
3032
|
"GENCOW_DIRECT_DDL_DATABASE_URL",
|
|
3011
3033
|
"DIRECT_DATABASE_URL",
|
package/lib/app-command.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { resolveCreatedAppId, resolveCreatedAppResponse } from "./app-create-res
|
|
|
10
10
|
import {
|
|
11
11
|
isExactAppDeleteResponse,
|
|
12
12
|
parseAppListResponse,
|
|
13
|
-
|
|
13
|
+
parseAppLookupResponse,
|
|
14
14
|
} from "./app-response-contract.mjs";
|
|
15
15
|
import { pollAppDeleteOperation } from "./app-delete-operation.mjs";
|
|
16
16
|
import { appResponseError } from "./app-response-error.mjs";
|
|
@@ -32,6 +32,10 @@ function invalidAppResponse(operation, response, kind) {
|
|
|
32
32
|
return `CLI_APP_RESPONSE_INVALID: ${operation} returned an invalid ${kind} response (${appResponseStatus(response)})`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function appNotFoundError() {
|
|
36
|
+
return "App not found or not owned by you.\nCode: APP_NOT_FOUND";
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
export function formatAppDeployedAgo(dateStr, now = Date.now()) {
|
|
36
40
|
if (!dateStr) return `${DIM}never${RESET}`;
|
|
37
41
|
const diff = now - new Date(dateStr).getTime();
|
|
@@ -322,22 +326,32 @@ ${dashboardLine}
|
|
|
322
326
|
const res = await rpcQueryImpl(creds, "apps.get", { name });
|
|
323
327
|
const body = await readJsonObjectResponse(res);
|
|
324
328
|
if (!res.ok) {
|
|
325
|
-
errorImpl(
|
|
329
|
+
errorImpl(
|
|
330
|
+
res.status === 404
|
|
331
|
+
? appNotFoundError()
|
|
332
|
+
: appResponseError(body, invalidAppResponse("app status", res, "error")),
|
|
333
|
+
);
|
|
326
334
|
processRef.exit(1);
|
|
327
335
|
return;
|
|
328
336
|
}
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
337
|
+
const lookup = parseAppLookupResponse(body, name);
|
|
338
|
+
if (lookup.kind === "not_found") {
|
|
339
|
+
errorImpl(appNotFoundError());
|
|
340
|
+
processRef.exit(1);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (lookup.kind === "invalid_response") {
|
|
331
344
|
errorImpl(invalidAppResponse("app status", res, "success"));
|
|
332
345
|
processRef.exit(1);
|
|
333
346
|
return;
|
|
334
347
|
}
|
|
348
|
+
const data = lookup.app;
|
|
335
349
|
logImpl(`\n ${BOLD}${data.name}${RESET}`);
|
|
336
350
|
const status = formatAppStatusForCli(data);
|
|
337
351
|
logImpl(` Status: ${status.colored}`);
|
|
338
352
|
const memoryExceededDetails = formatMemoryExceededDetails(data);
|
|
339
353
|
if (memoryExceededDetails) logImpl(memoryExceededDetails);
|
|
340
|
-
logImpl(` Port: ${data.port}`);
|
|
354
|
+
logImpl(` Port: ${data.port ?? `${DIM}not deployed${RESET}`}`);
|
|
341
355
|
if (isAppFailureStatus(data)) {
|
|
342
356
|
let diagnosticLines = selectAppDiagnosticLogLines(data);
|
|
343
357
|
if (diagnosticLines.length === 0) {
|
|
@@ -25,22 +25,26 @@ export function parseAppListResponse(value) {
|
|
|
25
25
|
: null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function
|
|
28
|
+
export function parseAppLookupResponse(value, expectedAppId) {
|
|
29
29
|
const body = record(value);
|
|
30
|
-
if (!body) return
|
|
30
|
+
if (!body) return { kind: "invalid_response" };
|
|
31
31
|
const hasEnvelope = Object.prototype.hasOwnProperty.call(body, "result");
|
|
32
|
+
if (hasEnvelope && body.result == null) return { kind: "not_found" };
|
|
32
33
|
const app = record(hasEnvelope ? body.result : body);
|
|
33
34
|
if (
|
|
34
35
|
!app ||
|
|
35
36
|
app.name !== expectedAppId ||
|
|
36
37
|
!nonEmptyString(app.status) ||
|
|
37
|
-
!Number.isSafeInteger(app.port) ||
|
|
38
|
-
app.port <= 0 ||
|
|
39
|
-
app.port > 65535
|
|
38
|
+
(app.port != null && (!Number.isSafeInteger(app.port) || app.port <= 0 || app.port > 65535))
|
|
40
39
|
) {
|
|
41
|
-
return
|
|
40
|
+
return { kind: "invalid_response" };
|
|
42
41
|
}
|
|
43
|
-
return app;
|
|
42
|
+
return { kind: "found", app };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseAppStatusResponse(value, expectedAppId) {
|
|
46
|
+
const result = parseAppLookupResponse(value, expectedAppId);
|
|
47
|
+
return result.kind === "found" ? result.app : null;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export function isExactAppDeleteResponse(value, expectedAppId) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const GENCOW_DRIZZLE_CONFIG_CONTRACT_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
function requiredEnvironment(name) {
|
|
7
|
+
const value = process.env[name]?.trim();
|
|
8
|
+
if (!value) throw new Error(`Gencow migration generator requires ${name}`);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function insideProject(projectRoot, candidate) {
|
|
13
|
+
const pathFromRoot = relative(projectRoot, candidate);
|
|
14
|
+
return pathFromRoot === "" || (!pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function canonicalOutputPath(projectRoot, rawPath) {
|
|
18
|
+
const candidate = resolve(projectRoot, rawPath);
|
|
19
|
+
if (existsSync(candidate)) return realpathSync(candidate);
|
|
20
|
+
return resolve(realpathSync(dirname(candidate)), basename(candidate));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveSchemaFiles(projectRoot) {
|
|
24
|
+
const rawPaths = requiredEnvironment("GENCOW_SCHEMA_PATHS").split("\n");
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const rawPath of rawPaths) {
|
|
28
|
+
const trimmed = rawPath.trim();
|
|
29
|
+
if (!trimmed) continue;
|
|
30
|
+
const candidate = resolve(projectRoot, trimmed);
|
|
31
|
+
const stat = lstatSync(candidate);
|
|
32
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
33
|
+
throw new Error("Gencow migration schema must be a regular project file");
|
|
34
|
+
}
|
|
35
|
+
const canonical = realpathSync(candidate);
|
|
36
|
+
if (!insideProject(projectRoot, canonical)) {
|
|
37
|
+
throw new Error("Gencow migration schema must stay inside the project root");
|
|
38
|
+
}
|
|
39
|
+
if (!seen.has(canonical)) {
|
|
40
|
+
seen.add(canonical);
|
|
41
|
+
files.push(canonical);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (files.length === 0) throw new Error("Gencow migration schema list is empty");
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const projectRoot = realpathSync(process.cwd());
|
|
49
|
+
const migrationsPath = canonicalOutputPath(projectRoot, requiredEnvironment("GENCOW_MIGRATIONS"));
|
|
50
|
+
if (!insideProject(projectRoot, migrationsPath)) {
|
|
51
|
+
throw new Error("Gencow migrations output must stay inside the project root");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
dialect: "postgresql",
|
|
56
|
+
schema: resolveSchemaFiles(projectRoot),
|
|
57
|
+
out: `./${relative(projectRoot, migrationsPath) || "."}`,
|
|
58
|
+
tablesFilter: ["!_system_*", "!_gencow_*"],
|
|
59
|
+
driver: "pglite",
|
|
60
|
+
dbCredentials: { url: process.env.GENCOW_DB_URL || "./.gencow/data" },
|
|
61
|
+
};
|
|
@@ -5,6 +5,7 @@ export const REQUIRED_CLI_TARBALL_FILES = Object.freeze([
|
|
|
5
5
|
"lib/deploy-auditor.mjs",
|
|
6
6
|
"lib/cron-manifest.mjs",
|
|
7
7
|
"lib/dev-cloud-bundle.mjs",
|
|
8
|
+
"lib/canonical-drizzle-generator.config.mjs",
|
|
8
9
|
"runtime/tooling.mjs",
|
|
9
10
|
"runtime/server.mjs",
|
|
10
11
|
"templateFeature/ai/manifest.json",
|
|
@@ -17,6 +18,7 @@ export const REQUIRED_CLI_TARBALL_FILES = Object.freeze([
|
|
|
17
18
|
]);
|
|
18
19
|
|
|
19
20
|
export const TOOLING_RUNTIME_BUNDLE_PATH = "runtime/tooling.mjs";
|
|
21
|
+
export const SERVER_RUNTIME_BUNDLE_PATH = "runtime/server.mjs";
|
|
20
22
|
|
|
21
23
|
const FORBIDDEN_TOOLING_RUNTIME_PATTERNS = Object.freeze([
|
|
22
24
|
{
|
|
@@ -38,6 +40,11 @@ const FORBIDDEN_CLI_TARBALL_FILES = Object.freeze([
|
|
|
38
40
|
path: "lib/codegen/index.mjs",
|
|
39
41
|
message: "legacy CLI codegen artifact는 npm tarball에 포함되면 안 됩니다",
|
|
40
42
|
},
|
|
43
|
+
{
|
|
44
|
+
code: "forbidden-runtime-source-map",
|
|
45
|
+
path: "runtime/server.mjs.map",
|
|
46
|
+
message: "server runtime source map은 npm tarball에 포함되면 안 됩니다",
|
|
47
|
+
},
|
|
41
48
|
]);
|
|
42
49
|
|
|
43
50
|
export function validateCliTarballFiles(files, requiredFiles = REQUIRED_CLI_TARBALL_FILES) {
|
|
@@ -93,6 +100,28 @@ export function validateGeneratedCodegenBundle(source, options = {}) {
|
|
|
93
100
|
};
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
export function validateServerRuntimeBundle(source, options = {}) {
|
|
104
|
+
const artifactPath = options.artifactPath ?? SERVER_RUNTIME_BUNDLE_PATH;
|
|
105
|
+
const repositorySourcePath = String.raw`(?:platform|server|route-gateway)[\\/]src[\\/][a-z0-9_./\\-]+\.(?:[cm]?[jt]s)`;
|
|
106
|
+
const repositorySourceImportPatterns = [
|
|
107
|
+
new RegExp("import\\(\\s*[`\"'][^`\"'\\n]{0,640}" + repositorySourcePath, "iu"),
|
|
108
|
+
new RegExp(
|
|
109
|
+
String.raw`(?:resolve|join|new\s+URL)\([^;\n]{0,640}(?:${repositorySourcePath}|["'](?:platform|server|route-gateway)["']\s*,\s*["']src["'])`,
|
|
110
|
+
"iu",
|
|
111
|
+
),
|
|
112
|
+
];
|
|
113
|
+
const errors = repositorySourceImportPatterns.some((pattern) => pattern.test(source))
|
|
114
|
+
? [
|
|
115
|
+
{
|
|
116
|
+
code: "repository-source-import",
|
|
117
|
+
path: artifactPath,
|
|
118
|
+
message: "server runtime이 npm package 밖 repository source를 참조합니다",
|
|
119
|
+
},
|
|
120
|
+
]
|
|
121
|
+
: [];
|
|
122
|
+
return { ok: errors.length === 0, errors };
|
|
123
|
+
}
|
|
124
|
+
|
|
96
125
|
export function formatArtifactGuardErrors(errors) {
|
|
97
126
|
return errors.map((error) => ` ❌ ${error.path} — ${error.message}`).join("\n");
|
|
98
127
|
}
|
|
@@ -18,6 +18,7 @@ export { readProjectDisplayName, updateEnvLocalUrl } from "./project-metadata-ru
|
|
|
18
18
|
import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
|
|
19
19
|
import { hasWorkspaceConfigSourceRuntime } from "./runtime-mode.mjs";
|
|
20
20
|
import { SUPPORTED_DRIZZLE_KIT_GENERATOR_VERSION } from "./drizzle-generate.mjs";
|
|
21
|
+
import { buildDrizzleGeneratorInvocation } from "./drizzle-generator-invocation.mjs";
|
|
21
22
|
|
|
22
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
|
|
@@ -327,7 +328,11 @@ export async function loadConfig(options = {}) {
|
|
|
327
328
|
existsSyncImpl,
|
|
328
329
|
resolvePathImpl,
|
|
329
330
|
});
|
|
330
|
-
warnProjectConfigSelectionOnce({
|
|
331
|
+
warnProjectConfigSelectionOnce({
|
|
332
|
+
cwd: projectSelection.projectDir,
|
|
333
|
+
selection: configSelection,
|
|
334
|
+
warnImpl,
|
|
335
|
+
});
|
|
331
336
|
}
|
|
332
337
|
} catch (caught) {
|
|
333
338
|
if (throwOnInvalid) {
|
|
@@ -530,6 +535,10 @@ function ensureEsbuildConsistency(options = {}) {
|
|
|
530
535
|
|
|
531
536
|
export function buildDrizzleKitCommand(subcmd, options = {}) {
|
|
532
537
|
const {
|
|
538
|
+
canonicalConfigPath = resolve(
|
|
539
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
540
|
+
"canonical-drizzle-generator.config.mjs",
|
|
541
|
+
),
|
|
533
542
|
cwd = process.cwd(),
|
|
534
543
|
createRequireImpl = createRequire,
|
|
535
544
|
dirnameImpl = dirname,
|
|
@@ -538,7 +547,7 @@ export function buildDrizzleKitCommand(subcmd, options = {}) {
|
|
|
538
547
|
readFileSyncImpl = readFileSync,
|
|
539
548
|
resolvePathImpl = resolve,
|
|
540
549
|
} = options;
|
|
541
|
-
if (!/^[a-z][a-z0-9:-]
|
|
550
|
+
if (!/^[a-z][a-z0-9:-]*$/u.test(subcmd)) throw new Error(`Invalid drizzle-kit command: ${subcmd}`);
|
|
542
551
|
try {
|
|
543
552
|
const cliRequire = createRequireImpl(import.meta.url);
|
|
544
553
|
const packageJsonPath = resolvePathImpl(dirnameImpl(cliRequire.resolve("drizzle-kit")), "package.json");
|
|
@@ -554,8 +563,14 @@ export function buildDrizzleKitCommand(subcmd, options = {}) {
|
|
|
554
563
|
const resolvedBin = resolvePathImpl(dirnameImpl(packageJsonPath), binRelative);
|
|
555
564
|
if (!existsSyncImpl(resolvedBin)) throw new Error("bin file missing");
|
|
556
565
|
const env = ensureEsbuildConsistency({ ...options, cwd });
|
|
566
|
+
const invocation = buildDrizzleGeneratorInvocation({
|
|
567
|
+
configPath: canonicalConfigPath,
|
|
568
|
+
generatorBinPath: resolvedBin,
|
|
569
|
+
processExecPath,
|
|
570
|
+
subcommand: subcmd,
|
|
571
|
+
});
|
|
557
572
|
return {
|
|
558
|
-
|
|
573
|
+
...invocation,
|
|
559
574
|
env,
|
|
560
575
|
generatorVersion: packageJson.version,
|
|
561
576
|
};
|
|
@@ -595,7 +610,7 @@ export function findServerRoot(options = {}) {
|
|
|
595
610
|
const siblingServer = resolvePathImpl(dirnameValue, "../../server");
|
|
596
611
|
if (existsSyncImpl(siblingServer)) return siblingServer;
|
|
597
612
|
|
|
598
|
-
errorImpl("Server runtime not found. Run '
|
|
613
|
+
errorImpl("Server runtime not found. Run 'pnpm install'.");
|
|
599
614
|
exitImpl(1);
|
|
600
615
|
return null;
|
|
601
616
|
}
|
|
@@ -180,9 +180,22 @@ export function emitCloudMigrationSuccess({
|
|
|
180
180
|
? "Migration state inspection completed; the plan is ready."
|
|
181
181
|
: "Migration state inspection completed; the plan remains blocked.",
|
|
182
182
|
);
|
|
183
|
-
deps.logImpl(
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
deps.logImpl(` ${DIM}Database unchanged${RESET}\n`);
|
|
184
|
+
const plan = checkData.inspection.plan;
|
|
185
|
+
if (
|
|
186
|
+
ready &&
|
|
187
|
+
plan?.safeToRewritePending === true &&
|
|
188
|
+
Array.isArray(plan.pendingMigrationNames) &&
|
|
189
|
+
plan.pendingMigrationNames.length > 0
|
|
190
|
+
) {
|
|
191
|
+
deps.logImpl(` Applied migrations: ${plan.appliedVerifiedCount} (keep unchanged)`);
|
|
192
|
+
deps.logImpl(" Pending migrations that may be regenerated:");
|
|
193
|
+
plan.pendingMigrationNames.forEach((name) => deps.logImpl(` - ${name}`));
|
|
194
|
+
deps.logImpl(" Next: back up or commit the project, then remove only the listed pending folders.");
|
|
195
|
+
deps.logImpl(" gencow db:generate");
|
|
196
|
+
deps.logImpl(" pnpm exec drizzle-kit check");
|
|
197
|
+
deps.logImpl(` gencow db:check${environment === "prod" ? " --prod" : ""}`);
|
|
198
|
+
}
|
|
186
199
|
return;
|
|
187
200
|
}
|
|
188
201
|
deps.successImpl("Migration plan is safe to proceed.");
|
package/lib/db-push-command.mjs
CHANGED
|
@@ -110,6 +110,21 @@ function extractReadOnlyMigrationInspection(data) {
|
|
|
110
110
|
return null;
|
|
111
111
|
}
|
|
112
112
|
const plan = inspection.plan;
|
|
113
|
+
const pendingMigrationNames = plan?.pendingMigrationNames;
|
|
114
|
+
const hasSafePendingTail =
|
|
115
|
+
plan?.safeToRewritePending === true &&
|
|
116
|
+
Array.isArray(pendingMigrationNames) &&
|
|
117
|
+
pendingMigrationNames.length === plan?.pendingCount &&
|
|
118
|
+
pendingMigrationNames.length > 0 &&
|
|
119
|
+
new Set(pendingMigrationNames).size === pendingMigrationNames.length &&
|
|
120
|
+
pendingMigrationNames.every(
|
|
121
|
+
(name) =>
|
|
122
|
+
typeof name === "string" &&
|
|
123
|
+
name.length <= 200 &&
|
|
124
|
+
name !== "." &&
|
|
125
|
+
name !== ".." &&
|
|
126
|
+
!/[\\/\u0000-\u001f\u007f]/u.test(name),
|
|
127
|
+
);
|
|
113
128
|
if (
|
|
114
129
|
inspection.outcome === "READY" &&
|
|
115
130
|
!(
|
|
@@ -135,6 +150,9 @@ function extractReadOnlyMigrationInspection(data) {
|
|
|
135
150
|
planId: plan.planId,
|
|
136
151
|
appliedVerifiedCount: plan.appliedVerifiedCount,
|
|
137
152
|
pendingCount: plan.pendingCount,
|
|
153
|
+
...(hasSafePendingTail
|
|
154
|
+
? { pendingMigrationNames: [...pendingMigrationNames], safeToRewritePending: true }
|
|
155
|
+
: {}),
|
|
138
156
|
...(plan.databaseBootstrapRequired === true ? { databaseBootstrapRequired: true } : {}),
|
|
139
157
|
...(plan.migrationStorePreparationRequired === true
|
|
140
158
|
? { migrationStorePreparationRequired: true }
|
|
@@ -374,10 +392,11 @@ function createDbMigrationCommand(deps, command) {
|
|
|
374
392
|
} else {
|
|
375
393
|
if (!jsonMode) deps.infoImpl("Generating schema migrations...");
|
|
376
394
|
try {
|
|
395
|
+
const generationEnv = deps.buildEnv(config, { cwd: deps.cwdImpl() });
|
|
377
396
|
generatorVersion = runDrizzleGenerateWithFallback({
|
|
378
397
|
cwd: deps.cwdImpl(),
|
|
379
398
|
drizzleKitCmdImpl: deps.drizzleKitCmdImpl,
|
|
380
|
-
env:
|
|
399
|
+
env: generationEnv,
|
|
381
400
|
execSyncImpl: deps.execSyncImpl,
|
|
382
401
|
stdio: jsonMode ? "pipe" : "inherit",
|
|
383
402
|
warnImpl: jsonMode ? () => {} : deps.warnImpl,
|
package/lib/deploy-auditor.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Deploy Auditor — Pre-deploy dependency analysis (informational).
|
|
3
3
|
*
|
|
4
4
|
* Detects user third-party dependencies that need auto-install on the
|
|
5
|
-
* Gencow cloud runtime. User deps are installed via `
|
|
5
|
+
* Gencow cloud runtime. User deps are installed via frozen `pnpm install` on
|
|
6
6
|
* the server during provisioning.
|
|
7
7
|
*
|
|
8
8
|
* How it works:
|
|
@@ -78,7 +78,7 @@ export function formatDependencyVersionAuditError(result) {
|
|
|
78
78
|
|
|
79
79
|
lines.push("");
|
|
80
80
|
lines.push(" Fix:");
|
|
81
|
-
lines.push(`
|
|
81
|
+
lines.push(` pnpm add ${installTargets.join(" ")}`);
|
|
82
82
|
lines.push(" bunx gencow@latest deploy");
|
|
83
83
|
lines.push("");
|
|
84
84
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MIGRATION_NEXT_ACTION_KINDS } from "@gencow/migration-contract";
|
|
2
2
|
|
|
3
|
+
import { APP_DELETE_OPERATION_PATTERN } from "./app-response-error.mjs";
|
|
3
4
|
import { formatDeployCorrelationId } from "./deploy-correlation-diagnostic.mjs";
|
|
4
5
|
|
|
5
6
|
const DEPLOY_FAILURE_STAGES = new Set([
|
|
@@ -13,9 +14,17 @@ const DEPLOY_FAILURE_STAGES = new Set([
|
|
|
13
14
|
"controller",
|
|
14
15
|
]);
|
|
15
16
|
const RETRY_CLASSES = new Set(["terminal-client", "retryable-infra", "platform-action"]);
|
|
16
|
-
const USER_ACTIONS = new Set([...MIGRATION_NEXT_ACTION_KINDS, "OPERATOR_ACTION"]);
|
|
17
|
-
const STRUCTURED_CONTRACT_FIELDS = [
|
|
18
|
-
|
|
17
|
+
const USER_ACTIONS = new Set([...MIGRATION_NEXT_ACTION_KINDS, "OPERATOR_ACTION", "VIEW_DELETE_STATUS"]);
|
|
18
|
+
const STRUCTURED_CONTRACT_FIELDS = [
|
|
19
|
+
"code",
|
|
20
|
+
"stage",
|
|
21
|
+
"retryClass",
|
|
22
|
+
"userAction",
|
|
23
|
+
"correlationId",
|
|
24
|
+
"operationId",
|
|
25
|
+
"appName",
|
|
26
|
+
];
|
|
27
|
+
const PLATFORM_RECOVERY_MESSAGE = "Gencow is recovering this deployment. Your app files do not need changes.";
|
|
19
28
|
|
|
20
29
|
function safeText(value, maxLength) {
|
|
21
30
|
if (typeof value !== "string") return null;
|
|
@@ -26,13 +35,14 @@ function safeText(value, maxLength) {
|
|
|
26
35
|
return normalized;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
function readDeployFailureContract(value) {
|
|
38
|
+
function readDeployFailureContract(value, expectedAppName) {
|
|
30
39
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
31
40
|
const code = safeText(value.code, 100);
|
|
32
41
|
const stage = safeText(value.stage, 20);
|
|
33
42
|
const retryClass = safeText(value.retryClass, 32);
|
|
34
43
|
const userAction = safeText(value.userAction, 32);
|
|
35
44
|
const correlationId = value.correlationId === undefined ? null : safeText(value.correlationId, 128);
|
|
45
|
+
const operationId = value.operationId === undefined ? null : safeText(value.operationId, 80);
|
|
36
46
|
if (
|
|
37
47
|
!code?.match(/^[A-Z][A-Z0-9_]{2,99}$/u) ||
|
|
38
48
|
!stage ||
|
|
@@ -42,27 +52,33 @@ function readDeployFailureContract(value) {
|
|
|
42
52
|
!userAction ||
|
|
43
53
|
!USER_ACTIONS.has(userAction) ||
|
|
44
54
|
(value.correlationId !== undefined && !correlationId) ||
|
|
45
|
-
(correlationId && !/^[A-Za-z0-9_-]{8,128}$/u.test(correlationId))
|
|
55
|
+
(correlationId && !/^[A-Za-z0-9_-]{8,128}$/u.test(correlationId)) ||
|
|
56
|
+
(userAction === "VIEW_DELETE_STATUS" &&
|
|
57
|
+
(!operationId ||
|
|
58
|
+
!APP_DELETE_OPERATION_PATTERN.test(operationId) ||
|
|
59
|
+
!/^[a-z][a-z0-9]*(?:-[a-z0-9]+){2}$/u.test(expectedAppName ?? "")))
|
|
46
60
|
) {
|
|
47
61
|
return null;
|
|
48
62
|
}
|
|
49
63
|
return {
|
|
50
64
|
code,
|
|
51
65
|
stage,
|
|
66
|
+
retryClass,
|
|
52
67
|
userAction: userAction === "CONTACT_SUPPORT" ? "OPERATOR_ACTION" : userAction,
|
|
53
68
|
correlationId,
|
|
69
|
+
operationId,
|
|
54
70
|
legacySupportAction: userAction === "CONTACT_SUPPORT",
|
|
55
71
|
};
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
function normalizeLegacySupportMessage(message) {
|
|
59
75
|
return typeof message === "string" && /contact support/iu.test(message)
|
|
60
|
-
?
|
|
76
|
+
? PLATFORM_RECOVERY_MESSAGE
|
|
61
77
|
: message;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
|
|
65
|
-
const contract = readDeployFailureContract(responseBody);
|
|
80
|
+
export function renderDeployFailureDiagnosticLines(responseBody, statusText, options = {}) {
|
|
81
|
+
const contract = readDeployFailureContract(responseBody, options.expectedAppName);
|
|
66
82
|
const fallbackMessage = safeText(statusText, 120) ?? "Request failed";
|
|
67
83
|
if (!contract) {
|
|
68
84
|
const structuredFields =
|
|
@@ -83,8 +99,29 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText) {
|
|
|
83
99
|
if (isLegacyCorrelationEnvelope) lines.push(correlationLine);
|
|
84
100
|
return lines;
|
|
85
101
|
}
|
|
102
|
+
const platformOwned =
|
|
103
|
+
contract.retryClass === "platform-action" && contract.userAction === "OPERATOR_ACTION";
|
|
104
|
+
if (platformOwned) {
|
|
105
|
+
const lines = [`Deploy failed: ${PLATFORM_RECOVERY_MESSAGE}`];
|
|
106
|
+
const correlationLine = formatDeployCorrelationId(contract.correlationId);
|
|
107
|
+
if (correlationLine) lines.push(correlationLine);
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
if (contract.userAction === "VIEW_DELETE_STATUS") {
|
|
111
|
+
const publicMessage = safeText(responseBody.error, 240) ?? fallbackMessage;
|
|
112
|
+
const lines = [
|
|
113
|
+
`Deploy failed: ${publicMessage}`,
|
|
114
|
+
`Code: ${contract.code}`,
|
|
115
|
+
`Stage: ${contract.stage}`,
|
|
116
|
+
`Operation ID: ${contract.operationId}`,
|
|
117
|
+
];
|
|
118
|
+
const correlationLine = formatDeployCorrelationId(contract.correlationId);
|
|
119
|
+
if (correlationLine) lines.push(correlationLine);
|
|
120
|
+
lines.push(`Next command: gencow app delete ${options.expectedAppName} --force`);
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
86
123
|
const publicMessage = contract.legacySupportAction
|
|
87
|
-
?
|
|
124
|
+
? PLATFORM_RECOVERY_MESSAGE
|
|
88
125
|
: (safeText(responseBody.error, 240) ?? fallbackMessage);
|
|
89
126
|
const lines = [`Deploy failed: ${publicMessage}`];
|
|
90
127
|
lines.push(`Code: ${contract.code}`, `Stage: ${contract.stage}`);
|