@kryd/cli 0.9.0 → 0.10.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 +347 -24
- 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,79 @@ 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
|
+
}
|
|
15411
15492
|
async function fetchResourceStatus(apiUrl, token, path) {
|
|
15412
15493
|
const res = await fetch(`${apiUrl}${path}`, {
|
|
15413
15494
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -15732,9 +15813,13 @@ async function triggerDeploy(apiUrl, token, projectId) {
|
|
|
15732
15813
|
if (!res.ok) {
|
|
15733
15814
|
throw new ApiError(`Deploy trigger failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15734
15815
|
}
|
|
15735
|
-
|
|
15816
|
+
const body = await res.json();
|
|
15817
|
+
return {
|
|
15818
|
+
deploymentId: body.deploymentId,
|
|
15819
|
+
...Array.isArray(body.apps) ? { apps: body.apps } : {}
|
|
15820
|
+
};
|
|
15736
15821
|
}
|
|
15737
|
-
async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
|
|
15822
|
+
async function rollbackDeploy(apiUrl, token, projectId, deploymentId, app) {
|
|
15738
15823
|
const res = await fetch(`${apiUrl}/deployments/rollback`, {
|
|
15739
15824
|
method: "POST",
|
|
15740
15825
|
headers: {
|
|
@@ -15743,7 +15828,8 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
|
|
|
15743
15828
|
},
|
|
15744
15829
|
body: JSON.stringify({
|
|
15745
15830
|
projectId,
|
|
15746
|
-
...deploymentId ? { deploymentId } : {}
|
|
15831
|
+
...deploymentId ? { deploymentId } : {},
|
|
15832
|
+
...app ? { app } : {}
|
|
15747
15833
|
})
|
|
15748
15834
|
});
|
|
15749
15835
|
if (!res.ok) {
|
|
@@ -15751,14 +15837,14 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
|
|
|
15751
15837
|
}
|
|
15752
15838
|
return await res.json();
|
|
15753
15839
|
}
|
|
15754
|
-
async function redeployProject(apiUrl, token, projectId) {
|
|
15840
|
+
async function redeployProject(apiUrl, token, projectId, app) {
|
|
15755
15841
|
const res = await fetch(`${apiUrl}/deployments/redeploy`, {
|
|
15756
15842
|
method: "POST",
|
|
15757
15843
|
headers: {
|
|
15758
15844
|
"content-type": "application/json",
|
|
15759
15845
|
authorization: `Bearer ${token}`
|
|
15760
15846
|
},
|
|
15761
|
-
body: JSON.stringify({ projectId })
|
|
15847
|
+
body: JSON.stringify({ projectId, ...app ? { app } : {} })
|
|
15762
15848
|
});
|
|
15763
15849
|
if (!res.ok) {
|
|
15764
15850
|
throw new ApiError(`Redeploy failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
@@ -15800,6 +15886,32 @@ async function pollForNewDeployment(fetchDeployments, match, opts) {
|
|
|
15800
15886
|
await sleep(intervalMs);
|
|
15801
15887
|
}
|
|
15802
15888
|
}
|
|
15889
|
+
async function pollForNewDeployments(fetchDeployments, match, opts) {
|
|
15890
|
+
const expected = opts?.expectedCount ?? null;
|
|
15891
|
+
if (expected === null || expected <= 1) {
|
|
15892
|
+
const first = await pollForNewDeployment(fetchDeployments, match, opts);
|
|
15893
|
+
return first ? [first] : [];
|
|
15894
|
+
}
|
|
15895
|
+
const intervalMs = opts?.intervalMs ?? 2e3;
|
|
15896
|
+
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
15897
|
+
const maxConsecutiveErrors = opts?.maxConsecutiveErrors ?? 5;
|
|
15898
|
+
const sleep = opts?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
15899
|
+
const started = Date.now();
|
|
15900
|
+
let consecutiveErrors = 0;
|
|
15901
|
+
let fresh = [];
|
|
15902
|
+
for (; ; ) {
|
|
15903
|
+
try {
|
|
15904
|
+
const items = await fetchDeployments();
|
|
15905
|
+
consecutiveErrors = 0;
|
|
15906
|
+
fresh = findDeploymentsForCommit(items, match.commitSha, match.branch).filter((d) => !match.knownIds.has(d.id)).reverse();
|
|
15907
|
+
if (fresh.length >= expected) return fresh;
|
|
15908
|
+
} catch (err) {
|
|
15909
|
+
if (++consecutiveErrors > maxConsecutiveErrors) throw err;
|
|
15910
|
+
}
|
|
15911
|
+
if (Date.now() - started >= timeoutMs) return fresh;
|
|
15912
|
+
await sleep(intervalMs);
|
|
15913
|
+
}
|
|
15914
|
+
}
|
|
15803
15915
|
function dataFromFrame(frame) {
|
|
15804
15916
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
15805
15917
|
return data.length > 0 ? data : void 0;
|
|
@@ -15884,6 +15996,7 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
|
|
|
15884
15996
|
while (!opts.signal?.aborted) {
|
|
15885
15997
|
try {
|
|
15886
15998
|
const url2 = new URL(`${apiUrl}/${path}`);
|
|
15999
|
+
if (opts.app && (opts.resource ?? "project") === "project") url2.searchParams.set("app", opts.app);
|
|
15887
16000
|
if (afterNs) url2.searchParams.set("after", afterNs);
|
|
15888
16001
|
else if (opts.since) url2.searchParams.set("since", opts.since);
|
|
15889
16002
|
const res = await doFetch(url2, {
|
|
@@ -17086,6 +17199,18 @@ function currentBranch(cwd) {
|
|
|
17086
17199
|
function headSha(cwd, ref) {
|
|
17087
17200
|
return probe(cwd, ["rev-parse", ref]);
|
|
17088
17201
|
}
|
|
17202
|
+
function declaredAppCount(cwd, sha) {
|
|
17203
|
+
const res = probe(cwd, ["show", `${sha}:kryd.json`]);
|
|
17204
|
+
if (res.status !== "ok") return null;
|
|
17205
|
+
try {
|
|
17206
|
+
const apps = JSON.parse(res.value).apps;
|
|
17207
|
+
if (apps === null || typeof apps !== "object" || Array.isArray(apps)) return null;
|
|
17208
|
+
const n = Object.keys(apps).length;
|
|
17209
|
+
return n > 0 ? n : null;
|
|
17210
|
+
} catch {
|
|
17211
|
+
return null;
|
|
17212
|
+
}
|
|
17213
|
+
}
|
|
17089
17214
|
function hasRemote(cwd, remote) {
|
|
17090
17215
|
const res = probe(cwd, ["remote"]);
|
|
17091
17216
|
if (res.status !== "ok") return res;
|
|
@@ -17471,11 +17596,33 @@ async function runLogs(opts) {
|
|
|
17471
17596
|
process.exitCode = 1;
|
|
17472
17597
|
return;
|
|
17473
17598
|
}
|
|
17599
|
+
if (opts.app && opts.deployment) {
|
|
17600
|
+
process.stderr.write(
|
|
17601
|
+
"--app picks the latest deploy of an app; a deployment id already names one deploy \u2014 pass one or the other.\n"
|
|
17602
|
+
);
|
|
17603
|
+
process.exitCode = 1;
|
|
17604
|
+
return;
|
|
17605
|
+
}
|
|
17606
|
+
if (opts.app && !resolveProjectId(void 0, opts.cwd)) {
|
|
17607
|
+
process.stderr.write(
|
|
17608
|
+
"--app needs a linked project: run it in a folder linked with `kryd init`, or pass a deployment id instead.\n"
|
|
17609
|
+
);
|
|
17610
|
+
process.exitCode = 1;
|
|
17611
|
+
return;
|
|
17612
|
+
}
|
|
17474
17613
|
try {
|
|
17475
17614
|
let deploymentId = opts.deployment;
|
|
17476
17615
|
if (!deploymentId) {
|
|
17477
17616
|
const linkedProjectId = resolveProjectId(void 0, opts.cwd);
|
|
17478
|
-
const deploys = await listDeployments(apiUrl, token, linkedProjectId ?? void 0)
|
|
17617
|
+
const deploys = (await listDeployments(apiUrl, token, linkedProjectId ?? void 0)).filter(
|
|
17618
|
+
(d) => !opts.app || (d.app ?? "web") === opts.app
|
|
17619
|
+
);
|
|
17620
|
+
if (deploys.length === 0 && opts.app) {
|
|
17621
|
+
process.stderr.write(`No deploys of app "${opts.app}" found.
|
|
17622
|
+
`);
|
|
17623
|
+
process.exitCode = 1;
|
|
17624
|
+
return;
|
|
17625
|
+
}
|
|
17479
17626
|
if (deploys.length === 0) {
|
|
17480
17627
|
process.stderr.write(
|
|
17481
17628
|
linkedProjectId ? "The linked project has no deploys yet \u2014 run `kryd deploy` first.\n" : "No deploys found for your account.\n"
|
|
@@ -17539,6 +17686,7 @@ async function runRuntimeLogs(opts) {
|
|
|
17539
17686
|
},
|
|
17540
17687
|
{
|
|
17541
17688
|
...opts.since ? { since: opts.since } : {},
|
|
17689
|
+
...opts.app ? { app: opts.app } : {},
|
|
17542
17690
|
signal: controller.signal,
|
|
17543
17691
|
...isDeployment ? { resource: "deployment" } : opts.worker ? { resource: "worker" } : {}
|
|
17544
17692
|
}
|
|
@@ -17563,7 +17711,17 @@ async function runDeploy(opts) {
|
|
|
17563
17711
|
return;
|
|
17564
17712
|
}
|
|
17565
17713
|
try {
|
|
17566
|
-
const deploymentId = await triggerDeploy(apiUrl, token, project2);
|
|
17714
|
+
const { deploymentId, apps } = await triggerDeploy(apiUrl, token, project2);
|
|
17715
|
+
if (apps && apps.length > 1) {
|
|
17716
|
+
await followApps(
|
|
17717
|
+
apiUrl,
|
|
17718
|
+
token,
|
|
17719
|
+
apps.map((a) => ({ id: a.deploymentId, app: a.app })),
|
|
17720
|
+
renderFor(opts.logs),
|
|
17721
|
+
[]
|
|
17722
|
+
);
|
|
17723
|
+
return;
|
|
17724
|
+
}
|
|
17567
17725
|
process.stdout.write(`Triggered deploy ${deploymentId}
|
|
17568
17726
|
`);
|
|
17569
17727
|
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
@@ -17678,14 +17836,17 @@ async function runPush(opts) {
|
|
|
17678
17836
|
return;
|
|
17679
17837
|
}
|
|
17680
17838
|
process.stdout.write("Waiting for the deploy to start\u2026\n");
|
|
17681
|
-
const
|
|
17839
|
+
const started = await pollForNewDeployments(
|
|
17682
17840
|
fetchDeployments,
|
|
17683
17841
|
{ commitSha, branch, knownIds: new Set(known.map((d) => d.id)) },
|
|
17684
17842
|
{
|
|
17685
17843
|
...opts.sleep ? { sleep: opts.sleep } : {},
|
|
17686
|
-
...opts.pollTimeoutMs !== void 0 ? { timeoutMs: opts.pollTimeoutMs } : {}
|
|
17844
|
+
...opts.pollTimeoutMs !== void 0 ? { timeoutMs: opts.pollTimeoutMs } : {},
|
|
17845
|
+
// The commit's own count of apps, so the follow waits for every row the fan-out creates.
|
|
17846
|
+
expectedCount: declaredAppCount(cwd, commitSha)
|
|
17687
17847
|
}
|
|
17688
17848
|
);
|
|
17849
|
+
const deployment = started[0];
|
|
17689
17850
|
if (!deployment) {
|
|
17690
17851
|
const previous = known[0];
|
|
17691
17852
|
if (previous) {
|
|
@@ -17701,14 +17862,29 @@ async function runPush(opts) {
|
|
|
17701
17862
|
}
|
|
17702
17863
|
return;
|
|
17703
17864
|
}
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17865
|
+
if (started.length === 1) {
|
|
17866
|
+
await followDeploy(apiUrl, token, deployment.id, {
|
|
17867
|
+
render: renderFor(opts.logs),
|
|
17868
|
+
estimateFrom: recent
|
|
17869
|
+
});
|
|
17870
|
+
return;
|
|
17871
|
+
}
|
|
17872
|
+
await followApps(apiUrl, token, started, renderFor(opts.logs), recent);
|
|
17708
17873
|
} catch (err) {
|
|
17709
17874
|
await reportError(err);
|
|
17710
17875
|
}
|
|
17711
17876
|
}
|
|
17877
|
+
async function followApps(apiUrl, token, deployments, render, estimateFrom) {
|
|
17878
|
+
process.stdout.write(
|
|
17879
|
+
`This push deploys ${deployments.length} apps: ` + deployments.map((d) => `${d.app ?? "web"} (${d.id})`).join(", ") + ".\n"
|
|
17880
|
+
);
|
|
17881
|
+
for (const d of deployments) {
|
|
17882
|
+
process.stdout.write(`
|
|
17883
|
+
\u2500\u2500 ${d.app ?? "web"} \u2500\u2500
|
|
17884
|
+
`);
|
|
17885
|
+
await followDeploy(apiUrl, token, d.id, { render, estimateFrom });
|
|
17886
|
+
}
|
|
17887
|
+
}
|
|
17712
17888
|
async function runRollback(opts) {
|
|
17713
17889
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17714
17890
|
const token = loadConfig().token;
|
|
@@ -17727,7 +17903,8 @@ async function runRollback(opts) {
|
|
|
17727
17903
|
apiUrl,
|
|
17728
17904
|
token,
|
|
17729
17905
|
project2,
|
|
17730
|
-
opts.deployment
|
|
17906
|
+
opts.deployment,
|
|
17907
|
+
opts.app
|
|
17731
17908
|
);
|
|
17732
17909
|
process.stdout.write(
|
|
17733
17910
|
`Rolling back to ${rolledBackTo} \u2192 ${deploymentId}
|
|
@@ -17756,7 +17933,8 @@ async function runRedeploy(opts) {
|
|
|
17756
17933
|
const { deploymentId, redeployedCommit, note } = await redeployProject(
|
|
17757
17934
|
apiUrl,
|
|
17758
17935
|
token,
|
|
17759
|
-
project2
|
|
17936
|
+
project2,
|
|
17937
|
+
opts.app
|
|
17760
17938
|
);
|
|
17761
17939
|
process.stdout.write(
|
|
17762
17940
|
`Redeploying ${redeployedCommit.slice(0, 7)} \u2192 ${deploymentId}
|
|
@@ -18282,7 +18460,7 @@ async function runWorkflowAdd(opts) {
|
|
|
18282
18460
|
const current = await getWorkflowStatus(apiUrl, token, project2);
|
|
18283
18461
|
if (current.enabled) {
|
|
18284
18462
|
process.stdout.write(
|
|
18285
|
-
`${project2} already has workflows \u2014 HATCHET_CLIENT_TOKEN is injected on
|
|
18463
|
+
`${project2} already has workflows \u2014 HATCHET_CLIENT_TOKEN is injected on production deploys (previews get none)` + (current.tokenExpiresAt ? ` and the token expires ${isoDay(current.tokenExpiresAt)}.
|
|
18286
18464
|
` : ".\n") + `Nothing was changed. To rotate the token, run \`kryd workflow remove\` and then \`kryd workflow add\`.
|
|
18287
18465
|
`
|
|
18288
18466
|
);
|
|
@@ -18291,7 +18469,7 @@ async function runWorkflowAdd(opts) {
|
|
|
18291
18469
|
const status = await addWorkflows(apiUrl, token, { projectId: project2 });
|
|
18292
18470
|
process.stdout.write(
|
|
18293
18471
|
`Workflows added to ${project2}.
|
|
18294
|
-
HATCHET_CLIENT_TOKEN will be injected on your next deploy (kryd deploy)` + (status.tokenExpiresAt ? `; the token expires ${isoDay(status.tokenExpiresAt)}.
|
|
18472
|
+
HATCHET_CLIENT_TOKEN will be injected on your next production deploy (kryd deploy; previews get none)` + (status.tokenExpiresAt ? `; the token expires ${isoDay(status.tokenExpiresAt)}.
|
|
18295
18473
|
` : ".\n")
|
|
18296
18474
|
);
|
|
18297
18475
|
} catch (err) {
|
|
@@ -18383,6 +18561,140 @@ async function runWorkflowStatus(opts) {
|
|
|
18383
18561
|
await reportError(err);
|
|
18384
18562
|
}
|
|
18385
18563
|
}
|
|
18564
|
+
var DOMAIN_STATUS_TEXT = {
|
|
18565
|
+
pending: "pending \u2014 waiting for the DNS records",
|
|
18566
|
+
verified: "verified \u2014 DNS is right, the certificate is being issued",
|
|
18567
|
+
active: "active \u2014 served over HTTPS",
|
|
18568
|
+
failed: "failed \u2014 certificate issuance is failing; Kryd retries automatically"
|
|
18569
|
+
};
|
|
18570
|
+
function formatDomainRecords(records) {
|
|
18571
|
+
const line = (r) => ` ${r.type.padEnd(5)} ${r.name} \u2192 ${r.value}
|
|
18572
|
+
`;
|
|
18573
|
+
const ownership = records.filter((r) => r.purpose === "ownership");
|
|
18574
|
+
const routing = records.filter((r) => r.purpose === "routing");
|
|
18575
|
+
let out = " 1. Prove you own it:\n" + ownership.map(line).join("");
|
|
18576
|
+
out += " 2. Point it at Kryd \u2014 exactly ONE of these:\n";
|
|
18577
|
+
for (const r of routing) {
|
|
18578
|
+
out += line(r) + (r.note ? ` (${r.note})
|
|
18579
|
+
` : "");
|
|
18580
|
+
}
|
|
18581
|
+
return out;
|
|
18582
|
+
}
|
|
18583
|
+
function domainContext(opts, usage) {
|
|
18584
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
18585
|
+
const token = loadConfig().token;
|
|
18586
|
+
if (!token) {
|
|
18587
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
18588
|
+
process.exitCode = 1;
|
|
18589
|
+
return null;
|
|
18590
|
+
}
|
|
18591
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
18592
|
+
if (!project2) {
|
|
18593
|
+
reportNotLinked(usage);
|
|
18594
|
+
return null;
|
|
18595
|
+
}
|
|
18596
|
+
return { apiUrl, token, project: project2 };
|
|
18597
|
+
}
|
|
18598
|
+
async function runDomainAdd(opts) {
|
|
18599
|
+
const ctx = domainContext(opts, "kryd domain add <host> --project <projectId>");
|
|
18600
|
+
if (!ctx) return;
|
|
18601
|
+
try {
|
|
18602
|
+
const [domain3, list] = await Promise.all([
|
|
18603
|
+
addDomain(ctx.apiUrl, ctx.token, ctx.project, opts.host, opts.app),
|
|
18604
|
+
listDomains(ctx.apiUrl, ctx.token, ctx.project).catch(() => null)
|
|
18605
|
+
]);
|
|
18606
|
+
if (domain3.status !== "pending") {
|
|
18607
|
+
process.stdout.write(
|
|
18608
|
+
`${domain3.host} is already on ${ctx.project}${appSuffix(domain3.app)}: ${DOMAIN_STATUS_TEXT[domain3.status]}.
|
|
18609
|
+
`
|
|
18610
|
+
);
|
|
18611
|
+
return;
|
|
18612
|
+
}
|
|
18613
|
+
process.stdout.write(
|
|
18614
|
+
`${domain3.host} added to ${ctx.project}${appSuffix(domain3.app)} \u2014 pending until its DNS records exist.
|
|
18615
|
+
|
|
18616
|
+
Create these records at your DNS provider:
|
|
18617
|
+
` + formatDomainRecords(domain3.records) + `
|
|
18618
|
+
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}\`.
|
|
18619
|
+
` + (list?.projectHost ? `${ctx.project} keeps its own host, ${list.projectHost}.
|
|
18620
|
+
` : "")
|
|
18621
|
+
);
|
|
18622
|
+
} catch (err) {
|
|
18623
|
+
await reportError(err);
|
|
18624
|
+
}
|
|
18625
|
+
}
|
|
18626
|
+
function appSuffix(app) {
|
|
18627
|
+
return app && app !== "web" ? ` (app ${app})` : "";
|
|
18628
|
+
}
|
|
18629
|
+
async function runDomainRemove(opts) {
|
|
18630
|
+
const ctx = domainContext(opts, "kryd domain remove <host> --project <projectId>");
|
|
18631
|
+
if (!ctx) return;
|
|
18632
|
+
const host = normalizeDomainHost(opts.host);
|
|
18633
|
+
const proceed = await confirmResourceRemoval({
|
|
18634
|
+
yes: opts.yes,
|
|
18635
|
+
io: opts.io,
|
|
18636
|
+
subject: `${host} from ${ctx.project}`,
|
|
18637
|
+
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.`,
|
|
18638
|
+
declined: `Left ${host} on ${ctx.project}.
|
|
18639
|
+
`
|
|
18640
|
+
});
|
|
18641
|
+
if (!proceed) return;
|
|
18642
|
+
try {
|
|
18643
|
+
await removeDomain(ctx.apiUrl, ctx.token, ctx.project, host);
|
|
18644
|
+
process.stdout.write(
|
|
18645
|
+
`${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.
|
|
18646
|
+
`
|
|
18647
|
+
);
|
|
18648
|
+
} catch (err) {
|
|
18649
|
+
await reportError(err);
|
|
18650
|
+
}
|
|
18651
|
+
}
|
|
18652
|
+
async function runDomainStatus(opts) {
|
|
18653
|
+
const ctx = domainContext(opts, "kryd domain status [host] --project <projectId>");
|
|
18654
|
+
if (!ctx) return;
|
|
18655
|
+
try {
|
|
18656
|
+
const list = await listDomains(ctx.apiUrl, ctx.token, ctx.project);
|
|
18657
|
+
const own = list.projectHost ? `${ctx.project} is always served at ${list.projectHost}.
|
|
18658
|
+
` : "";
|
|
18659
|
+
if (!opts.host) {
|
|
18660
|
+
if (list.domains.length === 0) {
|
|
18661
|
+
process.stdout.write(`${ctx.project}: no custom domains.
|
|
18662
|
+
${own}`);
|
|
18663
|
+
return;
|
|
18664
|
+
}
|
|
18665
|
+
let out2 = `${ctx.project}: ${list.domains.length} custom domain${list.domains.length === 1 ? "" : "s"}.
|
|
18666
|
+
`;
|
|
18667
|
+
for (const d2 of list.domains) {
|
|
18668
|
+
out2 += ` ${d2.host} ${DOMAIN_STATUS_TEXT[d2.status]}
|
|
18669
|
+
`;
|
|
18670
|
+
if (d2.detail) out2 += ` ${d2.detail}
|
|
18671
|
+
`;
|
|
18672
|
+
}
|
|
18673
|
+
process.stdout.write(out2 + own);
|
|
18674
|
+
return;
|
|
18675
|
+
}
|
|
18676
|
+
const wanted = normalizeDomainHost(opts.host);
|
|
18677
|
+
const d = list.domains.find((x) => x.host === wanted);
|
|
18678
|
+
if (!d) {
|
|
18679
|
+
process.stderr.write(`${wanted} is not a custom domain of ${ctx.project}. Add it with \`kryd domain add ${wanted}\`.
|
|
18680
|
+
`);
|
|
18681
|
+
process.exitCode = 1;
|
|
18682
|
+
return;
|
|
18683
|
+
}
|
|
18684
|
+
let out = `${d.host}: ${DOMAIN_STATUS_TEXT[d.status]}.
|
|
18685
|
+
`;
|
|
18686
|
+
if (d.detail) out += ` ${d.detail}
|
|
18687
|
+
`;
|
|
18688
|
+
if (d.status === "pending") {
|
|
18689
|
+
out += `
|
|
18690
|
+
The records to create:
|
|
18691
|
+
` + formatDomainRecords(d.records);
|
|
18692
|
+
}
|
|
18693
|
+
process.stdout.write(out + own);
|
|
18694
|
+
} catch (err) {
|
|
18695
|
+
await reportError(err);
|
|
18696
|
+
}
|
|
18697
|
+
}
|
|
18386
18698
|
async function runWorkflowInit(opts) {
|
|
18387
18699
|
const cwd = opts.cwd ?? process.cwd();
|
|
18388
18700
|
if (!opts.language) {
|
|
@@ -18482,7 +18794,7 @@ To add a worker to the app in that repository instead, run \`kryd workflow init\
|
|
|
18482
18794
|
next.push("kryd init", "kryd workflow add", "kryd push");
|
|
18483
18795
|
process.stdout.write(
|
|
18484
18796
|
`Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
|
|
18485
|
-
HATCHET_CLIENT_TOKEN is injected into your container on
|
|
18797
|
+
HATCHET_CLIENT_TOKEN is injected into your container on production deploys (previews get none); it is never available locally.
|
|
18486
18798
|
`
|
|
18487
18799
|
);
|
|
18488
18800
|
return;
|
|
@@ -18510,7 +18822,7 @@ HATCHET_CLIENT_TOKEN is injected into your container on deploy; it is never avai
|
|
|
18510
18822
|
process.stdout.write(
|
|
18511
18823
|
`
|
|
18512
18824
|
Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
|
|
18513
|
-
HATCHET_CLIENT_TOKEN is injected into your container on
|
|
18825
|
+
HATCHET_CLIENT_TOKEN is injected into your container on production deploys (previews get none); it is never available locally.
|
|
18514
18826
|
`
|
|
18515
18827
|
);
|
|
18516
18828
|
}
|
|
@@ -18632,7 +18944,7 @@ Nothing was left behind.
|
|
|
18632
18944
|
|
|
18633
18945
|
Every production push now builds your app AND this worker, and goes live only when both are healthy. Previews run the app only.
|
|
18634
18946
|
Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
|
|
18635
|
-
HATCHET_CLIENT_TOKEN is injected into your app and your worker on
|
|
18947
|
+
HATCHET_CLIENT_TOKEN is injected into your app and your worker on production deploys (previews get none) \u2014 your app triggers tasks with it; it is never available locally.
|
|
18636
18948
|
` + // The app is still built (and type-checked) from the root, so a tsconfig that includes every
|
|
18637
18949
|
// file would compile the worker too — against an SDK the app does not depend on.
|
|
18638
18950
|
(existsSync3(join3(target, "tsconfig.json")) ? `Add "${folder}" to the "exclude" of your tsconfig.json, so your app's build does not type-check the worker.
|
|
@@ -19125,7 +19437,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
19125
19437
|
await reportError(err);
|
|
19126
19438
|
}
|
|
19127
19439
|
}
|
|
19128
|
-
var CLI_VERSION = true ? "0.
|
|
19440
|
+
var CLI_VERSION = true ? "0.10.0" : "0.0.0-dev";
|
|
19129
19441
|
var program = new Command();
|
|
19130
19442
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
19131
19443
|
program.hook("postAction", async () => {
|
|
@@ -19170,13 +19482,17 @@ program.command("logs [target]").description(
|
|
|
19170
19482
|
).option(
|
|
19171
19483
|
"--since <dur>",
|
|
19172
19484
|
"with --runtime/--worker: backfill this window before going live (e.g. 30m, 2h, 1d; max 7d)"
|
|
19485
|
+
).option(
|
|
19486
|
+
"--app <name>",
|
|
19487
|
+
"a project with several web apps (kryd.json \u2192 apps): which app \u2014 its latest deploy, or with --runtime its live container"
|
|
19173
19488
|
).option("--api-url <url>", "control-plane API base URL").action(
|
|
19174
19489
|
(target, opts) => opts.runtime || opts.worker ? runRuntimeLogs({
|
|
19175
19490
|
project: target,
|
|
19176
19491
|
since: opts.since,
|
|
19177
19492
|
apiUrl: opts.apiUrl,
|
|
19493
|
+
...opts.app ? { app: opts.app } : {},
|
|
19178
19494
|
...opts.worker ? { worker: true } : {}
|
|
19179
|
-
}) : runLogs({ apiUrl: opts.apiUrl, deployment: target })
|
|
19495
|
+
}) : runLogs({ apiUrl: opts.apiUrl, deployment: target, ...opts.app ? { app: opts.app } : {} })
|
|
19180
19496
|
);
|
|
19181
19497
|
program.command("push [branch]").description(
|
|
19182
19498
|
"Push to the kryd remote and follow the deploy it triggers (defaults to the current branch)"
|
|
@@ -19184,12 +19500,12 @@ program.command("push [branch]").description(
|
|
|
19184
19500
|
(branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
|
|
19185
19501
|
);
|
|
19186
19502
|
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(
|
|
19503
|
+
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
19504
|
(project2, deployment, opts) => runRollback({ ...opts, project: project2, deployment })
|
|
19189
19505
|
);
|
|
19190
19506
|
program.command("redeploy [project]").description(
|
|
19191
19507
|
"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 }));
|
|
19508
|
+
).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
19509
|
var project = program.command("project").description("Manage your projects");
|
|
19194
19510
|
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
19511
|
project.command("rm [project]").description(
|
|
@@ -19250,6 +19566,10 @@ workflow.command("init").description(
|
|
|
19250
19566
|
"--language <language>",
|
|
19251
19567
|
`worker language: ${SCAFFOLD_LANGUAGES.join(" | ")}`
|
|
19252
19568
|
).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));
|
|
19569
|
+
var domain2 = program.command("domain").description("Serve the project on your own hostname (custom domains)");
|
|
19570
|
+
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 }));
|
|
19571
|
+
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 }));
|
|
19572
|
+
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
19573
|
function invokedDirectly() {
|
|
19254
19574
|
const entry = process.argv[1];
|
|
19255
19575
|
if (!entry) return false;
|
|
@@ -19278,6 +19598,9 @@ export {
|
|
|
19278
19598
|
runDbRemove,
|
|
19279
19599
|
runDbStatus,
|
|
19280
19600
|
runDeploy,
|
|
19601
|
+
runDomainAdd,
|
|
19602
|
+
runDomainRemove,
|
|
19603
|
+
runDomainStatus,
|
|
19281
19604
|
runEnvList,
|
|
19282
19605
|
runEnvPull,
|
|
19283
19606
|
runEnvRemove,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kryd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|