@kryd/cli 0.1.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 +8 -6
  2. package/dist/index.js +1114 -29
  3. package/package.json +4 -5
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);
@@ -15144,6 +15459,32 @@ async function listDeployments(apiUrl, token, projectId) {
15144
15459
  }
15145
15460
  return (await res.json()).items;
15146
15461
  }
15462
+ function findDeploymentsForCommit(deployments, commitSha, branch) {
15463
+ return deployments.filter((d) => d.commitSha === commitSha && d.branch === branch);
15464
+ }
15465
+ async function pollForNewDeployment(fetchDeployments, match, opts) {
15466
+ const intervalMs = opts?.intervalMs ?? 2e3;
15467
+ const timeoutMs = opts?.timeoutMs ?? 6e4;
15468
+ const maxConsecutiveErrors = opts?.maxConsecutiveErrors ?? 5;
15469
+ const sleep = opts?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
15470
+ const started = Date.now();
15471
+ let consecutiveErrors = 0;
15472
+ for (; ; ) {
15473
+ try {
15474
+ const items = await fetchDeployments();
15475
+ consecutiveErrors = 0;
15476
+ const fresh = findDeploymentsForCommit(items, match.commitSha, match.branch).filter(
15477
+ (d) => !match.knownIds.has(d.id)
15478
+ );
15479
+ const found = fresh[fresh.length - 1];
15480
+ if (found) return found;
15481
+ } catch (err) {
15482
+ if (++consecutiveErrors > maxConsecutiveErrors) throw err;
15483
+ }
15484
+ if (Date.now() - started >= timeoutMs) return null;
15485
+ await sleep(intervalMs);
15486
+ }
15487
+ }
15147
15488
  function dataFromFrame(frame) {
15148
15489
  const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
15149
15490
  return data.length > 0 ? data : void 0;
@@ -15170,14 +15511,14 @@ async function* readSseFrames(body) {
15170
15511
  }
15171
15512
  function abortableSleep(ms, signal) {
15172
15513
  if (signal?.aborted) return Promise.resolve();
15173
- return new Promise((resolve) => {
15514
+ return new Promise((resolve2) => {
15174
15515
  const onAbort = () => {
15175
15516
  clearTimeout(timer);
15176
- resolve();
15517
+ resolve2();
15177
15518
  };
15178
15519
  const timer = setTimeout(() => {
15179
15520
  signal?.removeEventListener("abort", onAbort);
15180
- resolve();
15521
+ resolve2();
15181
15522
  }, ms);
15182
15523
  signal?.addEventListener("abort", onAbort, { once: true });
15183
15524
  });
@@ -15220,13 +15561,14 @@ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15220
15561
  }
15221
15562
  return terminal;
15222
15563
  }
15223
- 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";
15224
15566
  const doFetch = opts.fetchImpl ?? fetch;
15225
15567
  const reconnectMs = opts.reconnectMs ?? 1e3;
15226
15568
  let afterNs;
15227
15569
  while (!opts.signal?.aborted) {
15228
15570
  try {
15229
- const url2 = new URL(`${apiUrl}/projects/${projectId}/runtime-logs`);
15571
+ const url2 = new URL(`${apiUrl}/${basePath}/${id}/runtime-logs`);
15230
15572
  if (afterNs) url2.searchParams.set("after", afterNs);
15231
15573
  else if (opts.since) url2.searchParams.set("since", opts.since);
15232
15574
  const res = await doFetch(url2, {
@@ -15260,7 +15602,12 @@ async function streamRuntime(apiUrl, token, projectId, handlers, opts = {}) {
15260
15602
  // src/framework.ts
15261
15603
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
15262
15604
  import { basename, join as join2 } from "node:path";
15263
- var FRAMEWORKS = ["react-router", "nextjs", "vite-spa"];
15605
+ var FRAMEWORKS = [
15606
+ "react-router",
15607
+ "nextjs",
15608
+ "vite-spa",
15609
+ "node"
15610
+ ];
15264
15611
  function isFramework(value) {
15265
15612
  return FRAMEWORKS.includes(value);
15266
15613
  }
@@ -15274,6 +15621,7 @@ var VITE_META_FRAMEWORKS = [
15274
15621
  "@builder.io/qwik-city",
15275
15622
  "vike"
15276
15623
  ];
15624
+ var SERVER_FRAMEWORKS = ["hono", "express", "fastify", "koa"];
15277
15625
  function readPackageJson(cwd) {
15278
15626
  try {
15279
15627
  return JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
@@ -15289,6 +15637,7 @@ function frameworkFrom(pkg, cwd) {
15289
15637
  return "react-router";
15290
15638
  }
15291
15639
  if (VITE_META_FRAMEWORKS.some(has)) return null;
15640
+ if (SERVER_FRAMEWORKS.some(has)) return "node";
15292
15641
  if (has("vite")) return "vite-spa";
15293
15642
  return null;
15294
15643
  }
@@ -15300,8 +15649,84 @@ function inspectProject(cwd) {
15300
15649
  return { framework: frameworkFrom(pkg, cwd), name: nameFrom(pkg, cwd) };
15301
15650
  }
15302
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
+
15303
15728
  // src/git.ts
15304
- import { execFileSync } from "node:child_process";
15729
+ import { execFileSync, spawnSync } from "node:child_process";
15305
15730
  function authenticatedRemoteUrl(cloneUrl, username, token) {
15306
15731
  const prefix = "https://";
15307
15732
  if (!cloneUrl.startsWith(prefix)) return cloneUrl;
@@ -15330,6 +15755,43 @@ function configureGitRemote(cwd, remote, url2) {
15330
15755
  return { status: "unavailable" };
15331
15756
  }
15332
15757
  }
15758
+ function probe(cwd, args) {
15759
+ try {
15760
+ const value = execFileSync("git", args, {
15761
+ cwd,
15762
+ stdio: ["ignore", "pipe", "ignore"]
15763
+ }).toString().trim();
15764
+ return { status: "ok", value };
15765
+ } catch (err) {
15766
+ if (err.code === "ENOENT") return { status: "no-git" };
15767
+ return { status: "failed" };
15768
+ }
15769
+ }
15770
+ function gitAvailable(cwd) {
15771
+ const res = probe(cwd, ["rev-parse", "--is-inside-work-tree"]);
15772
+ if (res.status === "no-git") return res;
15773
+ if (res.status !== "ok" || res.value !== "true") return { status: "not-a-repo" };
15774
+ return { status: "ok" };
15775
+ }
15776
+ function currentBranch(cwd) {
15777
+ return probe(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
15778
+ }
15779
+ function headSha(cwd, ref) {
15780
+ return probe(cwd, ["rev-parse", ref]);
15781
+ }
15782
+ function hasRemote(cwd, remote) {
15783
+ const res = probe(cwd, ["remote"]);
15784
+ if (res.status !== "ok") return res;
15785
+ return { status: "ok", value: res.value.split(/\s+/).filter(Boolean).includes(remote) };
15786
+ }
15787
+ function pushBranch(cwd, remote, branch, extraArgs) {
15788
+ const res = spawnSync("git", ["push", remote, branch, ...extraArgs], {
15789
+ cwd,
15790
+ stdio: "inherit"
15791
+ });
15792
+ if (res.status === 0) return { status: "ok" };
15793
+ return { status: "failed", code: res.status ?? 1 };
15794
+ }
15333
15795
 
15334
15796
  // src/index.ts
15335
15797
  function reportError(err) {
@@ -15404,7 +15866,7 @@ async function runInit(opts) {
15404
15866
  if (opts.framework) {
15405
15867
  if (!isFramework(opts.framework)) {
15406
15868
  process.stderr.write(
15407
- `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.
15408
15870
  `
15409
15871
  );
15410
15872
  process.exitCode = 1;
@@ -15414,7 +15876,7 @@ async function runInit(opts) {
15414
15876
  } else {
15415
15877
  if (!detected.framework) {
15416
15878
  process.stderr.write(
15417
- "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"
15418
15880
  );
15419
15881
  process.exitCode = 1;
15420
15882
  return;
@@ -15458,20 +15920,22 @@ async function runInit(opts) {
15458
15920
  case "added":
15459
15921
  case "updated":
15460
15922
  nextStep = `${remote.status === "added" ? "Added" : "Updated"} git remote "${remote.remote}" (with your push credential).
15461
- Next: git push ${remote.remote} ${branch} # push to deploy
15923
+ Next: kryd push # push to deploy (same as \`git push ${remote.remote} ${branch}\`)
15462
15924
  `;
15463
15925
  break;
15464
15926
  case "not-a-repo":
15465
15927
  nextStep = `No git repo here yet. To deploy (the URL carries your push token \u2014 keep it private):
15466
15928
  git init && git add -A && git commit -m "init"
15467
15929
  git remote add kryd ${authedUrl}
15468
- git push kryd ${branch}
15930
+ kryd push
15469
15931
  `;
15470
15932
  break;
15471
15933
  case "unavailable":
15472
- nextStep = `Add the remote, then push to deploy (the URL carries your push token \u2014 keep it private):
15934
+ nextStep = gitAvailable(cwd).status === "no-git" ? `git is not installed, so the deploy remote could not be configured.
15935
+ Install git (https://git-scm.com/downloads), then re-run \`kryd init\` here.
15936
+ ` : `Add the remote, then push to deploy (the URL carries your push token \u2014 keep it private):
15473
15937
  git remote add kryd ${authedUrl}
15474
- git push kryd ${branch}
15938
+ kryd push
15475
15939
  `;
15476
15940
  break;
15477
15941
  }
@@ -15547,8 +16011,9 @@ async function runRuntimeLogs(opts) {
15547
16011
  process.exitCode = 1;
15548
16012
  return;
15549
16013
  }
15550
- const project = resolveProjectId(opts.project, opts.cwd);
15551
- if (!project) {
16014
+ const isDeployment = opts.project?.startsWith("dpl_") ?? false;
16015
+ const target = isDeployment ? opts.project : resolveProjectId(opts.project, opts.cwd);
16016
+ if (!target) {
15552
16017
  reportNotLinked("kryd logs <projectId> --runtime");
15553
16018
  return;
15554
16019
  }
@@ -15567,7 +16032,7 @@ async function runRuntimeLogs(opts) {
15567
16032
  await streamRuntime(
15568
16033
  apiUrl,
15569
16034
  token,
15570
- project,
16035
+ target,
15571
16036
  {
15572
16037
  onLine: ({ stream, line }) => {
15573
16038
  const out = stream === "stderr" ? process.stderr : process.stdout;
@@ -15575,7 +16040,11 @@ async function runRuntimeLogs(opts) {
15575
16040
  `);
15576
16041
  }
15577
16042
  },
15578
- { ...opts.since ? { since: opts.since } : {}, signal: controller.signal }
16043
+ {
16044
+ ...opts.since ? { since: opts.since } : {},
16045
+ signal: controller.signal,
16046
+ ...isDeployment ? { resource: "deployment" } : {}
16047
+ }
15579
16048
  );
15580
16049
  } catch (err) {
15581
16050
  reportError(err);
@@ -15605,6 +16074,135 @@ async function runDeploy(opts) {
15605
16074
  reportError(err);
15606
16075
  }
15607
16076
  }
16077
+ async function runPush(opts) {
16078
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16079
+ const token = loadConfig().token;
16080
+ if (!token) {
16081
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16082
+ process.exitCode = 1;
16083
+ return;
16084
+ }
16085
+ const cwd = opts.cwd ?? process.cwd();
16086
+ const available = gitAvailable(cwd);
16087
+ if (available.status === "no-git") {
16088
+ process.stderr.write(
16089
+ "git is not installed, and `kryd push` needs it to deploy.\nInstall git (https://git-scm.com/downloads), then try again.\n"
16090
+ );
16091
+ process.exitCode = 1;
16092
+ return;
16093
+ }
16094
+ if (available.status !== "ok") {
16095
+ process.stderr.write(
16096
+ "Not a git repository \u2014 run `kryd push` from the root of your app's repo.\n"
16097
+ );
16098
+ process.exitCode = 1;
16099
+ return;
16100
+ }
16101
+ const remote = hasRemote(cwd, "kryd");
16102
+ if (remote.status !== "ok" || !remote.value) {
16103
+ process.stderr.write(
16104
+ 'No "kryd" git remote here. Run `kryd init` to link this project and configure it.\n'
16105
+ );
16106
+ process.exitCode = 1;
16107
+ return;
16108
+ }
16109
+ if (opts.branch?.startsWith("-")) {
16110
+ process.stderr.write(
16111
+ `"${opts.branch}" is not a branch. Pass the branch before \`--\`, e.g. \`kryd push main -- ${opts.branch}\`.
16112
+ `
16113
+ );
16114
+ process.exitCode = 1;
16115
+ return;
16116
+ }
16117
+ let branch = opts.branch;
16118
+ if (!branch) {
16119
+ const current = currentBranch(cwd);
16120
+ if (current.status !== "ok") {
16121
+ process.stderr.write("Could not determine the current branch \u2014 pass one, e.g. `kryd push main`.\n");
16122
+ process.exitCode = 1;
16123
+ return;
16124
+ }
16125
+ if (current.value === "HEAD") {
16126
+ process.stderr.write(
16127
+ "You are on a detached HEAD \u2014 pass the branch to push explicitly, e.g. `kryd push main`.\n"
16128
+ );
16129
+ process.exitCode = 1;
16130
+ return;
16131
+ }
16132
+ branch = current.value;
16133
+ }
16134
+ const gitArgs = opts.gitArgs ?? [];
16135
+ if (branch.includes(":") || branch.startsWith("+")) {
16136
+ const pushed = pushBranch(cwd, "kryd", branch, gitArgs);
16137
+ if (pushed.status === "failed") {
16138
+ process.exitCode = pushed.code;
16139
+ return;
16140
+ }
16141
+ process.stdout.write(
16142
+ "Pushed. `kryd push` does not follow refspec pushes \u2014 use `kryd logs` to watch a deploy.\n"
16143
+ );
16144
+ return;
16145
+ }
16146
+ try {
16147
+ const sha = headSha(cwd, branch);
16148
+ if (sha.status !== "ok") {
16149
+ process.stderr.write(`Unknown branch "${branch}" \u2014 nothing to push.
16150
+ `);
16151
+ process.exitCode = 1;
16152
+ return;
16153
+ }
16154
+ const commitSha = sha.value;
16155
+ const projectId = resolveProjectId(void 0, opts.cwd);
16156
+ if (!projectId) {
16157
+ process.stderr.write("tip: run `kryd init` here to scope `kryd push` to this project.\n");
16158
+ }
16159
+ const fetchDeployments = () => listDeployments(apiUrl, token, projectId ?? void 0);
16160
+ let known = null;
16161
+ try {
16162
+ known = findDeploymentsForCommit(await fetchDeployments(), commitSha, branch);
16163
+ } catch {
16164
+ known = null;
16165
+ }
16166
+ const pushed = pushBranch(cwd, "kryd", branch, gitArgs);
16167
+ if (pushed.status === "failed") {
16168
+ process.exitCode = pushed.code;
16169
+ return;
16170
+ }
16171
+ if (known === null) {
16172
+ process.stdout.write(
16173
+ "Pushed. Could not reach the API to follow the deploy \u2014 try `kryd logs`.\n"
16174
+ );
16175
+ return;
16176
+ }
16177
+ process.stdout.write("Waiting for the deploy to start\u2026\n");
16178
+ const deployment = await pollForNewDeployment(
16179
+ fetchDeployments,
16180
+ { commitSha, branch, knownIds: new Set(known.map((d) => d.id)) },
16181
+ {
16182
+ ...opts.sleep ? { sleep: opts.sleep } : {},
16183
+ ...opts.pollTimeoutMs !== void 0 ? { timeoutMs: opts.pollTimeoutMs } : {}
16184
+ }
16185
+ );
16186
+ if (!deployment) {
16187
+ const previous = known[0];
16188
+ if (previous) {
16189
+ const outcome = previous.status === "live" ? "is already deployed" : `has already been deployed once (that deploy ended \`${previous.status}\`)`;
16190
+ process.stdout.write(
16191
+ `Nothing new to push \u2014 ${commitSha.slice(0, 7)} ${outcome} (\`kryd logs ${previous.id}\` to review it).
16192
+ `
16193
+ );
16194
+ } else {
16195
+ process.stdout.write(
16196
+ "Pushed, but no deploy has started yet. Follow it with `kryd logs`.\n" + (projectId ? "" : "If this keeps happening, re-run `kryd init` to link this folder.\n")
16197
+ );
16198
+ }
16199
+ return;
16200
+ }
16201
+ await followDeploy(apiUrl, token, deployment.id);
16202
+ } catch (err) {
16203
+ reportError(err);
16204
+ }
16205
+ }
15608
16206
  async function runRollback(opts) {
15609
16207
  const apiUrl = resolveApiUrl(opts.apiUrl);
15610
16208
  const token = loadConfig().token;
@@ -15635,6 +16233,35 @@ async function runRollback(opts) {
15635
16233
  reportError(err);
15636
16234
  }
15637
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
+ }
15638
16265
  async function runDbCreate(opts) {
15639
16266
  const apiUrl = resolveApiUrl(opts.apiUrl);
15640
16267
  const token = loadConfig().token;
@@ -15811,7 +16438,7 @@ async function runAiEnable(opts) {
15811
16438
  const status = await enableAi(apiUrl, token, { projectId: project });
15812
16439
  process.stdout.write(
15813
16440
  `AI enabled for ${project}.
15814
- ` + (status.url ? `Endpoint: ${status.url}
16441
+ ` + (status.url ? `Endpoint: ${status.url} (OpenAI-compatible base URL \u2014 use it as-is)
15815
16442
  ` : "") + `AI_GATEWAY_URL + AI_GATEWAY_TOKEN will be injected on your next deploy (kryd deploy).
15816
16443
  `
15817
16444
  );
@@ -15849,7 +16476,420 @@ async function runStorageDetach(opts) {
15849
16476
  reportError(err);
15850
16477
  }
15851
16478
  }
15852
- var CLI_VERSION = true ? "0.1.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";
15853
16893
  var program = new Command();
15854
16894
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
15855
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(
@@ -15860,26 +16900,34 @@ program.command("whoami").description("Show the current account").option("--api-
15860
16900
  program.command("logout").description("Clear the stored token").action(() => runLogout());
15861
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(
15862
16902
  "--framework <framework>",
15863
- "react-router | nextjs | vite-spa (auto-detected if omitted)"
16903
+ "react-router | nextjs | vite-spa | node (auto-detected if omitted)"
15864
16904
  ).option(
15865
16905
  "--tenant <slug>",
15866
16906
  "your tenant slug (claimed once on first init \u2014 appears in every deploy URL)"
15867
16907
  ).option("--api-url <url>", "control-plane API base URL").action((opts) => runInit(opts));
15868
16908
  program.command("logs [target]").description(
15869
- "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)."
15870
16910
  ).option(
15871
16911
  "--runtime",
15872
- "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)"
15873
16913
  ).option(
15874
16914
  "--since <dur>",
15875
16915
  "with --runtime: backfill this window before going live (e.g. 30m, 2h, 1d; max 7d)"
15876
16916
  ).option("--api-url <url>", "control-plane API base URL").action(
15877
16917
  (target, opts) => opts.runtime ? runRuntimeLogs({ project: target, since: opts.since, apiUrl: opts.apiUrl }) : runLogs({ apiUrl: opts.apiUrl, deployment: target })
15878
16918
  );
16919
+ program.command("push [branch]").description(
16920
+ "Push to the kryd remote and follow the deploy it triggers (defaults to the current branch)"
16921
+ ).option("--api-url <url>", "control-plane API base URL").action(
16922
+ (branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
16923
+ );
15879
16924
  program.command("deploy [project]").description("Trigger a deploy of the project's production branch and follow it live").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runDeploy({ ...opts, project }));
15880
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(
15881
16926
  (project, deployment, opts) => runRollback({ ...opts, project, deployment })
15882
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 }));
15883
16931
  var db = program.command("db").description("Manage project databases");
15884
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(
15885
16933
  "--connection-string <url>",
@@ -15889,6 +16937,36 @@ db.command("detach [project]").description("Tear down the project's database and
15889
16937
  var storage = program.command("storage").description("Manage project object storage");
15890
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 }));
15891
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 }));
15892
16970
  var ai = program.command("ai").description("Manage the app's EU AI gateway");
15893
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 }));
15894
16972
  if (import.meta.url === `file://${process.argv[1]}`) {
@@ -15906,13 +16984,20 @@ export {
15906
16984
  runDbCreate,
15907
16985
  runDbDetach,
15908
16986
  runDeploy,
16987
+ runEnvList,
16988
+ runEnvPull,
16989
+ runEnvRemove,
16990
+ runEnvSet,
15909
16991
  runInit,
15910
16992
  runLogin,
15911
16993
  runLogout,
15912
16994
  runLogs,
16995
+ runPush,
16996
+ runRedeploy,
15913
16997
  runRollback,
15914
16998
  runRuntimeLogs,
15915
16999
  runStorageCreate,
15916
17000
  runStorageDetach,
15917
- runWhoami
17001
+ runWhoami,
17002
+ splitKeyValue
15918
17003
  };