@kryd/cli 0.17.0 → 0.18.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 +19 -0
  2. package/dist/index.js +356 -112
  3. package/package.json +1 -1
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). |
@@ -53,6 +56,22 @@ kryd support new --type bug --subject "Deploy hangs on build" --message "Every d
53
56
  kryd support show T-1042 --json
54
57
  ```
55
58
 
59
+ **`--json` on the read commands.** `kryd project list`, `whoami`, `db status`, `storage status`, `ai status`, `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:
60
+
61
+ | Command | `--json` prints |
62
+ |---|---|
63
+ | `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 |
64
+ | `kryd whoami` | the `GET /me` answer as is: you, the account commands run in, your role and your `memberships` |
65
+ | `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 |
66
+ | `kryd storage status` | `{ attached, status, failureReason }` |
67
+ | `kryd ai status` | `{ enabled, url }` |
68
+ | `kryd workflow status` | `{ enabled, tokenExpiresAt, tokenExpiresInDays }` — both `null` without an expiry; the days are negative once the token has expired |
69
+ | `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 |
70
+ | `kryd domain status <host>` | that one domain, the same fields, its DNS `records` included |
71
+ | `kryd env list` | `{ items }`, each `{ key, source, scope, updatedAt, environments? }` — names and origin, never values |
72
+
73
+ `kryd token list --json` and `kryd account list --json` print a bare array.
74
+
56
75
  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
76
 
58
77
  ```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
@@ -16796,9 +16796,125 @@ function parseCredentialInput(raw) {
16796
16796
  return out;
16797
16797
  }
16798
16798
 
16799
+ // src/agents-md.ts
16800
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
16801
+ import { join as join2 } from "node:path";
16802
+ var AGENTS_FILE = "AGENTS.md";
16803
+ var CLAUDE_FILE = "CLAUDE.md";
16804
+ var BEGIN_MARKER = "<!-- kryd:begin \u2014 written by the kryd CLI; edits inside are replaced on the next run -->";
16805
+ var END_MARKER = "<!-- kryd:end -->";
16806
+ var BEGIN_LINE = /^<!-- kryd:begin/gm;
16807
+ var END_LINE = /^<!-- kryd:end/gm;
16808
+ var AGENTS_PROSE = `## Deploying on Kryd
16809
+
16810
+ This project deploys to Kryd, a European cloud platform. Use what it already provides instead of adding another service:
16811
+
16812
+ - **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.
16813
+ - **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.
16814
+ - **File storage:** S3-compatible object storage \u2014 \`kryd storage add\`.
16815
+ - **AI:** an EU-hosted, OpenAI-compatible gateway with several models \u2014 \`kryd ai add\`. Don't add provider API keys.
16816
+ - **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.
16817
+ - **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.
16818
+ - **Your own domain:** \`kryd domain add <host>\` prints the DNS records to create; the HTTPS certificate is automatic.
16819
+ - Everything runs in the EU; keep data and model calls there.
16820
+
16821
+ ### Rules
16822
+
16823
+ - **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.
16824
+ - **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.
16825
+ - **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\`).
16826
+ - **Call the AI gateway from server code only.** \`AI_GATEWAY_TOKEN\` must never reach a browser.
16827
+ - **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.
16828
+ - **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.
16829
+ - **Destructive commands** ask for confirmation; without a terminal they need \`--yes\`.
16830
+ - **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 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.
16831
+ `;
16832
+ var AGENTS_FOOTER = "Run `kryd <command> --help` for details. After updating the CLI, refresh this block with `kryd init --agents`.\n";
16833
+ function isHidden(cmd) {
16834
+ return cmd._hidden === true;
16835
+ }
16836
+ function leafCommands(program2) {
16837
+ const out = [];
16838
+ const walk = (cmd, prefix) => {
16839
+ if (isHidden(cmd)) return;
16840
+ const path = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
16841
+ if (cmd.commands.length > 0) {
16842
+ for (const child of cmd.commands) walk(child, path);
16843
+ return;
16844
+ }
16845
+ const args = cmd.registeredArguments.map((a) => {
16846
+ const inner = `${a.name()}${a.variadic ? "..." : ""}`;
16847
+ return a.required ? `<${inner}>` : `[${inner}]`;
16848
+ });
16849
+ out.push({ path, usage: [path, ...args].join(" "), description: cmd.description() });
16850
+ };
16851
+ for (const child of program2.commands) walk(child, "");
16852
+ return out;
16853
+ }
16854
+ function renderAgentsBlock(program2) {
16855
+ const commands = leafCommands(program2).map((c) => `- \`kryd ${c.usage}\` \u2014 ${c.description}
16856
+ `).join("");
16857
+ return `${BEGIN_MARKER}
16858
+ ` + AGENTS_PROSE + "\n### Commands\n\n" + commands + "\n" + AGENTS_FOOTER + `${END_MARKER}
16859
+ `;
16860
+ }
16861
+ function lineStartsMatching(text, re) {
16862
+ return [...text.matchAll(re)].map((m) => m.index);
16863
+ }
16864
+ function writeAgentsBlock(dir, block) {
16865
+ const path = join2(dir, AGENTS_FILE);
16866
+ if (!existsSync2(path)) {
16867
+ writeFileSync2(path, block);
16868
+ return "created";
16869
+ }
16870
+ const text = readFileSync2(path, "utf8");
16871
+ const begins = lineStartsMatching(text, BEGIN_LINE);
16872
+ const ends = lineStartsMatching(text, END_LINE);
16873
+ let next;
16874
+ if (begins.length === 0 && ends.length === 0) {
16875
+ let base = text;
16876
+ if (base.length > 0) {
16877
+ if (!base.endsWith("\n")) base += "\n";
16878
+ if (!base.endsWith("\n\n")) base += "\n";
16879
+ }
16880
+ next = base + block;
16881
+ } else {
16882
+ const [begin] = begins;
16883
+ const [end] = ends;
16884
+ 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
16885
+ // the slice run on into the customer's text.
16886
+ !text.startsWith(END_MARKER, end)) {
16887
+ throw new Error(
16888
+ `${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`
16889
+ );
16890
+ }
16891
+ const endOfMarker = end + END_MARKER.length;
16892
+ next = text.slice(0, begin) + block.replace(/\n$/, "") + text.slice(endOfMarker);
16893
+ }
16894
+ if (next === text) return "unchanged";
16895
+ writeFileSync2(path, next);
16896
+ return "updated";
16897
+ }
16898
+ function ensureClaudeImport(dir) {
16899
+ const path = join2(dir, CLAUDE_FILE);
16900
+ if (!existsSync2(path)) return false;
16901
+ const agents = join2(dir, AGENTS_FILE);
16902
+ if (existsSync2(agents) && realpathSync(path) === realpathSync(agents)) return false;
16903
+ const text = readFileSync2(path, "utf8");
16904
+ if (/^[ \t]*@(\.\/)?AGENTS\.md[ \t]*\r?$/m.test(text)) return false;
16905
+ let base = text;
16906
+ if (base.length > 0) {
16907
+ if (!base.endsWith("\n")) base += "\n";
16908
+ if (!base.endsWith("\n\n")) base += "\n";
16909
+ }
16910
+ writeFileSync2(path, `${base}@AGENTS.md
16911
+ `);
16912
+ return true;
16913
+ }
16914
+
16799
16915
  // src/framework.ts
