@kryd/cli 0.17.0 → 0.19.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 +21 -0
  2. package/dist/index.js +425 -113
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -26,6 +26,8 @@ kryd logs <project> --worker # tail the workflow worker's stdout/stderr
26
26
 
27
27
  Once you've run `kryd init` in a directory, the commands below work **arg-less** there — the folder is linked via `.kryd/project.json` (git-ignored, no secret).
28
28
 
29
+ `kryd init` and `kryd workflow init` also write a short **Kryd block into `AGENTS.md`** — the file Codex, Cursor, Copilot and Claude Code read — so a coding agent knows the rules before it breaks one: listen on `$PORT`, which env vars Kryd injects, keep the AI gateway token on the server, make workflow tasks safe to run twice, and the CLI's commands (generated from the CLI itself). It sits between `<!-- kryd:begin … -->` and `<!-- kryd:end -->`; a later run replaces only what is between those markers and leaves the rest of your file alone. After updating the CLI, `kryd init --agents` refreshes the block in a linked project (no login needed) — also the way to add it to a project linked before the block existed. If the folder has a `CLAUDE.md`, one `@AGENTS.md` line is added to it so Claude Code reads the block too.
30
+
29
31
  ## Commands
30
32
 
31
33
  | Command | What it does |
@@ -33,6 +35,7 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
33
35
  | `kryd login` / `logout` / `whoami` | Authenticate the CLI (browser flow); the token is stored in `~/.kryd`. `whoami` shows the account commands run in, your role there, and your other accounts. |
34
36
  | `kryd account list \| use <slug>` | You can belong to several accounts (your own, a customer's as a member). `list` shows them with your role in each, `*` marking the current one; `use` makes every following command run in that account. |
35
37
  | `kryd init` | Link the current repo to a Kryd project + register the deploy webhook + add a `kryd` git remote. Not logged in and at a terminal? It asks, then runs the same browser login as `kryd login`; without a terminal it refuses (a deploy token, `KRYD_TOKEN`, cannot create a project). |
38
+ | `kryd init --agents` | Only write or refresh the Kryd block in `AGENTS.md` of the linked project (walks up to the link), plus the `@AGENTS.md` line in a `CLAUDE.md`. No login, no API call. In a linked folder a plain `kryd init` refuses and names this. |
36
39
  | `kryd push [branch]` | Push to the `kryd` remote and follow the deploy it triggers (defaults to the current branch). Shows which step the deploy is on, how long it has taken and a progress bar; `--logs` streams the full build log instead. |
37
40
  | `kryd deploy [project]` | Re-deploy the production-branch HEAD already on the forge — no new commit. |
38
41
  | `kryd logs [target]` | Follow a deploy's build/deploy log **in full** — this is where the build output lives (`--runtime` tails the live container instead; `--worker` tails the project's workflow worker, also when it has a web app). |
@@ -41,6 +44,7 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
41
44
  | `kryd storage add \| remove \| status [project]` | Attach, tear down or inspect S3-compatible object storage. `remove` destroys every object and asks first. |
42
45
  | `kryd env list \| set \| rm [project]` | Manage your own environment variables. `set KEY --stdin` (or a prompt) keeps a secret out of `ps` and your shell history; values are never printed back. Add `--build` for build-time variables (`VITE_*`, `NEXT_PUBLIC_*`) — these are compiled into your public bundle, so they must never be secrets, and they take effect at your next build rather than your next deploy. |
43
46
  | `kryd ai add \| remove \| status [project]` | Give the project an authenticated EU AI-gateway endpoint (injected on the next deploy), take it away again, or show what it has. |
47
+ | `kryd ai models` | List the AI models Kryd offers right now — the same catalogue the dashboard's AI tab shows, in the same order (popular ones marked `*`). A request's `model` field carries the `MODEL` value. The list is the same for every account, but the call runs in your current account; needs a login, no project. |
44
48
  | `kryd workflow add \| remove \| status [project]` | Give the project its own durable-workflow tenant — `HATCHET_CLIENT_TOKEN` is injected on the next deploy — revoke that token again, or show whether it has one and when it expires. `remove` keeps the tenant's history, crons and schedules until the project is deleted. |
45
49
  | `kryd workflow init --language <typescript\|python\|go>` | Scaffold a workflow worker here and link it as a new project. The worker ships with the small HTTP health listener Kryd's deploy gate needs — without it a deploy is torn down with "nothing listening yet" — plus a `kryd.json` that tells `kryd init` what this project is. `--no-link` writes the files only. |
46
50
  | `kryd token create \| list \| revoke` | Deploy tokens for CI and scripts. `create <name> [--expires 30\|90\|365\|never]` (default 90 days) prints the token once; it acts as you, with your role in the **current account**, is bound to that one account for good, and cannot create tokens itself. Managing tokens needs `kryd login`. A token can do what you can on your projects — read env values and push — so after a leak, revoke it and also rotate the secrets it could read. |
@@ -53,6 +57,23 @@ kryd support new --type bug --subject "Deploy hangs on build" --message "Every d
53
57
  kryd support show T-1042 --json
54
58
  ```
55
59
 
60
+ **`--json` on the read commands.** `kryd project list`, `whoami`, `db status`, `storage status`, `ai status`, `ai models`, `workflow status`, `domain status [host]` and `env list` take `--json` too, with the same contract: exactly one JSON document on stdout; an error is text on stderr with exit 1 and an empty stdout. "Nothing attached" is an answer, not an error (exit 0). The shapes:
61
+
62
+ | Command | `--json` prints |
63
+ |---|---|
64
+ | `kryd project list` | `{ items }`: each project as the API lists it (`id`, `name`, `subdomain`, `framework`, `repoUrl`, `productionBranch`, `previewsPublic`, `status`, `createdAt`, `updatedAt`, …) plus `linked` — `true` for the project this folder is linked to |
65
+ | `kryd whoami` | the `GET /me` answer as is: you, the account commands run in, your role and your `memberships` |
66
+ | `kryd db status` | `{ production, preview }`, each `{ attached, status, failureReason }`. When production is not attached the preview is not read, and is reported as not attached |
67
+ | `kryd storage status` | `{ attached, status, failureReason }` |
68
+ | `kryd ai status` | `{ enabled, url }` |
69
+ | `kryd ai models` | `{ items }`: the AI catalogue rows as the API returns them (`id`, `model`, `provider`, `tasks`, `stage`, `popular`, `sortOrder`), in its order. An empty catalogue is `{ "items": [] }` |
70
+ | `kryd workflow status` | `{ enabled, tokenExpiresAt, tokenExpiresInDays }` — both `null` without an expiry; the days are negative once the token has expired |
71
+ | `kryd domain status` | `{ projectHost, domains }`; each domain is `{ id, host, app?, redirectTo?, status, detail, checkedAt, createdAt, records }`, with `status` the raw value (`pending`, `verified`, `active`, `failed`), not the text-mode wording |
72
+ | `kryd domain status <host>` | that one domain, the same fields, its DNS `records` included |
73
+ | `kryd env list` | `{ items }`, each `{ key, source, scope, updatedAt, environments? }` — names and origin, never values |
74
+
75
+ `kryd token list --json` and `kryd account list --json` print a bare array.
76
+
56
77
  In CI, no `kryd login` and no config file: set a deploy token as the secret `KRYD_TOKEN` and every command uses it (it wins over a stored login).
57
78
 
58
79
  ```sh
