@kryd/cli 0.2.0 → 0.2.1

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 +3 -2
  2. package/dist/index.js +908 -23
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @kryd/cli
2
2
 
3
- The command-line client for **[Kryd](https://kryd.eu)** — the effortless **EU-sovereign** deploy stack on Scaleway. Push a React / Vite / Next.js app and seconds later it's live on a real URL with SSL, one-click managed Postgres and object storage, and an EU-hosted AI gateway already wired in. The same instant `git push → live` loop you know from Vercel/Netlify, on infrastructure that is genuinely European — code **and** AI inference stay inside the EU.
3
+ The command-line client for **[Kryd](https://kryd.eu)** — the **European cloud for your AI**. Push a React / Vite / Next.js app and seconds later it's live on a real URL with SSL, one-click managed Postgres and object storage, and an EU-hosted AI gateway already wired in. The same instant `git push → live` loop you know from Vercel/Netlify, on infrastructure that is genuinely European — code **and** AI inference stay inside the EU.
4
4
 
5
5
  ## Install
6
6
 
@@ -36,13 +36,14 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
36
36
  | `kryd rollback [project] [deployment]` | Roll back to a previous successful deploy — no rebuild. |
37
37
  | `kryd db create \| detach [project]` | Attach / tear down managed Postgres (shared or bring-your-own). |
38
38
  | `kryd storage create \| detach [project]` | Attach / tear down S3-compatible object storage. |
39
+ | `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. |
39
40
  | `kryd ai enable [project]` | Give the project an authenticated EU AI-gateway endpoint (injected on the next deploy). |
40
41
 
41
42
  Run `kryd <command> --help` for options. Every project-scoped command accepts an explicit `<project>` id, or resolves it from the `.kryd` link (walking up from the current directory, git-style).
42
43
 
43
44
  ## Sovereignty
44
45
 
45
- Your application code runs on Scaleway (EU), and the AI gateway routes **only** to Scaleway's EU-hosted models — the wedge is that both your code and your model inference stay inside genuinely European infrastructure. This CLI is the open client; the token you store is a credential and is never printed or bundled into any artifact.
46
+ Your application code runs in the EU, and the AI gateway routes **only** to EU-hosted models — the destination is a compile-time constant, so there is no setting that changes it. The wedge is that both your code and your model inference stay inside genuinely European infrastructure. We name our infrastructure provider on the sub-processor list rather than in the product copy: what we promise is the **jurisdiction** and the **absence of a non-EU code path**, not a particular supplier. This CLI is the open client; the token you store is a credential and is never printed or bundled into any artifact.
46
47
 
47
48
  ## Links
48
49
 
package/dist/index.js CHANGED
@@ -14637,6 +14637,143 @@ var accountUsageSchema = external_exports.object({
14637
14637
  storageBytes: external_exports.number().int().nonnegative()
14638
14638
  });
14639
14639
 
14640
+ // ../../packages/shared-types/dist/env-vars.js
14641
+ var MANAGED_ENV_VAR_NAMES = [
14642
+ "AI_GATEWAY_TOKEN",
14643
+ "AI_GATEWAY_URL",
14644
+ "AWS_ACCESS_KEY_ID",
14645
+ "AWS_ENDPOINT_URL_S3",
14646
+ "AWS_REGION",
14647
+ "AWS_SECRET_ACCESS_KEY",
14648
+ "BUCKET_NAME",
14649
+ "DATABASE_URL",
14650
+ "DEPLOY_TOKEN",
14651
+ "FORGE_PASSWORD",
14652
+ "PUSH_TOKEN",
14653
+ "WEBHOOK_SECRET"
14654
+ ];
14655
+ var PLATFORM_ENV_VAR_NAMES = ["PATH", "PORT"];
14656
+ var RESERVED_ENV_VAR_NAMES = [
14657
+ ...MANAGED_ENV_VAR_NAMES,
14658
+ ...PLATFORM_ENV_VAR_NAMES
14659
+ ];
14660
+ var RESERVED_ENV_VAR_PREFIX = "KRYD_";
14661
+ var ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
14662
+ var ENV_VAR_NAME_MAX_LENGTH = 128;
14663
+ var ENV_VAR_VALUE_MAX_BYTES = 16 * 1024;
14664
+ function validateEnvVarName(name) {
14665
+ if (name.length === 0)
14666
+ return "An environment variable name is required.";
14667
+ if (name.length > ENV_VAR_NAME_MAX_LENGTH) {
14668
+ return `Environment variable names may be at most ${ENV_VAR_NAME_MAX_LENGTH} characters.`;
14669
+ }
14670
+ if (!ENV_VAR_NAME_PATTERN.test(name)) {
14671
+ return "Environment variable names must match ^[A-Za-z_][A-Za-z0-9_]*$ \u2014 a letter or underscore, then letters, digits or underscores.";
14672
+ }
14673
+ if (isReservedEnvVarName(name)) {
14674
+ return `"${name}" is reserved by Kryd and is set for you when you attach the matching resource \u2014 choose another name.`;
14675
+ }
14676
+ return null;
14677
+ }
14678
+ function isReservedEnvVarName(name) {
14679
+ const upper = name.toUpperCase();
14680
+ return RESERVED_ENV_VAR_NAMES.includes(upper) || upper.startsWith(RESERVED_ENV_VAR_PREFIX);
14681
+ }
14682
+ function validateEnvVarValue(value) {
14683
+ if (value.length === 0) {
14684
+ return "An environment variable value is required (an empty value cannot be stored).";
14685
+ }
14686
+ if (value.trim().length === 0) {
14687
+ return "An environment variable value cannot be only whitespace.";
14688
+ }
14689
+ if (value.includes("\0")) {
14690
+ return "Environment variable values may not contain NUL bytes.";
14691
+ }
14692
+ const bytes = new TextEncoder().encode(value).length;
14693
+ if (bytes > ENV_VAR_VALUE_MAX_BYTES) {
14694
+ return `Environment variable values may be at most ${ENV_VAR_VALUE_MAX_BYTES} bytes (got ${bytes}).`;
14695
+ }
14696
+ return null;
14697
+ }
14698
+
14699
+ // ../../packages/shared-types/dist/build-env-vars.js
14700
+ var PUBLIC_BUILD_ENV_PREFIXES = [
14701
+ "ASTRO_PUBLIC_",
14702
+ "EXPO_PUBLIC_",
14703
+ "GATSBY_",
14704
+ "NEXT_PUBLIC_",
14705
+ "NUXT_PUBLIC_",
14706
+ "PUBLIC_",
14707
+ "REACT_APP_",
14708
+ "VITE_"
14709
+ ];
14710
+ var SECRET_NAME_WORDS = [
14711
+ "SECRET",
14712
+ "PASSWORD",
14713
+ "PASSWD",
14714
+ "PASSPHRASE",
14715
+ "PRIVATE_KEY",
14716
+ "PRIVATEKEY",
14717
+ "CREDENTIAL"
14718
+ ];
14719
+ var SECRET_VALUE_SHAPES = [
14720
+ { prefix: "sk_live_", label: "a Stripe secret key" },
14721
+ { prefix: "sk_test_", label: "a Stripe test secret key" },
14722
+ { prefix: "rk_live_", label: "a Stripe restricted key" },
14723
+ { prefix: "rk_test_", label: "a Stripe restricted test key" },
14724
+ { prefix: "ghp_", label: "a GitHub personal access token" },
14725
+ { prefix: "gho_", label: "a GitHub OAuth token" },
14726
+ { prefix: "ghs_", label: "a GitHub app token" },
14727
+ { prefix: "github_pat_", label: "a GitHub fine-grained token" },
14728
+ { prefix: "xoxb-", label: "a Slack bot token" },
14729
+ { prefix: "xoxp-", label: "a Slack user token" },
14730
+ { prefix: "xoxs-", label: "a Slack app token" }
14731
+ ];
14732
+ var PEM_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
14733
+ var URL_WITH_CREDENTIALS_PATTERN = /[a-z][a-z0-9+.-]*:\/\/[^/@\s:]*:[^/@\s]+@/i;
14734
+ var NEWLINE_PATTERN = /[\n\r]/;
14735
+ var BUILD_ENV_VAR_VALUE_MAX_BYTES = 4 * 1024;
14736
+ function hasPublicBuildPrefix(name) {
14737
+ return PUBLIC_BUILD_ENV_PREFIXES.some((p) => name.startsWith(p));
14738
+ }
14739
+ function publicBuildPrefixList() {
14740
+ return PUBLIC_BUILD_ENV_PREFIXES.join(", ");
14741
+ }
14742
+ function validateBuildEnvVarName(name) {
14743
+ if (!hasPublicBuildPrefix(name)) {
14744
+ return `Build-time variables are compiled into your app's public bundle, so their names must start with one of: ${publicBuildPrefixList()} \u2014 the prefixes bundlers use for values they will expose to the browser. "${name}" has none of them. If it is a secret, set it as a runtime variable instead (drop --build); if it really is public, rename it (e.g. VITE_${name}).`;
14745
+ }
14746
+ const upper = name.toUpperCase();
14747
+ const word = SECRET_NAME_WORDS.find((w) => upper.includes(w));
14748
+ if (word) {
14749
+ return `"${name}" contains "${word}", and a build-time value is compiled into your public bundle where anyone can read it. Set it as a runtime variable instead (drop --build) \u2014 runtime values are stored in Scaleway Secret Manager and injected into the container, never into the bundle.`;
14750
+ }
14751
+ return null;
14752
+ }
14753
+ function validateBuildEnvVarValue(value) {
14754
+ const bytes = new TextEncoder().encode(value).length;
14755
+ if (bytes > BUILD_ENV_VAR_VALUE_MAX_BYTES) {
14756
+ return `Build-time values may be at most ${BUILD_ENV_VAR_VALUE_MAX_BYTES} bytes (got ${bytes}) \u2014 they are passed to the build host as command-line arguments. A value this large is almost certainly a key or a certificate, which must not be compiled into a public bundle: set it as a runtime variable instead (drop --build).`;
14757
+ }
14758
+ const trimmed = value.trim();
14759
+ const shape = SECRET_VALUE_SHAPES.find((s) => trimmed.includes(s.prefix));
14760
+ if (shape) {
14761
+ return `That value looks like ${shape.label}. Build-time values are compiled into your app's public bundle \u2014 publishing it would mean rotating it immediately. Set it as a runtime variable instead (drop --build).`;
14762
+ }
14763
+ if (PEM_PRIVATE_KEY_PATTERN.test(value)) {
14764
+ return "That value contains a PEM private key. Build-time values are compiled into your app's public bundle, so it would be published to every visitor. Set it as a runtime variable instead (drop --build).";
14765
+ }
14766
+ if (URL_WITH_CREDENTIALS_PATTERN.test(value)) {
14767
+ return "That value is a URL with a username and password in it (for example a database connection string). Build-time values are compiled into your app's public bundle, so those credentials would be published to every visitor. Set it as a runtime variable instead (drop --build), or use a URL without embedded credentials.";
14768
+ }
14769
+ if (NEWLINE_PATTERN.test(value)) {
14770
+ return "Build-time values cannot contain line breaks: they are passed to the build as a single command-line argument, and the builder reads only the first line \u2014 the rest would be dropped silently and your app would receive a truncated value. Remove the line break if it is a stray one (a trailing newline from a paste or a file is the usual cause), or set it as a runtime variable instead (drop --build) \u2014 runtime values take a different route (Scaleway Secret Manager \u2192 the container's environment) and keep line breaks intact.";
14771
+ }
14772
+ return null;
14773
+ }
14774
+ var BUILD_ENV_PUBLIC_WARNING = "\u26A0 Build-time values are compiled into your app's public bundle \u2014 anyone who loads your site can read them. Never put a secret here.";
14775
+ var BUILD_ENV_REBUILD_NOTE = "This takes effect at your next BUILD, not your next deploy \u2014 the value is compiled into the image, so redeploying the existing image keeps the old value. Push a commit (`kryd push`) to rebuild.";
14776
+
14640
14777
  // ../../packages/shared-types/dist/index.js
