gencow 0.1.193 → 0.1.195
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/bin/gencow.mjs +8 -10
- package/lib/app-command.mjs +66 -38
- package/lib/app-response-error.mjs +24 -0
- package/lib/cli-command-runner.mjs +82 -0
- package/lib/deploy-runtime.mjs +6 -6
- package/lib/platform-client.mjs +28 -9
- package/lib/release-client.mjs +84 -10
- package/lib/static-command.mjs +14 -7
- package/lib/template-marketplace-command.mjs +12 -8
- package/package.json +3 -3
- package/runtime/server.mjs +1444 -1040
- package/server/index.js.map +7 -0
package/bin/gencow.mjs
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
updateEnvLocalUrl,
|
|
39
39
|
} from "../lib/cli-project-runtime.mjs";
|
|
40
40
|
import { createCodegenCommand } from "../lib/codegen-command.mjs";
|
|
41
|
+
import { runCliCommand } from "../lib/cli-command-runner.mjs";
|
|
41
42
|
import { createDoctorCommand } from "../lib/doctor-command.mjs";
|
|
42
43
|
import { maybeCheckCliVersion } from "../lib/cli-version-check.mjs";
|
|
43
44
|
import { updateComponentReadme } from "../lib/component-readme.mjs";
|
|
@@ -579,13 +580,10 @@ maybeCheckCliVersion({
|
|
|
579
580
|
warnImpl: warn,
|
|
580
581
|
});
|
|
581
582
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
commands.help();
|
|
590
|
-
process.exit(1);
|
|
591
|
-
}
|
|
583
|
+
process.exitCode = await runCliCommand({
|
|
584
|
+
args,
|
|
585
|
+
command: cmd,
|
|
586
|
+
commands,
|
|
587
|
+
errorImpl: error,
|
|
588
|
+
helpImpl: commands.help,
|
|
589
|
+
});
|
package/lib/app-command.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
3
|
import { resolve } from "path";
|
|
3
4
|
import {
|
|
@@ -24,6 +25,14 @@ import { updateEnvLocalUrl } from "./cli-project-runtime.mjs";
|
|
|
24
25
|
// Covers the 60s runtime-observer restart grace plus multiple 5s reconciliation scans.
|
|
25
26
|
const DEFAULT_DELETE_POLL_ATTEMPTS = 75;
|
|
26
27
|
const DELETE_POLL_INTERVAL_MS = 1_000;
|
|
28
|
+
const DEFAULT_APP_CREATE_TIMEOUT_MS = 60_000;
|
|
29
|
+
|
|
30
|
+
function resolveAppCreateTimeoutMs(value) {
|
|
31
|
+
const parsed = Number(value);
|
|
32
|
+
return Number.isSafeInteger(parsed) && parsed >= 100 && parsed <= 600_000
|
|
33
|
+
? parsed
|
|
34
|
+
: DEFAULT_APP_CREATE_TIMEOUT_MS;
|
|
35
|
+
}
|
|
27
36
|
|
|
28
37
|
function appResponseStatus(response) {
|
|
29
38
|
return Number.isSafeInteger(response?.status) ? `HTTP ${response.status}` : "unknown HTTP status";
|
|
@@ -105,23 +114,53 @@ async function confirmDelete(name) {
|
|
|
105
114
|
}
|
|
106
115
|
|
|
107
116
|
export function createAppCommand({
|
|
117
|
+
appCreateTimeoutMs = resolveAppCreateTimeoutMs(process.env.GENCOW_APP_CREATE_TIMEOUT_MS),
|
|
118
|
+
clearTimeoutImpl = clearTimeout,
|
|
119
|
+
confirmDeleteImpl = confirmDelete,
|
|
120
|
+
createAbortControllerImpl = () => new AbortController(),
|
|
121
|
+
createDeleteRequestIdImpl = () => `app_delete_request_${randomUUID()}`,
|
|
108
122
|
cwdImpl = () => process.cwd(),
|
|
109
123
|
loadConfig,
|
|
110
124
|
errorImpl = error,
|
|
111
125
|
infoImpl = info,
|
|
112
126
|
loadCredsImpl = loadCreds,
|
|
113
127
|
logImpl = log,
|
|
114
|
-
processRef = process,
|
|
115
128
|
requireCredsImpl = requireCreds,
|
|
116
129
|
rpcMutationImpl = rpcMutation,
|
|
117
130
|
rpcQueryImpl = rpcQuery,
|
|
118
131
|
saveCredsImpl = saveCreds,
|
|
132
|
+
setTimeoutImpl = setTimeout,
|
|
119
133
|
sleepImpl = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms)),
|
|
120
134
|
deletePollAttempts = DEFAULT_DELETE_POLL_ATTEMPTS,
|
|
121
135
|
successImpl = success,
|
|
122
136
|
updateEnvLocalUrlImpl = updateEnvLocalUrl,
|
|
123
137
|
warnImpl = warn,
|
|
124
138
|
}) {
|
|
139
|
+
const boundedAppCreateTimeoutMs = resolveAppCreateTimeoutMs(appCreateTimeoutMs);
|
|
140
|
+
|
|
141
|
+
async function requestAppCreate(creds, name) {
|
|
142
|
+
const controller = createAbortControllerImpl();
|
|
143
|
+
const timeoutError = new Error(
|
|
144
|
+
`App creation request timed out after ${boundedAppCreateTimeoutMs}ms. Please try again.`,
|
|
145
|
+
);
|
|
146
|
+
let timeoutHandle;
|
|
147
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
148
|
+
timeoutHandle = setTimeoutImpl(() => {
|
|
149
|
+
controller.abort(timeoutError);
|
|
150
|
+
reject(timeoutError);
|
|
151
|
+
}, boundedAppCreateTimeoutMs);
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
const request = (async () => {
|
|
155
|
+
const response = await rpcMutationImpl(creds, "apps.create", { name }, { signal: controller.signal });
|
|
156
|
+
return { response, data: await readJsonObjectResponse(response) };
|
|
157
|
+
})();
|
|
158
|
+
return await Promise.race([request, timeoutPromise]);
|
|
159
|
+
} finally {
|
|
160
|
+
clearTimeoutImpl(timeoutHandle);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
125
164
|
return async function app(subcmd, ...rest) {
|
|
126
165
|
if (subcmd === "--help" || subcmd === "-h" || rest.includes("--help") || rest.includes("-h")) {
|
|
127
166
|
logImpl(`\n${BOLD}${CYAN}gencow app${RESET} — App management\n`);
|
|
@@ -131,10 +170,10 @@ export function createAppCommand({
|
|
|
131
170
|
logImpl(` ${CYAN}create${RESET} Create a new app`);
|
|
132
171
|
logImpl(` ${CYAN}delete${RESET} Delete an app (confirmation required)`);
|
|
133
172
|
logImpl(` ${CYAN}status${RESET} Show app status\n`);
|
|
134
|
-
return;
|
|
173
|
+
return 0;
|
|
135
174
|
}
|
|
136
175
|
|
|
137
|
-
const creds = requireCredsImpl();
|
|
176
|
+
const creds = requireCredsImpl({ throwOnMissing: true });
|
|
138
177
|
|
|
139
178
|
if (!subcmd || subcmd === "list") {
|
|
140
179
|
logImpl(`\n${BOLD}${CYAN}Your Apps${RESET}\n`);
|
|
@@ -142,18 +181,16 @@ export function createAppCommand({
|
|
|
142
181
|
const body = await res.json().catch(() => null);
|
|
143
182
|
if (!res.ok) {
|
|
144
183
|
errorImpl(appResponseError(body, invalidAppResponse("app list", res, "error")));
|
|
145
|
-
|
|
146
|
-
return;
|
|
184
|
+
return 1;
|
|
147
185
|
}
|
|
148
186
|
const apps = parseAppListResponse(body);
|
|
149
187
|
if (!apps) {
|
|
150
188
|
errorImpl(invalidAppResponse("app list", res, "success"));
|
|
151
|
-
|
|
152
|
-
return;
|
|
189
|
+
return 1;
|
|
153
190
|
}
|
|
154
191
|
if (!apps.length) {
|
|
155
192
|
infoImpl("No apps yet. Run: gencow app create <name>");
|
|
156
|
-
return;
|
|
193
|
+
return 0;
|
|
157
194
|
}
|
|
158
195
|
|
|
159
196
|
logImpl(` ${"NAME".padEnd(22)} ${"STATUS".padEnd(12)} ${"URL".padEnd(38)} DEPLOYED`);
|
|
@@ -167,29 +204,26 @@ export function createAppCommand({
|
|
|
167
204
|
);
|
|
168
205
|
}
|
|
169
206
|
logImpl("");
|
|
170
|
-
return;
|
|
207
|
+
return 0;
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
if (subcmd === "create") {
|
|
174
211
|
const name = rest[0];
|
|
175
212
|
if (!name) {
|
|
176
213
|
errorImpl("Usage: gencow app create <name>");
|
|
177
|
-
|
|
214
|
+
return 1;
|
|
178
215
|
}
|
|
179
216
|
|
|
180
217
|
logImpl(`\n${BOLD}${CYAN}Gencow App Create${RESET}\n`);
|
|
181
218
|
infoImpl(`Creating app "${name}"...`);
|
|
182
|
-
const res = await
|
|
183
|
-
const data = await readJsonObjectResponse(res);
|
|
219
|
+
const { response: res, data } = await requestAppCreate(creds, name);
|
|
184
220
|
if (!res.ok) {
|
|
185
221
|
errorImpl(appResponseError(data, invalidAppResponse("app create", res, "error")));
|
|
186
|
-
|
|
187
|
-
return;
|
|
222
|
+
return 1;
|
|
188
223
|
}
|
|
189
224
|
if (!data) {
|
|
190
225
|
errorImpl(invalidAppResponse("app create", res, "success"));
|
|
191
|
-
|
|
192
|
-
return;
|
|
226
|
+
return 1;
|
|
193
227
|
}
|
|
194
228
|
|
|
195
229
|
let createdApp;
|
|
@@ -197,8 +231,7 @@ export function createAppCommand({
|
|
|
197
231
|
createdApp = resolveCreatedAppResponse(data, { platformUrl: creds.platformUrl });
|
|
198
232
|
} catch (caught) {
|
|
199
233
|
errorImpl(caught.message);
|
|
200
|
-
|
|
201
|
-
return;
|
|
234
|
+
return 1;
|
|
202
235
|
}
|
|
203
236
|
const { appId, appUrl } = createdApp;
|
|
204
237
|
|
|
@@ -259,7 +292,7 @@ ${dashboardLine}
|
|
|
259
292
|
|
|
260
293
|
${DIM}pnpm gencow dev — watch & auto-deploy to cloud + live logs${RESET}
|
|
261
294
|
`);
|
|
262
|
-
return;
|
|
295
|
+
return 0;
|
|
263
296
|
}
|
|
264
297
|
|
|
265
298
|
if (subcmd === "delete") {
|
|
@@ -270,20 +303,20 @@ ${dashboardLine}
|
|
|
270
303
|
const name = rest.find((value) => !value.startsWith("-")) || loadCredsImpl()?.currentApp;
|
|
271
304
|
if (!name || unsupportedOption) {
|
|
272
305
|
errorImpl("Usage: gencow app delete <name> [--force]");
|
|
273
|
-
|
|
274
|
-
return;
|
|
306
|
+
return 1;
|
|
275
307
|
}
|
|
276
308
|
|
|
277
309
|
if (!force) {
|
|
278
|
-
const confirmed = await
|
|
310
|
+
const confirmed = await confirmDeleteImpl(name);
|
|
279
311
|
if (!confirmed) {
|
|
280
312
|
warnImpl("Cancelled — name did not match.");
|
|
281
|
-
|
|
313
|
+
return 0;
|
|
282
314
|
}
|
|
283
315
|
}
|
|
284
316
|
|
|
285
317
|
infoImpl(`Deleting app "${name}"...`);
|
|
286
|
-
const
|
|
318
|
+
const requestId = createDeleteRequestIdImpl();
|
|
319
|
+
const delRes = await rpcMutationImpl(creds, "apps.delete", { name, requestId });
|
|
287
320
|
let delData = await readJsonObjectResponse(delRes);
|
|
288
321
|
if (!isExactAppDeleteResponse(delData, name)) {
|
|
289
322
|
const operationResult = await pollAppDeleteOperation({
|
|
@@ -304,8 +337,7 @@ ${dashboardLine}
|
|
|
304
337
|
invalidAppResponse("app delete", delRes, delRes.ok ? "success" : "error"),
|
|
305
338
|
),
|
|
306
339
|
);
|
|
307
|
-
|
|
308
|
-
return;
|
|
340
|
+
return 1;
|
|
309
341
|
}
|
|
310
342
|
|
|
311
343
|
const currentCreds = loadCredsImpl();
|
|
@@ -314,15 +346,14 @@ ${dashboardLine}
|
|
|
314
346
|
}
|
|
315
347
|
|
|
316
348
|
successImpl(`App "${name}" deleted`);
|
|
317
|
-
return;
|
|
349
|
+
return 0;
|
|
318
350
|
}
|
|
319
351
|
|
|
320
352
|
if (subcmd === "status") {
|
|
321
353
|
const name = rest[0] || loadCredsImpl()?.currentApp;
|
|
322
354
|
if (!name) {
|
|
323
355
|
errorImpl("Usage: gencow app status <name>");
|
|
324
|
-
|
|
325
|
-
return;
|
|
356
|
+
return 1;
|
|
326
357
|
}
|
|
327
358
|
const res = await rpcQueryImpl(creds, "apps.get", { name });
|
|
328
359
|
const decoded = await readJsonValueResponse(res);
|
|
@@ -333,24 +364,20 @@ ${dashboardLine}
|
|
|
333
364
|
? appNotFoundDiagnostic()
|
|
334
365
|
: appResponseError(body, invalidAppResponse("app status", res, "error")),
|
|
335
366
|
);
|
|
336
|
-
|
|
337
|
-
return;
|
|
367
|
+
return 1;
|
|
338
368
|
}
|
|
339
369
|
if (!decoded.parsed) {
|
|
340
370
|
errorImpl(invalidAppResponse("app status", res, "success"));
|
|
341
|
-
|
|
342
|
-
return;
|
|
371
|
+
return 1;
|
|
343
372
|
}
|
|
344
373
|
const lookup = parseAppLookupResponse(body, name);
|
|
345
374
|
if (lookup.kind === "not_found") {
|
|
346
375
|
errorImpl(appNotFoundDiagnostic());
|
|
347
|
-
|
|
348
|
-
return;
|
|
376
|
+
return 1;
|
|
349
377
|
}
|
|
350
378
|
if (lookup.kind === "invalid_response") {
|
|
351
379
|
errorImpl(invalidAppResponse("app status", res, "success"));
|
|
352
|
-
|
|
353
|
-
return;
|
|
380
|
+
return 1;
|
|
354
381
|
}
|
|
355
382
|
const data = lookup.app;
|
|
356
383
|
logImpl(`\n ${BOLD}${data.name}${RESET}`);
|
|
@@ -377,10 +404,11 @@ ${dashboardLine}
|
|
|
377
404
|
);
|
|
378
405
|
}
|
|
379
406
|
logImpl("");
|
|
380
|
-
return;
|
|
407
|
+
return 0;
|
|
381
408
|
}
|
|
382
409
|
|
|
383
410
|
errorImpl(`Unknown app subcommand: ${subcmd}`);
|
|
384
411
|
logImpl(` Usage: gencow app [list|create|delete|status]`);
|
|
412
|
+
return 1;
|
|
385
413
|
};
|
|
386
414
|
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
export const APP_DELETE_OPERATION_PATTERN =
|
|
2
2
|
/^app_delete_(?:[a-f0-9]{32}|[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$/u;
|
|
3
3
|
|
|
4
|
+
const RETRY_CLASSES = new Set(["operator-action", "platform-action", "retryable-infra", "terminal-client"]);
|
|
5
|
+
const USER_ACTIONS = new Set([
|
|
6
|
+
"CONTACT_SUPPORT",
|
|
7
|
+
"EDIT_AND_CHECK",
|
|
8
|
+
"INSPECT_STATE",
|
|
9
|
+
"OPERATOR_ACTION",
|
|
10
|
+
"RETRY",
|
|
11
|
+
"UPGRADE_CLI",
|
|
12
|
+
"VIEW_DELETE_STATUS",
|
|
13
|
+
"VIEW_STATUS",
|
|
14
|
+
"WAIT",
|
|
15
|
+
]);
|
|
16
|
+
|
|
4
17
|
export function appResponseError(body, fallback) {
|
|
5
18
|
if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
|
|
6
19
|
const message = typeof body.error === "string" && body.error.trim() ? body.error.trim() : fallback;
|
|
@@ -20,5 +33,16 @@ export function appResponseError(body, fallback) {
|
|
|
20
33
|
if (/^[a-z][a-z0-9_]{2,63}$/u.test(body.state ?? "")) {
|
|
21
34
|
lines.push(`State: ${body.state}`);
|
|
22
35
|
}
|
|
36
|
+
if (RETRY_CLASSES.has(body.retryClass) && USER_ACTIONS.has(body.userAction)) {
|
|
37
|
+
lines.push(`Retry class: ${body.retryClass}`);
|
|
38
|
+
lines.push(`Next action: ${body.userAction}`);
|
|
39
|
+
if (
|
|
40
|
+
Number.isSafeInteger(body.retryAfterMs) &&
|
|
41
|
+
body.retryAfterMs >= 0 &&
|
|
42
|
+
body.retryAfterMs <= 600_000
|
|
43
|
+
) {
|
|
44
|
+
lines.push(`Retry after: ${body.retryAfterMs}ms`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
23
47
|
return lines.join("\n");
|
|
24
48
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const SAFE_CORRELATION_ID = /^[A-Za-z0-9_-]{8,128}$/u;
|
|
2
|
+
const SAFE_PLATFORM_ERROR_DETAILS = new Map([
|
|
3
|
+
["ACCOUNT_DELETED", "This account has been deleted."],
|
|
4
|
+
["ACCOUNT_DELETION_PENDING", "This account is pending deletion."],
|
|
5
|
+
["ACCOUNT_SUSPENDED", "This account is suspended."],
|
|
6
|
+
["APP_RELEASE_CAPABILITY_UNAVAILABLE", "Release capability is temporarily unavailable."],
|
|
7
|
+
["APP_RELEASE_POLICY_READ_UNAVAILABLE", "Release policy is temporarily unavailable."],
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function safePlatformOrigin(value) {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(value);
|
|
13
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function commandErrorMessage(caught) {
|
|
20
|
+
if (caught instanceof Error && (caught.message === "APP_RELEASE_AUTH_REQUIRED" || caught.status === 401)) {
|
|
21
|
+
const lines = ["Login expired or invalid for this Platform."];
|
|
22
|
+
const platformOrigin = safePlatformOrigin(caught.platformUrl);
|
|
23
|
+
if (platformOrigin) lines.push(`Platform: ${platformOrigin}`);
|
|
24
|
+
const credentialEnvName =
|
|
25
|
+
caught.credentialSource === "environment" &&
|
|
26
|
+
(caught.credentialEnvName === "GENCOW_TOKEN" || caught.credentialEnvName === "GENCOW_DEPLOY_TOKEN")
|
|
27
|
+
? caught.credentialEnvName
|
|
28
|
+
: null;
|
|
29
|
+
if (credentialEnvName) {
|
|
30
|
+
lines.push(`Credential source: ${credentialEnvName}`);
|
|
31
|
+
lines.push(
|
|
32
|
+
`Action: replace or unset ${credentialEnvName}; environment credentials override interactive login.`,
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
lines.push("Run: npx gencow@latest login");
|
|
36
|
+
}
|
|
37
|
+
if (typeof caught.correlationId === "string" && SAFE_CORRELATION_ID.test(caught.correlationId)) {
|
|
38
|
+
lines.push(`Correlation ID: ${caught.correlationId}`);
|
|
39
|
+
}
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
if (caught instanceof Error && caught.message) {
|
|
43
|
+
const platformOrigin = safePlatformOrigin(caught.platformUrl);
|
|
44
|
+
const correlationId =
|
|
45
|
+
typeof caught.correlationId === "string" && SAFE_CORRELATION_ID.test(caught.correlationId)
|
|
46
|
+
? caught.correlationId
|
|
47
|
+
: null;
|
|
48
|
+
if (platformOrigin || correlationId || typeof caught.retryable === "boolean") {
|
|
49
|
+
const safeDetail = SAFE_PLATFORM_ERROR_DETAILS.get(caught.message);
|
|
50
|
+
const lines = [safeDetail ? `${caught.message}: ${safeDetail}` : caught.message];
|
|
51
|
+
if (platformOrigin) lines.push(`Platform: ${platformOrigin}`);
|
|
52
|
+
if (typeof caught.retryable === "boolean") lines.push(`Retryable: ${caught.retryable ? "yes" : "no"}`);
|
|
53
|
+
if (correlationId) lines.push(`Correlation ID: ${correlationId}`);
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (caught instanceof Error && caught.message) return caught.message;
|
|
58
|
+
return String(caught);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeCommandExitCode(exitCode) {
|
|
62
|
+
if (exitCode === undefined || exitCode === 0) return 0;
|
|
63
|
+
if (exitCode === 1) return 1;
|
|
64
|
+
throw new Error("CLI command returned an invalid exit code");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runCliCommand({ args, command, commands, errorImpl, helpImpl }) {
|
|
68
|
+
const handler = Object.hasOwn(commands, command) ? commands[command] : undefined;
|
|
69
|
+
if (typeof handler !== "function") {
|
|
70
|
+
errorImpl(`Unknown command: ${command}`);
|
|
71
|
+
helpImpl();
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const exitCode = await Reflect.apply(handler, commands, args);
|
|
77
|
+
return normalizeCommandExitCode(exitCode);
|
|
78
|
+
} catch (caught) {
|
|
79
|
+
errorImpl(commandErrorMessage(caught));
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/lib/deploy-runtime.mjs
CHANGED
|
@@ -468,10 +468,7 @@ export function createDeployCommand({
|
|
|
468
468
|
const declaredStaticClients = Array.isArray(workspaceConfig?.clients)
|
|
469
469
|
? workspaceConfig.clients.filter((client) => client?.static?.dir)
|
|
470
470
|
: [];
|
|
471
|
-
const clientSelection = selectClient(
|
|
472
|
-
declaredStaticClients,
|
|
473
|
-
cliSelection?.client ?? null,
|
|
474
|
-
);
|
|
471
|
+
const clientSelection = selectClient(declaredStaticClients, cliSelection?.client ?? null);
|
|
475
472
|
if (clientSelection.error) {
|
|
476
473
|
errorImpl(clientSelection.error);
|
|
477
474
|
exitImpl(1);
|
|
@@ -541,7 +538,9 @@ export function createDeployCommand({
|
|
|
541
538
|
resolvePathImpl,
|
|
542
539
|
}) || displayName;
|
|
543
540
|
const backendDeployRuntime =
|
|
544
|
-
backendProjectRoot === cwd
|
|
541
|
+
backendProjectRoot === cwd
|
|
542
|
+
? deployPackageRuntime
|
|
543
|
+
: createDeployPackageRuntimeForCwd(() => backendProjectRoot);
|
|
545
544
|
|
|
546
545
|
if (parsed.existingBundleMode && !appId) {
|
|
547
546
|
errorImpl("--existing-bundle requires an existing app target.");
|
|
@@ -607,7 +606,7 @@ export function createDeployCommand({
|
|
|
607
606
|
}
|
|
608
607
|
|
|
609
608
|
if (parsed.staticDeploy) {
|
|
610
|
-
|
|
609
|
+
await runStaticDeployFlow({
|
|
611
610
|
appId,
|
|
612
611
|
backendRoot: backendProjectRoot,
|
|
613
612
|
creds,
|
|
@@ -659,6 +658,7 @@ export function createDeployCommand({
|
|
|
659
658
|
})
|
|
660
659
|
: undefined,
|
|
661
660
|
});
|
|
661
|
+
return 0;
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
logImpl(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
|
package/lib/platform-client.mjs
CHANGED
|
@@ -27,24 +27,40 @@ export function clearCreds() {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function resolveCredsFromSources({ env = process.env, loadCredsImpl = loadCreds } = {}) {
|
|
30
|
-
const
|
|
30
|
+
const credentialEnvName = env.GENCOW_TOKEN
|
|
31
|
+
? "GENCOW_TOKEN"
|
|
32
|
+
: env.GENCOW_DEPLOY_TOKEN
|
|
33
|
+
? "GENCOW_DEPLOY_TOKEN"
|
|
34
|
+
: null;
|
|
35
|
+
const envToken = credentialEnvName ? env[credentialEnvName] : null;
|
|
31
36
|
if (envToken) {
|
|
32
37
|
const platformUrl = env.GENCOW_PLATFORM_URL || "https://gencow.app";
|
|
33
|
-
return {
|
|
38
|
+
return {
|
|
39
|
+
apiKey: envToken,
|
|
40
|
+
platformUrl: validatePlatformUrl(platformUrl),
|
|
41
|
+
credentialSource: "environment",
|
|
42
|
+
credentialEnvName,
|
|
43
|
+
};
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
const creds = loadCredsImpl();
|
|
37
47
|
if (!creds?.apiKey) return null;
|
|
38
48
|
const platformUrl = env.GENCOW_PLATFORM_URL || creds.platformUrl;
|
|
39
49
|
if (platformUrl) {
|
|
40
|
-
return { ...creds, platformUrl: validatePlatformUrl(platformUrl) };
|
|
50
|
+
return { ...creds, platformUrl: validatePlatformUrl(platformUrl), credentialSource: "stored" };
|
|
41
51
|
}
|
|
42
|
-
return creds;
|
|
52
|
+
return { ...creds, credentialSource: "stored" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatPlatformCredentialsRequiredMessage({ isCi = Boolean(process.env.CI) } = {}) {
|
|
56
|
+
return isCi
|
|
57
|
+
? "CI environment detected but no credentials found. Set GENCOW_TOKEN in your CI/CD settings."
|
|
58
|
+
: "Not logged in. Run: gencow login";
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
export class PlatformCredentialsRequiredError extends Error {
|
|
46
|
-
constructor() {
|
|
47
|
-
super(
|
|
62
|
+
constructor(message = "Platform credentials are required") {
|
|
63
|
+
super(message);
|
|
48
64
|
this.name = "PlatformCredentialsRequiredError";
|
|
49
65
|
}
|
|
50
66
|
}
|
|
@@ -52,7 +68,9 @@ export class PlatformCredentialsRequiredError extends Error {
|
|
|
52
68
|
export function requireCreds({ throwOnMissing = false } = {}) {
|
|
53
69
|
const creds = resolveCredsFromSources();
|
|
54
70
|
if (!creds?.apiKey) {
|
|
55
|
-
if (throwOnMissing)
|
|
71
|
+
if (throwOnMissing) {
|
|
72
|
+
throw new PlatformCredentialsRequiredError(formatPlatformCredentialsRequiredMessage());
|
|
73
|
+
}
|
|
56
74
|
if (process.env.CI) {
|
|
57
75
|
error("CI environment detected but no credentials found.");
|
|
58
76
|
info(`Set ${CYAN}GENCOW_TOKEN${RESET} secret in your CI/CD settings.`);
|
|
@@ -86,10 +104,11 @@ export async function rpcQuery(creds, queryName, args = {}) {
|
|
|
86
104
|
});
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
export async function rpcMutation(creds, mutationName, args = {}) {
|
|
107
|
+
export async function rpcMutation(creds, mutationName, args = {}, requestOptions = {}) {
|
|
90
108
|
return platformFetch(creds, "/api/mutation", {
|
|
109
|
+
...requestOptions,
|
|
91
110
|
method: "POST",
|
|
92
|
-
headers: { "Content-Type": "application/json" },
|
|
111
|
+
headers: { ...requestOptions.headers, "Content-Type": "application/json" },
|
|
93
112
|
body: JSON.stringify({ name: mutationName, args }),
|
|
94
113
|
});
|
|
95
114
|
}
|
package/lib/release-client.mjs
CHANGED
|
@@ -8,22 +8,87 @@ function releaseRequestId(explicit) {
|
|
|
8
8
|
return explicit || `release_${randomUUID()}`;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const SAFE_CORRELATION_ID = /^[A-Za-z0-9_-]{8,128}$/u;
|
|
12
|
+
const PUBLIC_RELEASE_ERROR_CODES = new Set([
|
|
13
|
+
"ACCOUNT_DELETED",
|
|
14
|
+
"ACCOUNT_DELETION_PENDING",
|
|
15
|
+
"ACCOUNT_SUSPENDED",
|
|
16
|
+
"APP_RELEASE_APP_NOT_FOUND",
|
|
17
|
+
"APP_RELEASE_ATTEMPT_NOT_FOUND",
|
|
18
|
+
"APP_RELEASE_ATTEMPT_TERMINAL",
|
|
19
|
+
"APP_RELEASE_AUTH_REQUIRED",
|
|
20
|
+
"APP_RELEASE_CAPABILITY_UNAVAILABLE",
|
|
21
|
+
"APP_RELEASE_COMPONENT_CONFLICT",
|
|
22
|
+
"APP_RELEASE_COMPONENT_INCOMPLETE",
|
|
23
|
+
"APP_RELEASE_COMPONENT_UNEXPECTED",
|
|
24
|
+
"APP_RELEASE_ENVIRONMENT_INVALID",
|
|
25
|
+
"APP_RELEASE_ENVIRONMENT_MISMATCH",
|
|
26
|
+
"APP_RELEASE_ENV_KEYS_MISSING",
|
|
27
|
+
"APP_RELEASE_IDEMPOTENCY_CONFLICT",
|
|
28
|
+
"APP_RELEASE_METADATA_INVALID",
|
|
29
|
+
"APP_RELEASE_POLICY_READ_UNAVAILABLE",
|
|
30
|
+
"APP_RELEASE_SOURCE_CONFLICT",
|
|
31
|
+
"APP_RELEASE_SOURCE_INVALID",
|
|
32
|
+
"APP_RELEASE_STATE_CONFLICT",
|
|
33
|
+
"APP_RELEASE_STORAGE_QUOTA_EXCEEDED",
|
|
34
|
+
"APP_RELEASE_VERSIONS_DISABLED",
|
|
35
|
+
"APP_SERVING_ROUTE_COMMIT_BLOCKED",
|
|
36
|
+
"APP_SERVING_ROUTE_COMMIT_PENDING",
|
|
37
|
+
"RELEASE_SOURCE_COMPONENT_MISMATCH",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function safePlatformOrigin(value) {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(value);
|
|
43
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.origin : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function throwResponseError(response, fallback, body, creds = {}) {
|
|
50
|
+
const authRequired = response.status === 401;
|
|
51
|
+
const responseCode = PUBLIC_RELEASE_ERROR_CODES.has(body?.code) ? body.code : fallback;
|
|
52
|
+
const error = new Error(authRequired ? "APP_RELEASE_AUTH_REQUIRED" : responseCode);
|
|
15
53
|
error.status = response.status;
|
|
54
|
+
const platformOrigin = safePlatformOrigin(creds.platformUrl);
|
|
55
|
+
if (platformOrigin) error.platformUrl = platformOrigin;
|
|
56
|
+
if (creds.credentialSource === "environment" || creds.credentialSource === "stored") {
|
|
57
|
+
error.credentialSource = creds.credentialSource;
|
|
58
|
+
}
|
|
59
|
+
if (creds.credentialEnvName === "GENCOW_TOKEN" || creds.credentialEnvName === "GENCOW_DEPLOY_TOKEN") {
|
|
60
|
+
error.credentialEnvName = creds.credentialEnvName;
|
|
61
|
+
}
|
|
62
|
+
if (typeof body.correlationId === "string" && SAFE_CORRELATION_ID.test(body.correlationId)) {
|
|
63
|
+
error.correlationId = body.correlationId;
|
|
64
|
+
}
|
|
65
|
+
error.retryable = authRequired
|
|
66
|
+
? false
|
|
67
|
+
: typeof body.retryable === "boolean"
|
|
68
|
+
? body.retryable
|
|
69
|
+
: response.status >= 500;
|
|
16
70
|
throw error;
|
|
17
71
|
}
|
|
18
72
|
|
|
73
|
+
async function responseError(response, fallback, creds, parsedBody) {
|
|
74
|
+
const body = parsedBody ?? ((await response.json().catch(() => ({}))) || {});
|
|
75
|
+
throwResponseError(response, fallback, body, creds);
|
|
76
|
+
}
|
|
77
|
+
|
|
19
78
|
async function releaseCapability({ creds, appId, environment, platformFetchImpl }) {
|
|
20
79
|
const response = await platformFetchImpl(
|
|
21
80
|
creds,
|
|
22
81
|
`/platform/apps/${appId}/release-capability?environment=${environment}`,
|
|
23
82
|
{ method: "GET" },
|
|
24
83
|
);
|
|
25
|
-
if (response.status === 404 || response.status === 405)
|
|
26
|
-
|
|
84
|
+
if (response.status === 404 || response.status === 405) {
|
|
85
|
+
const body = (await response.json().catch(() => ({}))) || {};
|
|
86
|
+
if (typeof body?.code !== "string" || body.code.length === 0) return null;
|
|
87
|
+
await responseError(response, "APP_RELEASE_CAPABILITY_FAILED", creds, body);
|
|
88
|
+
}
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
await responseError(response, "APP_RELEASE_CAPABILITY_FAILED", creds);
|
|
91
|
+
}
|
|
27
92
|
const body = await response.json().catch(() => ({}));
|
|
28
93
|
if (body?.protocol !== "app-release-v1" || typeof body?.enabled !== "boolean") {
|
|
29
94
|
throw new Error("APP_RELEASE_CAPABILITY_RESPONSE_INVALID");
|
|
@@ -66,7 +131,9 @@ export async function prepareReleaseAttempt({
|
|
|
66
131
|
},
|
|
67
132
|
body: sourcePackage.bundleBuffer,
|
|
68
133
|
});
|
|
69
|
-
if (!response.ok)
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
await responseError(response, "APP_RELEASE_SOURCE_UPLOAD_FAILED", creds);
|
|
136
|
+
}
|
|
70
137
|
const data = await response.json();
|
|
71
138
|
if (!data.attemptId || data.state !== "source_stored") {
|
|
72
139
|
throw new Error("APP_RELEASE_SOURCE_RESPONSE_INVALID");
|
|
@@ -97,14 +164,19 @@ export async function finalizeReleaseAttempt({
|
|
|
97
164
|
body: JSON.stringify({ environment }),
|
|
98
165
|
},
|
|
99
166
|
);
|
|
100
|
-
if (!response.ok)
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
await responseError(response, "APP_RELEASE_FINALIZE_FAILED", creds);
|
|
169
|
+
}
|
|
101
170
|
const data = await response.json();
|
|
102
171
|
if (!data.release?.id || !Number.isSafeInteger(data.release.version)) {
|
|
103
172
|
throw new Error("APP_RELEASE_FINALIZE_RESPONSE_INVALID");
|
|
104
173
|
}
|
|
105
174
|
return data.release;
|
|
106
175
|
} catch (error) {
|
|
107
|
-
const retryable =
|
|
176
|
+
const retryable =
|
|
177
|
+
typeof error?.retryable === "boolean"
|
|
178
|
+
? error.retryable
|
|
179
|
+
: typeof error?.status !== "number" || error.status >= 500;
|
|
108
180
|
if (!retryable || attempt >= retryDelaysMs.length) throw error;
|
|
109
181
|
await delayImpl(retryDelaysMs[attempt]);
|
|
110
182
|
}
|
|
@@ -129,6 +201,8 @@ export async function failReleaseAttempt({
|
|
|
129
201
|
body: JSON.stringify({ environment, failureStage, failureCode }),
|
|
130
202
|
},
|
|
131
203
|
);
|
|
132
|
-
if (!response.ok)
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
await responseError(response, "APP_RELEASE_FAIL_RECORD_FAILED", creds);
|
|
206
|
+
}
|
|
133
207
|
return await response.json();
|
|
134
208
|
}
|