@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.
Files changed (3) hide show
  1. package/README.md +0 -1
  2. package/dist/index.js +347 -24
  3. package/package.json +3 -11
package/README.md CHANGED
@@ -52,7 +52,6 @@ Your application code runs in the EU, and the AI gateway routes **only** to EU-h
52
52
 
53
53
  - Website: <https://kryd.eu>
54
54
  - Dashboard: <https://app.kryd.eu>
55
- - Issues: <https://github.com/24BEEPS/kryd/issues>
56
55
 
57
56
  ## License
58
57
 
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
- return (await res.json()).deploymentId;
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 deployment = await pollForNewDeployment(
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
- await followDeploy(apiUrl, token, deployment.id, {
17705
- render: renderFor(opts.logs),
17706
- estimateFrom: recent
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 deploy` + (current.tokenExpiresAt ? ` and the token expires ${isoDay(current.tokenExpiresAt)}.
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 deploy; it is never available locally.
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 deploy; it is never available locally.
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 deploy \u2014 your app triggers tasks with it; it is never available locally.
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.9.0" : "0.0.0-dev";
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.9.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-eslint": "0.0.0",
54
- "@kryd/config-ts": "0.0.0"
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",