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.
@@ -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: "Static deploy stopped before upload: the build output's index.html is empty.",
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 a non-empty index.html into ${target} — run \`ls -l ${target}/index.html\` to confirm its size is above 0 bytes, then rerun \`gencow static ${target}\`.`;
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
  ],
@@ -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
- if (!projectTargets.prodApp && !explicitApp) {
68
- return { error: missingProdMessage };
69
- }
70
- if (explicitApp && projectTargets.prodApp && appId !== projectTargets.prodApp) {
71
- return { error: missingProdMessage };
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) {
@@ -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 prodApp = gencowJson.prodApp || null;
39
- const appId = prodApp ? (!explicitAppId || explicitAppId === prodApp ? prodApp : null) : explicitAppId;
40
- return { appId, isProd, platformUrl };
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 { appId, isProd, platformUrl } = resolveDbCloudTarget(args, gencowJson);
381
+ const target = resolveDbCloudTarget(args, gencowJson);
382
+ const { appId, isProd, platformUrl } = target;
373
383
 
374
384
  if (isProd && !appId) {
375
- errorImpl("Prod app not found. Run 'gencow deploy' first to create a prod app.");
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
 
@@ -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("Prod app not found. Run 'gencow deploy' first to create a prod app.");
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}`);
@@ -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(manifestError instanceof Error ? manifestError.message : String(manifestError));
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(manifestError instanceof Error ? manifestError.message : String(manifestError));
312
+ errorImpl(formatCommandError(manifestError));
312
313
  exitImpl(1);
313
314
  return;
314
315
  }
@@ -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
- const prodApp = gencowJson?.prodApp || null;
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("Prod app not found. Run 'gencow deploy' first to create a prod app.");
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
 
@@ -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 resolveJobsAppId(params) {
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 ? null : (params.explicitAppId ?? null);
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
- const prodApp = gencowJson.prodApp || null;
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 appId = resolveJobsAppId({
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 with bytes.
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 stat = lstatSyncImpl(join(rootDir, "index.html"));
24
- if (stat.isFile()) entrypoint = stat.size === 0 ? "empty" : "usable";
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(`${source} has an empty index.html at its root. Refusing to replace the current frontend.`),
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
  }
@@ -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(manifestError instanceof Error ? manifestError.message : String(manifestError));
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.256",
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/server": "0.1.3"
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",