16800
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
16801
- import { basename, join as join2 } from "node:path";
16916
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
16917
+ import { basename, join as join3 } from "node:path";
16802
16918
  var FRAMEWORKS = [
16803
16919
  "react-router",
16804
16920
  "nextjs",
@@ -16853,7 +16969,7 @@ function startScriptLooksLikePlainNode(start) {
16853
16969
  }
16854
16970
  function readPackageJson(cwd) {
16855
16971
  try {
16856
- return JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
16972
+ return JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf8"));
16857
16973
  } catch {
16858
16974
  return null;
16859
16975
  }
@@ -16861,7 +16977,7 @@ function readPackageJson(cwd) {
16861
16977
  function readDeclaration(cwd) {
16862
16978
  let parsed;
16863
16979
  try {
16864
- parsed = JSON.parse(readFileSync2(join2(cwd, DECLARATION_FILE), "utf8"));
16980
+ parsed = JSON.parse(readFileSync3(join3(cwd, DECLARATION_FILE), "utf8"));
16865
16981
  } catch {
16866
16982
  return null;
16867
16983
  }
@@ -16881,7 +16997,7 @@ function frameworkFrom(pkg, cwd) {
16881
16997
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
16882
16998
  const has = (name) => name in deps;
16883
16999
  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"))) {
17000
+ if (has("@react-router/dev") || existsSync3(join3(cwd, "react-router.config.ts")) || existsSync3(join3(cwd, "react-router.config.js"))) {
16885
17001
  return "react-router";
16886
17002
  }
16887
17003
  if (VITE_META_FRAMEWORKS.some(has)) return null;
@@ -18032,6 +18148,10 @@ function reportNotLinked(example) {
18032
18148
  );
18033
18149
  process.exitCode = 1;
18034
18150
  }
18151
+ function printJson2(value) {
18152
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
18153
+ `);
18154
+ }
18035
18155
  async function runLogin(opts) {
18036
18156
  const apiUrl = resolveApiUrl(opts.apiUrl);
18037
18157
  const pastedToken = opts.token ?? process.env.KRYD_TOKEN;
@@ -18055,6 +18175,10 @@ async function runWhoami(opts = {}) {
18055
18175
  const token = resolveToken() ?? "";
18056
18176
  try {
18057
18177
  const me = await whoami(apiUrl, token);
18178
+ if (opts.json) {
18179
+ printJson2(me);
18180
+ return;
18181
+ }
18058
18182
  const current = me.memberships?.find((m) => m.accountId === me.accountId);
18059
18183
  const label = current?.slug ?? me.accountSlug ?? me.accountId ?? "?";
18060
18184
  process.stdout.write(`${me.email} (account ${label}, role ${current?.role ?? me.role ?? "?"})
@@ -18082,48 +18206,110 @@ function runLogout() {
18082
18206
  process.stdout.write("Logged out.\n");
18083
18207
  warnIfEnvTokenOverrides();
18084
18208
  }
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;
18209
+ function installAgentsBlock(dir) {
18210
+ let note = "";
18211
+ const changed = [];
18212
+ let result;
18213
+ try {
18214
+ result = writeAgentsBlock(dir, renderAgentsBlock(program));
18215
+ if (result !== "unchanged") changed.push(AGENTS_FILE);
18216
+ if (result === "created") {
18217
+ note += "Wrote AGENTS.md \u2014 what coding agents need to know to deploy this project on Kryd.\n";
18218
+ } else if (result === "updated") {
18219
+ note += "Updated the Kryd block in AGENTS.md.\n";
18104
18220
  }
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;
18221
+ } catch (err) {
18222
+ process.stderr.write(
18223
+ `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.
18224
+ `
18225
+ );
18226
+ return { note, block: "failed", changed };
18227
+ }
18228
+ try {
18229
+ if (ensureClaudeImport(dir)) {
18230
+ changed.push(CLAUDE_FILE);
18231
+ note += "Added `@AGENTS.md` to CLAUDE.md, so Claude Code reads that block too.\n";
18110
18232
  }
18111
- try {
18112
- await browserLoginAndSave(apiUrl);
18113
- } catch (err) {
18114
- process.stderr.write(`${err instanceof Error ? err.message : String(err)}
18233
+ } catch (err) {
18234
+ process.stderr.write(
18235
+ `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.
18236
+ `
18237
+ );
18238
+ }
18239
+ return { note, block: result, changed };
18240
+ }
18241
+ async function settleLogin(opts) {
18242
+ let token = resolveToken();
18243
+ if (token) return { token, apiUrl: opts.apiUrl };
18244
+ const refusal = "Not logged in \u2014 run `kryd login` first.\n";
18245
+ const io = opts.io ?? defaultPromptIO();
18246
+ if (!opts.offerLogin) {
18247
+ process.stderr.write(refusal);
18248
+ process.exitCode = 1;
18249
+ return null;
18250
+ }
18251
+ if (!isInteractive(io)) {
18252
+ process.stderr.write(
18253
+ refusal + // KRYD-385 iteration 1: no KRYD_TOKEN advice — `POST /repos` refuses a deploy token (KRYD-578).
18254
+ `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.
18255
+ `
18256
+ );
18257
+ process.exitCode = 1;
18258
+ return null;
18259
+ }
18260
+ io.output.write(`You are not logged in to Kryd; \`${opts.command}\` can log you in through your browser first.
18115
18261
  `);
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();
18262
+ if (!await promptConfirm("Log in now in your browser?", io)) {
18263
+ process.stderr.write(refusal);
18264
+ process.exitCode = 1;
18265
+ return null;
18126
18266
  }
18267
+ try {
18268
+ await browserLoginAndSave(opts.apiUrl);
18269
+ } catch (err) {
18270
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
18271
+ `);
18272
+ process.exitCode = 1;
18273
+ return null;
18274
+ }
18275
+ token = resolveToken();
18276
+ if (!token) {
18277
+ process.stderr.write(refusal);
18278
+ process.exitCode = 1;
18279
+ return null;
18280
+ }
18281
+ return { token, apiUrl: opts.apiUrlFlag ? opts.apiUrl : resolveApiUrl() };
18282
+ }
18283
+ function runInitAgents(cwd) {
18284
+ const dir = findProjectLinkDir(cwd);
18285
+ if (!dir) {
18286
+ process.stderr.write(
18287
+ "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"
18288
+ );
18289
+ process.exitCode = 1;
18290
+ return;
18291
+ }
18292
+ const { note, block } = installAgentsBlock(dir);
18293
+ process.stdout.write(block === "unchanged" ? `${note}The Kryd block in AGENTS.md is up to date.
18294
+ ` : note);
18295
+ }
18296
+ async function runInit(opts) {
18297
+ const cwd = opts.cwd ?? process.cwd();
18298
+ if (opts.agents) {
18299
+ runInitAgents(cwd);
18300
+ return;
18301
+ }
18302
+ let apiUrl = resolveApiUrl(opts.apiUrl);
18303
+ const session = await settleLogin({
18304
+ apiUrl,
18305
+ ...opts.apiUrl ? { apiUrlFlag: opts.apiUrl } : {},
18306
+ ...opts.io ? { io: opts.io } : {},
18307
+ offerLogin: opts.offerLogin !== false,
18308
+ command: "kryd init"
18309
+ });
18310
+ if (!session) return;
18311
+ const { token } = session;
18312
+ apiUrl = session.apiUrl;
18127
18313
  const existingLink = directoryIsLinked(cwd);
18128
18314
  if (existingLink) {
18129
18315
  const remote = hasRemote(cwd, "kryd");
@@ -18133,6 +18319,7 @@ async function runInit(opts) {
18133
18319
  `This folder is already linked to ${existingLink.projectId} (.kryd/project.json).
18134
18320
  Running \`kryd init\` again would create a SECOND project and leave that one orphaned.
18135
18321
  To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete .kryd/project.json to unlink this folder.
18322
+ To write or refresh the Kryd block in AGENTS.md: \`kryd init --agents\`.
18136
18323
  `
18137
18324
  );
18138
18325
  process.exitCode = 1;
@@ -18182,10 +18369,10 @@ To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete
18182
18369
  try {
18183
18370
  saveProjectLink(cwd, { projectId: project2.id });
18184
18371
  try {
18185
- const session = await whoami(apiUrl, token);
18372
+ const session2 = await whoami(apiUrl, token);
18186
18373
  rememberLinkAccount(cwd, {
18187
- accountId: session.accountId,
18188
- accountSlug: session.accountSlug
18374
+ accountId: session2.accountId,
18375
+ accountSlug: session2.accountSlug
18189
18376
  });
18190
18377
  } catch {
18191
18378
  }
@@ -18244,9 +18431,10 @@ Scheduled and triggered work runs on your production deploy; preview branches bu
18244
18431
  Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
18245
18432
  ` + (declaration?.workflowsPath ? `Workflows in ${declaration.workflowsPath}/ run as a worker beside it on production deploys; previews run the web app only.
18246
18433
  ` : "");
18434
+ const agentsNote = installAgentsBlock(cwd).note;
18247
18435
  process.stdout.write(
18248
18436
  `Linked "${name}" (${project2.framework}) \u2192 ${repo.htmlUrl}
18249
- ` + routing + `${linkNote}
18437
+ ` + routing + `${linkNote}` + agentsNote + `
18250
18438
  ${nextStep}`
18251
18439
  );
18252
18440
  } catch (err) {
@@ -19265,26 +19453,34 @@ AI_GATEWAY_URL and AI_GATEWAY_TOKEN stop being injected from your next deploy (k
19265
19453
  await reportError(err);
19266
19454
  }
19267
19455
  }
19268
- async function reportResourceStatus(opts) {
19456
+ async function readResourceStatus(opts) {
19269
19457
  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
- `);
19458
+ return toResourceState(await opts.read());
19281
19459
  } catch (err) {
19282
19460
  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;
19461
+ return NOT_ATTACHED;
19286
19462
  }
19287
19463
  await reportError(err);
19464
+ return null;
19465
+ }
19466
+ }
19467
+ var NOT_ATTACHED = { attached: false, status: null, failureReason: null };
19468
+ function toResourceState(row) {
19469
+ return { attached: true, status: row.status, failureReason: row.failureReason ?? null };
19470
+ }
19471
+ function printResourceStatus(state, noun, project2) {
19472
+ if (!state.attached) {
19473
+ process.stdout.write(`${project2}: no ${noun} attached.
19474
+ `);
19475
+ } else if (state.status === "failed") {
19476
+ process.stdout.write(
19477
+ `${project2}: ${noun} failed \u2014 ${state.failureReason ?? "unknown error"}.
19478
+ Remove it before attaching another.
19479
+ `
19480
+ );
19481
+ } else {
19482
+ process.stdout.write(`${project2}: ${noun} ${state.status}.
19483
+ `);
19288
19484
  }
19289
19485
  }
19290
19486
  async function runDbStatus(opts) {
@@ -19300,21 +19496,22 @@ async function runDbStatus(opts) {
19300
19496
  reportNotLinked("kryd db status <projectId>");
19301
19497
  return;
19302
19498
  }
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",
19499
+ const production = await readResourceStatus({
19500
+ read: () => getDatabaseStatus(apiUrl, token, project2),
19311
19501
  project: project2,
19312
19502
  token
19313
19503
  });
19314
- if (!productionAttached) return;
19504
+ if (!production) return;
19505
+ if (!opts.json) printResourceStatus(production, "database", project2);
19506
+ if (!production.attached) {
19507
+ if (opts.json) printJson2({ production, preview: NOT_ATTACHED });
19508
+ return;
19509
+ }
19315
19510
  try {
19316
19511
  const preview = await readPreviewDatabase(apiUrl, token, project2);
19317
- if (!preview) {
19512
+ if (opts.json) {
19513
+ printJson2({ production, preview: preview ? toResourceState(preview) : NOT_ATTACHED });
19514
+ } else if (!preview) {
19318
19515
  process.stdout.write(
19319
19516
  `${project2}: no preview database \u2014 preview deploys use the production database.
19320
19517
  `
@@ -19353,12 +19550,14 @@ async function runStorageStatus(opts) {
19353
19550
  reportNotLinked("kryd storage status <projectId>");
19354
19551
  return;
19355
19552
  }
19356
- await reportResourceStatus({
19553
+ const state = await readResourceStatus({
19357
19554
  read: () => getStorageStatus(apiUrl, token, project2),
19358
- noun: "object storage",
19359
19555
  project: project2,
19360
19556
  token
19361
19557
  });
19558
+ if (!state) return;
19559
+ if (opts.json) printJson2(state);
19560
+ else printResourceStatus(state, "object storage", project2);
19362
19561
  }
19363
19562
  async function runAiStatus(opts) {
19364
19563
  const apiUrl = resolveApiUrl(opts.apiUrl);
@@ -19375,6 +19574,10 @@ async function runAiStatus(opts) {
19375
19574
  }
19376
19575
  try {
19377
19576
  const status = await getAiStatus(apiUrl, token, project2);
19577
+ if (opts.json) {
19578
+ printJson2(status);
19579
+ return;
19580
+ }
19378
19581
  if (!status.enabled) {
19379
19582
  process.stdout.write(`${project2}: no AI endpoint.
19380
19583
  `);
@@ -19489,6 +19692,14 @@ async function runWorkflowStatus(opts) {
19489
19692
  }
19490
19693
  try {
19491
19694
  const status = await getWorkflowStatus(apiUrl, token, project2);
19695
+ if (opts.json) {
19696
+ printJson2({
19697
+ enabled: status.enabled,
19698
+ tokenExpiresAt: status.tokenExpiresAt,
19699
+ tokenExpiresInDays: status.tokenExpiresAt ? daysUntil(status.tokenExpiresAt) : null
19700
+ });
19701
+ return;
19702
+ }
19492
19703
  if (!status.enabled) {
19493
19704
  process.stdout.write(`${project2}: no workflows.
19494
19705
  `);
@@ -19644,6 +19855,10 @@ async function runDomainStatus(opts) {
19644
19855
  const own = list.projectHost ? `${ctx.project} is always served at ${list.projectHost}.
19645
19856
  ` : "";
19646
19857
  if (!opts.host) {
19858
+ if (opts.json) {
19859
+ printJson2(list);
19860
+ return;
19861
+ }
19647
19862
  if (list.domains.length === 0) {
19648
19863
  process.stdout.write(`${ctx.project}: no custom domains.
19649
19864
  ${own}`);
@@ -19668,6 +19883,10 @@ ${own}`);
19668
19883
  process.exitCode = 1;
19669
19884
  return;
19670
19885
  }
19886
+ if (opts.json) {
19887
+ printJson2(d);
19888
+ return;
19889
+ }
19671
19890
  let out = `${d.host}: ${DOMAIN_STATUS_TEXT[d.status]}.
19672
19891
  `;
19673
19892
  if (d.redirectTo) out += ` \u2192 redirects to ${d.redirectTo}
@@ -19722,7 +19941,7 @@ async function runWorkflowInit(opts) {
19722
19941
  }
19723
19942
  const workerName = workerNameFrom(opts.name ?? basename2(target));
19724
19943
  const files = scaffoldFiles(language, workerName);
19725
- const clashes = files.map((f) => f.path).filter((p) => existsSync3(join3(target, p)));
19944
+ const clashes = files.map((f) => f.path).filter((p) => existsSync4(join4(target, p)));
19726
19945
  if (clashes.length > 0) {
19727
19946
  process.stderr.write(
19728
19947
  `Refusing to overwrite: ${clashes.join(", ")}.
@@ -19732,12 +19951,22 @@ Nothing was written. Run this in an empty directory, or pass --dir <path>.
19732
19951
  process.exitCode = 1;
19733
19952
  return;
19734
19953
  }
19954
+ if (opts.link !== false) {
19955
+ const session = await settleLogin({
19956
+ apiUrl: resolveApiUrl(opts.apiUrl),
19957
+ ...opts.apiUrl ? { apiUrlFlag: opts.apiUrl } : {},
19958
+ ...opts.io ? { io: opts.io } : {},
19959
+ offerLogin: true,
19960
+ command: "kryd workflow init"
19961
+ });
19962
+ if (!session) return;
19963
+ }
19735
19964
  const written = [];
19736
19965
  try {
19737
19966
  for (const file2 of files) {
19738
- const full = join3(target, file2.path);
19967
+ const full = join4(target, file2.path);
19739
19968
  mkdirSync2(dirname2(full), { recursive: true });
19740
- writeFileSync2(full, file2.contents);
19969
+ writeFileSync3(full, file2.contents);
19741
19970
  written.push(full);
19742
19971
  }
19743
19972
  } catch (err) {
@@ -19774,6 +20003,7 @@ To add a worker to the app in that repository instead, run \`kryd workflow init\
19774
20003
  process.exitCode = 1;
19775
20004
  return;
19776
20005
  }
20006
+ process.stdout.write(installAgentsBlock(target).note);
19777
20007
  const next = [];
19778
20008
  if (opts.dir) next.push(`cd ${opts.dir}`);
19779
20009
  const first = firstCommand(language);
@@ -19823,7 +20053,7 @@ function scaffoldContainedWorker(opts) {
19823
20053
  "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
20054
  );
19825
20055
  process.exitCode = 1;
19826
- return;
20056
+ return false;
19827
20057
  }
19828
20058
  const raw = (opts.path ?? DEFAULT_WORKFLOWS_PATH).trim();
19829
20059
  const segments = raw.split("/").filter((seg) => seg !== "" && seg !== ".");
@@ -19833,14 +20063,14 @@ function scaffoldContainedWorker(opts) {
19833
20063
  `
19834
20064
  );
19835
20065
  process.exitCode = 1;
19836
- return;
20066
+ return false;
19837
20067
  }
19838
20068
  const folder = segments.join("/");
19839
- const declarationPath = join3(target, DECLARATION_FILE);
20069
+ const declarationPath = join4(target, DECLARATION_FILE);
19840
20070
  let existing = {};
19841
20071
  let existingText = null;
19842
- if (existsSync3(declarationPath)) {
19843
- existingText = readFileSync3(declarationPath, "utf8");
20072
+ if (existsSync4(declarationPath)) {
20073
+ existingText = readFileSync4(declarationPath, "utf8");
19844
20074
  let parsed;
19845
20075
  try {
19846
20076
  parsed = JSON.parse(existingText);
@@ -19853,7 +20083,7 @@ function scaffoldContainedWorker(opts) {
19853
20083
  `
19854
20084
  );
19855
20085
  process.exitCode = 1;
19856
- return;
20086
+ return false;
19857
20087
  }
19858
20088
  existing = parsed;
19859
20089
  if ("kind" in existing) {
@@ -19862,7 +20092,7 @@ function scaffoldContainedWorker(opts) {
19862
20092
  `
19863
20093
  );
19864
20094
  process.exitCode = 1;
19865
- return;
20095
+ return false;
19866
20096
  }
19867
20097
  if ("workflows" in existing || existing["web"] === false) {
19868
20098
  process.stderr.write(
@@ -19870,7 +20100,7 @@ function scaffoldContainedWorker(opts) {
19870
20100
  `
19871
20101
  );
19872
20102
  process.exitCode = 1;
19873
- return;
20103
+ return false;
19874
20104
  }
19875
20105
  }
19876
20106
  const workerName = workerNameFrom(opts.name ?? basename2(target));
@@ -19878,7 +20108,7 @@ function scaffoldContainedWorker(opts) {
19878
20108
  path: `${folder}/${f.path}`,
19879
20109
  contents: f.contents
19880
20110
  }));
19881
- const clashes = files.map((f) => f.path).filter((p) => existsSync3(join3(target, p)));
20111
+ const clashes = files.map((f) => f.path).filter((p) => existsSync4(join4(target, p)));
19882
20112
  if (clashes.length > 0) {
19883
20113
  process.stderr.write(
19884
20114
  `Refusing to overwrite: ${clashes.join(", ")}.
@@ -19886,18 +20116,18 @@ Nothing was written. Pick another folder with --path <folder>.
19886
20116
  `
19887
20117
  );
19888
20118
  process.exitCode = 1;
19889
- return;
20119
+ return false;
19890
20120
  }
19891
20121
  const declaration = { ...existing, workflows: { path: folder } };
19892
20122
  const written = [];
19893
20123
  try {
19894
20124
  for (const file2 of files) {
19895
- const full = join3(target, file2.path);
20125
+ const full = join4(target, file2.path);
19896
20126
  mkdirSync2(dirname2(full), { recursive: true });
19897
- writeFileSync2(full, file2.contents);
20127
+ writeFileSync3(full, file2.contents);
19898
20128
  written.push(full);
19899
20129
  }
19900
- writeFileSync2(declarationPath, `${JSON.stringify(declaration, null, 2)}
20130
+ writeFileSync3(declarationPath, `${JSON.stringify(declaration, null, 2)}
19901
20131
  `);
19902
20132
  } catch (err) {
19903
20133
  for (const path of written.reverse()) {
@@ -19908,7 +20138,7 @@ Nothing was written. Pick another folder with --path <folder>.
19908
20138
  }
19909
20139
  try {
19910
20140
  if (existingText === null) rmSync2(declarationPath, { force: true });
19911
- else writeFileSync2(declarationPath, existingText);
20141
+ else writeFileSync3(declarationPath, existingText);
19912
20142
  } catch {
19913
20143
  }
19914
20144
  process.stderr.write(
@@ -19917,14 +20147,15 @@ Nothing was left behind.
19917
20147
  `
19918
20148
  );
19919
20149
  process.exitCode = 1;
19920
- return;
20150
+ return false;
19921
20151
  }
20152
+ const agents = installAgentsBlock(target);
19922
20153
  const next = [];
19923
20154
  const first = firstCommand(language);
19924
20155
  if (first) next.push(`(cd ${folder} && ${first})`);
19925
20156
  next.push(
19926
20157
  "kryd workflow add",
19927
- `git add ${DECLARATION_FILE} ${folder} && git commit -m "add workflows"`,
20158
+ `git add ${[DECLARATION_FILE, folder, ...agents.changed].join(" ")} && git commit -m "add workflows"`,
19928
20159
  "kryd push"
19929
20160
  );
19930
20161
  process.stdout.write(
@@ -19937,9 +20168,10 @@ Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
19937
20168
  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
20169
  ` + // The app is still built (and type-checked) from the root, so a tsconfig that includes every
19939
20170
  // 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
- ` : "")
20171
+ (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.
20172
+ ` : "") + agents.note
19942
20173
  );
20174
+ return true;
19943
20175
  }
19944
20176
  async function runStorageRemove(opts) {
19945
20177
  const apiUrl = resolveApiUrl(opts.apiUrl);
@@ -20019,6 +20251,11 @@ async function runProjectList(opts) {
20019
20251
  await reportError(err);
20020
20252
  return;
20021
20253
  }
20254
+ if (opts.json) {
20255
+ const linked = resolveProjectId(void 0, opts.cwd);
20256
+ printJson2({ items: projects.map((p) => ({ ...p, linked: p.id === linked })) });
20257
+ return;
20258
+ }
20022
20259
  if (projects.length === 0) {
20023
20260
  process.stdout.write("No projects yet \u2014 run `kryd init` in a project directory.\n");
20024
20261
  return;
@@ -20070,6 +20307,10 @@ async function runEnvList(opts) {
20070
20307
  }
20071
20308
  try {
20072
20309
  const vars = await listEnvVars(apiUrl, token, project2);
20310
+ if (opts.json) {
20311
+ printJson2({ items: vars });
20312
+ return;
20313
+ }
20073
20314
  if (vars.length === 0) {
20074
20315
  process.stdout.write(
20075
20316
  `No environment variables for ${project2} yet.
@@ -20155,7 +20396,7 @@ async function runEnvPull(opts) {
20155
20396
  const root = findProjectLinkDir(cwd) ?? cwd;
20156
20397
  const outRel = opts.out ?? ".env.kryd";
20157
20398
  const outPath = resolve2(root, outRel);
20158
- if (opts.out !== void 0 && existsSync3(outPath) && !opts.force) {
20399
+ if (opts.out !== void 0 && existsSync4(outPath) && !opts.force) {
20159
20400
  process.stderr.write(
20160
20401
  `Refusing to overwrite ${outRel} \u2014 pass --force to replace it (or omit --out to write .env.kryd).
20161
20402
  `
@@ -20429,7 +20670,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
20429
20670
  await reportError(err);
20430
20671
  }
20431
20672
  }
20432
- var CLI_VERSION = true ? "0.17.0" : "0.0.0-dev";
20673
+ var CLI_VERSION = true ? "0.18.0" : "0.0.0-dev";
20433
20674
  var program = new Command();
20434
20675
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
20435
20676
  program.hook("postAction", async () => {
@@ -20439,7 +20680,7 @@ program.command("login").description("Sign in via the browser (default) and stor
20439
20680
  "--token <token>",
20440
20681
  "supply a token instead of the browser flow (prefer $KRYD_TOKEN \u2014 flags are visible in `ps`)"
20441
20682
  ).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));
20683
+ 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
20684
  var accountCommand = program.command("account").description("The accounts you belong to, and which one commands run in");
20444
20685
  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
20686
  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 +20708,10 @@ program.command("init").description(
20467
20708
  "--public",
20468
20709
  // KRYD-184a. Chosen at creation; there is no command to flip it afterwards yet (KRYD-341).
20469
20710
  "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));
20711
+ ).option("--api-url <url>", "control-plane API base URL").option(
20712
+ "--agents",
20713
+ "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"
20714
+ ).action((opts) => runInit(opts));
20471
20715
  program.command("logs [target]").description(
20472
20716
  "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
20717
  ).option(
@@ -20504,7 +20748,7 @@ program.command("redeploy [project]").description(
20504
20748
  "Redeploy the current live commit (no rebuild) to apply config/env changes, and follow it live"
20505
20749
  ).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
20750
  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));
20751
+ 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
20752
  project.command("rm [project]").description(
20509
20753
  "Permanently delete a project and everything Kryd created for it (by name or id; asks you to type its name)"
20510
20754
  ).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 +20765,13 @@ db.command("add [project]").description("Attach a shared or bring-your-own Postg
20521
20765
  ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbAdd({ ...opts, project: project2 }));
20522
20766
  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
20767
  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 }));
20768
+ 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
20769
  var storage = program.command("storage").description("Manage project object storage");
20526
20770
  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
20771
  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 }));
20772
+ 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
20773
  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 }));
20774
+ 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
20775
  env.command("set <key> [project]").description(
20532
20776
  "Set one of your environment variables \u2014 `KEY=VALUE`, or `KEY` with --stdin / a prompt"
20533
20777
  ).option(
@@ -20559,11 +20803,11 @@ env.command("pull [project]").description(
20559
20803
  var ai = program.command("ai").description("Manage the app's EU AI gateway");
20560
20804
  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
20805
  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 }));
20806
+ 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 }));
20563
20807
  var workflow = program.command("workflow").description("Manage the project's durable workflows (its own workflow tenant)");
20564
20808
  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
20809
  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 }));
20810
+ 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
20811
  workflow.command("init").description(
20568
20812
  "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
20813
  ).requiredOption(
@@ -20576,7 +20820,7 @@ domain2.command("add <host>").description("Add a custom domain and print the DNS
20576
20820
  "redirect instead of serving: every request answers 301 to https://<host>, path and query kept"
20577
20821
  ).option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainAdd({ ...opts, host }));
20578
20822
  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 }));
20823
+ 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
20824
  var support = program.command("support").description(
20581
20825
  "Ask Kryd support a question, report a bug or a wish, and read the answer \u2014 non-interactive, with --json for agents"
20582
20826
  );
@@ -20594,7 +20838,7 @@ function invokedDirectly() {
20594
20838
  const entry = process.argv[1];
20595
20839
  if (!entry) return false;
20596
20840
  try {
20597
- return import.meta.url === pathToFileURL(realpathSync(entry)).href;
20841
+ return import.meta.url === pathToFileURL(realpathSync2(entry)).href;
20598
20842
  } catch {
20599
20843
  return false;
20600
20844
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kryd/cli",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",