package/dist/index.js CHANGED
@@ -14989,14 +14989,14 @@ function isTerminalResourceStatus(status) {
14989
14989
 
14990
14990
  // src/index.ts
14991
14991
  import {
14992
- existsSync as existsSync3,
14992
+ existsSync as existsSync4,
14993
14993
  mkdirSync as mkdirSync2,
14994
- readFileSync as readFileSync3,
14995
- realpathSync,
14994
+ readFileSync as readFileSync4,
14995
+ realpathSync as realpathSync2,
14996
14996
  rmSync as rmSync2,
14997
- writeFileSync as writeFileSync2
14997
+ writeFileSync as writeFileSync3
14998
14998
  } from "node:fs";
14999
- import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "node:path";
14999
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join4, relative, resolve as resolve2 } from "node:path";
15000
15000
  import { pathToFileURL } from "node:url";
15001
15001
 
15002
15002
  // src/config.ts
@@ -15364,6 +15364,28 @@ async function getAiStatus(apiUrl, token, projectId) {
15364
15364
  }
15365
15365
  return { enabled: parsed.enabled, url: parsed.url };
15366
15366
  }
15367
+ async function getAiModels(apiUrl, token) {
15368
+ const res = await fetch(`${apiUrl}/ai-models`, {
15369
+ headers: { ...authHeaders(token) }
15370
+ });
15371
+ if (!res.ok) {
15372
+ throw new ApiError(
15373
+ `AI catalogue read failed (${res.status})`,
15374
+ await parseEnvelope(res),
15375
+ res.status
15376
+ );
15377
+ }
15378
+ const parsed = await res.json().catch(() => void 0);
15379
+ const isRow = (row) => {
15380
+ if (!row || typeof row !== "object") return false;
15381
+ const r = row;
15382
+ return typeof r.model === "string" && typeof r.provider === "string" && Array.isArray(r.tasks) && r.tasks.every((t) => typeof t === "string") && typeof r.stage === "string" && typeof r.popular === "boolean";
15383
+ };
15384
+ if (!Array.isArray(parsed) || !parsed.every(isRow)) {
15385
+ throw new ApiError("The API returned an unexpected AI-catalogue response.");
15386
+ }
15387
+ return parsed;
15388
+ }
15367
15389
  async function removeAi(apiUrl, token, projectId) {
15368
15390
  const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15369
15391
  method: "DELETE",
@@ -16796,9 +16818,125 @@ function parseCredentialInput(raw) {
16796
16818
  return out;
16797
16819
  }
16798
16820
 
16821
+ // src/agents-md.ts
16822
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
16823
+ import { join as join2 } from "node:path";
16824
+ var AGENTS_FILE = "AGENTS.md";
16825
+ var CLAUDE_FILE = "CLAUDE.md";
16826
+ var BEGIN_MARKER = "<!-- kryd:begin \u2014 written by the kryd CLI; edits inside are replaced on the next run -->";
16827
+ var END_MARKER = "<!-- kryd:end -->";
16828
+ var BEGIN_LINE = /^<!-- kryd:begin/gm;
16829
+ var END_LINE = /^<!-- kryd:end/gm;
16830
+ var AGENTS_PROSE = `## Deploying on Kryd
16831
+
16832
+ This project deploys to Kryd, a European cloud platform. Use what it already provides instead of adding another service:
16833
+
16834
+ - **Git is the deploy.** The repository lives on Kryd's own git server in the EU (remote \`kryd\`). \`git push kryd <branch>\` or \`kryd push\` builds and deploys; every other branch gets its own preview URL. Don't add CI deploy pipelines, hosting Dockerfiles or another platform's config.
16835
+ - **Database:** managed Postgres \u2014 \`kryd db add\`, then use \`DATABASE_URL\`. Preview deploys can get a database of their own: \`kryd db add --preview\`. Don't add a database container or SQLite for production data.
16836
+ - **File storage:** S3-compatible object storage \u2014 \`kryd storage add\`.
16837
+ - **AI:** an EU-hosted, OpenAI-compatible gateway with several models \u2014 \`kryd ai add\`; list the current models with \`kryd ai models\`. Don't add provider API keys.
16838
+ - **Long-running or background work** \u2014 jobs, retries, schedules \u2014 belongs in a workflow (\`kryd workflow init\`), not in a request handler, a timer loop or a separate queue.
16839
+ - **Incoming webhooks** (Stripe, GitHub, \u2026) go to a workflow webhook, created in the Kryd dashboard under Workflows, so every delivery is stored and retried instead of handled inside a web request.
16840
+ - **Your own domain:** \`kryd domain add <host>\` prints the DNS records to create; the HTTPS certificate is automatic.
16841
+ - Everything runs in the EU; keep data and model calls there.
16842
+
16843
+ ### Rules
16844
+
16845
+ - **Listen on \`$PORT\`.** The platform sets it; never hardcode a port. An app that listens anywhere else fails the health check, and its deploy never goes live.
16846
+ - **The project link** is \`.kryd/project.json\` (per developer, gitignored). Commands find it by walking up to the git root, so they need no project id inside this repository.
16847
+ - **Injected env vars \u2014 never set these yourself:** \`DATABASE_URL\`; \`AI_GATEWAY_URL\` (already ends in \`/v1\`) and \`AI_GATEWAY_TOKEN\`; \`HATCHET_CLIENT_TOKEN\`; and for storage \`AWS_ACCESS_KEY_ID\`, \`AWS_SECRET_ACCESS_KEY\`, \`AWS_REGION\`, \`AWS_ENDPOINT_URL_S3\`, \`BUCKET_NAME\`. Kryd injects each one once its resource is added (\`kryd db add\`, \`kryd ai add\`, \`kryd workflow add\`, \`kryd storage add\`).
16848
+ - **Call the AI gateway from server code only.** \`AI_GATEWAY_TOKEN\` must never reach a browser.
16849
+ - **Workflows:** \`kryd workflow init --language typescript|python|go\` scaffolds a worker, declared in \`kryd.json\` (\`"web": false\` = the project is a worker only). Make every task safe to run twice: a worker stops within seconds on SIGTERM, and the engine re-runs the task.
16850
+ - **Login:** without a terminal there is no login \u2014 ask the user to run \`kryd init\` or \`kryd login\`. A deploy token (\`KRYD_TOKEN\`) can deploy but cannot create a project.
16851
+ - **Destructive commands** ask for confirmation; without a terminal they need \`--yes\`.
16852
+ - **Read commands take \`--json\`** for one JSON document on stdout \u2014 \`kryd project list --json\`, \`kryd whoami --json\`, \`kryd db status --json\`, \`kryd storage status --json\`, \`kryd ai status --json\`, \`kryd ai models --json\`, \`kryd workflow status --json\`, \`kryd domain status --json\`, \`kryd env list --json\`. "Nothing attached" is an answer (exit 0); an error is text on stderr with exit 1 and an empty stdout.
16853
+ `;
16854
+ var AGENTS_FOOTER = "Run `kryd <command> --help` for details. After updating the CLI, refresh this block with `kryd init --agents`.\n";
16855
+ function isHidden(cmd) {
16856
+ return cmd._hidden === true;
16857
+ }
16858
+ function leafCommands(program2) {
16859
+ const out = [];
16860
+ const walk = (cmd, prefix) => {
16861
+ if (isHidden(cmd)) return;
16862
+ const path = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
16863
+ if (cmd.commands.length > 0) {
16864
+ for (const child of cmd.commands) walk(child, path);
16865
+ return;
16866
+ }
16867
+ const args = cmd.registeredArguments.map((a) => {
16868
+ const inner = `${a.name()}${a.variadic ? "..." : ""}`;
16869
+ return a.required ? `<${inner}>` : `[${inner}]`;
16870
+ });
16871
+ out.push({ path, usage: [path, ...args].join(" "), description: cmd.description() });
16872
+ };
16873
+ for (const child of program2.commands) walk(child, "");
16874
+ return out;
16875
+ }
16876
+ function renderAgentsBlock(program2) {
16877
+ const commands = leafCommands(program2).map((c) => `- \`kryd ${c.usage}\` \u2014 ${c.description}
16878
+ `).join("");
16879
+ return `${BEGIN_MARKER}
16880
+ ` + AGENTS_PROSE + "\n### Commands\n\n" + commands + "\n" + AGENTS_FOOTER + `${END_MARKER}
16881
+ `;
16882
+ }
16883
+ function lineStartsMatching(text, re) {
16884
+ return [...text.matchAll(re)].map((m) => m.index);
16885
+ }
16886
+ function writeAgentsBlock(dir, block) {
16887
+ const path = join2(dir, AGENTS_FILE);
16888
+ if (!existsSync2(path)) {
16889
+ writeFileSync2(path, block);
16890
+ return "created";
16891
+ }
16892
+ const text = readFileSync2(path, "utf8");
16893
+ const begins = lineStartsMatching(text, BEGIN_LINE);
16894
+ const ends = lineStartsMatching(text, END_LINE);
16895
+ let next;
16896
+ if (begins.length === 0 && ends.length === 0) {
16897
+ let base = text;
16898
+ if (base.length > 0) {
16899
+ if (!base.endsWith("\n")) base += "\n";
16900
+ if (!base.endsWith("\n\n")) base += "\n";
16901
+ }
16902
+ next = base + block;
16903
+ } else {
16904
+ const [begin] = begins;
16905
+ const [end] = ends;
16906
+ if (begins.length !== 1 || ends.length !== 1 || begin === void 0 || end === void 0 || end < begin || // An end marker that is not exactly ours (say `<!-- kryd:end` without its `-->`) would make
16907
+ // the slice run on into the customer's text.
16908
+ !text.startsWith(END_MARKER, end)) {
16909
+ throw new Error(
16910
+ `${AGENTS_FILE} has broken kryd markers (a begin without an end, or one of them twice) \u2014 left it untouched; fix or remove the markers and run this again`
16911
+ );
16912
+ }
16913
+ const endOfMarker = end + END_MARKER.length;
16914
+ next = text.slice(0, begin) + block.replace(/\n$/, "") + text.slice(endOfMarker);
16915
+ }
16916
+ if (next === text) return "unchanged";
16917
+ writeFileSync2(path, next);
16918
+ return "updated";
16919
+ }
16920
+ function ensureClaudeImport(dir) {
16921
+ const path = join2(dir, CLAUDE_FILE);
16922
+ if (!existsSync2(path)) return false;
16923
+ const agents = join2(dir, AGENTS_FILE);
16924
+ if (existsSync2(agents) && realpathSync(path) === realpathSync(agents)) return false;
16925
+ const text = readFileSync2(path, "utf8");
16926
+ if (/^[ \t]*@(\.\/)?AGENTS\.md[ \t]*\r?$/m.test(text)) return false;
16927
+ let base = text;
16928
+ if (base.length > 0) {
16929
+ if (!base.endsWith("\n")) base += "\n";
16930
+ if (!base.endsWith("\n\n")) base += "\n";
16931
+ }
16932
+ writeFileSync2(path, `${base}@AGENTS.md
16933
+ `);
16934
+ return true;
16935
+ }
16936
+
16799
16937
  // src/framework.ts
16800
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
16801
- import { basename, join as join2 } from "node:path";
16938
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
16939
+ import { basename, join as join3 } from "node:path";
16802
16940
  var FRAMEWORKS = [
16803
16941
  "react-router",
16804
16942
  "nextjs",
@@ -16853,7 +16991,7 @@ function startScriptLooksLikePlainNode(start) {
16853
16991
  }
16854
16992
  function readPackageJson(cwd) {
16855
16993
  try {
16856
- return JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
16994
+ return JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
16857
16995
  } catch {
16858
16996
  return null;
16859
16997
  }
@@ -16861,7 +16999,7 @@ function readPackageJson(cwd) {
16861
16999
  function readDeclaration(cwd) {
16862
17000
  let parsed;
16863
17001
  try {
16864
- parsed = JSON.parse(readFileSync2(join2(cwd, DECLARATION_FILE), "utf8"));
17002
+ parsed = JSON.parse(readFileSync3(join3(cwd, DECLARATION_FILE), "utf8"));
16865
17003
  } catch {
16866
17004
  return null;
16867
17005
  }
@@ -16881,7 +17019,7 @@ function frameworkFrom(pkg, cwd) {
16881
17019
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
16882
17020
  const has = (name) => name in deps;
16883
17021
  if (has("next")) return "nextjs";
16884
- if (has("@react-router/dev") || existsSync2(join2(cwd, "react-router.config.ts")) || existsSync2(join2(cwd, "react-router.config.js"))) {
17022
+ if (has("@react-router/dev") || existsSync3(join3(cwd, "react-router.config.ts")) || existsSync3(join3(cwd, "react-router.config.js"))) {
16885
17023
  return "react-router";
16886
17024
  }
16887
17025
  if (VITE_META_FRAMEWORKS.some(has)) return null;
@@ -18032,6 +18170,10 @@ function reportNotLinked(example) {
18032
18170
  );
18033
18171
  process.exitCode = 1;
18034
18172
  }
18173
+ function printJson2(value) {
18174
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
18175
+ `);
18176
+ }
18035
18177
  async function runLogin(opts) {
18036
18178
  const apiUrl = resolveApiUrl(opts.apiUrl);
18037
18179
  const pastedToken = opts.token ?? process.env.KRYD_TOKEN;
@@ -18055,6 +18197,10 @@ async function runWhoami(opts = {}) {
18055
18197
  const token = resolveToken() ?? "";
18056
18198
  try {
18057
18199
  const me = await whoami(apiUrl, token);
18200
+ if (opts.json) {
18201
+ printJson2(me);
18202
+ return;
18203
+ }
18058
18204
  const current = me.memberships?.find((m) => m.accountId === me.accountId);
18059
18205
  const label = current?.slug ?? me.accountSlug ?? me.accountId ?? "?";
18060
18206
  process.stdout.write(`${me.email} (account ${label}, role ${current?.role ?? me.role ?? "?"})
@@ -18082,48 +18228,110 @@ function runLogout() {
18082
18228
  process.stdout.write("Logged out.\n");
18083
18229
  warnIfEnvTokenOverrides();
18084
18230
  }
18085
- async function runInit(opts) {
18086
- let apiUrl = resolveApiUrl(opts.apiUrl);
18087
- const cwd = opts.cwd ?? process.cwd();
18088
- let token = resolveToken();
18089
- if (!token) {
18090
- const refusal = "Not logged in \u2014 run `kryd login` first.\n";
18091
- const io = opts.io ?? defaultPromptIO();
18092
- if (opts.offerLogin === false) {
18093
- process.stderr.write(refusal);
18094
- process.exitCode = 1;
18095
- return;
18096
- }
18097
- if (!isInteractive(io)) {
18098
- process.stderr.write(
18099
- refusal + // KRYD-385 iteration 1: no KRYD_TOKEN advice — `POST /repos` refuses a deploy token (KRYD-578).
18100
- "No terminal to log in from: ask the user to run `kryd init` (or `kryd login`) in their own terminal. A deploy token (KRYD_TOKEN) cannot create a project.\n"
18101
- );
18102
- process.exitCode = 1;
18103
- return;
18231
+ function installAgentsBlock(dir) {
18232
+ let note = "";
18233
+ const changed = [];
18234
+ let result;
18235
+ try {
18236
+ result = writeAgentsBlock(dir, renderAgentsBlock(program));
18237
+ if (result !== "unchanged") changed.push(AGENTS_FILE);
18238
+ if (result === "created") {
18239
+ note += "Wrote AGENTS.md \u2014 what coding agents need to know to deploy this project on Kryd.\n";
18240
+ } else if (result === "updated") {
18241
+ note += "Updated the Kryd block in AGENTS.md.\n";
18104
18242
  }
18105
- io.output.write("You are not logged in to Kryd; `kryd init` can log you in through your browser first.\n");
18106
- if (!await promptConfirm("Log in now in your browser?", io)) {
18107
- process.stderr.write(refusal);
18108
- process.exitCode = 1;
18109
- return;
18243
+ } catch (err) {
18244
+ process.stderr.write(
18245
+ `warning: could not write the Kryd block to AGENTS.md (${err instanceof Error ? err.message : String(err)}) \u2014 everything else worked; coding agents just will not see it.
18246
+ `
18247
+ );
18248
+ return { note, block: "failed", changed };
18249
+ }
18250
+ try {
18251
+ if (ensureClaudeImport(dir)) {
18252
+ changed.push(CLAUDE_FILE);
18253
+ note += "Added `@AGENTS.md` to CLAUDE.md, so Claude Code reads that block too.\n";
18110
18254
  }
18111
- try {
18112
- await browserLoginAndSave(apiUrl);
18113
- } catch (err) {
18114
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}
18255
+ } catch (err) {
18256
+ process.stderr.write(
18257
+ `warning: could not add \`@AGENTS.md\` to CLAUDE.md (${err instanceof Error ? err.message : String(err)}) \u2014 add that line yourself so Claude Code reads AGENTS.md.
18258
+ `
18259
+ );
18260
+ }
18261
+ return { note, block: result, changed };
18262
+ }
18263
+ async function settleLogin(opts) {
18264
+ let token = resolveToken();
18265
+ if (token) return { token, apiUrl: opts.apiUrl };
18266
+ const refusal = "Not logged in \u2014 run `kryd login` first.\n";
18267
+ const io = opts.io ?? defaultPromptIO();
18268
+ if (!opts.offerLogin) {
18269
+ process.stderr.write(refusal);
18270
+ process.exitCode = 1;
18271
+ return null;
18272
+ }
18273
+ if (!isInteractive(io)) {
18274
+ process.stderr.write(
18275
+ refusal + // KRYD-385 iteration 1: no KRYD_TOKEN advice — `POST /repos` refuses a deploy token (KRYD-578).
18276
+ `No terminal to log in from: ask the user to run \`${opts.command}\` (or \`kryd login\`) in their own terminal. A deploy token (KRYD_TOKEN) cannot create a project.
18277
+ `
18278
+ );
18279
+ process.exitCode = 1;
18280
+ return null;
18281
+ }
18282
+ io.output.write(`You are not logged in to Kryd; \`${opts.command}\` can log you in through your browser first.
18115
18283
  `);
18116
- process.exitCode = 1;
18117
- return;
18118
- }
18119
- token = resolveToken();
18120
- if (!token) {
18121
- process.stderr.write(refusal);
18122
- process.exitCode = 1;
18123
- return;
18124
- }
18125
- if (!opts.apiUrl) apiUrl = resolveApiUrl();
18284
+ if (!await promptConfirm("Log in now in your browser?", io)) {
18285
+ process.stderr.write(refusal);
18286
+ process.exitCode = 1;
18287
+ return null;
18288
+ }
18289
+ try {
18290
+ await browserLoginAndSave(opts.apiUrl);
18291
+ } catch (err) {
18292
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
18293
+ `);
18294
+ process.exitCode = 1;
18295
+ return null;
18296
+ }
18297
+ token = resolveToken();
18298
+ if (!token) {
18299
+ process.stderr.write(refusal);
18300
+ process.exitCode = 1;
18301
+ return null;
18126
18302
  }
18303
+ return { token, apiUrl: opts.apiUrlFlag ? opts.apiUrl : resolveApiUrl() };
18304
+ }
18305
+ function runInitAgents(cwd) {
18306
+ const dir = findProjectLinkDir(cwd);
18307
+ if (!dir) {
18308
+ process.stderr.write(
18309
+ "This folder is not linked to a Kryd project (no .kryd/project.json up to the git root), so there is no block to refresh.\nRun `kryd init` to link it \u2014 that writes the Kryd block into AGENTS.md as well.\n"
18310
+ );
18311
+ process.exitCode = 1;
18312
+ return;
18313
+ }
18314
+ const { note, block } = installAgentsBlock(dir);
18315
+ process.stdout.write(block === "unchanged" ? `${note}The Kryd block in AGENTS.md is up to date.
18316
+ ` : note);
18317
+ }
18318
+ async function runInit(opts) {
18319
+ const cwd = opts.cwd ?? process.cwd();
18320
+ if (opts.agents) {
18321
+ runInitAgents(cwd);
18322
+ return;
18323
+ }
18324
+ let apiUrl = resolveApiUrl(opts.apiUrl);
18325
+ const session = await settleLogin({
18326
+ apiUrl,
18327
+ ...opts.apiUrl ? { apiUrlFlag: opts.apiUrl } : {},
18328
+ ...opts.io ? { io: opts.io } : {},
18329
+ offerLogin: opts.offerLogin !== false,
18330
+ command: "kryd init"
18331
+ });
18332
+ if (!session) return;
18333
+ const { token } = session;
18334
+ apiUrl = session.apiUrl;
18127
18335
  const existingLink = directoryIsLinked(cwd);
18128
18336
  if (existingLink) {
18129
18337
  const remote = hasRemote(cwd, "kryd");
@@ -18133,6 +18341,7 @@ async function runInit(opts) {
18133
18341
  `This folder is already linked to ${existingLink.projectId} (.kryd/project.json).
18134
18342
  Running \`kryd init\` again would create a SECOND project and leave that one orphaned.
18135
18343
  To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete .kryd/project.json to unlink this folder.
18344
+ To write or refresh the Kryd block in AGENTS.md: \`kryd init --agents\`.
18136
18345
  `
18137
18346
  );
18138
18347
  process.exitCode = 1;
@@ -18182,10 +18391,10 @@ To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete
18182
18391
  try {
18183
18392
  saveProjectLink(cwd, { projectId: project2.id });
18184
18393
  try {
18185
- const session = await whoami(apiUrl, token);
18394
+ const session2 = await whoami(apiUrl, token);
18186
18395
  rememberLinkAccount(cwd, {
18187
- accountId: session.accountId,
18188
- accountSlug: session.accountSlug
18396
+ accountId: session2.accountId,
18397
+ accountSlug: session2.accountSlug
18189
18398
  });
18190
18399
  } catch {
18191
18400
  }
@@ -18244,9 +18453,10 @@ Scheduled and triggered work runs on your production deploy; preview branches bu
18244
18453
  Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
18245
18454
  ` + (declaration?.workflowsPath ? `Workflows in ${declaration.workflowsPath}/ run as a worker beside it on production deploys; previews run the web app only.
18246
18455
  ` : "");
18456
+ const agentsNote = installAgentsBlock(cwd).note;
18247
18457
  process.stdout.write(
18248
18458
  `Linked "${name}" (${project2.framework}) \u2192 ${repo.htmlUrl}
18249
- ` + routing + `${linkNote}
18459
+ ` + routing + `${linkNote}` + agentsNote + `
18250
18460
  ${nextStep}`
18251
18461
  );
18252
18462
  } catch (err) {
@@ -19265,26 +19475,34 @@ AI_GATEWAY_URL and AI_GATEWAY_TOKEN stop being injected from your next deploy (k
19265
19475
  await reportError(err);
19266
19476
  }
19267
19477
  }
19268
- async function reportResourceStatus(opts) {
19478
+ async function readResourceStatus(opts) {
19269
19479
  try {
19270
- const result = await opts.read();
19271
- if (result.status === "failed") {
19272
- process.stdout.write(
19273
- `${opts.project}: ${opts.noun} failed \u2014 ${result.failureReason ?? "unknown error"}.
19274
- Remove it before attaching another.
19275
- `
19276
- );
19277
- return;
19278
- }
19279
- process.stdout.write(`${opts.project}: ${opts.noun} ${result.status}.
19280
- `);
19480
+ return toResourceState(await opts.read());
19281
19481
  } catch (err) {
19282
19482
  if (err instanceof ApiError && err.status === 404 && await projectIsVisible(opts.token, opts.project) === true) {
19283
- process.stdout.write(`${opts.project}: no ${opts.noun} attached.
19284
- `);
19285
- return;
19483
+ return NOT_ATTACHED;
19286
19484
  }
19287
19485
  await reportError(err);
19486
+ return null;
19487
+ }
19488
+ }
19489
+ var NOT_ATTACHED = { attached: false, status: null, failureReason: null };
19490
+ function toResourceState(row) {
19491
+ return { attached: true, status: row.status, failureReason: row.failureReason ?? null };
19492
+ }
19493
+ function printResourceStatus(state, noun, project2) {
19494
+ if (!state.attached) {
19495
+ process.stdout.write(`${project2}: no ${noun} attached.
19496
+ `);
19497
+ } else if (state.status === "failed") {
19498
+ process.stdout.write(
19499
+ `${project2}: ${noun} failed \u2014 ${state.failureReason ?? "unknown error"}.
19500
+ Remove it before attaching another.
19501
+ `
19502
+ );
19503
+ } else {
19504
+ process.stdout.write(`${project2}: ${noun} ${state.status}.
19505
+ `);
19288
19506
  }
19289
19507
  }
19290
19508
  async function runDbStatus(opts) {
@@ -19300,21 +19518,22 @@ async function runDbStatus(opts) {
19300
19518
  reportNotLinked("kryd db status <projectId>");
19301
19519
  return;
19302
19520
  }
19303
- let productionAttached = false;
19304
- await reportResourceStatus({
19305
- read: async () => {
19306
- const row = await getDatabaseStatus(apiUrl, token, project2);
19307
- productionAttached = true;
19308
- return row;
19309
- },
19310
- noun: "database",
19521
+ const production = await readResourceStatus({
19522
+ read: () => getDatabaseStatus(apiUrl, token, project2),
19311
19523
  project: project2,
19312
19524
  token
19313
19525
  });
19314
- if (!productionAttached) return;
19526
+ if (!production) return;
19527
+ if (!opts.json) printResourceStatus(production, "database", project2);
19528
+ if (!production.attached) {
19529
+ if (opts.json) printJson2({ production, preview: NOT_ATTACHED });
19530
+ return;
19531
+ }
19315
19532
  try {
19316
19533
  const preview = await readPreviewDatabase(apiUrl, token, project2);
19317
- if (!preview) {
19534
+ if (opts.json) {
19535
+ printJson2({ production, preview: preview ? toResourceState(preview) : NOT_ATTACHED });
19536
+ } else if (!preview) {
19318
19537
  process.stdout.write(
19319
19538
  `${project2}: no preview database \u2014 preview deploys use the production database.
19320
19539
  `
@@ -19353,12 +19572,14 @@ async function runStorageStatus(opts) {
19353
19572
  reportNotLinked("kryd storage status <projectId>");
19354
19573
  return;
19355
19574
  }
19356
- await reportResourceStatus({
19575
+ const state = await readResourceStatus({
19357
19576
  read: () => getStorageStatus(apiUrl, token, project2),
19358
- noun: "object storage",
19359
19577
  project: project2,
19360
19578
  token
19361
19579
  });
19580
+ if (!state) return;
19581
+ if (opts.json) printJson2(state);
19582
+ else printResourceStatus(state, "object storage", project2);
19362
19583
  }
19363
19584
  async function runAiStatus(opts) {
19364
19585
  const apiUrl = resolveApiUrl(opts.apiUrl);
@@ -19375,6 +19596,10 @@ async function runAiStatus(opts) {
19375
19596
  }
19376
19597
  try {
19377
19598
  const status = await getAiStatus(apiUrl, token, project2);
19599
+ if (opts.json) {
19600
+ printJson2(status);
19601
+ return;
19602
+ }
19378
19603
  if (!status.enabled) {
19379
19604
  process.stdout.write(`${project2}: no AI endpoint.
19380
19605
  `);
@@ -19389,6 +19614,50 @@ async function runAiStatus(opts) {
19389
19614
  await reportError(err);
19390
19615
  }
19391
19616
  }
19617
+ async function runAiModels(opts) {
19618
+ const apiUrl = resolveApiUrl(opts.apiUrl);
19619
+ const token = resolveToken();
19620
+ if (!token) {
19621
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
19622
+ process.exitCode = 1;
19623
+ return;
19624
+ }
19625
+ let models;
19626
+ try {
19627
+ models = await getAiModels(apiUrl, token);
19628
+ } catch (err) {
19629
+ await reportError(err);
19630
+ return;
19631
+ }
19632
+ if (opts.json) {
19633
+ printJson2({ items: models });
19634
+ return;
19635
+ }
19636
+ if (models.length === 0) {
19637
+ process.stdout.write("No models in the AI catalogue right now.\n");
19638
+ return;
19639
+ }
19640
+ const rows = models.map((m) => ({
19641
+ mark: m.popular ? "*" : " ",
19642
+ model: m.model,
19643
+ provider: m.provider,
19644
+ tasks: m.tasks.join(","),
19645
+ stage: m.stage
19646
+ }));
19647
+ const width = (label, pick2) => rows.reduce((max, r) => Math.max(max, pick2(r).length), label.length);
19648
+ const modelW = width("MODEL", (r) => r.model);
19649
+ const providerW = width("PROVIDER", (r) => r.provider);
19650
+ const tasksW = width("TASKS", (r) => r.tasks);
19651
+ const line = (mark, model, provider, tasks, stage) => `${mark} ${model.padEnd(modelW)} ${provider.padEnd(providerW)} ${tasks.padEnd(tasksW)} ${stage}
19652
+ `;
19653
+ process.stdout.write(line(" ", "MODEL", "PROVIDER", "TASKS", "STAGE"));
19654
+ for (const r of rows) process.stdout.write(line(r.mark, r.model, r.provider, r.tasks, r.stage));
19655
+ process.stdout.write("\n");
19656
+ if (rows.some((r) => r.mark === "*")) process.stdout.write("* popular\n");
19657
+ process.stdout.write(
19658
+ "Use one by putting its MODEL value in the request's `model` field (after `kryd ai add`).\n"
19659
+ );
19660
+ }
19392
19661
  function isoDay(iso) {
19393
19662
  return iso.slice(0, 10);
19394
19663
  }
@@ -19489,6 +19758,14 @@ async function runWorkflowStatus(opts) {
19489
19758
  }
19490
19759
  try {
19491
19760
  const status = await getWorkflowStatus(apiUrl, token, project2);
19761
+ if (opts.json) {
19762
+ printJson2({
19763
+ enabled: status.enabled,
19764
+ tokenExpiresAt: status.tokenExpiresAt,
19765
+ tokenExpiresInDays: status.tokenExpiresAt ? daysUntil(status.tokenExpiresAt) : null
19766
+ });
19767
+ return;
19768
+ }
19492
19769
  if (!status.enabled) {
19493
19770
  process.stdout.write(`${project2}: no workflows.
19494
19771
  `);
@@ -19644,6 +19921,10 @@ async function runDomainStatus(opts) {
19644
19921
  const own = list.projectHost ? `${ctx.project} is always served at ${list.projectHost}.
19645
19922
  ` : "";
19646
19923
  if (!opts.host) {
19924
+ if (opts.json) {
19925
+ printJson2(list);
19926
+ return;
19927
+ }
19647
19928
  if (list.domains.length === 0) {
19648
19929
  process.stdout.write(`${ctx.project}: no custom domains.
19649
19930
  ${own}`);
@@ -19668,6 +19949,10 @@ ${own}`);
19668
19949
  process.exitCode = 1;
19669
19950
  return;
19670
19951
  }
19952
+ if (opts.json) {
19953
+ printJson2(d);
19954
+ return;
19955
+ }
19671
19956
  let out = `${d.host}: ${DOMAIN_STATUS_TEXT[d.status]}.
19672
19957
  `;
19673
19958
  if (d.redirectTo) out += ` \u2192 redirects to ${d.redirectTo}
@@ -19722,7 +20007,7 @@ async function runWorkflowInit(opts) {
19722
20007
  }
19723
20008
  const workerName = workerNameFrom(opts.name ?? basename2(target));
19724
20009
  const files = scaffoldFiles(language, workerName);
19725
- const clashes = files.map((f) => f.path).filter((p) => existsSync3(join3(target, p)));
20010
+ const clashes = files.map((f) => f.path).filter((p) => existsSync4(join4(target, p)));
19726
20011
  if (clashes.length > 0) {
19727
20012
  process.stderr.write(
19728
20013
  `Refusing to overwrite: ${clashes.join(", ")}.
@@ -19732,12 +20017,22 @@ Nothing was written. Run this in an empty directory, or pass --dir <path>.
19732
20017
  process.exitCode = 1;
19733
20018
  return;
19734
20019
  }
20020
+ if (opts.link !== false) {
20021
+ const session = await settleLogin({
20022
+ apiUrl: resolveApiUrl(opts.apiUrl),
20023
+ ...opts.apiUrl ? { apiUrlFlag: opts.apiUrl } : {},
20024
+ ...opts.io ? { io: opts.io } : {},
20025
+ offerLogin: true,
20026
+ command: "kryd workflow init"
20027
+ });
20028
+ if (!session) return;
20029
+ }
19735
20030
  const written = [];
19736
20031
  try {
19737
20032
  for (const file2 of files) {
19738
- const full = join3(target, file2.path);
20033
+ const full = join4(target, file2.path);
19739
20034
  mkdirSync2(dirname2(full), { recursive: true });
19740
- writeFileSync2(full, file2.contents);
20035
+ writeFileSync3(full, file2.contents);
19741
20036
  written.push(full);
19742
20037
  }
19743
20038
  } catch (err) {
@@ -19774,6 +20069,7 @@ To add a worker to the app in that repository instead, run \`kryd workflow init\
19774
20069
  process.exitCode = 1;
19775
20070
  return;
19776
20071
  }
20072
+ process.stdout.write(installAgentsBlock(target).note);
19777
20073
  const next = [];
19778
20074
  if (opts.dir) next.push(`cd ${opts.dir}`);
19779
20075
  const first = firstCommand(language);
@@ -19823,7 +20119,7 @@ function scaffoldContainedWorker(opts) {
19823
20119
  "This directory is not linked to a Kryd project, so there is no app to add a worker to.\nLink your app first (`kryd init`), then run this again \u2014 or pass --only for a project that runs nothing but a worker.\n"
19824
20120
  );
19825
20121
  process.exitCode = 1;
19826
- return;
20122
+ return false;
19827
20123
  }
19828
20124
  const raw = (opts.path ?? DEFAULT_WORKFLOWS_PATH).trim();
19829
20125
  const segments = raw.split("/").filter((seg) => seg !== "" && seg !== ".");
@@ -19833,14 +20129,14 @@ function scaffoldContainedWorker(opts) {
19833
20129
  `
19834
20130
  );
19835
20131
  process.exitCode = 1;
19836
- return;
20132
+ return false;
19837
20133
  }
19838
20134
  const folder = segments.join("/");
19839
- const declarationPath = join3(target, DECLARATION_FILE);
20135
+ const declarationPath = join4(target, DECLARATION_FILE);
19840
20136
  let existing = {};
19841
20137
  let existingText = null;
19842
- if (existsSync3(declarationPath)) {
19843
- existingText = readFileSync3(declarationPath, "utf8");
20138
+ if (existsSync4(declarationPath)) {
20139
+ existingText = readFileSync4(declarationPath, "utf8");
19844
20140
  let parsed;
19845
20141
  try {
19846
20142
  parsed = JSON.parse(existingText);
@@ -19853,7 +20149,7 @@ function scaffoldContainedWorker(opts) {
19853
20149
  `
19854
20150
  );
19855
20151
  process.exitCode = 1;
19856
- return;
20152
+ return false;
19857
20153
  }
19858
20154
  existing = parsed;
19859
20155
  if ("kind" in existing) {
@@ -19862,7 +20158,7 @@ function scaffoldContainedWorker(opts) {
19862
20158
  `
19863
20159
  );
19864
20160
  process.exitCode = 1;
19865
- return;
20161
+ return false;
19866
20162
  }
19867
20163
  if ("workflows" in existing || existing["web"] === false) {
19868
20164
  process.stderr.write(
@@ -19870,7 +20166,7 @@ function scaffoldContainedWorker(opts) {
19870
20166
  `
19871
20167
  );
19872
20168
  process.exitCode = 1;
19873
- return;
20169
+ return false;
19874
20170
  }
19875
20171
  }
19876
20172
  const workerName = workerNameFrom(opts.name ?? basename2(target));
@@ -19878,7 +20174,7 @@ function scaffoldContainedWorker(opts) {
19878
20174
  path: `${folder}/${f.path}`,
19879
20175
  contents: f.contents
19880
20176
  }));
19881
- const clashes = files.map((f) => f.path).filter((p) => existsSync3(join3(target, p)));
20177
+ const clashes = files.map((f) => f.path).filter((p) => existsSync4(join4(target, p)));
19882
20178
  if (clashes.length > 0) {
19883
20179
  process.stderr.write(
19884
20180
  `Refusing to overwrite: ${clashes.join(", ")}.
@@ -19886,18 +20182,18 @@ Nothing was written. Pick another folder with --path <folder>.
19886
20182
  `
19887
20183
  );
19888
20184
  process.exitCode = 1;
19889
- return;
20185
+ return false;
19890
20186
  }
19891
20187
  const declaration = { ...existing, workflows: { path: folder } };
19892
20188
  const written = [];
19893
20189
  try {
19894
20190
  for (const file2 of files) {
19895
- const full = join3(target, file2.path);
20191
+ const full = join4(target, file2.path);
19896
20192
  mkdirSync2(dirname2(full), { recursive: true });
19897
- writeFileSync2(full, file2.contents);
20193
+ writeFileSync3(full, file2.contents);
19898
20194
  written.push(full);
19899
20195
  }
19900
- writeFileSync2(declarationPath, `${JSON.stringify(declaration, null, 2)}
20196
+ writeFileSync3(declarationPath, `${JSON.stringify(declaration, null, 2)}
19901
20197
  `);
19902
20198
  } catch (err) {
19903
20199
  for (const path of written.reverse()) {
@@ -19908,7 +20204,7 @@ Nothing was written. Pick another folder with --path <folder>.
19908
20204
  }
19909
20205
  try {
19910
20206
  if (existingText === null) rmSync2(declarationPath, { force: true });
19911
- else writeFileSync2(declarationPath, existingText);
20207
+ else writeFileSync3(declarationPath, existingText);
19912
20208
  } catch {
19913
20209
  }
19914
20210
  process.stderr.write(
@@ -19917,14 +20213,15 @@ Nothing was left behind.
19917
20213
  `
19918
20214
  );
19919
20215
  process.exitCode = 1;
19920
- return;
20216
+ return false;
19921
20217
  }
20218
+ const agents = installAgentsBlock(target);
19922
20219
  const next = [];
19923
20220
  const first = firstCommand(language);
19924
20221
  if (first) next.push(`(cd ${folder} && ${first})`);
19925
20222
  next.push(
19926
20223
  "kryd workflow add",
19927
- `git add ${DECLARATION_FILE} ${folder} && git commit -m "add workflows"`,
20224
+ `git add ${[DECLARATION_FILE, folder, ...agents.changed].join(" ")} && git commit -m "add workflows"`,
19928
20225
  "kryd push"
19929
20226
  );
19930
20227
  process.stdout.write(
@@ -19937,9 +20234,10 @@ Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
19937
20234
  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.
19938
20235
  ` + // The app is still built (and type-checked) from the root, so a tsconfig that includes every
19939
20236
  // file would compile the worker too — against an SDK the app does not depend on.
19940
- (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.
19941
- ` : "")
20237
+ (existsSync4(join4(target, "tsconfig.json")) ? `Add "${folder}" to the "exclude" of your tsconfig.json, so your app's build does not type-check the worker.
20238
+ ` : "") + agents.note
19942
20239
  );
20240
+ return true;
19943
20241
  }
19944
20242
  async function runStorageRemove(opts) {
19945
20243
  const apiUrl = resolveApiUrl(opts.apiUrl);
@@ -20019,6 +20317,11 @@ async function runProjectList(opts) {
20019
20317
  await reportError(err);
20020
20318
  return;
20021
20319
  }
20320
+ if (opts.json) {
20321
+ const linked = resolveProjectId(void 0, opts.cwd);
20322
+ printJson2({ items: projects.map((p) => ({ ...p, linked: p.id === linked })) });
20323
+ return;
20324
+ }
20022
20325
  if (projects.length === 0) {
20023
20326
  process.stdout.write("No projects yet \u2014 run `kryd init` in a project directory.\n");
20024
20327
  return;
@@ -20070,6 +20373,10 @@ async function runEnvList(opts) {
20070
20373
  }
20071
20374
  try {
20072
20375
  const vars = await listEnvVars(apiUrl, token, project2);
20376
+ if (opts.json) {
20377
+ printJson2({ items: vars });
20378
+ return;
20379
+ }
20073
20380
  if (vars.length === 0) {
20074
20381
  process.stdout.write(
20075
20382
  `No environment variables for ${project2} yet.
@@ -20155,7 +20462,7 @@ async function runEnvPull(opts) {
20155
20462
  const root = findProjectLinkDir(cwd) ?? cwd;
20156
20463
  const outRel = opts.out ?? ".env.kryd";
20157
20464
  const outPath = resolve2(root, outRel);
20158
- if (opts.out !== void 0 && existsSync3(outPath) && !opts.force) {
20465
+ if (opts.out !== void 0 && existsSync4(outPath) && !opts.force) {
20159
20466
  process.stderr.write(
20160
20467
  `Refusing to overwrite ${outRel} \u2014 pass --force to replace it (or omit --out to write .env.kryd).
20161
20468
  `
@@ -20429,7 +20736,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
20429
20736
  await reportError(err);
20430
20737
  }
20431
20738
  }
20432
- var CLI_VERSION = true ? "0.17.0" : "0.0.0-dev";
20739
+ var CLI_VERSION = true ? "0.19.0" : "0.0.0-dev";
20433
20740
  var program = new Command();
20434
20741
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
20435
20742
  program.hook("postAction", async () => {
@@ -20439,7 +20746,7 @@ program.command("login").description("Sign in via the browser (default) and stor
20439
20746
  "--token <token>",
20440
20747
  "supply a token instead of the browser flow (prefer $KRYD_TOKEN \u2014 flags are visible in `ps`)"
20441
20748
  ).option("--api-url <url>", "control-plane API base URL").option("--dashboard-url <url>", "dashboard base URL for the browser approve page (default app.kryd.eu)").action((opts) => runLogin(opts));
20442
- program.command("whoami").description("Show the current account").option("--api-url <url>", "control-plane API base URL").action((opts) => runWhoami(opts));
20749
+ program.command("whoami").description("Show the current account").option("--json", "print the user and their accounts as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((opts) => runWhoami(opts));
20443
20750
  var accountCommand = program.command("account").description("The accounts you belong to, and which one commands run in");
20444
20751
  accountCommand.command("list").description("List your accounts and your role in each; * marks the one commands run in").option("--json", "print the accounts as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((opts) => runAccountList(opts));
20445
20752
  accountCommand.command("use <slug>").description("Run every following command in this account").option("--api-url <url>", "control-plane API base URL").action((slug, opts) => runAccountUse(slug, opts));
@@ -20467,7 +20774,10 @@ program.command("init").description(
20467
20774
  "--public",
20468
20775
  // KRYD-184a. Chosen at creation; there is no command to flip it afterwards yet (KRYD-341).
20469
20776
  "create the repository PUBLIC \u2014 readable by anyone signed in to the forge (default: private). Set at creation only"
20470
- ).option("--api-url <url>", "control-plane API base URL").action((opts) => runInit(opts));
20777
+ ).option("--api-url <url>", "control-plane API base URL").option(
20778
+ "--agents",
20779
+ "only write or refresh the Kryd block in AGENTS.md of the linked project (no login, no API call) \u2014 e.g. after updating the CLI"
20780
+ ).action((opts) => runInit(opts));
20471
20781
  program.command("logs [target]").description(
20472
20782
  "Follow logs. Default: a deploy's build/deploy logs ([target]=deployment id, latest if omitted). With --runtime: a container's stdout/stderr \u2014 [target]=a project id tails its live app, a deployment id (dpl_\u2026) tails THAT deploy's container (incl. a failed one, to see why it crashed). With --worker: a project's workflow worker, also when the project has a web app."
20473
20783
  ).option(
@@ -20504,7 +20814,7 @@ program.command("redeploy [project]").description(
20504
20814
  "Redeploy the current live commit (no rebuild) to apply config/env changes, and follow it live"
20505
20815
  ).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 }));
20506
20816
  var project = program.command("project").description("Manage your projects");
20507
- 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));
20817
+ project.command("list").description("List your projects \u2014 name, subdomain, status and id").option("--json", "print { items } as JSON on stdout, each with `linked`").option("--api-url <url>", "control-plane API base URL").action((opts) => runProjectList(opts));
20508
20818
  project.command("rm [project]").description(
20509
20819
  "Permanently delete a project and everything Kryd created for it (by name or id; asks you to type its name)"
20510
20820
  ).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 }));
@@ -20521,13 +20831,13 @@ db.command("add [project]").description("Attach a shared or bring-your-own Postg
20521
20831
  ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbAdd({ ...opts, project: project2 }));
20522
20832
  db.command("remove [project]").description("Tear down the project's database and everything in it (asks for confirmation)").option("--preview", "remove the preview database; production is not touched").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbRemove({ ...opts, project: project2 }));
20523
20833
  db.command("reset [project]").description("Empty the preview database: every table dropped, credentials kept (asks for confirmation)").option("--preview", "required \u2014 only the preview database can be reset").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbReset({ ...opts, project: project2 }));
20524
- db.command("status [project]").description("Show the project's database and preview database, and what state they are in").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbStatus({ ...opts, project: project2 }));
20834
+ db.command("status [project]").description("Show the project's database and preview database, and what state they are in").option("--json", "print { production, preview } as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbStatus({ ...opts, project: project2 }));
20525
20835
  var storage = program.command("storage").description("Manage project object storage");
20526
20836
  storage.command("add [project]").description("Create an S3-compatible object-storage bucket for a project").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageAdd({ ...opts, project: project2 }));
20527
20837
  storage.command("remove [project]").description("Tear down the project's bucket and every object in it (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageRemove({ ...opts, project: project2 }));
20528
- storage.command("status [project]").description("Show whether the project has object storage, and what state it is in").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageStatus({ ...opts, project: project2 }));
20838
+ storage.command("status [project]").description("Show whether the project has object storage, and what state it is in").option("--json", "print { attached, status, failureReason } as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageStatus({ ...opts, project: project2 }));
20529
20839
  var env = program.command("env").description("Manage your app's environment variables");
20530
- env.command("list [project]").description("List the project's environment variables (names and origin \u2014 never values)").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvList({ ...opts, project: project2 }));
20840
+ env.command("list [project]").description("List the project's environment variables (names and origin \u2014 never values)").option("--json", "print { items } as JSON on stdout (never values)").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvList({ ...opts, project: project2 }));
20531
20841
  env.command("set <key> [project]").description(
20532
20842
  "Set one of your environment variables \u2014 `KEY=VALUE`, or `KEY` with --stdin / a prompt"
20533
20843
  ).option(
@@ -20556,14 +20866,15 @@ env.command("pull [project]").description(
20556
20866
  "--out <path>",
20557
20867
  "file to write, relative to the project root (default: .env.kryd \u2014 never your hand-maintained .env)"
20558
20868
  ).option("--force", "overwrite the --out file if it already exists").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvPull({ ...opts, project: project2 }));
20559
- var ai = program.command("ai").description("Manage the app's EU AI gateway");
20869
+ var ai = program.command("ai").description("Manage the app's EU AI gateway and list its models");
20560
20870
  ai.command("add [project]").description("Give the project an authenticated EU AI endpoint (injects on next deploy)").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runAiAdd({ ...opts, project: project2 }));
20561
20871
  ai.command("remove [project]").description("Stop injecting the project's AI endpoint (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runAiRemove({ ...opts, project: project2 }));
20562
- ai.command("status [project]").description("Show whether the project has an AI endpoint, and what it is").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runAiStatus({ ...opts, project: project2 }));
20872
+ ai.command("status [project]").description("Show whether the project has an AI endpoint, and what it is").option("--json", "print { enabled, url } as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runAiStatus({ ...opts, project: project2 }));
20873
+ ai.command("models").description("List the AI models Kryd offers right now (the catalogue the dashboard shows)").option("--json", "print { items } as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((opts) => runAiModels(opts));
20563
20874
  var workflow = program.command("workflow").description("Manage the project's durable workflows (its own workflow tenant)");
20564
20875
  workflow.command("add [project]").description("Give the project a workflow tenant and inject HATCHET_CLIENT_TOKEN on next deploy").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runWorkflowAdd({ ...opts, project: project2 }));
20565
20876
  workflow.command("remove [project]").description("Revoke the project's workflow token and stop injecting it (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runWorkflowRemove({ ...opts, project: project2 }));
20566
- workflow.command("status [project]").description("Show whether the project has workflows, and when its token expires").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runWorkflowStatus({ ...opts, project: project2 }));
20877
+ workflow.command("status [project]").description("Show whether the project has workflows, and when its token expires").option("--json", "print { enabled, tokenExpiresAt, tokenExpiresInDays } as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runWorkflowStatus({ ...opts, project: project2 }));
20567
20878
  workflow.command("init").description(
20568
20879
  "Add a workflow worker to the linked app here (in ./workflows, declared in kryd.json); --only scaffolds a worker-only project instead and links it"
20569
20880
  ).requiredOption(
@@ -20576,7 +20887,7 @@ domain2.command("add <host>").description("Add a custom domain and print the DNS
20576
20887
  "redirect instead of serving: every request answers 301 to https://<host>, path and query kept"
20577
20888
  ).option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainAdd({ ...opts, host }));
20578
20889
  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 }));
20579
- 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 }));
20890
+ 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("--json", "print the domain list, or the one domain, as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainStatus({ ...opts, host }));
20580
20891
  var support = program.command("support").description(
20581
20892
  "Ask Kryd support a question, report a bug or a wish, and read the answer \u2014 non-interactive, with --json for agents"
20582
20893
  );
@@ -20594,7 +20905,7 @@ function invokedDirectly() {
20594
20905
  const entry = process.argv[1];
20595
20906
  if (!entry) return false;
20596
20907
  try {
20597
- return import.meta.url === pathToFileURL(realpathSync(entry)).href;
20908
+ return import.meta.url === pathToFileURL(realpathSync2(entry)).href;
20598
20909
  } catch {
20599
20910
  return false;
20600
20911
  }
@@ -20612,6 +20923,7 @@ export {
20612
20923
  backfillLinkAccount,
20613
20924
  program,
20614
20925
  runAiAdd,
20926
+ runAiModels,
20615
20927
  runAiRemove,
20616
20928
  runAiStatus,
20617
20929
  runDbAdd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kryd/cli",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Kryd CLI — git push → live on the European cloud for your AI: SSL, managed Postgres & object storage, and an EU-hosted AI gateway. Your code and model calls stay in the EU. Beta: sign up at https://kryd.eu/beta",
5
5
  "keywords": [
6
6
  "kryd",
@@ -42,8 +42,8 @@
42
42
  "typescript": "^5.6.3",
43
43
  "vitest": "^2.1.8",
44
44
  "@kryd/shared-types": "0.0.0",
45
- "@kryd/config-eslint": "0.0.0",
46
- "@kryd/config-ts": "0.0.0"
45
+ "@kryd/config-ts": "0.0.0",
46
+ "@kryd/config-eslint": "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",