14641
14778
  var TERMINAL_DEPLOY_STATES = [
14642
14779
  "live",
@@ -14658,6 +14795,10 @@ function isTerminalResourceStatus(status) {
14658
14795
  return status === "ready" || status === "failed";
14659
14796
  }
14660
14797
 
14798
+ // src/index.ts
14799
+ import { existsSync as existsSync3 } from "node:fs";
14800
+ import { isAbsolute, relative, resolve } from "node:path";
14801
+
14661
14802
  // src/config.ts
14662
14803
  import {
14663
14804
  appendFileSync,
@@ -14759,19 +14900,27 @@ function saveProjectLink(cwd, link) {
14759
14900
  writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
14760
14901
  return path;
14761
14902
  }
14762
- function ensureKrydIgnored(cwd) {
14903
+ function ensureIgnored(cwd, entry, aliases = []) {
14763
14904
  const path = join(cwd, ".gitignore");
14764
- const entry = `${PROJECT_LINK_DIR}/`;
14765
14905
  let current = "";
14766
14906
  try {
14767
14907
  current = readFileSync(path, "utf8");
14768
14908
  } catch {
14769
14909
  }
14770
- const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || l === PROJECT_LINK_DIR);
14771
- if (present) return;
14910
+ const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
14911
+ if (present) return false;
14772
14912
  const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
14773
14913
  appendFileSync(path, `${prefix}${entry}
14774
14914
  `);
14915
+ return true;
14916
+ }
14917
+ function ensureKrydIgnored(cwd) {
14918
+ ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
14919
+ }
14920
+ function writeSecretFile(path, contents) {
14921
+ mkdirSync(dirname(path), { recursive: true });
14922
+ writeFileSync(path, contents, { mode: 384 });
14923
+ chmodSync(path, 384);
14775
14924
  }
14776
14925
  function resolveProjectId(explicit, cwd = process.cwd()) {
14777
14926
  if (explicit) return explicit;
@@ -14796,7 +14945,7 @@ function browserLogin(dashboardUrl, opts = {}) {
14796
14945
  const state = randomUUID();
14797
14946
  const timeoutMs = opts.timeoutMs ?? 12e4;
14798
14947
  const shouldOpen = opts.open ?? true;
14799
- return new Promise((resolve, reject) => {
14948
+ return new Promise((resolve2, reject) => {
14800
14949
  let settled = false;
14801
14950
  const settle = (fn) => {
14802
14951
  if (settled) return;
@@ -14826,7 +14975,7 @@ function browserLogin(dashboardUrl, opts = {}) {
14826
14975
  res.end(SUCCESS_HTML);
14827
14976
  settle(() => {
14828
14977
  server.close();
14829
- resolve({ token, ...apiUrl ? { apiUrl } : {} });
14978
+ resolve2({ token, ...apiUrl ? { apiUrl } : {} });
14830
14979
  });
14831
14980
  });
14832
14981
  server.on("error", (err) => settle(() => reject(err)));
@@ -15104,6 +15253,158 @@ async function detachStorage(apiUrl, token, projectId) {
15104
15253
  );
15105
15254
  }
15106
15255
  }
15256
+ async function listEnvVars(apiUrl, token, projectId, include = "all") {
15257
+ const res = await fetch(
15258
+ `${apiUrl}/projects/${encodeURIComponent(projectId)}/env?scope=${encodeURIComponent(include)}`,
15259
+ { headers: { authorization: `Bearer ${token}` } }
15260
+ );
15261
+ if (!res.ok) {
15262
+ throw new ApiError(
15263
+ `Listing environment variables failed (${res.status})`,
15264
+ await parseEnvelope(res),
15265
+ res.status
15266
+ );
15267
+ }
15268
+ const parsed = await res.json().catch(() => void 0);
15269
+ if (!parsed || !Array.isArray(parsed.vars)) {
15270
+ throw new ApiError("The API returned an unexpected environment response.");
15271
+ }
15272
+ return parsed.vars.map((raw) => {
15273
+ if (typeof raw !== "object" || raw === null) {
15274
+ throw new ApiError("The API returned an unexpected environment response.");
15275
+ }
15276
+ const entry = raw;
15277
+ if (typeof entry.key !== "string" || entry.key.length === 0 || typeof entry.source !== "string") {
15278
+ throw new ApiError("The API returned an unexpected environment response.");
15279
+ }
15280
+ return {
15281
+ key: entry.key,
15282
+ // Which environments hold an explicit value (KRYD-159). An api older than KRYD-159 omits it, and
15283
+ // the renderer reads a missing/empty list as "one value, all environments" — the correct reading
15284
+ // of its absence. Kept as strings without rejecting an unknown one, same tolerance as `source`.
15285
+ ...Array.isArray(entry.environments) ? {
15286
+ environments: entry.environments.filter(
15287
+ (e) => e === "production" || e === "preview"
15288
+ )
15289
+ } : {},
15290
+ // Deliberately NOT rejected when it is outside `SECRET_SOURCES`. `SecretSource` is branded
15291
+ // plain text precisely so the value set can grow without a migration, and the CLI is published
15292
+ // to npm — so an installed copy is routinely older than the api. Rejecting an unrecognised
15293
+ // source would make one new row break `kryd env list` entirely, including for the variables
15294
+ // the user set themselves. The renderer groups anything it does not recognise instead. The
15295
+ // malformed-body guard still holds: a proxy error page has no `vars` array at all.
15296
+ source: entry.source,
15297
+ // Same tolerance, same reason (KRYD-156): an api older than this CLI omits `scope` entirely,
15298
+ // and every row such an api can return IS a runtime var — so the absent field has one correct
15299
+ // reading. An unrecognised NEW scope is passed through rather than rejected, and the renderer
15300
+ // groups it under "other" instead of dropping the variable from the listing.
15301
+ scope: typeof entry.scope === "string" ? entry.scope : "runtime",
15302
+ // The contract types `updatedAt` as nullable precisely so a missing timestamp is rendered
15303
+ // rather than assumed — normalise anything non-string to `null` instead of failing.
15304
+ updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : null
15305
+ };
15306
+ });
15307
+ }
15308
+ async function pullEnvValues(apiUrl, token, projectId, environment = "production") {
15309
+ const query = environment === "production" ? "" : `?environment=${encodeURIComponent(environment)}`;
15310
+ const res = await fetch(
15311
+ `${apiUrl}/projects/${encodeURIComponent(projectId)}/env/values${query}`,
15312
+ { headers: { authorization: `Bearer ${token}` } }
15313
+ );
15314
+ if (!res.ok) {
15315
+ throw new ApiError(
15316
+ `Pulling environment values failed (${res.status})`,
15317
+ await parseEnvelope(res),
15318
+ res.status
15319
+ );
15320
+ }
15321
+ const parsed = await res.json().catch(() => void 0);
15322
+ if (!parsed || typeof parsed.values !== "object" || parsed.values === null || Array.isArray(parsed.values)) {
15323
+ throw new ApiError("The API returned an unexpected environment-values response.");
15324
+ }
15325
+ const values = {};
15326
+ for (const [key, value] of Object.entries(
15327
+ parsed.values
15328
+ )) {
15329
+ if (typeof value !== "string") {
15330
+ throw new ApiError("The API returned an unexpected environment-values response.");
15331
+ }
15332
+ values[key] = value;
15333
+ }
15334
+ return {
15335
+ environment: parsed.environment === "preview" ? "preview" : "production",
15336
+ values
15337
+ };
15338
+ }
15339
+ async function setEnvVar(apiUrl, token, projectId, key, value, scope = "runtime", environment = "production") {
15340
+ const res = await fetch(
15341
+ `${apiUrl}/projects/${encodeURIComponent(projectId)}/env`,
15342
+ {
15343
+ method: "PUT",
15344
+ headers: {
15345
+ "content-type": "application/json",
15346
+ authorization: `Bearer ${token}`
15347
+ },
15348
+ // `scope`/`environment` are sent only when NOT their defaults. An api older than KRYD-156/159
15349
+ // ignores unknown body fields, so this is belt-and-braces — but it keeps a plain runtime
15350
+ // production request byte-identical to the one KRYD-157 shipped.
15351
+ body: JSON.stringify({
15352
+ key,
15353
+ value,
15354
+ ...scope === "runtime" ? {} : { scope },
15355
+ ...environment === "production" ? {} : { environment }
15356
+ })
15357
+ }
15358
+ );
15359
+ if (!res.ok) {
15360
+ throw new ApiError(
15361
+ `Setting ${key} failed (${res.status})`,
15362
+ await parseEnvelope(res),
15363
+ res.status
15364
+ );
15365
+ }
15366
+ const parsed = await res.json().catch(() => void 0);
15367
+ const expected = scope === "build" ? "appliesAtNextBuild" : "appliesAtNextDeploy";
15368
+ if (!parsed || typeof parsed.key !== "string" || typeof parsed.created !== "boolean" || parsed[expected] !== true) {
15369
+ throw new ApiError(
15370
+ scope === "build" ? `The API returned an unexpected environment response \u2014 it may be an older version that does not support build-time variables. If so it has stored "${key}" as a RUNTIME variable instead: check with \`kryd env list\` and remove it with \`kryd env rm ${key}\` if you did not want that.` : "The API returned an unexpected environment response."
15371
+ );
15372
+ }
15373
+ return {
15374
+ key: parsed.key,
15375
+ created: parsed.created,
15376
+ appliesAtNextDeploy: scope !== "build",
15377
+ appliesAtNextBuild: scope === "build"
15378
+ };
15379
+ }
15380
+ async function removeEnvVar(apiUrl, token, projectId, key, scope = "runtime", environment = "production") {
15381
+ const params = new URLSearchParams();
15382
+ if (scope !== "runtime") params.set("scope", scope);
15383
+ if (environment !== "production") params.set("environment", environment);
15384
+ const query = params.toString() ? `?${params.toString()}` : "";
15385
+ const res = await fetch(
15386
+ `${apiUrl}/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(key)}${query}`,
15387
+ { method: "DELETE", headers: { authorization: `Bearer ${token}` } }
15388
+ );
15389
+ if (!res.ok) {
15390
+ throw new ApiError(
15391
+ `Removing ${key} failed (${res.status})`,
15392
+ await parseEnvelope(res),
15393
+ res.status
15394
+ );
15395
+ }
15396
+ const parsed = await res.json().catch(() => void 0);
15397
+ if (scope === "build" && parsed?.appliesAtNextBuild !== true) {
15398
+ throw new ApiError(
15399
+ `Could not confirm that the build-time variable "${key}" was removed. The API may be an older version that does not support build-time variables \u2014 in which case it has removed the RUNTIME variable "${key}" instead. Run \`kryd env list\` to see what is actually set.`
15400
+ );
15401
+ }
15402
+ return {
15403
+ key: typeof parsed?.key === "string" ? parsed.key : key,
15404
+ appliesAtNextDeploy: scope !== "build",
15405
+ appliesAtNextBuild: scope === "build"
15406
+ };
15407
+ }
15107
15408
  async function triggerDeploy(apiUrl, token, projectId) {
15108
15409
  const res = await fetch(`${apiUrl}/deployments`, {
15109
15410
  method: "POST",
@@ -15135,6 +15436,20 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
15135
15436
  }
15136
15437
  return await res.json();
15137
15438
  }
15439
+ async function redeployProject(apiUrl, token, projectId) {
15440
+ const res = await fetch(`${apiUrl}/deployments/redeploy`, {
15441
+ method: "POST",
15442
+ headers: {
15443
+ "content-type": "application/json",
15444
+ authorization: `Bearer ${token}`
15445
+ },
15446
+ body: JSON.stringify({ projectId })
15447
+ });
15448
+ if (!res.ok) {
15449
+ throw new ApiError(`Redeploy failed (${res.status})`, await parseEnvelope(res));
15450
+ }
15451
+ return await res.json();
15452
+ }
15138
15453
  async function listDeployments(apiUrl, token, projectId) {
15139
15454
  const url2 = new URL(`${apiUrl}/deployments`);
15140
15455
  if (projectId) url2.searchParams.set("projectId", projectId);
@@ -15196,14 +15511,14 @@ async function* readSseFrames(body) {
15196
15511
  }
15197
15512
  function abortableSleep(ms, signal) {
15198
15513
  if (signal?.aborted) return Promise.resolve();
15199
- return new Promise((resolve) => {
15514
+ return new Promise((resolve2) => {
15200
15515
  const onAbort = () => {
15201
15516
  clearTimeout(timer);
15202
- resolve();
15517
+ resolve2();
15203
15518
  };
15204
15519
  const timer = setTimeout(() => {
15205
15520
  signal?.removeEventListener("abort", onAbort);
15206
- resolve();
15521
+ resolve2();
15207
15522
  }, ms);
15208
15523
  signal?.addEventListener("abort", onAbort, { once: true });
15209
15524
  });
@@ -15246,13 +15561,14 @@ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15246
15561
  }
15247
15562
  return terminal;
15248
15563
  }
15249
- async function streamRuntime(apiUrl, token, projectId, handlers, opts = {}) {
15564
+ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15565
+ const basePath = opts.resource === "deployment" ? "deployments" : "projects";
15250
15566
  const doFetch = opts.fetchImpl ?? fetch;
15251
15567
  const reconnectMs = opts.reconnectMs ?? 1e3;
15252
15568
  let afterNs;
15253
15569
  while (!opts.signal?.aborted) {
15254
15570
  try {
15255
- const url2 = new URL(`${apiUrl}/projects/${projectId}/runtime-logs`);
15571
+ const url2 = new URL(`${apiUrl}/${basePath}/${id}/runtime-logs`);
15256
15572
  if (afterNs) url2.searchParams.set("after", afterNs);
15257
15573
  else if (opts.since) url2.searchParams.set("since", opts.since);
15258
15574
  const res = await doFetch(url2, {
@@ -15286,7 +15602,12 @@ async function streamRuntime(apiUrl, token, projectId, handlers, opts = {}) {
15286
15602
  // src/framework.ts
15287
15603
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
15288
15604
  import { basename, join as join2 } from "node:path";
15289
- var FRAMEWORKS = ["react-router", "nextjs", "vite-spa"];
15605
+ var FRAMEWORKS = [
15606
+ "react-router",
15607
+ "nextjs",
15608
+ "vite-spa",
15609
+ "node"
15610
+ ];
15290
15611
  function isFramework(value) {
15291
15612
  return FRAMEWORKS.includes(value);
15292
15613
  }
@@ -15300,6 +15621,7 @@ var VITE_META_FRAMEWORKS = [
15300
15621
  "@builder.io/qwik-city",
15301
15622
  "vike"
15302
15623
  ];
15624
+ var SERVER_FRAMEWORKS = ["hono", "express", "fastify", "koa"];
15303
15625
  function readPackageJson(cwd) {
15304
15626
  try {
15305
15627
  return JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
@@ -15315,6 +15637,7 @@ function frameworkFrom(pkg, cwd) {
15315
15637
  return "react-router";
15316
15638
  }
15317
15639
  if (VITE_META_FRAMEWORKS.some(has)) return null;
15640
+ if (SERVER_FRAMEWORKS.some(has)) return "node";
15318
15641
  if (has("vite")) return "vite-spa";
15319
15642
  return null;
15320
15643
  }
@@ -15326,6 +15649,82 @@ function inspectProject(cwd) {
15326
15649
  return { framework: frameworkFrom(pkg, cwd), name: nameFrom(pkg, cwd) };
15327
15650
  }
15328
15651
 
15652
+ // src/input.ts
15653
+ import { createInterface } from "node:readline";
15654
+ import { Writable } from "node:stream";
15655
+ function defaultPromptIO() {
15656
+ return {
15657
+ input: process.stdin,
15658
+ output: process.stderr,
15659
+ isTTY: Boolean(process.stdin.isTTY) && Boolean(process.stderr.isTTY)
15660
+ };
15661
+ }
15662
+ function isInteractive(io) {
15663
+ if (io.isTTY !== void 0) return io.isTTY;
15664
+ const output = io.output;
15665
+ return Boolean(io.input.isTTY) && Boolean(output.isTTY);
15666
+ }
15667
+ async function readAll(input, maxBytes) {
15668
+ const chunks = [];
15669
+ let bytes = 0;
15670
+ for await (const chunk of input) {
15671
+ const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
15672
+ bytes += buf.length;
15673
+ if (maxBytes !== void 0 && bytes > maxBytes) return { tooLarge: true };
15674
+ chunks.push(buf);
15675
+ }
15676
+ return { value: Buffer.concat(chunks).toString("utf8") };
15677
+ }
15678
+ function stripOneTrailingNewline(value) {
15679
+ if (value.endsWith("\r\n")) return value.slice(0, -2);
15680
+ if (value.endsWith("\n")) return value.slice(0, -1);
15681
+ return value;
15682
+ }
15683
+ async function promptHidden(question, io) {
15684
+ io.output.write(question);
15685
+ const restore = () => {
15686
+ const tty = io.input;
15687
+ if (tty.isTTY) tty.setRawMode?.(false);
15688
+ };
15689
+ process.once("SIGTERM", restore);
15690
+ process.once("SIGHUP", restore);
15691
+ const sink = new Writable({
15692
+ write(_chunk, _encoding, callback) {
15693
+ callback();
15694
+ }
15695
+ });
15696
+ const rl = createInterface({
15697
+ input: io.input,
15698
+ output: sink,
15699
+ terminal: true
15700
+ });
15701
+ try {
15702
+ return await new Promise((resolve2) => {
15703
+ rl.once("SIGINT", () => resolve2(null));
15704
+ rl.once("close", () => resolve2(null));
15705
+ rl.question("", (answer) => resolve2(answer));
15706
+ });
15707
+ } finally {
15708
+ rl.close();
15709
+ process.off("SIGTERM", restore);
15710
+ process.off("SIGHUP", restore);
15711
+ io.output.write("\n");
15712
+ }
15713
+ }
15714
+ async function promptConfirm(question, io) {
15715
+ const rl = createInterface({ input: io.input, output: io.output });
15716
+ try {
15717
+ const answer = await new Promise((resolve2) => {
15718
+ rl.once("SIGINT", () => resolve2(null));
15719
+ rl.once("close", () => resolve2(null));
15720
+ rl.question(`${question} [y/N] `, (a) => resolve2(a));
15721
+ });
15722
+ return answer !== null && /^y(es)?$/i.test(answer.trim());
15723
+ } finally {
15724
+ rl.close();
15725
+ }
15726
+ }
15727
+
15329
15728
  // src/git.ts
15330
15729
  import { execFileSync, spawnSync } from "node:child_process";
15331
15730
  function authenticatedRemoteUrl(cloneUrl, username, token) {
@@ -15467,7 +15866,7 @@ async function runInit(opts) {
15467
15866
  if (opts.framework) {
15468
15867
  if (!isFramework(opts.framework)) {
15469
15868
  process.stderr.write(
15470
- `Unknown framework "${opts.framework}" \u2014 expected react-router, nextjs, or vite-spa.
15869
+ `Unknown framework "${opts.framework}" \u2014 expected react-router, nextjs, vite-spa, or node.
15471
15870
  `
15472
15871
  );
15473
15872
  process.exitCode = 1;
@@ -15477,7 +15876,7 @@ async function runInit(opts) {
15477
15876
  } else {
15478
15877
  if (!detected.framework) {
15479
15878
  process.stderr.write(
15480
- "Could not detect a supported framework (React Router, Next.js, or a Vite SPA). Pass --framework <react-router|nextjs|vite-spa>.\n"
15879
+ "Could not detect a supported framework (React Router, Next.js, a Vite SPA, or a Node service). Pass --framework <react-router|nextjs|vite-spa|node>.\n"
15481
15880
  );
15482
15881
  process.exitCode = 1;
15483
15882
  return;
@@ -15612,8 +16011,9 @@ async function runRuntimeLogs(opts) {
15612
16011
  process.exitCode = 1;
15613
16012
  return;
15614
16013
  }
15615
- const project = resolveProjectId(opts.project, opts.cwd);
15616
- if (!project) {
16014
+ const isDeployment = opts.project?.startsWith("dpl_") ?? false;
16015
+ const target = isDeployment ? opts.project : resolveProjectId(opts.project, opts.cwd);
16016
+ if (!target) {
15617
16017
  reportNotLinked("kryd logs <projectId> --runtime");
15618
16018
  return;
15619
16019
  }
@@ -15632,7 +16032,7 @@ async function runRuntimeLogs(opts) {
15632
16032
  await streamRuntime(
15633
16033
  apiUrl,
15634
16034
  token,
15635
- project,
16035
+ target,
15636
16036
  {
15637
16037
  onLine: ({ stream, line }) => {
15638
16038
  const out = stream === "stderr" ? process.stderr : process.stdout;
@@ -15640,7 +16040,11 @@ async function runRuntimeLogs(opts) {
15640
16040
  `);
15641
16041
  }
15642
16042
  },
15643
- { ...opts.since ? { since: opts.since } : {}, signal: controller.signal }
16043
+ {
16044
+ ...opts.since ? { since: opts.since } : {},
16045
+ signal: controller.signal,
16046
+ ...isDeployment ? { resource: "deployment" } : {}
16047
+ }
15644
16048
  );
15645
16049
  } catch (err) {
15646
16050
  reportError(err);
@@ -15829,6 +16233,35 @@ async function runRollback(opts) {
15829
16233
  reportError(err);
15830
16234
  }
15831
16235
  }
16236
+ async function runRedeploy(opts) {
16237
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16238
+ const token = loadConfig().token;
16239
+ if (!token) {
16240
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16241
+ process.exitCode = 1;
16242
+ return;
16243
+ }
16244
+ const project = resolveProjectId(opts.project, opts.cwd);
16245
+ if (!project) {
16246
+ reportNotLinked("kryd redeploy <projectId>");
16247
+ return;
16248
+ }
16249
+ try {
16250
+ const { deploymentId, redeployedCommit, note } = await redeployProject(
16251
+ apiUrl,
16252
+ token,
16253
+ project
16254
+ );
16255
+ process.stdout.write(
16256
+ `Redeploying ${redeployedCommit.slice(0, 7)} \u2192 ${deploymentId}
16257
+ ${note}
16258
+ `
16259
+ );
16260
+ await followDeploy(apiUrl, token, deploymentId);
16261
+ } catch (err) {
16262
+ reportError(err);
16263
+ }
16264
+ }
15832
16265
  async function runDbCreate(opts) {
15833
16266
  const apiUrl = resolveApiUrl(opts.apiUrl);
15834
16267
  const token = loadConfig().token;
@@ -16043,7 +16476,420 @@ async function runStorageDetach(opts) {
16043
16476
  reportError(err);
16044
16477
  }
16045
16478
  }
16046
- var CLI_VERSION = true ? "0.2.0" : "0.0.0-dev";
16479
+ function splitKeyValue(spec) {
16480
+ const eq = spec.indexOf("=");
16481
+ if (eq === -1) return { key: spec };
16482
+ return { key: spec.slice(0, eq), value: spec.slice(eq + 1) };
16483
+ }
16484
+ var NEXT_DEPLOY_NOTE = "This takes effect on your next deploy \u2014 run `kryd redeploy` to apply it now (no rebuild), or it lands on your next `kryd push`.\n";
16485
+ var NEXT_BUILD_NOTE = `${BUILD_ENV_REBUILD_NOTE}
16486
+ `;
16487
+ var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T/;
16488
+ function isoDate(value) {
16489
+ if (!value || !ISO_TIMESTAMP.test(value)) return null;
16490
+ const date5 = new Date(value);
16491
+ if (Number.isNaN(date5.getTime())) return null;
16492
+ const pad = (n) => String(n).padStart(2, "0");
16493
+ return `${date5.getFullYear()}-${pad(date5.getMonth() + 1)}-${pad(date5.getDate())}`;
16494
+ }
16495
+ function envAnnotation(environments) {
16496
+ if (!environments || environments.length === 0) return "";
16497
+ const prod = environments.includes("production");
16498
+ const preview = environments.includes("preview");
16499
+ if (prod && preview) return " \u2192 preview override";
16500
+ if (preview && !prod) return " \u2192 preview only";
16501
+ return "";
16502
+ }
16503
+ function renderEnvGroup(heading, vars, opts) {
16504
+ const width = vars.reduce((max, v) => Math.max(max, v.key.length), 0);
16505
+ const dates = opts.showUpdated ? vars.map((v) => isoDate(v.updatedAt)) : [];
16506
+ const anyDate = dates.some((d) => d !== null);
16507
+ const rows = vars.map((v, i) => {
16508
+ const envNote = opts.showEnvironments ? envAnnotation(v.environments) : "";
16509
+ if (!anyDate) return ` ${v.key}${envNote}
16510
+ `;
16511
+ const updated = dates[i];
16512
+ return ` ${v.key.padEnd(width)} ${updated ? `updated ${updated}` : ""}${envNote}
16513
+ `.trimEnd() + "\n";
16514
+ }).join("");
16515
+ return `${heading}
16516
+ ${rows}`;
16517
+ }
16518
+ async function runEnvList(opts) {
16519
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16520
+ const token = loadConfig().token;
16521
+ if (!token) {
16522
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16523
+ process.exitCode = 1;
16524
+ return;
16525
+ }
16526
+ const project = resolveProjectId(opts.project, opts.cwd);
16527
+ if (!project) {
16528
+ reportNotLinked("kryd env list <projectId>");
16529
+ return;
16530
+ }
16531
+ try {
16532
+ const vars = await listEnvVars(apiUrl, token, project);
16533
+ if (vars.length === 0) {
16534
+ process.stdout.write(
16535
+ `No environment variables for ${project} yet.
16536
+ Set one with \`kryd env set KEY --stdin\`, or attach a database, storage or the AI gateway.
16537
+ `
16538
+ );
16539
+ return;
16540
+ }
16541
+ const byKey = (a, b) => a.key.localeCompare(b.key);
16542
+ const buildVars = vars.filter((v) => v.scope === "build").sort(byKey);
16543
+ const runtimeVars = vars.filter((v) => v.scope !== "build");
16544
+ const mine = runtimeVars.filter((v) => v.source === "customer").sort(byKey);
16545
+ const managed = runtimeVars.filter((v) => v.source === "managed").sort(byKey);
16546
+ const other = runtimeVars.filter((v) => v.source !== "customer" && v.source !== "managed").sort(byKey);
16547
+ let out = "";
16548
+ if (mine.length > 0) {
16549
+ out += renderEnvGroup(`Set by you (${mine.length})`, mine, {
16550
+ showUpdated: true,
16551
+ // Your customer vars can carry a preview override (runtime KRYD-159, build KRYD-215); annotate
16552
+ // which ones do.
16553
+ showEnvironments: true
16554
+ });
16555
+ }
16556
+ if (managed.length > 0) {
16557
+ if (out) out += "\n";
16558
+ out += renderEnvGroup(
16559
+ `Managed by Kryd (${managed.length}) \u2014 set when you attach a resource; not editable`,
16560
+ managed,
16561
+ // A managed row's timestamp is Kryd's own bookkeeping, not something the user did; showing
16562
+ // it invites "why did my DATABASE_URL change today?".
16563
+ { showUpdated: false }
16564
+ );
16565
+ }
16566
+ if (other.length > 0) {
16567
+ if (out) out += "\n";
16568
+ out += renderEnvGroup(
16569
+ `Other (${other.length}) \u2014 set by a newer version of Kryd; upgrade the CLI (\`npm install -g @kryd/cli\`) to see what they are`,
16570
+ other,
16571
+ { showUpdated: false }
16572
+ );
16573
+ }
16574
+ if (buildVars.length > 0) {
16575
+ if (out) out += "\n";
16576
+ out += renderEnvGroup(
16577
+ `Build-time (${buildVars.length}) \u2014 compiled into your public bundle`,
16578
+ buildVars,
16579
+ // Build vars are per-environment too now (KRYD-215) — annotate a preview override / preview-only
16580
+ // build var, exactly as the runtime group does.
16581
+ { showUpdated: true, showEnvironments: true }
16582
+ );
16583
+ out += `${BUILD_ENV_PUBLIC_WARNING}
16584
+ `;
16585
+ }
16586
+ out += "\nValues are never shown. This is the stored set \u2014 runtime changes reach your container at its next deploy (run `kryd redeploy` to apply them now)" + (buildVars.length > 0 ? ", and build-time changes at its next build.\n" : ".\n");
16587
+ process.stdout.write(out);
16588
+ } catch (err) {
16589
+ reportError(err);
16590
+ }
16591
+ }
16592
+ function toDotenvLine(key, value) {
16593
+ const hasNewline = /[\n\r]/.test(value);
16594
+ const needsQuote = hasNewline || value === "" || value !== value.trim() || value.includes("#") || /^["'`]/.test(value);
16595
+ if (!needsQuote) return `${key}=${value}`;
16596
+ if (!hasNewline && !value.includes("'")) return `${key}='${value}'`;
16597
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
16598
+ return `${key}="${escaped}"`;
16599
+ }
16600
+ async function runEnvPull(opts) {
16601
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16602
+ const token = loadConfig().token;
16603
+ if (!token) {
16604
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16605
+ process.exitCode = 1;
16606
+ return;
16607
+ }
16608
+ const project = resolveProjectId(opts.project, opts.cwd);
16609
+ if (!project) {
16610
+ reportNotLinked("kryd env pull <projectId>");
16611
+ return;
16612
+ }
16613
+ const environment = opts.preview ? "preview" : "production";
16614
+ const cwd = opts.cwd ?? process.cwd();
16615
+ const root = findProjectLinkDir(cwd) ?? cwd;
16616
+ const outRel = opts.out ?? ".env.kryd";
16617
+ const outPath = resolve(root, outRel);
16618
+ if (opts.out !== void 0 && existsSync3(outPath) && !opts.force) {
16619
+ process.stderr.write(
16620
+ `Refusing to overwrite ${outRel} \u2014 pass --force to replace it (or omit --out to write .env.kryd).
16621
+ `
16622
+ );
16623
+ process.exitCode = 1;
16624
+ return;
16625
+ }
16626
+ try {
16627
+ const { values } = await pullEnvValues(apiUrl, token, project, environment);
16628
+ const entries = Object.entries(values).sort(
16629
+ ([a], [b]) => a.localeCompare(b)
16630
+ );
16631
+ const header = `# Written by \`kryd env pull\` \u2014 your customer-set variables for ${project} (${environment}).
16632
+ # Regenerate with \`kryd env pull\`. Do not commit this file.
16633
+ `;
16634
+ const body = entries.map(([k, v]) => toDotenvLine(k, v)).join("\n");
16635
+ const rel = relative(root, outPath);
16636
+ const insideRoot = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
16637
+ const added = insideRoot ? ensureIgnored(root, rel) : false;
16638
+ writeSecretFile(outPath, body.length > 0 ? `${header}
16639
+ ${body}
16640
+ ` : header);
16641
+ const count = entries.length;
16642
+ process.stdout.write(
16643
+ count === 0 ? `No customer environment variables for ${project} (${environment}) \u2014 wrote an empty ${outRel}.
16644
+ ` : `Wrote ${count} variable${count === 1 ? "" : "s"} to ${outRel} (${environment}), mode 0600.
16645
+ `
16646
+ );
16647
+ if (added) process.stdout.write(`Added ${rel} to .gitignore.
16648
+ `);
16649
+ if (!insideRoot)
16650
+ process.stderr.write(
16651
+ `Warning: ${outRel} is outside the project root \u2014 NOT added to .gitignore. Make sure you don't commit it.
16652
+ `
16653
+ );
16654
+ process.stdout.write(
16655
+ "These are your own values, read back from Kryd. Managed values (DATABASE_URL, storage, the AI gateway) are never pulled.\n"
16656
+ );
16657
+ } catch (err) {
16658
+ reportError(err);
16659
+ }
16660
+ }
16661
+ async function resolveEnvValue(inline, opts) {
16662
+ if (opts.stdin && inline !== void 0) {
16663
+ return {
16664
+ error: "Pass the value with --stdin OR as KEY=VALUE, not both \u2014 it is ambiguous which one you meant."
16665
+ };
16666
+ }
16667
+ if (opts.stdin) {
16668
+ if (isInteractive(opts.io)) {
16669
+ return {
16670
+ error: `--stdin expects a value on standard input, but nothing is piped in (e.g. \`\u2026 | kryd env set ${opts.key} --stdin\`). To type it instead, run \`kryd env set ${opts.key}\` \u2014 you will be prompted, and nothing is echoed.`
16671
+ };
16672
+ }
16673
+ const read = await readAll(opts.io.input, ENV_VAR_VALUE_MAX_BYTES + 1);
16674
+ if ("tooLarge" in read) {
16675
+ return {
16676
+ error: `That value is larger than the ${ENV_VAR_VALUE_MAX_BYTES}-byte limit for an environment variable.`
16677
+ };
16678
+ }
16679
+ return { value: stripOneTrailingNewline(read.value) };
16680
+ }
16681
+ if (inline !== void 0) {
16682
+ if (inline.length > 0) {
16683
+ process.stderr.write(
16684
+ `tip: a value in \`KEY=VALUE\` is visible in \`ps\` and is saved to your shell history. For a secret, pipe it (\`kryd env set ${opts.key} --stdin\`) or omit the value to be prompted.
16685
+ `
16686
+ );
16687
+ }
16688
+ return { value: inline };
16689
+ }
16690
+ if (!isInteractive(opts.io)) {
16691
+ return {
16692
+ error: `No value given for ${opts.key}, and there is no terminal to prompt on. Pipe it with \`kryd env set ${opts.key} --stdin\`.`
16693
+ };
16694
+ }
16695
+ const typed = await promptHidden(`Value for ${opts.key} (not echoed): `, opts.io);
16696
+ if (typed === null) return { aborted: true };
16697
+ return { value: typed };
16698
+ }
16699
+ async function runEnvSet(opts) {
16700
+ const scope = opts.build ? "build" : "runtime";
16701
+ const environment = opts.preview ? "preview" : "production";
16702
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16703
+ const token = loadConfig().token;
16704
+ if (!token) {
16705
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16706
+ process.exitCode = 1;
16707
+ return;
16708
+ }
16709
+ const project = resolveProjectId(opts.project, opts.cwd);
16710
+ if (!project) {
16711
+ reportNotLinked("kryd env set KEY=VALUE <projectId>");
16712
+ return;
16713
+ }
16714
+ if (!opts.spec) {
16715
+ process.stderr.write(
16716
+ "Which variable? e.g. `kryd env set MY_API_KEY --stdin` or `kryd env set FEATURE_X=on`.\n"
16717
+ );
16718
+ process.exitCode = 1;
16719
+ return;
16720
+ }
16721
+ const { key, value: inline } = splitKeyValue(opts.spec);
16722
+ const nameError = validateEnvVarName(key);
16723
+ if (nameError) {
16724
+ process.stderr.write(`${nameError}
16725
+ `);
16726
+ process.exitCode = 1;
16727
+ return;
16728
+ }
16729
+ if (scope === "build") {
16730
+ const buildNameError = validateBuildEnvVarName(key);
16731
+ if (buildNameError) {
16732
+ process.stderr.write(`${buildNameError}
16733
+ `);
16734
+ process.exitCode = 1;
16735
+ return;
16736
+ }
16737
+ }
16738
+ const io = opts.io ?? defaultPromptIO();
16739
+ const resolved = await resolveEnvValue(inline, {
16740
+ ...opts.stdin !== void 0 ? { stdin: opts.stdin } : {},
16741
+ io,
16742
+ key
16743
+ });
16744
+ if ("aborted" in resolved) {
16745
+ process.stderr.write(`Aborted \u2014 ${key} was not set.
16746
+ `);
16747
+ process.exitCode = 1;
16748
+ return;
16749
+ }
16750
+ if ("error" in resolved) {
16751
+ process.stderr.write(`${resolved.error}
16752
+ `);
16753
+ process.exitCode = 1;
16754
+ return;
16755
+ }
16756
+ const valueError = validateEnvVarValue(resolved.value);
16757
+ if (valueError) {
16758
+ process.stderr.write(`${valueError}
16759
+ `);
16760
+ process.exitCode = 1;
16761
+ return;
16762
+ }
16763
+ if (scope === "build") {
16764
+ const buildValueError = validateBuildEnvVarValue(resolved.value);
16765
+ if (buildValueError) {
16766
+ process.stderr.write(`${buildValueError}
16767
+ `);
16768
+ process.exitCode = 1;
16769
+ return;
16770
+ }
16771
+ }
16772
+ try {
16773
+ const written = await setEnvVar(
16774
+ apiUrl,
16775
+ token,
16776
+ project,
16777
+ key,
16778
+ resolved.value,
16779
+ scope,
16780
+ environment
16781
+ );
16782
+ const envTag = environment === "preview" ? " (preview)" : "";
16783
+ process.stdout.write(
16784
+ `${written.created ? "Set" : "Updated"} ${written.key}${scope === "build" ? " (build-time)" : envTag} for ${project}.
16785
+ ` + // The public-bundle warning comes FIRST, before the applies-when note. Someone who reads one
16786
+ // line of output should read the consequential one.
16787
+ (scope === "build" ? `${BUILD_ENV_PUBLIC_WARNING}
16788
+ ${NEXT_BUILD_NOTE}` : NEXT_DEPLOY_NOTE)
16789
+ );
16790
+ } catch (err) {
16791
+ reportError(err);
16792
+ }
16793
+ }
16794
+ async function runEnvRemove(opts) {
16795
+ const scope = opts.build ? "build" : "runtime";
16796
+ const environment = opts.preview ? "preview" : "production";
16797
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16798
+ const token = loadConfig().token;
16799
+ if (!token) {
16800
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16801
+ process.exitCode = 1;
16802
+ return;
16803
+ }
16804
+ const project = resolveProjectId(opts.project, opts.cwd);
16805
+ if (!project) {
16806
+ reportNotLinked("kryd env rm KEY <projectId>");
16807
+ return;
16808
+ }
16809
+ const key = opts.key;
16810
+ if (!key) {
16811
+ process.stderr.write("Which variable? e.g. `kryd env rm MY_API_KEY`.\n");
16812
+ process.exitCode = 1;
16813
+ return;
16814
+ }
16815
+ if (!ENV_VAR_NAME_PATTERN.test(key)) {
16816
+ process.stderr.write(
16817
+ "Environment variable names must match ^[A-Za-z_][A-Za-z0-9_]*$ \u2014 a letter or underscore, then letters, digits or underscores.\n"
16818
+ );
16819
+ process.exitCode = 1;
16820
+ return;
16821
+ }
16822
+ if (isReservedEnvVarName(key)) {
16823
+ process.stderr.write(
16824
+ `${key} is reserved by Kryd and cannot be removed here.
16825
+ If it comes from a resource you attached, detach that instead (\`kryd db detach\`, \`kryd storage detach\`).
16826
+ `
16827
+ );
16828
+ process.exitCode = 1;
16829
+ return;
16830
+ }
16831
+ const nameError = validateEnvVarName(key);
16832
+ if (nameError) {
16833
+ process.stderr.write(`${nameError}
16834
+ `);
16835
+ process.exitCode = 1;
16836
+ return;
16837
+ }
16838
+ if (scope === "build") {
16839
+ try {
16840
+ const vars = await listEnvVars(apiUrl, token, project, "build");
16841
+ if (!vars.some((v) => v.key === key && v.scope === "build")) {
16842
+ process.stderr.write(
16843
+ `${key} is not set as a build-time variable on ${project}.
16844
+ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of that name, drop --build.)
16845
+ `
16846
+ );
16847
+ process.exitCode = 1;
16848
+ return;
16849
+ }
16850
+ } catch (err) {
16851
+ reportError(err);
16852
+ return;
16853
+ }
16854
+ }
16855
+ if (!opts.yes) {
16856
+ const io = opts.io ?? defaultPromptIO();
16857
+ if (!isInteractive(io)) {
16858
+ process.stderr.write(
16859
+ `Refusing to remove ${key} without confirmation. Re-run with --yes to confirm non-interactively.
16860
+ `
16861
+ );
16862
+ process.exitCode = 1;
16863
+ return;
16864
+ }
16865
+ const confirmed = await promptConfirm(
16866
+ scope === "build" ? `Remove the build-time variable ${key} from ${project}? Your next build will not have it (the current image still does).` : environment === "preview" ? `Remove the preview override for ${key} on ${project}? Previews will revert to inheriting the production value.` : `Remove ${key} from ${project}? Its stored value (production and any preview override) is deleted and cannot be recovered.`,
16867
+ io
16868
+ );
16869
+ if (!confirmed) {
16870
+ process.stdout.write(`Left ${key} in place.
16871
+ `);
16872
+ return;
16873
+ }
16874
+ }
16875
+ try {
16876
+ const removed = await removeEnvVar(apiUrl, token, project, key, scope, environment);
16877
+ const envTag = environment === "preview" ? " (preview override)" : "";
16878
+ process.stdout.write(
16879
+ `Removed ${removed.key}${scope === "build" ? " (build-time)" : envTag} from ${project}.
16880
+ ` + // The build-scope warning is deliberately bleaker than the runtime one, because the truth is.
16881
+ // A runtime value is gone from the container at the next deploy. A build-time value is
16882
+ // COMPILED INTO the live image and into every bundle already downloaded from it — removing
16883
+ // it stops future builds baking it in and does nothing else. Someone removing a value they
16884
+ // now realise was sensitive must not read "removed" as "contained".
16885
+ (scope === "build" ? "\u26A0 This does not un-publish it. The value is already compiled into the live image and into every bundle served from it \u2014 rotate it at the provider, then push a commit to rebuild without it.\n" : `\u26A0 Your running container still has the old value until your next deploy \u2014 run \`kryd redeploy\` to apply the removal now (no rebuild), or it lands on your next \`kryd push\`.
16886
+ `)
16887
+ );
16888
+ } catch (err) {
16889
+ reportError(err);
16890
+ }
16891
+ }
16892
+ var CLI_VERSION = true ? "0.2.1" : "0.0.0-dev";
16047
16893
  var program = new Command();
16048
16894
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
16049
16895
  program.command("login").description("Sign in via the browser (default) and store a token").option("--email <email>", "account email (with --password; non-interactive escape hatch)").option("--password <password>", "account password (with --email; visible in `ps` \u2014 prefer the browser flow)").option(
@@ -16054,16 +16900,16 @@ program.command("whoami").description("Show the current account").option("--api-
16054
16900
  program.command("logout").description("Clear the stored token").action(() => runLogout());
16055
16901
  program.command("init").description("Link this project's repo + register its push webhook").option("--name <name>", "project name (defaults to package.json name / dir)").option(
16056
16902
  "--framework <framework>",
16057
- "react-router | nextjs | vite-spa (auto-detected if omitted)"
16903
+ "react-router | nextjs | vite-spa | node (auto-detected if omitted)"
16058
16904
  ).option(
16059
16905
  "--tenant <slug>",
16060
16906
  "your tenant slug (claimed once on first init \u2014 appears in every deploy URL)"
16061
16907
  ).option("--api-url <url>", "control-plane API base URL").action((opts) => runInit(opts));
16062
16908
  program.command("logs [target]").description(
16063
- "Follow logs. Default: a deploy's build/deploy logs ([target]=deployment id, latest if omitted). With --runtime: the project's live-app runtime logs ([target]=project id)."
16909
+ "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)."
16064
16910
  ).option(
16065
16911
  "--runtime",
16066
- "stream the live container's runtime stdout/stderr (the running app) instead of build/deploy logs"
16912
+ "stream a container's runtime stdout/stderr instead of build/deploy logs \u2014 the live app (project id) or one deploy's container (dpl_ id)"
16067
16913
  ).option(
16068
16914
  "--since <dur>",
16069
16915
  "with --runtime: backfill this window before going live (e.g. 30m, 2h, 1d; max 7d)"
@@ -16079,6 +16925,9 @@ program.command("deploy [project]").description("Trigger a deploy of the project
16079
16925
  program.command("rollback [project] [deployment]").description("Roll back to a previous successful deploy (no rebuild) and follow it live").option("--api-url <url>", "control-plane API base URL").action(
16080
16926
  (project, deployment, opts) => runRollback({ ...opts, project, deployment })
16081
16927
  );
16928
+ program.command("redeploy [project]").description(
16929
+ "Redeploy the current live commit (no rebuild) to apply config/env changes, and follow it live"
16930
+ ).option("--api-url <url>", "control-plane API base URL").action((project, opts) => runRedeploy({ ...opts, project }));
16082
16931
  var db = program.command("db").description("Manage project databases");
16083
16932
  db.command("create [project]").description("Attach a shared or bring-your-own Postgres database to a project").option("--shared", "attach a shared managed database (the default)").option(
16084
16933
  "--connection-string <url>",
@@ -16088,6 +16937,36 @@ db.command("detach [project]").description("Tear down the project's database and
16088
16937
  var storage = program.command("storage").description("Manage project object storage");
16089
16938
  storage.command("create [project]").description("Create an S3-compatible object-storage bucket for a project").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runStorageCreate({ ...opts, project }));
16090
16939
  storage.command("detach [project]").description("Tear down the project's object storage and reclaim its credentials").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runStorageDetach({ ...opts, project }));
16940
+ var env = program.command("env").description("Manage your app's environment variables");
16941
+ 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((project, opts) => runEnvList({ ...opts, project }));
16942
+ env.command("set <key> [project]").description(
16943
+ "Set one of your environment variables \u2014 `KEY=VALUE`, or `KEY` with --stdin / a prompt"
16944
+ ).option(
16945
+ "--stdin",
16946
+ "read the value from a pipe instead of the argument (a value in `KEY=VALUE` is visible in `ps` and saved to your shell history); one trailing newline is stripped, and this is the only way to set a multi-line value"
16947
+ ).option(
16948
+ "--build",
16949
+ "set it as a BUILD-TIME variable, for `import.meta.env` / `process.env` while your bundler runs (VITE_*, NEXT_PUBLIC_*, PUBLIC_*, \u2026). The value is compiled into your public bundle, so it must not be a secret, and it takes effect at your next build rather than your next deploy"
16950
+ ).option(
16951
+ "--preview",
16952
+ "set the value for PREVIEW deploys only \u2014 an override that shadows production for previews (without it, a set targets production, which previews inherit)"
16953
+ ).option("--api-url <url>", "control-plane API base URL").action((key, project, opts) => runEnvSet({ ...opts, spec: key, project }));
16954
+ env.command("rm <key> [project]").description("Remove one of your environment variables (asks for confirmation)").option("--yes", "skip the confirmation prompt (required when there is no terminal)").option(
16955
+ "--build",
16956
+ "remove the build-time variable of this name rather than the runtime one (the same name can exist as both)"
16957
+ ).option(
16958
+ "--preview",
16959
+ "remove only the PREVIEW override, reverting previews to inheriting production (without it, the whole key is removed \u2014 production and any preview override)"
16960
+ ).option("--api-url <url>", "control-plane API base URL").action((key, project, opts) => runEnvRemove({ ...opts, key, project }));
16961
+ env.command("pull [project]").description(
16962
+ "Write your customer-set environment variables (values included) to a local .env file for development"
16963
+ ).option(
16964
+ "--preview",
16965
+ "pull the values a PREVIEW deploy would see (preview overrides plus inherited production), instead of production"
16966
+ ).option(
16967
+ "--out <path>",
16968
+ "file to write, relative to the project root (default: .env.kryd \u2014 never your hand-maintained .env)"
16969
+ ).option("--force", "overwrite the --out file if it already exists").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runEnvPull({ ...opts, project }));
16091
16970
  var ai = program.command("ai").description("Manage the app's EU AI gateway");
16092
16971
  ai.command("enable [project]").description("Give the project an authenticated EU AI endpoint (injects on next deploy)").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runAiEnable({ ...opts, project }));
16093
16972
  if (import.meta.url === `file://${process.argv[1]}`) {
@@ -16105,14 +16984,20 @@ export {
16105
16984
  runDbCreate,
16106
16985
  runDbDetach,
16107
16986
  runDeploy,
16987
+ runEnvList,
16988
+ runEnvPull,
16989
+ runEnvRemove,
16990
+ runEnvSet,
16108
16991
  runInit,
16109
16992
  runLogin,
16110
16993
  runLogout,
16111
16994
  runLogs,
16112
16995
  runPush,
16996
+ runRedeploy,
16113
16997
  runRollback,
16114
16998
  runRuntimeLogs,
16115
16999
  runStorageCreate,
16116
17000
  runStorageDetach,
16117
- runWhoami
17001
+ runWhoami,
17002
+ splitKeyValue
16118
17003
  };
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@kryd/cli",
3
- "version": "0.2.0",
4
- "description": "Kryd CLI — push a React / Vite / Next.js app to the effortless EU-sovereign deploy stack on Scaleway: git push → live with SSL, one-click managed Postgres & object storage, and an EU-hosted AI gateway already wired in.",
3
+ "version": "0.2.1",
4
+ "description": "Kryd CLI — push a React / Vite / Next.js app to the European cloud for your AI: git push → live with SSL, one-click managed Postgres & object storage, and an EU-hosted AI gateway already wired in. Your code and your model calls stay in the EU.",
5
5
  "keywords": [
6
6
  "kryd",
7
7
  "deploy",
8
- "scaleway",
9
8
  "eu-sovereign",
10
9
  "paas",
11
10
  "cli",