gencow 0.1.256 → 0.1.258
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/lib/app-command.mjs +2 -0
- package/lib/cli-command-runner.mjs +22 -2
- package/lib/cloud-targets.mjs +34 -5
- package/lib/db-command.mjs +20 -5
- package/lib/db-push-command.mjs +7 -1
- package/lib/deploy-runtime.mjs +5 -1
- package/lib/deploy-static-fullstack-runtime.mjs +2 -1
- package/lib/files-command.mjs +8 -4
- package/lib/jobs-command.mjs +14 -8
- package/lib/static-artifact-manifest.mjs +75 -5
- package/lib/static-command.mjs +14 -0
- package/lib/static-deploy-artifact-preflight.mjs +85 -0
- package/lib/static-deploy-command.mjs +5 -50
- package/package.json +4 -4
- package/runtime/server.mjs +74 -74
package/lib/app-command.mjs
CHANGED
|
@@ -488,6 +488,8 @@ ${dashboardLine}
|
|
|
488
488
|
if (delData.stopErrorCode) {
|
|
489
489
|
warnImpl(`App stop needs administrator review. Code: ${delData.stopErrorCode}. Handoff ID: ${delData.handoffId}`);
|
|
490
490
|
}
|
|
491
|
+
// The handoff changes ownership only; the app keeps serving until an administrator processes it.
|
|
492
|
+
warnImpl(`Until an administrator processes the deletion, the app's public URL can keep responding. Request ID: ${requestId}. This request is already recorded; do not repeat the delete.`);
|
|
491
493
|
return 0;
|
|
492
494
|
}
|
|
493
495
|
|
|
@@ -111,15 +111,35 @@ const SAFE_LOCAL_ERROR_CODES = new Map([
|
|
|
111
111
|
"Retryable: no, until `npx gencow@latest codegen` passes locally — run it, fix what it reports, then rerun your deploy command.",
|
|
112
112
|
},
|
|
113
113
|
],
|
|
114
|
+
[
|
|
115
|
+
"STATIC_DIR_NOT_FOUND",
|
|
116
|
+
{
|
|
117
|
+
message: (caught) => {
|
|
118
|
+
const dir = safeDisplayDir(caught);
|
|
119
|
+
return dir
|
|
120
|
+
? `Static deploy stopped before upload: the static directory "${dir}" does not exist or is not a directory.`
|
|
121
|
+
: "Static deploy stopped before upload: the static directory does not exist or is not a directory.";
|
|
122
|
+
},
|
|
123
|
+
stage: "preflight",
|
|
124
|
+
action: (caught) => {
|
|
125
|
+
const dir = safeDisplayDir(caught) ?? "<static-dir>";
|
|
126
|
+
if (caught.staticDirSource === "config") {
|
|
127
|
+
return `Retryable: yes, after ${dir} exists as a directory — run \`ls -ld ${dir}\` to confirm, or correct the client static.dir setting in your Gencow config, then rerun the same command.`;
|
|
128
|
+
}
|
|
129
|
+
return `Retryable: yes, after ${dir} exists as a directory — run \`ls -ld ${dir}\` to confirm, then rerun the same command with a correct path, or omit the directory argument to auto-detect (dist, out, build, .next/out).`;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
],
|
|
114
133
|
[
|
|
115
134
|
"STATIC_ARTIFACT_INDEX_EMPTY",
|
|
116
135
|
{
|
|
117
|
-
message:
|
|
136
|
+
message:
|
|
137
|
+
"Static deploy stopped before upload: the build output's index.html is empty or contains only whitespace, a BOM, or HTML comments.",
|
|
118
138
|
stage: "artifact",
|
|
119
139
|
action: (caught) => {
|
|
120
140
|
const dir = safeDisplayDir(caught);
|
|
121
141
|
const target = dir ?? "<build-output-dir>";
|
|
122
|
-
return `Retryable: yes, after the build writes
|
|
142
|
+
return `Retryable: yes, after the build writes an index.html with real content into ${target} — run \`ls -l ${target}/index.html\` to confirm its size is above 0 bytes and that it holds more than whitespace or HTML comments, then rerun \`gencow static ${target}\`.`;
|
|
123
143
|
},
|
|
124
144
|
},
|
|
125
145
|
],
|
package/lib/cloud-targets.mjs
CHANGED
|
@@ -33,6 +33,28 @@ export function readProjectAppTargets(cwd = process.cwd()) {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Single decision for "which prod app does --prod (+ optional --app) target".
|
|
37
|
+
// acceptExplicitWithoutProdApp preserves call sites that trust an explicit app
|
|
38
|
+
// when the project has no prodApp binding (legacy metadata); others fail closed.
|
|
39
|
+
export function resolveProdAppTarget({ explicitAppId = null, prodApp = null, acceptExplicitWithoutProdApp = false }) {
|
|
40
|
+
if (!prodApp) {
|
|
41
|
+
if (explicitAppId && acceptExplicitWithoutProdApp) return { appId: explicitAppId };
|
|
42
|
+
return { appId: null, reason: "no_prod_app" };
|
|
43
|
+
}
|
|
44
|
+
if (explicitAppId && explicitAppId !== prodApp) {
|
|
45
|
+
return { appId: null, reason: "explicit_app_not_prod_app", prodApp };
|
|
46
|
+
}
|
|
47
|
+
return { appId: prodApp };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Shared human message for a failed prod target; falls back to the call site's own text.
|
|
51
|
+
export function describeProdTargetFailure(result, fallbackMessage) {
|
|
52
|
+
if (result?.reason === "explicit_app_not_prod_app") {
|
|
53
|
+
return `\`--app\` with \`--prod\` must name this project's prod app: ${result.prodApp}. Run again with --app ${result.prodApp} or without --app.`;
|
|
54
|
+
}
|
|
55
|
+
return fallbackMessage;
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
export function resolveCloudAppTarget(restArgs, options = {}) {
|
|
37
59
|
const {
|
|
38
60
|
cwd = process.cwd(),
|
|
@@ -59,17 +81,24 @@ export function resolveCloudAppTarget(restArgs, options = {}) {
|
|
|
59
81
|
if (projectTargets.invalid) {
|
|
60
82
|
return { error: "MIGRATION_PROJECT_CONFIG_INVALID" };
|
|
61
83
|
}
|
|
84
|
+
// The project's own prod app id names the prod environment even without --prod.
|
|
85
|
+
if (explicitApp && envTarget === "dev" && projectTargets.prodApp && appId === projectTargets.prodApp) {
|
|
86
|
+
envTarget = "prod";
|
|
87
|
+
}
|
|
62
88
|
if (!appId) {
|
|
63
89
|
appId = envTarget === "prod" ? projectTargets.prodApp : projectTargets.appId;
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
if (envTarget === "prod") {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
const prodTarget = resolveProdAppTarget({
|
|
94
|
+
explicitAppId: explicitApp ? appId : null,
|
|
95
|
+
prodApp: projectTargets.prodApp,
|
|
96
|
+
acceptExplicitWithoutProdApp: true,
|
|
97
|
+
});
|
|
98
|
+
if (!prodTarget.appId) {
|
|
99
|
+
return { error: describeProdTargetFailure(prodTarget, missingProdMessage) };
|
|
72
100
|
}
|
|
101
|
+
appId = prodTarget.appId;
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
if (!appId) {
|
package/lib/db-command.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { createDbCheckCommand, createDbPushCommand } from "./db-push-command.mjs
|
|
|
17
17
|
import { logEnvFileOperationWithType } from "./backend-env-resolver.mjs";
|
|
18
18
|
import { runDrizzleGenerateWithFallback } from "./drizzle-generate.mjs";
|
|
19
19
|
import { BOLD, CYAN, DIM, GREEN, RED, RESET, error, info, log, success, warn } from "./output.mjs";
|
|
20
|
+
import { describeProdTargetFailure, resolveProdAppTarget } from "./cloud-targets.mjs";
|
|
20
21
|
import { platformFetch, requireCreds } from "./platform-client.mjs";
|
|
21
22
|
import { readProjectMetadata } from "./project-context.mjs";
|
|
22
23
|
|
|
@@ -35,9 +36,17 @@ export function resolveDbCloudTarget(args, gencowJson = null) {
|
|
|
35
36
|
if (gencowJson) {
|
|
36
37
|
platformUrl = gencowJson.platformUrl || null;
|
|
37
38
|
if (isProd) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const prodTarget = resolveProdAppTarget({
|
|
40
|
+
explicitAppId,
|
|
41
|
+
prodApp: gencowJson.prodApp || null,
|
|
42
|
+
acceptExplicitWithoutProdApp: true,
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
appId: prodTarget.appId,
|
|
46
|
+
isProd,
|
|
47
|
+
platformUrl,
|
|
48
|
+
prodTargetFailure: prodTarget.appId ? undefined : prodTarget,
|
|
49
|
+
};
|
|
41
50
|
}
|
|
42
51
|
return { appId: explicitAppId || gencowJson.appId || gencowJson.appName || null, isProd, platformUrl };
|
|
43
52
|
}
|
|
@@ -369,10 +378,16 @@ export function createDbCommands({
|
|
|
369
378
|
readFileSyncImpl,
|
|
370
379
|
resolvePathImpl,
|
|
371
380
|
});
|
|
372
|
-
const
|
|
381
|
+
const target = resolveDbCloudTarget(args, gencowJson);
|
|
382
|
+
const { appId, isProd, platformUrl } = target;
|
|
373
383
|
|
|
374
384
|
if (isProd && !appId) {
|
|
375
|
-
errorImpl(
|
|
385
|
+
errorImpl(
|
|
386
|
+
describeProdTargetFailure(
|
|
387
|
+
target.prodTargetFailure,
|
|
388
|
+
"Prod app not found. Run 'gencow deploy' first to create a prod app.",
|
|
389
|
+
),
|
|
390
|
+
);
|
|
376
391
|
return 1;
|
|
377
392
|
}
|
|
378
393
|
|
package/lib/db-push-command.mjs
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
resolveConfirmationMode,
|
|
54
54
|
} from "./confirm-gate.mjs";
|
|
55
55
|
import { emitMigrationResponsibilityNotice } from "./migration-advisory.mjs";
|
|
56
|
+
import { describeProdTargetFailure } from "./cloud-targets.mjs";
|
|
56
57
|
import { readProjectMetadata } from "./project-context.mjs";
|
|
57
58
|
|
|
58
59
|
function clientCorrelationId() {
|
|
@@ -246,7 +247,12 @@ function createDbMigrationCommand(deps, command) {
|
|
|
246
247
|
buildClientUserFixableDiagnostic("MIGRATION_APP_TARGET_MISSING", clientCorrelationId()),
|
|
247
248
|
);
|
|
248
249
|
} else if (isProd) {
|
|
249
|
-
deps.errorImpl(
|
|
250
|
+
deps.errorImpl(
|
|
251
|
+
describeProdTargetFailure(
|
|
252
|
+
target.prodTargetFailure,
|
|
253
|
+
"Prod app not found. Run 'gencow deploy' first to create a prod app.",
|
|
254
|
+
),
|
|
255
|
+
);
|
|
250
256
|
} else {
|
|
251
257
|
deps.errorImpl("Cloud app not found. Make sure gencow.json exists with an appId.");
|
|
252
258
|
deps.infoImpl(`${DIM}Run 'gencow dev' first to create your app.${RESET}`);
|
package/lib/deploy-runtime.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
runStaticDeployFlow,
|
|
17
17
|
} from "./deploy-static-fullstack-runtime.mjs";
|
|
18
18
|
import { detectStaticDeployBackend, preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
|
|
19
|
+
import { formatCommandError } from "./cli-command-runner.mjs";
|
|
19
20
|
import { prodCreateFailureMessage } from "./prod-create-failure.mjs";
|
|
20
21
|
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
21
22
|
import { reconcileProductionAppTarget } from "./deploy-app-target.mjs";
|
|
@@ -528,12 +529,15 @@ export function createDeployCommand({
|
|
|
528
529
|
try {
|
|
529
530
|
staticArtifactPreflight = preflightStaticDeployArtifactImpl({
|
|
530
531
|
staticDirArg: resolvedStaticDirArg,
|
|
532
|
+
// Display text is what the user typed or the client config says, never the resolved path.
|
|
533
|
+
staticDirDisplay: parsed.staticDirArg ?? selectedClient?.static?.dir ?? null,
|
|
534
|
+
staticDirSource: parsed.staticDirArg ? "argument" : "config",
|
|
531
535
|
cwd: staticArtifactCwd,
|
|
532
536
|
existsSyncImpl,
|
|
533
537
|
resolvePathImpl,
|
|
534
538
|
});
|
|
535
539
|
} catch (manifestError) {
|
|
536
|
-
errorImpl(
|
|
540
|
+
errorImpl(formatCommandError(manifestError));
|
|
537
541
|
exitImpl(1);
|
|
538
542
|
return;
|
|
539
543
|
}
|
|
@@ -5,6 +5,7 @@ import { createDeployPackageRuntime, writeProjectMetadata } from "./deploy-packa
|
|
|
5
5
|
import { attachProductionTarget } from "./deploy-codegen-failure.mjs";
|
|
6
6
|
import { BOLD, CYAN, DIM, RESET, error, info, log, success } from "./output.mjs";
|
|
7
7
|
import { preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
|
|
8
|
+
import { formatCommandError } from "./cli-command-runner.mjs";
|
|
8
9
|
|
|
9
10
|
function shouldPreserveFinalizeAttempt(error) {
|
|
10
11
|
return error?.retryable === true || error?.message === "APP_SERVING_ROUTE_COMMIT_PENDING";
|
|
@@ -308,7 +309,7 @@ export async function runStaticDeployFlow({
|
|
|
308
309
|
resolvePathImpl,
|
|
309
310
|
});
|
|
310
311
|
} catch (manifestError) {
|
|
311
|
-
errorImpl(
|
|
312
|
+
errorImpl(formatCommandError(manifestError));
|
|
312
313
|
exitImpl(1);
|
|
313
314
|
return;
|
|
314
315
|
}
|
package/lib/files-command.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { basename, resolve } from "path";
|
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
3
3
|
import { resolveProjectMetadataPath, resolveProjectSelection } from "./project-context.mjs";
|
|
4
4
|
import { getAppUrl } from "./domain-helpers.mjs";
|
|
5
|
+
import { describeProdTargetFailure, resolveProdAppTarget } from "./cloud-targets.mjs";
|
|
5
6
|
import { BOLD, CYAN, DIM, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
|
|
6
7
|
import { requireCreds } from "./platform-client.mjs";
|
|
7
8
|
|
|
@@ -30,9 +31,7 @@ export function parseFilesCommandArgs(restArgs) {
|
|
|
30
31
|
|
|
31
32
|
export function resolveFilesAppId({ explicitAppId = null, gencowJson = null, isProd = false }) {
|
|
32
33
|
if (isProd) {
|
|
33
|
-
|
|
34
|
-
if (!prodApp) return null;
|
|
35
|
-
return explicitAppId && explicitAppId !== prodApp ? null : prodApp;
|
|
34
|
+
return resolveProdAppTarget({ explicitAppId, prodApp: gencowJson?.prodApp || null }).appId;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
return explicitAppId || gencowJson?.appId || gencowJson?.appName || null;
|
|
@@ -150,7 +149,12 @@ export function createFilesCommand({
|
|
|
150
149
|
const appId = resolveFilesAppId({ explicitAppId, gencowJson, isProd });
|
|
151
150
|
|
|
152
151
|
if (isProd && !appId) {
|
|
153
|
-
errorImpl(
|
|
152
|
+
errorImpl(
|
|
153
|
+
describeProdTargetFailure(
|
|
154
|
+
resolveProdAppTarget({ explicitAppId, prodApp: gencowJson?.prodApp || null }),
|
|
155
|
+
"Prod app not found. Run 'gencow deploy' first to create a prod app.",
|
|
156
|
+
),
|
|
157
|
+
);
|
|
154
158
|
return;
|
|
155
159
|
}
|
|
156
160
|
|
package/lib/jobs-command.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { describeProdTargetFailure, resolveProdAppTarget } from "./cloud-targets.mjs";
|
|
2
3
|
import { resolveProjectMetadataPath, resolveProjectSelection } from "./project-context.mjs";
|
|
3
4
|
import { parseDurationToMinutes } from "./domain-helpers.mjs";
|
|
4
5
|
import { BOLD, CYAN, DIM, GREEN, RED, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
|
|
@@ -115,7 +116,7 @@ export function parseJobsCommandArgs(jobsArgs) {
|
|
|
115
116
|
};
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
export function
|
|
119
|
+
export function resolveJobsAppTarget(params) {
|
|
119
120
|
const cwd = params.cwd ?? process.cwd();
|
|
120
121
|
const existsSyncImpl = params.existsSyncImpl ?? existsSync;
|
|
121
122
|
const processEnv = params.processEnv ?? process.env;
|
|
@@ -133,17 +134,21 @@ export function resolveJobsAppId(params) {
|
|
|
133
134
|
}
|
|
134
135
|
const gencowJsonPath = resolveProjectMetadataPath(projectDir);
|
|
135
136
|
if (!existsSyncImpl(gencowJsonPath)) {
|
|
136
|
-
return params.isProd
|
|
137
|
+
return params.isProd
|
|
138
|
+
? resolveProdAppTarget({ prodApp: null })
|
|
139
|
+
: { appId: params.explicitAppId ?? null };
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
const gencowJson = JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8"));
|
|
140
143
|
if (params.isProd) {
|
|
141
|
-
|
|
142
|
-
if (!prodApp) return null;
|
|
143
|
-
return params.explicitAppId && params.explicitAppId !== prodApp ? null : prodApp;
|
|
144
|
+
return resolveProdAppTarget({ explicitAppId: params.explicitAppId, prodApp: gencowJson.prodApp || null });
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
return params.explicitAppId || gencowJson.appId || gencowJson.appName || null;
|
|
147
|
+
return { appId: params.explicitAppId || gencowJson.appId || gencowJson.appName || null };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resolveJobsAppId(params) {
|
|
151
|
+
return resolveJobsAppTarget(params).appId;
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
export function getWorkflowCliDerivedStatus(run) {
|
|
@@ -680,13 +685,14 @@ export function createJobsCommand() {
|
|
|
680
685
|
|
|
681
686
|
const creds = requireCreds();
|
|
682
687
|
const parsed = parseJobsCommandArgs(jobsArgs);
|
|
683
|
-
const
|
|
688
|
+
const appTarget = resolveJobsAppTarget({
|
|
684
689
|
explicitAppId: parsed.appId,
|
|
685
690
|
isProd: parsed.isProd,
|
|
686
691
|
});
|
|
692
|
+
const appId = appTarget.appId;
|
|
687
693
|
|
|
688
694
|
if (parsed.isProd && !appId) {
|
|
689
|
-
error("No prod app yet. Run gencow deploy --prod first.");
|
|
695
|
+
error(describeProdTargetFailure(appTarget, "No prod app yet. Run gencow deploy --prod first."));
|
|
690
696
|
return;
|
|
691
697
|
}
|
|
692
698
|
if (!appId) {
|
|
@@ -1,6 +1,66 @@
|
|
|
1
|
-
import { lstatSync, readdirSync } from "fs";
|
|
1
|
+
import { lstatSync, readFileSync, readdirSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
|
|
4
|
+
// Mirrors the Platform policy in packages/platform/src/static-site-entrypoint-policy.ts
|
|
5
|
+
// (isBlankIndexBytes, including its HTML comment states). Keep both in step; the contract test in
|
|
6
|
+
// packages/platform/src/__tests__ runs the same byte cases and generated documents through both entrypoints.
|
|
7
|
+
const STATIC_INDEX_BLANK_SCAN_LIMIT_BYTES = 64 * 1024;
|
|
8
|
+
const UTF8_BOM = [0xef, 0xbb, 0xbf];
|
|
9
|
+
const COMMENT_OPENER = [0x3c, 0x21, 0x2d, 0x2d];
|
|
10
|
+
const ASCII_WHITESPACE = new Set([0x20, 0x09, 0x0a, 0x0d, 0x0c, 0x0b]);
|
|
11
|
+
const LT = 0x3c;
|
|
12
|
+
const GT = 0x3e;
|
|
13
|
+
const BANG = 0x21;
|
|
14
|
+
const DASH = 0x2d;
|
|
15
|
+
|
|
16
|
+
function isBlankIndexBytes(bytes) {
|
|
17
|
+
if (bytes.length > STATIC_INDEX_BLANK_SCAN_LIMIT_BYTES) return false;
|
|
18
|
+
let index = 0;
|
|
19
|
+
if (bytes.length >= UTF8_BOM.length && UTF8_BOM.every((byte, offset) => bytes[offset] === byte)) {
|
|
20
|
+
index = UTF8_BOM.length;
|
|
21
|
+
}
|
|
22
|
+
let state = "text";
|
|
23
|
+
let openerMatched = 0;
|
|
24
|
+
for (; index < bytes.length; index += 1) {
|
|
25
|
+
const byte = bytes[index];
|
|
26
|
+
if (state === "text") {
|
|
27
|
+
if (openerMatched > 0) {
|
|
28
|
+
if (byte !== COMMENT_OPENER[openerMatched]) return false;
|
|
29
|
+
openerMatched += 1;
|
|
30
|
+
if (openerMatched === COMMENT_OPENER.length) {
|
|
31
|
+
openerMatched = 0;
|
|
32
|
+
state = "commentStart";
|
|
33
|
+
}
|
|
34
|
+
} else if (byte === LT) {
|
|
35
|
+
openerMatched = 1;
|
|
36
|
+
} else if (!ASCII_WHITESPACE.has(byte)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
} else if (state === "commentStart") {
|
|
40
|
+
if (byte === GT) state = "text";
|
|
41
|
+
else state = byte === DASH ? "commentStartDash" : "comment";
|
|
42
|
+
} else if (state === "commentStartDash") {
|
|
43
|
+
if (byte === DASH) state = "commentEnd";
|
|
44
|
+
else if (byte === GT) state = "text";
|
|
45
|
+
else state = "comment";
|
|
46
|
+
} else if (state === "comment") {
|
|
47
|
+
if (byte === DASH) state = "commentEndDash";
|
|
48
|
+
} else if (state === "commentEndDash") {
|
|
49
|
+
state = byte === DASH ? "commentEnd" : "comment";
|
|
50
|
+
} else if (state === "commentEnd") {
|
|
51
|
+
if (byte === GT) state = "text";
|
|
52
|
+
else if (byte === BANG) state = "commentEndBang";
|
|
53
|
+
else if (byte !== DASH) state = "comment";
|
|
54
|
+
} else if (state === "commentEndBang") {
|
|
55
|
+
if (byte === GT) state = "text";
|
|
56
|
+
else state = byte === DASH ? "commentEndDash" : "comment";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// A partial opener at end of file ("<", "<!", "<!-") is not a comment, so it stays content.
|
|
60
|
+
// Otherwise text or an open comment at end of file renders nothing, so the file is blank.
|
|
61
|
+
return openerMatched === 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
4
64
|
function countFiles(dir, readdirSyncImpl) {
|
|
5
65
|
let count = 0;
|
|
6
66
|
for (const entry of readdirSyncImpl(dir, { withFileTypes: true })) {
|
|
@@ -16,12 +76,20 @@ export function buildStaticArtifactManifest({
|
|
|
16
76
|
displayDir = null,
|
|
17
77
|
lstatSyncImpl = lstatSync,
|
|
18
78
|
readdirSyncImpl = readdirSync,
|
|
79
|
+
readFileSyncImpl = readFileSync,
|
|
19
80
|
}) {
|
|
20
|
-
// Mirrors the Platform's static entrypoint policy: a root index.html must be a regular file
|
|
81
|
+
// Mirrors the Platform's static entrypoint policy: a root index.html must be a regular file that is
|
|
82
|
+
// not empty and not blank (optional BOM, ASCII whitespace, and HTML comments only).
|
|
21
83
|
let entrypoint = "missing";
|
|
22
84
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
85
|
+
const indexPath = join(rootDir, "index.html");
|
|
86
|
+
const stat = lstatSyncImpl(indexPath);
|
|
87
|
+
if (stat.isFile()) {
|
|
88
|
+
const blank =
|
|
89
|
+
stat.size <= STATIC_INDEX_BLANK_SCAN_LIMIT_BYTES &&
|
|
90
|
+
isBlankIndexBytes(readFileSyncImpl(indexPath));
|
|
91
|
+
entrypoint = stat.size === 0 || blank ? "empty" : "usable";
|
|
92
|
+
}
|
|
25
93
|
} catch {
|
|
26
94
|
entrypoint = "missing";
|
|
27
95
|
}
|
|
@@ -33,7 +101,9 @@ export function buildStaticArtifactManifest({
|
|
|
33
101
|
// attached as a field (not interpolated) so the public renderer decides what is safe to show.
|
|
34
102
|
if (entrypoint === "empty") {
|
|
35
103
|
throw Object.assign(
|
|
36
|
-
new Error(
|
|
104
|
+
new Error(
|
|
105
|
+
`${source} has an empty or blank index.html at its root (only whitespace or HTML comments). Refusing to replace the current frontend.`,
|
|
106
|
+
),
|
|
37
107
|
{ code: "STATIC_ARTIFACT_INDEX_EMPTY", displayDir },
|
|
38
108
|
);
|
|
39
109
|
}
|
package/lib/static-command.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { isAbsolute, resolve } from "path";
|
|
|
3
3
|
|
|
4
4
|
import { readProjectDisplayName } from "./cli-project-runtime.mjs";
|
|
5
5
|
import { formatCommandError } from "./cli-command-runner.mjs";
|
|
6
|
+
import { describeProdTargetFailure, resolveProdAppTarget } from "./cloud-targets.mjs";
|
|
6
7
|
import {
|
|
7
8
|
getCliInvocationSelection,
|
|
8
9
|
resolveProjectMetadataPath,
|
|
@@ -255,10 +256,14 @@ export function createStaticCommand(options = {}) {
|
|
|
255
256
|
? resolvePathImpl(frontendRoot, selectedClient.static.dir)
|
|
256
257
|
: parsed.staticDir;
|
|
257
258
|
|
|
259
|
+
// The display value is what the user typed or the client config says, never the resolved absolute path.
|
|
260
|
+
const staticDirDisplay = parsed.staticDir || selectedClient?.static?.dir || null;
|
|
258
261
|
let artifactPreflight;
|
|
259
262
|
try {
|
|
260
263
|
artifactPreflight = preflightStaticDeployArtifactImpl({
|
|
261
264
|
staticDirArg: configuredStaticDir,
|
|
265
|
+
staticDirDisplay,
|
|
266
|
+
staticDirSource: parsed.staticDir ? "argument" : "config",
|
|
262
267
|
cwd: selectedClient ? frontendRoot : cwdImpl(),
|
|
263
268
|
});
|
|
264
269
|
} catch (manifestError) {
|
|
@@ -277,6 +282,15 @@ export function createStaticCommand(options = {}) {
|
|
|
277
282
|
selectedClient && artifactPreflight.targetDir && !isAbsolute(artifactPreflight.targetDir)
|
|
278
283
|
? resolvePathImpl(frontendRoot, artifactPreflight.targetDir)
|
|
279
284
|
: artifactPreflight.targetDir;
|
|
285
|
+
if (parsed.isProd && prodAppId) {
|
|
286
|
+
// An explicit --app must be this project's prod app; it is never silently replaced by the default.
|
|
287
|
+
const prodTarget = resolveProdAppTarget({ explicitAppId: parsed.appId, prodApp: prodAppId });
|
|
288
|
+
if (!prodTarget.appId) {
|
|
289
|
+
errorImpl(describeProdTargetFailure(prodTarget, "Prod app not found. Run 'gencow deploy' first to create a prod app."));
|
|
290
|
+
exitImpl(1);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
280
294
|
let deployTarget = parsed.isProd ? prodAppId || appId : appId;
|
|
281
295
|
const envTarget = parsed.isProd ? "prod" : "dev";
|
|
282
296
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, statSync } from "fs";
|
|
2
|
+
import { isAbsolute, resolve } from "path";
|
|
3
|
+
|
|
4
|
+
import { buildStaticArtifactManifest } from "./static-artifact-manifest.mjs";
|
|
5
|
+
|
|
6
|
+
function isExistingDirectory(path, { existsSyncImpl, statSyncImpl }) {
|
|
7
|
+
if (!existsSyncImpl(path)) return false;
|
|
8
|
+
try {
|
|
9
|
+
return statSyncImpl(path).isDirectory();
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// An explicitly named directory (CLI argument or client static.dir setting) is never silently replaced
|
|
16
|
+
// by auto-detection. displayDir is only what the user typed or configured; the resolved absolute path
|
|
17
|
+
// stays out of the error so the renderer cannot leak it.
|
|
18
|
+
function createStaticDirNotFoundError({ staticDirArg, staticDirDisplay, staticDirSource }) {
|
|
19
|
+
const displayDir = staticDirDisplay ?? (isAbsolute(staticDirArg) ? null : staticDirArg);
|
|
20
|
+
return Object.assign(new Error("Static directory does not exist or is not a directory."), {
|
|
21
|
+
code: "STATIC_DIR_NOT_FOUND",
|
|
22
|
+
displayDir,
|
|
23
|
+
staticDirSource,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveStaticDeployDir({
|
|
28
|
+
staticDirArg,
|
|
29
|
+
staticDirDisplay = null,
|
|
30
|
+
staticDirSource = "argument",
|
|
31
|
+
cwd = process.cwd(),
|
|
32
|
+
autoDetectDirs = ["dist", "out", "build", ".next/out"],
|
|
33
|
+
existsSyncImpl = existsSync,
|
|
34
|
+
resolvePathImpl = resolve,
|
|
35
|
+
statSyncImpl = statSync,
|
|
36
|
+
}) {
|
|
37
|
+
if (staticDirArg) {
|
|
38
|
+
if (!isExistingDirectory(resolvePathImpl(cwd, staticDirArg), { existsSyncImpl, statSyncImpl })) {
|
|
39
|
+
throw createStaticDirNotFoundError({ staticDirArg, staticDirDisplay, staticDirSource });
|
|
40
|
+
}
|
|
41
|
+
return staticDirArg;
|
|
42
|
+
}
|
|
43
|
+
for (const candidate of autoDetectDirs) {
|
|
44
|
+
if (existsSyncImpl(resolvePathImpl(cwd, candidate))) {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function preflightStaticDeployArtifact({
|
|
52
|
+
staticDirArg,
|
|
53
|
+
staticDirDisplay = null,
|
|
54
|
+
staticDirSource = "argument",
|
|
55
|
+
cwd = process.cwd(),
|
|
56
|
+
autoDetectDirs,
|
|
57
|
+
existsSyncImpl = existsSync,
|
|
58
|
+
lstatSyncImpl = lstatSync,
|
|
59
|
+
readdirSyncImpl = readdirSync,
|
|
60
|
+
resolvePathImpl = resolve,
|
|
61
|
+
statSyncImpl = statSync,
|
|
62
|
+
}) {
|
|
63
|
+
const targetDir = resolveStaticDeployDir({
|
|
64
|
+
staticDirArg,
|
|
65
|
+
staticDirDisplay,
|
|
66
|
+
staticDirSource,
|
|
67
|
+
cwd,
|
|
68
|
+
autoDetectDirs,
|
|
69
|
+
existsSyncImpl,
|
|
70
|
+
resolvePathImpl,
|
|
71
|
+
statSyncImpl,
|
|
72
|
+
});
|
|
73
|
+
if (!targetDir) return null;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
targetDir,
|
|
77
|
+
artifactManifest: buildStaticArtifactManifest({
|
|
78
|
+
rootDir: resolvePathImpl(cwd, targetDir),
|
|
79
|
+
selection: staticDirArg ? "explicit" : `auto:${targetDir}`,
|
|
80
|
+
displayDir: targetDir,
|
|
81
|
+
lstatSyncImpl,
|
|
82
|
+
readdirSyncImpl,
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -12,10 +12,13 @@ import { relative, resolve } from "path";
|
|
|
12
12
|
import { BOLD, CYAN, DIM, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
|
|
13
13
|
import { resolveCreatedAppResponse } from "./app-create-response.mjs";
|
|
14
14
|
import { readJsonObjectResponse } from "./http-response-json.mjs";
|
|
15
|
+
import { formatCommandError } from "./cli-command-runner.mjs";
|
|
16
|
+
import { preflightStaticDeployArtifact, resolveStaticDeployDir } from "./static-deploy-artifact-preflight.mjs";
|
|
17
|
+
|
|
18
|
+
export { preflightStaticDeployArtifact, resolveStaticDeployDir };
|
|
15
19
|
import { platformFetch, rpcMutation } from "./platform-client.mjs";
|
|
16
20
|
import { createTarGzipArchive } from "./tar-archive.mjs";
|
|
17
21
|
import { buildDeployClaimFromArchive } from "./deploy-package-claim.mjs";
|
|
18
|
-
import { buildStaticArtifactManifest } from "./static-artifact-manifest.mjs";
|
|
19
22
|
import { getCliInvocationSelection } from "./project-context.mjs";
|
|
20
23
|
import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs";
|
|
21
24
|
import { renderDeployFailureDiagnosticLines } from "./deploy-failure-diagnostic.mjs";
|
|
@@ -73,54 +76,6 @@ export async function postStaticDeployWithTransientProxyRetry({
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
export function resolveStaticDeployDir({
|
|
77
|
-
staticDirArg,
|
|
78
|
-
cwd = process.cwd(),
|
|
79
|
-
autoDetectDirs = ["dist", "out", "build", ".next/out"],
|
|
80
|
-
existsSyncImpl = existsSync,
|
|
81
|
-
resolvePathImpl = resolve,
|
|
82
|
-
}) {
|
|
83
|
-
if (staticDirArg && existsSyncImpl(resolvePathImpl(cwd, staticDirArg))) {
|
|
84
|
-
return staticDirArg;
|
|
85
|
-
}
|
|
86
|
-
for (const candidate of autoDetectDirs) {
|
|
87
|
-
if (existsSyncImpl(resolvePathImpl(cwd, candidate))) {
|
|
88
|
-
return candidate;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function preflightStaticDeployArtifact({
|
|
95
|
-
staticDirArg,
|
|
96
|
-
cwd = process.cwd(),
|
|
97
|
-
autoDetectDirs,
|
|
98
|
-
existsSyncImpl = existsSync,
|
|
99
|
-
lstatSyncImpl = lstatSync,
|
|
100
|
-
readdirSyncImpl = readdirSync,
|
|
101
|
-
resolvePathImpl = resolve,
|
|
102
|
-
}) {
|
|
103
|
-
const targetDir = resolveStaticDeployDir({
|
|
104
|
-
staticDirArg,
|
|
105
|
-
cwd,
|
|
106
|
-
autoDetectDirs,
|
|
107
|
-
existsSyncImpl,
|
|
108
|
-
resolvePathImpl,
|
|
109
|
-
});
|
|
110
|
-
if (!targetDir) return null;
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
targetDir,
|
|
114
|
-
artifactManifest: buildStaticArtifactManifest({
|
|
115
|
-
rootDir: resolvePathImpl(cwd, targetDir),
|
|
116
|
-
selection: staticDirArg ? "explicit" : `auto:${targetDir}`,
|
|
117
|
-
displayDir: targetDir,
|
|
118
|
-
lstatSyncImpl,
|
|
119
|
-
readdirSyncImpl,
|
|
120
|
-
}),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
79
|
export function detectStaticDeployBackend({
|
|
125
80
|
cwd = process.cwd(),
|
|
126
81
|
existsSyncImpl = existsSync,
|
|
@@ -307,7 +262,7 @@ export function createStaticDeployRuntime({
|
|
|
307
262
|
resolvePathImpl,
|
|
308
263
|
});
|
|
309
264
|
} catch (manifestError) {
|
|
310
|
-
errorImpl(
|
|
265
|
+
errorImpl(formatCommandError(manifestError));
|
|
311
266
|
exitImpl(1);
|
|
312
267
|
return;
|
|
313
268
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gencow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.258",
|
|
4
4
|
"description": "Gencow — AI Backend Engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"better-auth": "1.7.11",
|
|
42
42
|
"postgres": "^3.4.9",
|
|
43
43
|
"@gencow/client": "0.2.8",
|
|
44
|
-
"@gencow/react": "0.2.8",
|
|
45
|
-
"@gencow/shared": "0.0.0",
|
|
46
44
|
"@gencow/core": "0.1.47",
|
|
47
|
-
"@gencow/
|
|
45
|
+
"@gencow/react": "0.2.8",
|
|
46
|
+
"@gencow/server": "0.1.3",
|
|
47
|
+
"@gencow/shared": "0.0.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/bundle-core.mjs && node scripts/bundle-shared.mjs && node scripts/bundle-server.mjs",
|