@kryd/cli 0.10.0 → 0.12.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 +2 -0
- package/dist/index.js +98 -19
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The command-line client for **[Kryd](https://kryd.eu)** — the **European cloud for your AI**. Push a React / Vite / Next.js app and seconds later it's live on a real URL with SSL, one-click managed Postgres and object storage, and an EU-hosted AI gateway already wired in. The same instant `git push → live` loop you know from Vercel/Netlify, on infrastructure that is genuinely European — code **and** AI inference stay inside the EU.
|
|
4
4
|
|
|
5
|
+
**Kryd is in beta.** `kryd login` needs a Kryd account: sign up for a beta account at **[kryd.eu/beta](https://kryd.eu/beta)**.
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```sh
|
package/dist/index.js
CHANGED
|
@@ -15433,7 +15433,7 @@ function parseDomainRecord(body) {
|
|
|
15433
15433
|
function parseProjectDomain(body, label) {
|
|
15434
15434
|
const d = body;
|
|
15435
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)) {
|
|
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) || d.redirectTo !== void 0 && typeof d.redirectTo !== "string") {
|
|
15437
15437
|
throw bad();
|
|
15438
15438
|
}
|
|
15439
15439
|
const records = d.records.map(parseDomainRecord);
|
|
@@ -15443,6 +15443,8 @@ function parseProjectDomain(body, label) {
|
|
|
15443
15443
|
host: d.host,
|
|
15444
15444
|
// KRYD-381a — absent from an older API: then it is `web`, the only app there was.
|
|
15445
15445
|
...typeof d.app === "string" ? { app: d.app } : {},
|
|
15446
|
+
// KRYD-541 — present only on a redirect domain.
|
|
15447
|
+
...typeof d.redirectTo === "string" ? { redirectTo: d.redirectTo } : {},
|
|
15446
15448
|
status: d.status,
|
|
15447
15449
|
detail: d.detail,
|
|
15448
15450
|
checkedAt: d.checkedAt,
|
|
@@ -15469,11 +15471,11 @@ async function listDomains(apiUrl, token, projectId) {
|
|
|
15469
15471
|
domains: body.domains.map((d) => parseProjectDomain(d, "domain-list"))
|
|
15470
15472
|
};
|
|
15471
15473
|
}
|
|
15472
|
-
async function addDomain(apiUrl, token, projectId, host, app) {
|
|
15474
|
+
async function addDomain(apiUrl, token, projectId, host, app, redirectTo) {
|
|
15473
15475
|
const res = await fetch(domainsUrl(apiUrl, projectId), {
|
|
15474
15476
|
method: "POST",
|
|
15475
15477
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
15476
|
-
body: JSON.stringify({ host, ...app ? { app } : {} })
|
|
15478
|
+
body: JSON.stringify({ host, ...app ? { app } : {}, ...redirectTo !== void 0 ? { redirectTo } : {} })
|
|
15477
15479
|
});
|
|
15478
15480
|
if (!res.ok) {
|
|
15479
15481
|
throw new ApiError(`Domain add failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
@@ -15489,6 +15491,37 @@ async function removeDomain(apiUrl, token, projectId, host) {
|
|
|
15489
15491
|
throw new ApiError(`Domain removal failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15490
15492
|
}
|
|
15491
15493
|
}
|
|
15494
|
+
function deployHookUrl(apiUrl, projectId) {
|
|
15495
|
+
return `${apiUrl}/projects/${encodeURIComponent(projectId)}/deploy-hook`;
|
|
15496
|
+
}
|
|
15497
|
+
async function createDeployHook(apiUrl, token, projectId) {
|
|
15498
|
+
const res = await fetch(deployHookUrl(apiUrl, projectId), {
|
|
15499
|
+
method: "PUT",
|
|
15500
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15501
|
+
});
|
|
15502
|
+
if (!res.ok) {
|
|
15503
|
+
throw new ApiError(`Deploy hook creation failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15504
|
+
}
|
|
15505
|
+
const body = await res.json().catch(() => void 0);
|
|
15506
|
+
if (typeof body?.url !== "string" || typeof body.replaced !== "boolean") {
|
|
15507
|
+
throw new Error("Unexpected response from the API (deploy-hook create).");
|
|
15508
|
+
}
|
|
15509
|
+
return { url: body.url, replaced: body.replaced };
|
|
15510
|
+
}
|
|
15511
|
+
async function removeDeployHook(apiUrl, token, projectId) {
|
|
15512
|
+
const res = await fetch(deployHookUrl(apiUrl, projectId), {
|
|
15513
|
+
method: "DELETE",
|
|
15514
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15515
|
+
});
|
|
15516
|
+
if (!res.ok) {
|
|
15517
|
+
throw new ApiError(`Deploy hook removal failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15518
|
+
}
|
|
15519
|
+
const body = await res.json().catch(() => void 0);
|
|
15520
|
+
if (typeof body?.removed !== "boolean") {
|
|
15521
|
+
throw new Error("Unexpected response from the API (deploy-hook rm).");
|
|
15522
|
+
}
|
|
15523
|
+
return { removed: body.removed };
|
|
15524
|
+
}
|
|
15492
15525
|
async function fetchResourceStatus(apiUrl, token, path) {
|
|
15493
15526
|
const res = await fetch(`${apiUrl}${path}`, {
|
|
15494
15527
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -18104,7 +18137,7 @@ async function resolveProjectIdByName(apiUrl, token, name) {
|
|
|
18104
18137
|
const matches = projects.filter((p) => p.name === name);
|
|
18105
18138
|
if (matches.length === 0) {
|
|
18106
18139
|
process.stderr.write(
|
|
18107
|
-
`No project named ${name} on this account. Run \`kryd project list\`
|
|
18140
|
+
`No project named ${name} on this account. Run \`kryd project list\` and use the NAME column, not SUBDOMAIN.
|
|
18108
18141
|
`
|
|
18109
18142
|
);
|
|
18110
18143
|
process.exitCode = 1;
|
|
@@ -18600,22 +18633,22 @@ async function runDomainAdd(opts) {
|
|
|
18600
18633
|
if (!ctx) return;
|
|
18601
18634
|
try {
|
|
18602
18635
|
const [domain3, list] = await Promise.all([
|
|
18603
|
-
addDomain(ctx.apiUrl, ctx.token, ctx.project, opts.host, opts.app),
|
|
18636
|
+
addDomain(ctx.apiUrl, ctx.token, ctx.project, opts.host, opts.app, opts.redirect),
|
|
18604
18637
|
listDomains(ctx.apiUrl, ctx.token, ctx.project).catch(() => null)
|
|
18605
18638
|
]);
|
|
18606
18639
|
if (domain3.status !== "pending") {
|
|
18607
18640
|
process.stdout.write(
|
|
18608
|
-
`${domain3.host} is already on ${ctx.project}${
|
|
18641
|
+
`${domain3.host} is already on ${ctx.project}${domainRole(domain3)}: ${DOMAIN_STATUS_TEXT[domain3.status]}.
|
|
18609
18642
|
`
|
|
18610
18643
|
);
|
|
18611
18644
|
return;
|
|
18612
18645
|
}
|
|
18613
18646
|
process.stdout.write(
|
|
18614
|
-
`${domain3.host} added to ${ctx.project}${
|
|
18647
|
+
`${domain3.host} added to ${ctx.project}${domainRole(domain3)} \u2014 pending until its DNS records exist.
|
|
18615
18648
|
|
|
18616
18649
|
Create these records at your DNS provider:
|
|
18617
18650
|
` + 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
|
|
18651
|
+
Kryd checks every minute; once they are right it issues a certificate and ` + (domain3.redirectTo ? `answers every request on ${domain3.host} with a permanent redirect (301) to https://${domain3.redirectTo}, path and query kept.` : `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
18652
|
` + (list?.projectHost ? `${ctx.project} keeps its own host, ${list.projectHost}.
|
|
18620
18653
|
` : "")
|
|
18621
18654
|
);
|
|
@@ -18623,9 +18656,43 @@ Kryd checks every minute; once they are right it issues a certificate and serves
|
|
|
18623
18656
|
await reportError(err);
|
|
18624
18657
|
}
|
|
18625
18658
|
}
|
|
18659
|
+
async function runDeployHookCreate(opts) {
|
|
18660
|
+
const ctx = domainContext(opts, "kryd project deploy-hook create --project <projectId>");
|
|
18661
|
+
if (!ctx) return;
|
|
18662
|
+
try {
|
|
18663
|
+
const { url: url2, replaced } = await createDeployHook(ctx.apiUrl, ctx.token, ctx.project);
|
|
18664
|
+
process.stdout.write(
|
|
18665
|
+
`Deploy hook for ${ctx.project}${replaced ? " \u2014 replaces the previous one, which no longer works" : ""}:
|
|
18666
|
+
|
|
18667
|
+
${url2}
|
|
18668
|
+
|
|
18669
|
+
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.
|
|
18670
|
+
`
|
|
18671
|
+
);
|
|
18672
|
+
} catch (err) {
|
|
18673
|
+
await reportError(err);
|
|
18674
|
+
}
|
|
18675
|
+
}
|
|
18676
|
+
async function runDeployHookRemove(opts) {
|
|
18677
|
+
const ctx = domainContext(opts, "kryd project deploy-hook rm --project <projectId>");
|
|
18678
|
+
if (!ctx) return;
|
|
18679
|
+
try {
|
|
18680
|
+
const { removed } = await removeDeployHook(ctx.apiUrl, ctx.token, ctx.project);
|
|
18681
|
+
process.stdout.write(
|
|
18682
|
+
removed ? `Deploy hook of ${ctx.project} removed \u2014 its URL no longer works.
|
|
18683
|
+
` : `${ctx.project} has no deploy hook \u2014 nothing to remove.
|
|
18684
|
+
`
|
|
18685
|
+
);
|
|
18686
|
+
} catch (err) {
|
|
18687
|
+
await reportError(err);
|
|
18688
|
+
}
|
|
18689
|
+
}
|
|
18626
18690
|
function appSuffix(app) {
|
|
18627
18691
|
return app && app !== "web" ? ` (app ${app})` : "";
|
|
18628
18692
|
}
|
|
18693
|
+
function domainRole(d) {
|
|
18694
|
+
return d.redirectTo ? ` \u2192 redirects to ${d.redirectTo}` : appSuffix(d.app);
|
|
18695
|
+
}
|
|
18629
18696
|
async function runDomainRemove(opts) {
|
|
18630
18697
|
const ctx = domainContext(opts, "kryd domain remove <host> --project <projectId>");
|
|
18631
18698
|
if (!ctx) return;
|
|
@@ -18665,7 +18732,7 @@ ${own}`);
|
|
|
18665
18732
|
let out2 = `${ctx.project}: ${list.domains.length} custom domain${list.domains.length === 1 ? "" : "s"}.
|
|
18666
18733
|
`;
|
|
18667
18734
|
for (const d2 of list.domains) {
|
|
18668
|
-
out2 += ` ${d2.host} ${DOMAIN_STATUS_TEXT[d2.status]}
|
|
18735
|
+
out2 += ` ${d2.host} ${DOMAIN_STATUS_TEXT[d2.status]}${d2.redirectTo ? ` \u2192 redirects to ${d2.redirectTo}` : ""}
|
|
18669
18736
|
`;
|
|
18670
18737
|
if (d2.detail) out2 += ` ${d2.detail}
|
|
18671
18738
|
`;
|
|
@@ -18682,6 +18749,8 @@ ${own}`);
|
|
|
18682
18749
|
return;
|
|
18683
18750
|
}
|
|
18684
18751
|
let out = `${d.host}: ${DOMAIN_STATUS_TEXT[d.status]}.
|
|
18752
|
+
`;
|
|
18753
|
+
if (d.redirectTo) out += ` \u2192 redirects to ${d.redirectTo}
|
|
18685
18754
|
`;
|
|
18686
18755
|
if (d.detail) out += ` ${d.detail}
|
|
18687
18756
|
`;
|
|
@@ -19034,15 +19103,17 @@ async function runProjectList(opts) {
|
|
|
19034
19103
|
return;
|
|
19035
19104
|
}
|
|
19036
19105
|
const linkedId = resolveProjectId(void 0, opts.cwd);
|
|
19037
|
-
const nameWidth = projects.reduce((max, p) => Math.max(max, p.name.length),
|
|
19038
|
-
const subWidth = projects.reduce(
|
|
19039
|
-
|
|
19106
|
+
const nameWidth = projects.reduce((max, p) => Math.max(max, p.name.length), "NAME".length);
|
|
19107
|
+
const subWidth = projects.reduce(
|
|
19108
|
+
(max, p) => Math.max(max, p.subdomain.length),
|
|
19109
|
+
"SUBDOMAIN".length
|
|
19110
|
+
);
|
|
19111
|
+
const statusWidth = projects.reduce((max, p) => Math.max(max, p.status.length), "STATUS".length);
|
|
19112
|
+
const row = (mark, name, sub, status, id) => `${mark} ${name.padEnd(nameWidth)} ${sub.padEnd(subWidth)} ${status.padEnd(statusWidth)} ${id}
|
|
19113
|
+
`;
|
|
19114
|
+
process.stdout.write(row(" ", "NAME", "SUBDOMAIN", "STATUS", "ID"));
|
|
19040
19115
|
for (const p of projects) {
|
|
19041
|
-
|
|
19042
|
-
process.stdout.write(
|
|
19043
|
-
`${mark} ${p.name.padEnd(nameWidth)} ${p.subdomain.padEnd(subWidth)} ${p.status.padEnd(statusWidth)} ${p.id}
|
|
19044
|
-
`
|
|
19045
|
-
);
|
|
19116
|
+
process.stdout.write(row(p.id === linkedId ? "*" : " ", p.name, p.subdomain, p.status, p.id));
|
|
19046
19117
|
}
|
|
19047
19118
|
if (projects.some((p) => p.id === linkedId)) {
|
|
19048
19119
|
process.stdout.write("\n* linked to this directory\n");
|
|
@@ -19437,7 +19508,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
19437
19508
|
await reportError(err);
|
|
19438
19509
|
}
|
|
19439
19510
|
}
|
|
19440
|
-
var CLI_VERSION = true ? "0.
|
|
19511
|
+
var CLI_VERSION = true ? "0.12.0" : "0.0.0-dev";
|
|
19441
19512
|
var program = new Command();
|
|
19442
19513
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
19443
19514
|
program.hook("postAction", async () => {
|
|
@@ -19511,6 +19582,9 @@ project.command("list").description("List your projects \u2014 name, subdomain,
|
|
|
19511
19582
|
project.command("rm [project]").description(
|
|
19512
19583
|
"Permanently delete a project and everything Kryd created for it (by name or id; asks you to type its name)"
|
|
19513
19584
|
).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 }));
|
|
19585
|
+
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)");
|
|
19586
|
+
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));
|
|
19587
|
+
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));
|
|
19514
19588
|
var db = program.command("db").description("Manage project databases");
|
|
19515
19589
|
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(
|
|
19516
19590
|
"--connection-string <url>",
|
|
@@ -19567,7 +19641,10 @@ workflow.command("init").description(
|
|
|
19567
19641
|
`worker language: ${SCAFFOLD_LANGUAGES.join(" | ")}`
|
|
19568
19642
|
).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
19643
|
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(
|
|
19644
|
+
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(
|
|
19645
|
+
"--redirect <host>",
|
|
19646
|
+
"redirect instead of serving: every request answers 301 to https://<host>, path and query kept"
|
|
19647
|
+
).option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainAdd({ ...opts, host }));
|
|
19571
19648
|
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
19649
|
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 }));
|
|
19573
19650
|
function invokedDirectly() {
|
|
@@ -19598,6 +19675,8 @@ export {
|
|
|
19598
19675
|
runDbRemove,
|
|
19599
19676
|
runDbStatus,
|
|
19600
19677
|
runDeploy,
|
|
19678
|
+
runDeployHookCreate,
|
|
19679
|
+
runDeployHookRemove,
|
|
19601
19680
|
runDomainAdd,
|
|
19602
19681
|
runDomainRemove,
|
|
19603
19682
|
runDomainStatus,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kryd/cli",
|
|
3
|
-
"version": "0.
|
|
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.",
|
|
3
|
+
"version": "0.12.0",
|
|
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. Kryd is in beta: sign up for a beta account at https://kryd.eu/beta.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kryd",
|
|
7
7
|
"deploy",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"tsx": "^4.19.2",
|
|
42
42
|
"typescript": "^5.6.3",
|
|
43
43
|
"vitest": "^2.1.8",
|
|
44
|
+
"@kryd/config-eslint": "0.0.0",
|
|
44
45
|
"@kryd/shared-types": "0.0.0",
|
|
45
|
-
"@kryd/config-ts": "0.0.0"
|
|
46
|
-
"@kryd/config-eslint": "0.0.0"
|
|
46
|
+
"@kryd/config-ts": "0.0.0"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --external:commander --define:__KRYD_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/index.js",
|