@kryd/cli 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/index.js +409 -19
- package/package.json +3 -11
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -89,6 +89,8 @@ var MAX_TENANT_SLUG = 20;
|
|
|
89
89
|
var PRODUCTION_TLD = "kryd.cloud";
|
|
90
90
|
var PREVIEW_TLD = "kryd.dev";
|
|
91
91
|
var TENANT_SLUG_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_TENANT_SLUG - 2}}[a-z0-9])?$`);
|
|
92
|
+
var MAX_APP_NAME = 20;
|
|
93
|
+
var APP_NAME_RE = new RegExp(`^[a-z](?:[a-z0-9-]{0,${MAX_APP_NAME - 2}}[a-z0-9])?$`);
|
|
92
94
|
|
|
93
95
|
// ../../packages/shared-types/dist/reserved-slugs.js
|
|
94
96
|
var FORGEJO_RESERVED_USERNAMES = [
|
|
@@ -14861,6 +14863,12 @@ var BUILD_ENV_REBUILD_NOTE = "This takes effect at your next BUILD, not your nex
|
|
|
14861
14863
|
var MAX_WEBHOOK_NAME = 32;
|
|
14862
14864
|
var WEBHOOK_NAME_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_WEBHOOK_NAME - 2}}[a-z0-9])?$`);
|
|
14863
14865
|
|
|
14866
|
+
// ../../packages/shared-types/dist/domain.js
|
|
14867
|
+
var DOMAIN_STATUSES = ["pending", "verified", "active", "failed"];
|
|
14868
|
+
function normalizeDomainHost(input) {
|
|
14869
|
+
return input.trim().toLowerCase().replace(/\.$/, "");
|
|
14870
|
+
}
|
|
14871
|
+
|
|
14864
14872
|
// ../../packages/shared-types/dist/index.js
|
|
14865
14873
|
var DEPLOY_STATES = [
|
|
14866
14874
|
"queued",
|
|
@@ -15408,6 +15416,110 @@ async function removeWorkflows(apiUrl, token, projectId) {
|
|
|
15408
15416
|
);
|
|
15409
15417
|
}
|
|
15410
15418
|
}
|
|
15419
|
+
var RECORD_TYPES = ["TXT", "CNAME", "A"];
|
|
15420
|
+
function parseDomainRecord(body) {
|
|
15421
|
+
const r = body;
|
|
15422
|
+
if (!r || typeof r.type !== "string" || !RECORD_TYPES.includes(r.type) || typeof r.name !== "string" || typeof r.value !== "string" || r.purpose !== "ownership" && r.purpose !== "routing" || r.note !== void 0 && typeof r.note !== "string") {
|
|
15423
|
+
return null;
|
|
15424
|
+
}
|
|
15425
|
+
return {
|
|
15426
|
+
type: r.type,
|
|
15427
|
+
name: r.name,
|
|
15428
|
+
value: r.value,
|
|
15429
|
+
purpose: r.purpose,
|
|
15430
|
+
...r.note !== void 0 ? { note: r.note } : {}
|
|
15431
|
+
};
|
|
15432
|
+
}
|
|
15433
|
+
function parseProjectDomain(body, label) {
|
|
15434
|
+
const d = body;
|
|
15435
|
+
const bad = () => new ApiError(`The API returned an unexpected ${label} response.`);
|
|
15436
|
+
if (!d || typeof d.id !== "string" || typeof d.host !== "string" || typeof d.status !== "string" || !DOMAIN_STATUSES.includes(d.status) || typeof d.detail !== "string" && d.detail !== null || typeof d.checkedAt !== "string" && d.checkedAt !== null || typeof d.createdAt !== "string" || !Array.isArray(d.records)) {
|
|
15437
|
+
throw bad();
|
|
15438
|
+
}
|
|
15439
|
+
const records = d.records.map(parseDomainRecord);
|
|
15440
|
+
if (records.some((r) => r === null)) throw bad();
|
|
15441
|
+
return {
|
|
15442
|
+
id: d.id,
|
|
15443
|
+
host: d.host,
|
|
15444
|
+
// KRYD-381a — absent from an older API: then it is `web`, the only app there was.
|
|
15445
|
+
...typeof d.app === "string" ? { app: d.app } : {},
|
|
15446
|
+
status: d.status,
|
|
15447
|
+
detail: d.detail,
|
|
15448
|
+
checkedAt: d.checkedAt,
|
|
15449
|
+
createdAt: d.createdAt,
|
|
15450
|
+
records
|
|
15451
|
+
};
|
|
15452
|
+
}
|
|
15453
|
+
function domainsUrl(apiUrl, projectId) {
|
|
15454
|
+
return `${apiUrl}/projects/${encodeURIComponent(projectId)}/domains`;
|
|
15455
|
+
}
|
|
15456
|
+
async function listDomains(apiUrl, token, projectId) {
|
|
15457
|
+
const res = await fetch(domainsUrl(apiUrl, projectId), {
|
|
15458
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15459
|
+
});
|
|
15460
|
+
if (!res.ok) {
|
|
15461
|
+
throw new ApiError(`Domain listing failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15462
|
+
}
|
|
15463
|
+
const body = await res.json().catch(() => void 0);
|
|
15464
|
+
if (!body || typeof body.projectHost !== "string" && body.projectHost !== null || !Array.isArray(body.domains)) {
|
|
15465
|
+
throw new ApiError("The API returned an unexpected domain-list response.");
|
|
15466
|
+
}
|
|
15467
|
+
return {
|
|
15468
|
+
projectHost: body.projectHost,
|
|
15469
|
+
domains: body.domains.map((d) => parseProjectDomain(d, "domain-list"))
|
|
15470
|
+
};
|
|
15471
|
+
}
|
|
15472
|
+
async function addDomain(apiUrl, token, projectId, host, app) {
|
|
15473
|
+
const res = await fetch(domainsUrl(apiUrl, projectId), {
|
|
15474
|
+
method: "POST",
|
|
15475
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
15476
|
+
body: JSON.stringify({ host, ...app ? { app } : {} })
|
|
15477
|
+
});
|
|
15478
|
+
if (!res.ok) {
|
|
15479
|
+
throw new ApiError(`Domain add failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15480
|
+
}
|
|
15481
|
+
return parseProjectDomain(await res.json().catch(() => void 0), "domain-add");
|
|
15482
|
+
}
|
|
15483
|
+
async function removeDomain(apiUrl, token, projectId, host) {
|
|
15484
|
+
const res = await fetch(`${domainsUrl(apiUrl, projectId)}/${encodeURIComponent(host)}`, {
|
|
15485
|
+
method: "DELETE",
|
|
15486
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15487
|
+
});
|
|
15488
|
+
if (!res.ok) {
|
|
15489
|
+
throw new ApiError(`Domain removal failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15490
|
+
}
|
|
15491
|
+
}
|
|
15492
|
+
function deployHookUrl(apiUrl, projectId) {
|
|
15493
|
+
return `${apiUrl}/projects/${encodeURIComponent(projectId)}/deploy-hook`;
|
|
15494
|
+
}
|
|
15495
|
+
async function createDeployHook(apiUrl, token, projectId) {
|
|
15496
|
+
const res = await fetch(deployHookUrl(apiUrl, projectId), {
|
|
15497
|
+
method: "PUT",
|
|
15498
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15499
|
+
});
|
|
15500
|
+
if (!res.ok) {
|
|
15501
|
+
throw new ApiError(`Deploy hook creation failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15502
|
+
}
|
|
15503
|
+
const body = await res.json().catch(() => void 0);
|
|
15504
|
+
if (typeof body?.url !== "string" || typeof body.replaced !== "boolean") {
|
|
15505
|
+
throw new Error("Unexpected response from the API (deploy-hook create).");
|
|
15506
|
+
}
|
|
15507
|
+
return { url: body.url, replaced: body.replaced };
|
|
15508
|
+
}
|
|
15509
|
+
async function removeDeployHook(apiUrl, token, projectId) {
|
|
15510
|
+
const res = await fetch(deployHookUrl(apiUrl, projectId), {
|
|
15511
|
+
method: "DELETE",
|
|
15512
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15513
|
+
});
|
|
15514
|
+
if (!res.ok) {
|
|
15515
|
+
throw new ApiError(`Deploy hook removal failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15516
|
+
}
|
|
15517
|
+
const body = await res.json().catch(() => void 0);
|
|
15518
|
+
if (typeof body?.removed !== "boolean") {
|
|
15519
|
+
throw new Error("Unexpected response from the API (deploy-hook rm).");
|
|
15520
|
+
}
|
|
15521
|
+
return { removed: body.removed };
|
|
15522
|
+
}
|
|
15411
15523
|
async function fetchResourceStatus(apiUrl, token, path) {
|
|
15412
15524
|
const res = await fetch(`${apiUrl}${path}`, {
|
|
15413
15525
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -15732,9 +15844,13 @@ async function triggerDeploy(apiUrl, token, projectId) {
|
|
|
15732
15844
|
if (!res.ok) {
|
|
15733
15845
|
throw new ApiError(`Deploy trigger failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15734
15846
|
}
|
|
15735
|
-
|
|
15847
|
+
const body = await res.json();
|
|
15848
|
+
return {
|
|
15849
|
+
deploymentId: body.deploymentId,
|
|
15850
|
+
...Array.isArray(body.apps) ? { apps: body.apps } : {}
|
|
15851
|
+
};
|
|
15736
15852
|
}
|
|
15737
|
-
async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
|
|
15853
|
+
async function rollbackDeploy(apiUrl, token, projectId, deploymentId, app) {
|
|
15738
15854
|
const res = await fetch(`${apiUrl}/deployments/rollback`, {
|
|
15739
15855
|
method: "POST",
|
|
15740
15856
|
headers: {
|
|
@@ -15743,7 +15859,8 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
|
|
|
15743
15859
|
},
|
|
15744
15860
|
body: JSON.stringify({
|
|
15745
15861
|
projectId,
|
|
15746
|
-
...deploymentId ? { deploymentId } : {}
|
|
15862
|
+
...deploymentId ? { deploymentId } : {},
|
|
15863
|
+
...app ? { app } : {}
|
|
15747
15864
|
})
|
|
15748
15865
|
});
|
|
15749
15866
|
if (!res.ok) {
|
|
@@ -15751,14 +15868,14 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
|
|
|
15751
15868
|
}
|
|
15752
15869
|
return await res.json();
|
|
15753
15870
|
}
|
|
15754
|
-
async function redeployProject(apiUrl, token, projectId) {
|
|
15871
|
+
async function redeployProject(apiUrl, token, projectId, app) {
|
|
15755
15872
|
const res = await fetch(`${apiUrl}/deployments/redeploy`, {
|
|
15756
15873
|
method: "POST",
|
|
15757
15874
|
headers: {
|
|
15758
15875
|
"content-type": "application/json",
|
|
15759
15876
|
authorization: `Bearer ${token}`
|
|
15760
15877
|
},
|
|
15761
|
-
body: JSON.stringify({ projectId })
|
|
15878
|
+
body: JSON.stringify({ projectId, ...app ? { app } : {} })
|
|
15762
15879
|
});
|
|
15763
15880
|
if (!res.ok) {
|
|
15764
15881
|
throw new ApiError(`Redeploy failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
@@ -15800,6 +15917,32 @@ async function pollForNewDeployment(fetchDeployments, match, opts) {
|
|
|
15800
15917
|
await sleep(intervalMs);
|
|
15801
15918
|
}
|
|
15802
15919
|
}
|
|
15920
|
+
async function pollForNewDeployments(fetchDeployments, match, opts) {
|
|
15921
|
+
const expected = opts?.expectedCount ?? null;
|
|
15922
|
+
if (expected === null || expected <= 1) {
|
|
15923
|
+
const first = await pollForNewDeployment(fetchDeployments, match, opts);
|
|
15924
|
+
return first ? [first] : [];
|
|
15925
|
+
}
|
|
15926
|
+
const intervalMs = opts?.intervalMs ?? 2e3;
|
|
15927
|
+
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
15928
|
+
const maxConsecutiveErrors = opts?.maxConsecutiveErrors ?? 5;
|
|
15929
|
+
const sleep = opts?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
15930
|
+
const started = Date.now();
|
|
15931
|
+
let consecutiveErrors = 0;
|
|
15932
|
+
let fresh = [];
|
|
15933
|
+
for (; ; ) {
|
|
15934
|
+
try {
|
|
15935
|
+
const items = await fetchDeployments();
|
|
15936
|
+
consecutiveErrors = 0;
|
|
15937
|
+
fresh = findDeploymentsForCommit(items, match.commitSha, match.branch).filter((d) => !match.knownIds.has(d.id)).reverse();
|
|
15938
|
+
if (fresh.length >= expected) return fresh;
|
|
15939
|
+
} catch (err) {
|
|
15940
|
+
if (++consecutiveErrors > maxConsecutiveErrors) throw err;
|
|
15941
|
+
}
|
|
15942
|
+
if (Date.now() - started >= timeoutMs) return fresh;
|
|
15943
|
+
await sleep(intervalMs);
|
|
15944
|
+
}
|
|
15945
|
+
}
|
|
15803
15946
|
function dataFromFrame(frame) {
|
|
15804
15947
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
15805
15948
|
return data.length > 0 ? data : void 0;
|
|
@@ -15884,6 +16027,7 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
|
|
|
15884
16027
|
while (!opts.signal?.aborted) {
|
|
15885
16028
|
try {
|
|
15886
16029
|
const url2 = new URL(`${apiUrl}/${path}`);
|
|
16030
|
+
if (opts.app && (opts.resource ?? "project") === "project") url2.searchParams.set("app", opts.app);
|
|
15887
16031
|
if (afterNs) url2.searchParams.set("after", afterNs);
|
|
15888
16032
|
else if (opts.since) url2.searchParams.set("since", opts.since);
|
|
15889
16033
|
const res = await doFetch(url2, {
|
|
@@ -17086,6 +17230,18 @@ function currentBranch(cwd) {
|
|
|
17086
17230
|
function headSha(cwd, ref) {
|
|
17087
17231
|
return probe(cwd, ["rev-parse", ref]);
|
|
17088
17232
|
}
|
|
17233
|
+
function declaredAppCount(cwd, sha) {
|
|
17234
|
+
const res = probe(cwd, ["show", `${sha}:kryd.json`]);
|
|
17235
|
+
if (res.status !== "ok") return null;
|
|
17236
|
+
try {
|
|
17237
|
+
const apps = JSON.parse(res.value).apps;
|
|
17238
|
+
if (apps === null || typeof apps !== "object" || Array.isArray(apps)) return null;
|
|
17239
|
+
const n = Object.keys(apps).length;
|
|
17240
|
+
return n > 0 ? n : null;
|
|
17241
|
+
} catch {
|
|
17242
|
+
return null;
|
|
17243
|
+
}
|
|
17244
|
+
}
|
|
17089
17245
|
function hasRemote(cwd, remote) {
|
|
17090
17246
|
const res = probe(cwd, ["remote"]);
|
|
17091
17247
|
if (res.status !== "ok") return res;
|
|
@@ -17471,11 +17627,33 @@ async function runLogs(opts) {
|
|
|
17471
17627
|
process.exitCode = 1;
|
|
17472
17628
|
return;
|
|
17473
17629
|
}
|
|
17630
|
+
if (opts.app && opts.deployment) {
|
|
17631
|
+
process.stderr.write(
|
|
17632
|
+
"--app picks the latest deploy of an app; a deployment id already names one deploy \u2014 pass one or the other.\n"
|
|
17633
|
+
);
|
|
17634
|
+
process.exitCode = 1;
|
|
17635
|
+
return;
|
|
17636
|
+
}
|
|
17637
|
+
if (opts.app && !resolveProjectId(void 0, opts.cwd)) {
|
|
17638
|
+
process.stderr.write(
|
|
17639
|
+
"--app needs a linked project: run it in a folder linked with `kryd init`, or pass a deployment id instead.\n"
|
|
17640
|
+
);
|
|
17641
|
+
process.exitCode = 1;
|
|
17642
|
+
return;
|
|
17643
|
+
}
|
|
17474
17644
|
try {
|
|
17475
17645
|
let deploymentId = opts.deployment;
|
|
17476
17646
|
if (!deploymentId) {
|
|
17477
17647
|
const linkedProjectId = resolveProjectId(void 0, opts.cwd);
|
|
17478
|
-
const deploys = await listDeployments(apiUrl, token, linkedProjectId ?? void 0)
|
|
17648
|
+
const deploys = (await listDeployments(apiUrl, token, linkedProjectId ?? void 0)).filter(
|
|
17649
|
+
(d) => !opts.app || (d.app ?? "web") === opts.app
|
|
17650
|
+
);
|
|
17651
|
+
if (deploys.length === 0 && opts.app) {
|
|
17652
|
+
process.stderr.write(`No deploys of app "${opts.app}" found.
|
|
17653
|
+
`);
|
|
17654
|
+
process.exitCode = 1;
|
|
17655
|
+
return;
|
|
17656
|
+
}
|
|
17479
17657
|
if (deploys.length === 0) {
|
|
17480
17658
|
process.stderr.write(
|
|
17481
17659
|
linkedProjectId ? "The linked project has no deploys yet \u2014 run `kryd deploy` first.\n" : "No deploys found for your account.\n"
|
|
@@ -17539,6 +17717,7 @@ async function runRuntimeLogs(opts) {
|
|
|
17539
17717
|
},
|
|
17540
17718
|
{
|
|
17541
17719
|
...opts.since ? { since: opts.since } : {},
|
|
17720
|
+
...opts.app ? { app: opts.app } : {},
|
|
17542
17721
|
signal: controller.signal,
|
|
17543
17722
|
...isDeployment ? { resource: "deployment" } : opts.worker ? { resource: "worker" } : {}
|
|
17544
17723
|
}
|
|
@@ -17563,7 +17742,17 @@ async function runDeploy(opts) {
|
|
|
17563
17742
|
return;
|
|
17564
17743
|
}
|
|
17565
17744
|
try {
|
|
17566
|
-
const deploymentId = await triggerDeploy(apiUrl, token, project2);
|
|
17745
|
+
const { deploymentId, apps } = await triggerDeploy(apiUrl, token, project2);
|
|
17746
|
+
if (apps && apps.length > 1) {
|
|
17747
|
+
await followApps(
|
|
17748
|
+
apiUrl,
|
|
17749
|
+
token,
|
|
17750
|
+
apps.map((a) => ({ id: a.deploymentId, app: a.app })),
|
|
17751
|
+
renderFor(opts.logs),
|
|
17752
|
+
[]
|
|
17753
|
+
);
|
|
17754
|
+
return;
|
|
17755
|
+
}
|
|
17567
17756
|
process.stdout.write(`Triggered deploy ${deploymentId}
|
|
17568
17757
|
`);
|
|
17569
17758
|
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
@@ -17678,14 +17867,17 @@ async function runPush(opts) {
|
|
|
17678
17867
|
return;
|
|
17679
17868
|
}
|
|
17680
17869
|
process.stdout.write("Waiting for the deploy to start\u2026\n");
|
|
17681
|
-
const
|
|
17870
|
+
const started = await pollForNewDeployments(
|
|
17682
17871
|
fetchDeployments,
|
|
17683
17872
|
{ commitSha, branch, knownIds: new Set(known.map((d) => d.id)) },
|
|
17684
17873
|
{
|
|
17685
17874
|
...opts.sleep ? { sleep: opts.sleep } : {},
|
|
17686
|
-
...opts.pollTimeoutMs !== void 0 ? { timeoutMs: opts.pollTimeoutMs } : {}
|
|
17875
|
+
...opts.pollTimeoutMs !== void 0 ? { timeoutMs: opts.pollTimeoutMs } : {},
|
|
17876
|
+
// The commit's own count of apps, so the follow waits for every row the fan-out creates.
|
|
17877
|
+
expectedCount: declaredAppCount(cwd, commitSha)
|
|
17687
17878
|
}
|
|
17688
17879
|
);
|
|
17880
|
+
const deployment = started[0];
|
|
17689
17881
|
if (!deployment) {
|
|
17690
17882
|
const previous = known[0];
|
|
17691
17883
|
if (previous) {
|
|
@@ -17701,14 +17893,29 @@ async function runPush(opts) {
|
|
|
17701
17893
|
}
|
|
17702
17894
|
return;
|
|
17703
17895
|
}
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17896
|
+
if (started.length === 1) {
|
|
17897
|
+
await followDeploy(apiUrl, token, deployment.id, {
|
|
17898
|
+
render: renderFor(opts.logs),
|
|
17899
|
+
estimateFrom: recent
|
|
17900
|
+
});
|
|
17901
|
+
return;
|
|
17902
|
+
}
|
|
17903
|
+
await followApps(apiUrl, token, started, renderFor(opts.logs), recent);
|
|
17708
17904
|
} catch (err) {
|
|
17709
17905
|
await reportError(err);
|
|
17710
17906
|
}
|
|
17711
17907
|
}
|
|
17908
|
+
async function followApps(apiUrl, token, deployments, render, estimateFrom) {
|
|
17909
|
+
process.stdout.write(
|
|
17910
|
+
`This push deploys ${deployments.length} apps: ` + deployments.map((d) => `${d.app ?? "web"} (${d.id})`).join(", ") + ".\n"
|
|
17911
|
+
);
|
|
17912
|
+
for (const d of deployments) {
|
|
17913
|
+
process.stdout.write(`
|
|
17914
|
+
\u2500\u2500 ${d.app ?? "web"} \u2500\u2500
|
|
17915
|
+
`);
|
|
17916
|
+
await followDeploy(apiUrl, token, d.id, { render, estimateFrom });
|
|
17917
|
+
}
|
|
17918
|
+
}
|
|
17712
17919
|
async function runRollback(opts) {
|
|
17713
17920
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17714
17921
|
const token = loadConfig().token;
|
|
@@ -17727,7 +17934,8 @@ async function runRollback(opts) {
|
|
|
17727
17934
|
apiUrl,
|
|
17728
17935
|
token,
|
|
17729
17936
|
project2,
|
|
17730
|
-
opts.deployment
|
|
17937
|
+
opts.deployment,
|
|
17938
|
+
opts.app
|
|
17731
17939
|
);
|
|
17732
17940
|
process.stdout.write(
|
|
17733
17941
|
`Rolling back to ${rolledBackTo} \u2192 ${deploymentId}
|
|
@@ -17756,7 +17964,8 @@ async function runRedeploy(opts) {
|
|
|
17756
17964
|
const { deploymentId, redeployedCommit, note } = await redeployProject(
|
|
17757
17965
|
apiUrl,
|
|
17758
17966
|
token,
|
|
17759
|
-
project2
|
|
17967
|
+
project2,
|
|
17968
|
+
opts.app
|
|
17760
17969
|
);
|
|
17761
17970
|
process.stdout.write(
|
|
17762
17971
|
`Redeploying ${redeployedCommit.slice(0, 7)} \u2192 ${deploymentId}
|
|
@@ -18383,6 +18592,171 @@ async function runWorkflowStatus(opts) {
|
|
|
18383
18592
|
await reportError(err);
|
|
18384
18593
|
}
|
|
18385
18594
|
}
|
|
18595
|
+
var DOMAIN_STATUS_TEXT = {
|
|
18596
|
+
pending: "pending \u2014 waiting for the DNS records",
|
|
18597
|
+
verified: "verified \u2014 DNS is right, the certificate is being issued",
|
|
18598
|
+
active: "active \u2014 served over HTTPS",
|
|
18599
|
+
failed: "failed \u2014 certificate issuance is failing; Kryd retries automatically"
|
|
18600
|
+
};
|
|
18601
|
+
function formatDomainRecords(records) {
|
|
18602
|
+
const line = (r) => ` ${r.type.padEnd(5)} ${r.name} \u2192 ${r.value}
|
|
18603
|
+
`;
|
|
18604
|
+
const ownership = records.filter((r) => r.purpose === "ownership");
|
|
18605
|
+
const routing = records.filter((r) => r.purpose === "routing");
|
|
18606
|
+
let out = " 1. Prove you own it:\n" + ownership.map(line).join("");
|
|
18607
|
+
out += " 2. Point it at Kryd \u2014 exactly ONE of these:\n";
|
|
18608
|
+
for (const r of routing) {
|
|
18609
|
+
out += line(r) + (r.note ? ` (${r.note})
|
|
18610
|
+
` : "");
|
|
18611
|
+
}
|
|
18612
|
+
return out;
|
|
18613
|
+
}
|
|
18614
|
+
function domainContext(opts, usage) {
|
|
18615
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
18616
|
+
const token = loadConfig().token;
|
|
18617
|
+
if (!token) {
|
|
18618
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
18619
|
+
process.exitCode = 1;
|
|
18620
|
+
return null;
|
|
18621
|
+
}
|
|
18622
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
18623
|
+
if (!project2) {
|
|
18624
|
+
reportNotLinked(usage);
|
|
18625
|
+
return null;
|
|
18626
|
+
}
|
|
18627
|
+
return { apiUrl, token, project: project2 };
|
|
18628
|
+
}
|
|
18629
|
+
async function runDomainAdd(opts) {
|
|
18630
|
+
const ctx = domainContext(opts, "kryd domain add <host> --project <projectId>");
|
|
18631
|
+
if (!ctx) return;
|
|
18632
|
+
try {
|
|
18633
|
+
const [domain3, list] = await Promise.all([
|
|
18634
|
+
addDomain(ctx.apiUrl, ctx.token, ctx.project, opts.host, opts.app),
|
|
18635
|
+
listDomains(ctx.apiUrl, ctx.token, ctx.project).catch(() => null)
|
|
18636
|
+
]);
|
|
18637
|
+
if (domain3.status !== "pending") {
|
|
18638
|
+
process.stdout.write(
|
|
18639
|
+
`${domain3.host} is already on ${ctx.project}${appSuffix(domain3.app)}: ${DOMAIN_STATUS_TEXT[domain3.status]}.
|
|
18640
|
+
`
|
|
18641
|
+
);
|
|
18642
|
+
return;
|
|
18643
|
+
}
|
|
18644
|
+
process.stdout.write(
|
|
18645
|
+
`${domain3.host} added to ${ctx.project}${appSuffix(domain3.app)} \u2014 pending until its DNS records exist.
|
|
18646
|
+
|
|
18647
|
+
Create these records at your DNS provider:
|
|
18648
|
+
` + formatDomainRecords(domain3.records) + `
|
|
18649
|
+
Kryd checks every minute; once they are right it issues a certificate and serves ${domain3.host} from ${domain3.app && domain3.app !== "web" ? `app ${domain3.app}'s` : "your"} production deploy. Follow it with \`kryd domain status ${domain3.host}\`.
|
|
18650
|
+
` + (list?.projectHost ? `${ctx.project} keeps its own host, ${list.projectHost}.
|
|
18651
|
+
` : "")
|
|
18652
|
+
);
|
|
18653
|
+
} catch (err) {
|
|
18654
|
+
await reportError(err);
|
|
18655
|
+
}
|
|
18656
|
+
}
|
|
18657
|
+
async function runDeployHookCreate(opts) {
|
|
18658
|
+
const ctx = domainContext(opts, "kryd project deploy-hook create --project <projectId>");
|
|
18659
|
+
if (!ctx) return;
|
|
18660
|
+
try {
|
|
18661
|
+
const { url: url2, replaced } = await createDeployHook(ctx.apiUrl, ctx.token, ctx.project);
|
|
18662
|
+
process.stdout.write(
|
|
18663
|
+
`Deploy hook for ${ctx.project}${replaced ? " \u2014 replaces the previous one, which no longer works" : ""}:
|
|
18664
|
+
|
|
18665
|
+
${url2}
|
|
18666
|
+
|
|
18667
|
+
A POST to this URL rebuilds production from the latest commit, without cache. It is shown only now: anyone with it can start a rebuild, so keep it secret. Lost or leaked? Run this command again to replace it, or \`kryd project deploy-hook rm\` to remove it.
|
|
18668
|
+
`
|
|
18669
|
+
);
|
|
18670
|
+
} catch (err) {
|
|
18671
|
+
await reportError(err);
|
|
18672
|
+
}
|
|
18673
|
+
}
|
|
18674
|
+
async function runDeployHookRemove(opts) {
|
|
18675
|
+
const ctx = domainContext(opts, "kryd project deploy-hook rm --project <projectId>");
|
|
18676
|
+
if (!ctx) return;
|
|
18677
|
+
try {
|
|
18678
|
+
const { removed } = await removeDeployHook(ctx.apiUrl, ctx.token, ctx.project);
|
|
18679
|
+
process.stdout.write(
|
|
18680
|
+
removed ? `Deploy hook of ${ctx.project} removed \u2014 its URL no longer works.
|
|
18681
|
+
` : `${ctx.project} has no deploy hook \u2014 nothing to remove.
|
|
18682
|
+
`
|
|
18683
|
+
);
|
|
18684
|
+
} catch (err) {
|
|
18685
|
+
await reportError(err);
|
|
18686
|
+
}
|
|
18687
|
+
}
|
|
18688
|
+
function appSuffix(app) {
|
|
18689
|
+
return app && app !== "web" ? ` (app ${app})` : "";
|
|
18690
|
+
}
|
|
18691
|
+
async function runDomainRemove(opts) {
|
|
18692
|
+
const ctx = domainContext(opts, "kryd domain remove <host> --project <projectId>");
|
|
18693
|
+
if (!ctx) return;
|
|
18694
|
+
const host = normalizeDomainHost(opts.host);
|
|
18695
|
+
const proceed = await confirmResourceRemoval({
|
|
18696
|
+
yes: opts.yes,
|
|
18697
|
+
io: opts.io,
|
|
18698
|
+
subject: `${host} from ${ctx.project}`,
|
|
18699
|
+
question: `Remove ${host} from ${ctx.project}? Its route and certificate go within a minute and it then answers 404. The project's own kryd.cloud host keeps working.`,
|
|
18700
|
+
declined: `Left ${host} on ${ctx.project}.
|
|
18701
|
+
`
|
|
18702
|
+
});
|
|
18703
|
+
if (!proceed) return;
|
|
18704
|
+
try {
|
|
18705
|
+
await removeDomain(ctx.apiUrl, ctx.token, ctx.project, host);
|
|
18706
|
+
process.stdout.write(
|
|
18707
|
+
`${host} removed from ${ctx.project}: its route and certificate go within a minute, then it answers 404. The project's own kryd.cloud host keeps working.
|
|
18708
|
+
`
|
|
18709
|
+
);
|
|
18710
|
+
} catch (err) {
|
|
18711
|
+
await reportError(err);
|
|
18712
|
+
}
|
|
18713
|
+
}
|
|
18714
|
+
async function runDomainStatus(opts) {
|
|
18715
|
+
const ctx = domainContext(opts, "kryd domain status [host] --project <projectId>");
|
|
18716
|
+
if (!ctx) return;
|
|
18717
|
+
try {
|
|
18718
|
+
const list = await listDomains(ctx.apiUrl, ctx.token, ctx.project);
|
|
18719
|
+
const own = list.projectHost ? `${ctx.project} is always served at ${list.projectHost}.
|
|
18720
|
+
` : "";
|
|
18721
|
+
if (!opts.host) {
|
|
18722
|
+
if (list.domains.length === 0) {
|
|
18723
|
+
process.stdout.write(`${ctx.project}: no custom domains.
|
|
18724
|
+
${own}`);
|
|
18725
|
+
return;
|
|
18726
|
+
}
|
|
18727
|
+
let out2 = `${ctx.project}: ${list.domains.length} custom domain${list.domains.length === 1 ? "" : "s"}.
|
|
18728
|
+
`;
|
|
18729
|
+
for (const d2 of list.domains) {
|
|
18730
|
+
out2 += ` ${d2.host} ${DOMAIN_STATUS_TEXT[d2.status]}
|
|
18731
|
+
`;
|
|
18732
|
+
if (d2.detail) out2 += ` ${d2.detail}
|
|
18733
|
+
`;
|
|
18734
|
+
}
|
|
18735
|
+
process.stdout.write(out2 + own);
|
|
18736
|
+
return;
|
|
18737
|
+
}
|
|
18738
|
+
const wanted = normalizeDomainHost(opts.host);
|
|
18739
|
+
const d = list.domains.find((x) => x.host === wanted);
|
|
18740
|
+
if (!d) {
|
|
18741
|
+
process.stderr.write(`${wanted} is not a custom domain of ${ctx.project}. Add it with \`kryd domain add ${wanted}\`.
|
|
18742
|
+
`);
|
|
18743
|
+
process.exitCode = 1;
|
|
18744
|
+
return;
|
|
18745
|
+
}
|
|
18746
|
+
let out = `${d.host}: ${DOMAIN_STATUS_TEXT[d.status]}.
|
|
18747
|
+
`;
|
|
18748
|
+
if (d.detail) out += ` ${d.detail}
|
|
18749
|
+
`;
|
|
18750
|
+
if (d.status === "pending") {
|
|
18751
|
+
out += `
|
|
18752
|
+
The records to create:
|
|
18753
|
+
` + formatDomainRecords(d.records);
|
|
18754
|
+
}
|
|
18755
|
+
process.stdout.write(out + own);
|
|
18756
|
+
} catch (err) {
|
|
18757
|
+
await reportError(err);
|
|
18758
|
+
}
|
|
18759
|
+
}
|
|
18386
18760
|
async function runWorkflowInit(opts) {
|
|
18387
18761
|
const cwd = opts.cwd ?? process.cwd();
|
|
18388
18762
|
if (!opts.language) {
|
|
@@ -19125,7 +19499,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
19125
19499
|
await reportError(err);
|
|
19126
19500
|
}
|
|
19127
19501
|
}
|
|
19128
|
-
var CLI_VERSION = true ? "0.
|
|
19502
|
+
var CLI_VERSION = true ? "0.11.0" : "0.0.0-dev";
|
|
19129
19503
|
var program = new Command();
|
|
19130
19504
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
19131
19505
|
program.hook("postAction", async () => {
|
|
@@ -19170,13 +19544,17 @@ program.command("logs [target]").description(
|
|
|
19170
19544
|
).option(
|
|
19171
19545
|
"--since <dur>",
|
|
19172
19546
|
"with --runtime/--worker: backfill this window before going live (e.g. 30m, 2h, 1d; max 7d)"
|
|
19547
|
+
).option(
|
|
19548
|
+
"--app <name>",
|
|
19549
|
+
"a project with several web apps (kryd.json \u2192 apps): which app \u2014 its latest deploy, or with --runtime its live container"
|
|
19173
19550
|
).option("--api-url <url>", "control-plane API base URL").action(
|
|
19174
19551
|
(target, opts) => opts.runtime || opts.worker ? runRuntimeLogs({
|
|
19175
19552
|
project: target,
|
|
19176
19553
|
since: opts.since,
|
|
19177
19554
|
apiUrl: opts.apiUrl,
|
|
19555
|
+
...opts.app ? { app: opts.app } : {},
|
|
19178
19556
|
...opts.worker ? { worker: true } : {}
|
|
19179
|
-
}) : runLogs({ apiUrl: opts.apiUrl, deployment: target })
|
|
19557
|
+
}) : runLogs({ apiUrl: opts.apiUrl, deployment: target, ...opts.app ? { app: opts.app } : {} })
|
|
19180
19558
|
);
|
|
19181
19559
|
program.command("push [branch]").description(
|
|
19182
19560
|
"Push to the kryd remote and follow the deploy it triggers (defaults to the current branch)"
|
|
@@ -19184,17 +19562,20 @@ program.command("push [branch]").description(
|
|
|
19184
19562
|
(branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
|
|
19185
19563
|
);
|
|
19186
19564
|
program.command("deploy [project]").description("Trigger a deploy of the project's production branch and follow it live").option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDeploy({ ...opts, project: project2 }));
|
|
19187
|
-
program.command("rollback [project] [deployment]").description("Roll back to a previous successful deploy (no rebuild) and follow it live").option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action(
|
|
19565
|
+
program.command("rollback [project] [deployment]").description("Roll back to a previous successful deploy (no rebuild) and follow it live").option("--app <name>", "a project with several web apps: which app to roll back").option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action(
|
|
19188
19566
|
(project2, deployment, opts) => runRollback({ ...opts, project: project2, deployment })
|
|
19189
19567
|
);
|
|
19190
19568
|
program.command("redeploy [project]").description(
|
|
19191
19569
|
"Redeploy the current live commit (no rebuild) to apply config/env changes, and follow it live"
|
|
19192
|
-
).option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
|
|
19570
|
+
).option("--app <name>", "a project with several web apps: which app to redeploy").option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
|
|
19193
19571
|
var project = program.command("project").description("Manage your projects");
|
|
19194
19572
|
project.command("list").description("List your projects \u2014 name, subdomain, status and id").option("--api-url <url>", "control-plane API base URL").action((opts) => runProjectList(opts));
|
|
19195
19573
|
project.command("rm [project]").description(
|
|
19196
19574
|
"Permanently delete a project and everything Kryd created for it (by name or id; asks you to type its name)"
|
|
19197
19575
|
).option("--yes", "skip the confirmation (for non-interactive use)").option("--api-url <url>", "control-plane API base URL").action((projectArg, opts) => runProjectRemove({ ...opts, project: projectArg }));
|
|
19576
|
+
var deployHook = project.command("deploy-hook").description("A secret URL that rebuilds production on a POST \u2014 for a CMS webhook (e.g. Sanity on publish)");
|
|
19577
|
+
deployHook.command("create").description("Create the deploy hook and print its URL once (replaces an existing one)").option("--project <projectId>", "the project (default: the one linked here)").option("--api-url <url>", "control-plane API base URL").action((opts) => runDeployHookCreate(opts));
|
|
19578
|
+
deployHook.command("rm").description("Remove the deploy hook \u2014 its URL stops working").option("--project <projectId>", "the project (default: the one linked here)").option("--api-url <url>", "control-plane API base URL").action((opts) => runDeployHookRemove(opts));
|
|
19198
19579
|
var db = program.command("db").description("Manage project databases");
|
|
19199
19580
|
db.command("add [project]").description("Attach a shared or bring-your-own Postgres database to a project").option("--shared", "attach a shared managed database (the default)").option(
|
|
19200
19581
|
"--connection-string <url>",
|
|
@@ -19250,6 +19631,10 @@ workflow.command("init").description(
|
|
|
19250
19631
|
"--language <language>",
|
|
19251
19632
|
`worker language: ${SCAFFOLD_LANGUAGES.join(" | ")}`
|
|
19252
19633
|
).option("--path <folder>", "the worker's folder inside the app (default: workflows)").option("--only", "a project that runs ONLY a worker: scaffold it at the root, git init, and link").option("--dir <path>", "the project directory, instead of the current one").option("--name <name>", "worker name (and, with --only, project name); defaults to the directory name").option("--no-link", "with --only: only write the files; do not create a Kryd project").option("--api-url <url>", "control-plane API base URL").action((opts) => runWorkflowInit(opts));
|
|
19634
|
+
var domain2 = program.command("domain").description("Serve the project on your own hostname (custom domains)");
|
|
19635
|
+
domain2.command("add <host>").description("Add a custom domain and print the DNS records to create").option("--project <projectId>", "the project (default: the one linked here)").option("--app <name>", "a project with several web apps: the app the domain serves (default: web)").option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainAdd({ ...opts, host }));
|
|
19636
|
+
domain2.command("remove <host>").description("Remove a custom domain \u2014 its route and certificate go (asks for confirmation)").option("--project <projectId>", "the project (default: the one linked here)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainRemove({ ...opts, host }));
|
|
19637
|
+
domain2.command("status [host]").description("Show the project's custom domains, or one in detail with what is missing").option("--project <projectId>", "the project (default: the one linked here)").option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainStatus({ ...opts, host }));
|
|
19253
19638
|
function invokedDirectly() {
|
|
19254
19639
|
const entry = process.argv[1];
|
|
19255
19640
|
if (!entry) return false;
|
|
@@ -19278,6 +19663,11 @@ export {
|
|
|
19278
19663
|
runDbRemove,
|
|
19279
19664
|
runDbStatus,
|
|
19280
19665
|
runDeploy,
|
|
19666
|
+
runDeployHookCreate,
|
|
19667
|
+
runDeployHookRemove,
|
|
19668
|
+
runDomainAdd,
|
|
19669
|
+
runDomainRemove,
|
|
19670
|
+
runDomainStatus,
|
|
19281
19671
|
runEnvList,
|
|
19282
19672
|
runEnvPull,
|
|
19283
19673
|
runEnvRemove,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kryd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Kryd CLI — push a React / Vite / Next.js app to the European cloud for your AI: git push → live with SSL, one-click managed Postgres & object storage, and an EU-hosted AI gateway already wired in. Your code and your model calls stay in the EU.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kryd",
|
|
@@ -14,14 +14,6 @@
|
|
|
14
14
|
"ai-gateway"
|
|
15
15
|
],
|
|
16
16
|
"homepage": "https://kryd.eu",
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/24BEEPS/kryd.git",
|
|
20
|
-
"directory": "apps/cli"
|
|
21
|
-
},
|
|
22
|
-
"bugs": {
|
|
23
|
-
"url": "https://github.com/24BEEPS/kryd/issues"
|
|
24
|
-
},
|
|
25
17
|
"license": "Apache-2.0",
|
|
26
18
|
"author": "Kryd <boris@beep24.nl>",
|
|
27
19
|
"type": "module",
|
|
@@ -50,8 +42,8 @@
|
|
|
50
42
|
"typescript": "^5.6.3",
|
|
51
43
|
"vitest": "^2.1.8",
|
|
52
44
|
"@kryd/shared-types": "0.0.0",
|
|
53
|
-
"@kryd/config-
|
|
54
|
-
"@kryd/config-
|
|
45
|
+
"@kryd/config-ts": "0.0.0",
|
|
46
|
+
"@kryd/config-eslint": "0.0.0"
|
|
55
47
|
},
|
|
56
48
|
"scripts": {
|
|
57
49
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --external:commander --define:__KRYD_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/index.js",
|