@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +98 -19
  3. 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\` to see the names.
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}${appSuffix(domain3.app)}: ${DOMAIN_STATUS_TEXT[domain3.status]}.
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}${appSuffix(domain3.app)} \u2014 pending until its DNS records exist.
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. Follow it with \`kryd domain status ${domain3.host}\`.
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), 0);
19038
- const subWidth = projects.reduce((max, p) => Math.max(max, p.subdomain.length), 0);
19039
- const statusWidth = projects.reduce((max, p) => Math.max(max, p.status.length), 0);
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
- const mark = p.id === linkedId ? "*" : " ";
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.10.0" : "0.0.0-dev";
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("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainAdd({ ...opts, host }));
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.10.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",