@kryd/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/index.js +1207 -76
  3. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -88,6 +88,69 @@ var PRODUCTION_TLD = "kryd.cloud";
88
88
  var PREVIEW_TLD = "kryd.dev";
89
89
  var TENANT_SLUG_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_TENANT_SLUG - 2}}[a-z0-9])?$`);
90
90
 
91
+ // ../../packages/shared-types/dist/reserved-slugs.js
92
+ var FORGEJO_RESERVED_USERNAMES = [
93
+ ".",
94
+ "..",
95
+ "-",
96
+ ".well-known",
97
+ "api",
98
+ "metrics",
99
+ "v2",
100
+ "assets",
101
+ "attachments",
102
+ "avatar",
103
+ "avatars",
104
+ "repo-avatars",
105
+ "captcha",
106
+ "login",
107
+ "org",
108
+ "repo",
109
+ "user",
110
+ "admin",
111
+ "explore",
112
+ "issues",
113
+ "pulls",
114
+ "milestones",
115
+ "notifications",
116
+ "report_abuse",
117
+ "favicon.ico",
118
+ "manifest.json",
119
+ "robots.txt",
120
+ "sitemap.xml",
121
+ "ssh_info",
122
+ "swagger.v1.json",
123
+ "ghost",
124
+ "gitea-actions",
125
+ "forgejo-actions"
126
+ ];
127
+ var PLATFORM_RESERVED_SLUGS = [
128
+ "kryd",
129
+ "krydhq",
130
+ "www",
131
+ "api",
132
+ "app",
133
+ "ai",
134
+ "docs",
135
+ "forge",
136
+ "admin",
137
+ "console",
138
+ "status",
139
+ "mail",
140
+ "support",
141
+ "billing",
142
+ "account",
143
+ "accounts",
144
+ "static",
145
+ "assets",
146
+ "cdn",
147
+ "dashboard"
148
+ ];
149
+ var RESERVED = /* @__PURE__ */ new Set([
150
+ ...FORGEJO_RESERVED_USERNAMES,
151
+ ...PLATFORM_RESERVED_SLUGS
152
+ ]);
153
+
91
154
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
92
155
  var external_exports = {};
93
156
  __export(external_exports, {
@@ -14637,6 +14700,143 @@ var accountUsageSchema = external_exports.object({
14637
14700
  storageBytes: external_exports.number().int().nonnegative()
14638
14701
  });
14639
14702
 
14703
+ // ../../packages/shared-types/dist/env-vars.js
14704
+ var MANAGED_ENV_VAR_NAMES = [
14705
+ "AI_GATEWAY_TOKEN",
14706
+ "AI_GATEWAY_URL",
14707
+ "AWS_ACCESS_KEY_ID",
14708
+ "AWS_ENDPOINT_URL_S3",
14709
+ "AWS_REGION",
14710
+ "AWS_SECRET_ACCESS_KEY",
14711
+ "BUCKET_NAME",
14712
+ "DATABASE_URL",
14713
+ "DEPLOY_TOKEN",
14714
+ "FORGE_PASSWORD",
14715
+ "PUSH_TOKEN",
14716
+ "WEBHOOK_SECRET"
14717
+ ];
14718
+ var PLATFORM_ENV_VAR_NAMES = ["PATH", "PORT"];
14719
+ var RESERVED_ENV_VAR_NAMES = [
14720
+ ...MANAGED_ENV_VAR_NAMES,
14721
+ ...PLATFORM_ENV_VAR_NAMES
14722
+ ];
14723
+ var RESERVED_ENV_VAR_PREFIX = "KRYD_";
14724
+ var ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
14725
+ var ENV_VAR_NAME_MAX_LENGTH = 128;
14726
+ var ENV_VAR_VALUE_MAX_BYTES = 16 * 1024;
14727
+ function validateEnvVarName(name) {
14728
+ if (name.length === 0)
14729
+ return "An environment variable name is required.";
14730
+ if (name.length > ENV_VAR_NAME_MAX_LENGTH) {
14731
+ return `Environment variable names may be at most ${ENV_VAR_NAME_MAX_LENGTH} characters.`;
14732
+ }
14733
+ if (!ENV_VAR_NAME_PATTERN.test(name)) {
14734
+ return "Environment variable names must match ^[A-Za-z_][A-Za-z0-9_]*$ \u2014 a letter or underscore, then letters, digits or underscores.";
14735
+ }
14736
+ if (isReservedEnvVarName(name)) {
14737
+ return `"${name}" is reserved by Kryd and is set for you when you attach the matching resource \u2014 choose another name.`;
14738
+ }
14739
+ return null;
14740
+ }
14741
+ function isReservedEnvVarName(name) {
14742
+ const upper = name.toUpperCase();
14743
+ return RESERVED_ENV_VAR_NAMES.includes(upper) || upper.startsWith(RESERVED_ENV_VAR_PREFIX);
14744
+ }
14745
+ function validateEnvVarValue(value) {
14746
+ if (value.length === 0) {
14747
+ return "An environment variable value is required (an empty value cannot be stored).";
14748
+ }
14749
+ if (value.trim().length === 0) {
14750
+ return "An environment variable value cannot be only whitespace.";
14751
+ }
14752
+ if (value.includes("\0")) {
14753
+ return "Environment variable values may not contain NUL bytes.";
14754
+ }
14755
+ const bytes = new TextEncoder().encode(value).length;
14756
+ if (bytes > ENV_VAR_VALUE_MAX_BYTES) {
14757
+ return `Environment variable values may be at most ${ENV_VAR_VALUE_MAX_BYTES} bytes (got ${bytes}).`;
14758
+ }
14759
+ return null;
14760
+ }
14761
+
14762
+ // ../../packages/shared-types/dist/build-env-vars.js
14763
+ var PUBLIC_BUILD_ENV_PREFIXES = [
14764
+ "ASTRO_PUBLIC_",
14765
+ "EXPO_PUBLIC_",
14766
+ "GATSBY_",
14767
+ "NEXT_PUBLIC_",
14768
+ "NUXT_PUBLIC_",
14769
+ "PUBLIC_",
14770
+ "REACT_APP_",
14771
+ "VITE_"
14772
+ ];
14773
+ var SECRET_NAME_WORDS = [
14774
+ "SECRET",
14775
+ "PASSWORD",
14776
+ "PASSWD",
14777
+ "PASSPHRASE",
14778
+ "PRIVATE_KEY",
14779
+ "PRIVATEKEY",
14780
+ "CREDENTIAL"
14781
+ ];
14782
+ var SECRET_VALUE_SHAPES = [
14783
+ { prefix: "sk_live_", label: "a Stripe secret key" },
14784
+ { prefix: "sk_test_", label: "a Stripe test secret key" },
14785
+ { prefix: "rk_live_", label: "a Stripe restricted key" },
14786
+ { prefix: "rk_test_", label: "a Stripe restricted test key" },
14787
+ { prefix: "ghp_", label: "a GitHub personal access token" },
14788
+ { prefix: "gho_", label: "a GitHub OAuth token" },
14789
+ { prefix: "ghs_", label: "a GitHub app token" },
14790
+ { prefix: "github_pat_", label: "a GitHub fine-grained token" },
14791
+ { prefix: "xoxb-", label: "a Slack bot token" },
14792
+ { prefix: "xoxp-", label: "a Slack user token" },
14793
+ { prefix: "xoxs-", label: "a Slack app token" }
14794
+ ];
14795
+ var PEM_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
14796
+ var URL_WITH_CREDENTIALS_PATTERN = /[a-z][a-z0-9+.-]*:\/\/[^/@\s:]*:[^/@\s]+@/i;
14797
+ var NEWLINE_PATTERN = /[\n\r]/;
14798
+ var BUILD_ENV_VAR_VALUE_MAX_BYTES = 4 * 1024;
14799
+ function hasPublicBuildPrefix(name) {
14800
+ return PUBLIC_BUILD_ENV_PREFIXES.some((p) => name.startsWith(p));
14801
+ }
14802
+ function publicBuildPrefixList() {
14803
+ return PUBLIC_BUILD_ENV_PREFIXES.join(", ");
14804
+ }
14805
+ function validateBuildEnvVarName(name) {
14806
+ if (!hasPublicBuildPrefix(name)) {
14807
+ 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}).`;
14808
+ }
14809
+ const upper = name.toUpperCase();
14810
+ const word = SECRET_NAME_WORDS.find((w) => upper.includes(w));
14811
+ if (word) {
14812
+ 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.`;
14813
+ }
14814
+ return null;
14815
+ }
14816
+ function validateBuildEnvVarValue(value) {
14817
+ const bytes = new TextEncoder().encode(value).length;
14818
+ if (bytes > BUILD_ENV_VAR_VALUE_MAX_BYTES) {
14819
+ 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).`;
14820
+ }
14821
+ const trimmed = value.trim();
14822
+ const shape = SECRET_VALUE_SHAPES.find((s) => trimmed.includes(s.prefix));
14823
+ if (shape) {
14824
+ 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).`;
14825
+ }
14826
+ if (PEM_PRIVATE_KEY_PATTERN.test(value)) {
14827
+ 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).";
14828
+ }
14829
+ if (URL_WITH_CREDENTIALS_PATTERN.test(value)) {
14830
+ 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.";
14831
+ }
14832
+ if (NEWLINE_PATTERN.test(value)) {
14833
+ 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.";
14834
+ }
14835
+ return null;
14836
+ }
14837
+ 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.";
14838
+ 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.";
14839
+
14640
14840
  // ../../packages/shared-types/dist/index.js
14641
14841
  var TERMINAL_DEPLOY_STATES = [
14642
14842
  "live",
@@ -14658,6 +14858,10 @@ function isTerminalResourceStatus(status) {
14658
14858
  return status === "ready" || status === "failed";
14659
14859
  }
14660
14860
 
14861
+ // src/index.ts
14862
+ import { existsSync as existsSync3 } from "node:fs";
14863
+ import { isAbsolute, relative, resolve } from "node:path";
14864
+
14661
14865
  // src/config.ts
14662
14866
  import {
14663
14867
  appendFileSync,
@@ -14665,6 +14869,7 @@ import {
14665
14869
  existsSync,
14666
14870
  mkdirSync,
14667
14871
  readFileSync,
14872
+ rmSync,
14668
14873
  writeFileSync
14669
14874
  } from "node:fs";
14670
14875
  import { homedir } from "node:os";
@@ -14759,19 +14964,39 @@ function saveProjectLink(cwd, link) {
14759
14964
  writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
14760
14965
  return path;
14761
14966
  }
14762
- function ensureKrydIgnored(cwd) {
14967
+ function ensureIgnored(cwd, entry, aliases = []) {
14763
14968
  const path = join(cwd, ".gitignore");
14764
- const entry = `${PROJECT_LINK_DIR}/`;
14765
14969
  let current = "";
14766
14970
  try {
14767
14971
  current = readFileSync(path, "utf8");
14768
14972
  } catch {
14769
14973
  }
14770
- const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || l === PROJECT_LINK_DIR);
14771
- if (present) return;
14974
+ const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
14975
+ if (present) return false;
14772
14976
  const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
14773
14977
  appendFileSync(path, `${prefix}${entry}
14774
14978
  `);
14979
+ return true;
14980
+ }
14981
+ function ensureKrydIgnored(cwd) {
14982
+ ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
14983
+ }
14984
+ function writeSecretFile(path, contents) {
14985
+ mkdirSync(dirname(path), { recursive: true });
14986
+ writeFileSync(path, contents, { mode: 384 });
14987
+ chmodSync(path, 384);
14988
+ }
14989
+ function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
14990
+ const root = findProjectLinkDir(cwd);
14991
+ if (!root) return false;
14992
+ const link = loadProjectLink(cwd);
14993
+ if (link?.projectId !== projectId) return false;
14994
+ try {
14995
+ rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
14996
+ return true;
14997
+ } catch {
14998
+ return false;
14999
+ }
14775
15000
  }
14776
15001
  function resolveProjectId(explicit, cwd = process.cwd()) {
14777
15002
  if (explicit) return explicit;
@@ -14796,7 +15021,7 @@ function browserLogin(dashboardUrl, opts = {}) {
14796
15021
  const state = randomUUID();
14797
15022
  const timeoutMs = opts.timeoutMs ?? 12e4;
14798
15023
  const shouldOpen = opts.open ?? true;
14799
- return new Promise((resolve, reject) => {
15024
+ return new Promise((resolve2, reject) => {
14800
15025
  let settled = false;
14801
15026
  const settle = (fn) => {
14802
15027
  if (settled) return;
@@ -14826,7 +15051,7 @@ function browserLogin(dashboardUrl, opts = {}) {
14826
15051
  res.end(SUCCESS_HTML);
14827
15052
  settle(() => {
14828
15053
  server.close();
14829
- resolve({ token, ...apiUrl ? { apiUrl } : {} });
15054
+ resolve2({ token, ...apiUrl ? { apiUrl } : {} });
14830
15055
  });
14831
15056
  });
14832
15057
  server.on("error", (err) => settle(() => reject(err)));
@@ -15092,6 +15317,49 @@ async function detachDatabase(apiUrl, token, projectId) {
15092
15317
  );
15093
15318
  }
15094
15319
  }
15320
+ async function getProject(apiUrl, token, projectId) {
15321
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15322
+ headers: { authorization: `Bearer ${token}` }
15323
+ });
15324
+ if (res.status === 404) return null;
15325
+ if (!res.ok) {
15326
+ throw new ApiError(`Reading the project failed (${res.status})`, await parseEnvelope(res));
15327
+ }
15328
+ return await res.json();
15329
+ }
15330
+ async function deleteProject(apiUrl, token, projectId) {
15331
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15332
+ method: "DELETE",
15333
+ headers: { authorization: `Bearer ${token}` }
15334
+ });
15335
+ if (!res.ok) {
15336
+ throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res));
15337
+ }
15338
+ return await res.json();
15339
+ }
15340
+ async function pollProjectDeleted(read, opts) {
15341
+ const intervalMs = opts?.intervalMs ?? 2e3;
15342
+ const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
15343
+ const maxConsecutiveErrors = opts?.maxConsecutiveErrors ?? 5;
15344
+ const sleep = opts?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
15345
+ const started = Date.now();
15346
+ let consecutiveErrors = 0;
15347
+ let last = null;
15348
+ for (; ; ) {
15349
+ try {
15350
+ const project2 = await read();
15351
+ consecutiveErrors = 0;
15352
+ if (project2 === null) return { gone: true, status: null };
15353
+ last = project2.status;
15354
+ opts?.onTick?.(project2.status);
15355
+ if (project2.status === "delete_failed") return { gone: false, status: last };
15356
+ } catch (err) {
15357
+ if (++consecutiveErrors >= maxConsecutiveErrors) throw err;
15358
+ }
15359
+ if (Date.now() - started >= timeoutMs) return { gone: false, status: last };
15360
+ await sleep(intervalMs);
15361
+ }
15362
+ }
15095
15363
  async function detachStorage(apiUrl, token, projectId) {
15096
15364
  const res = await fetch(`${apiUrl}/storage/${encodeURIComponent(projectId)}`, {
15097
15365
  method: "DELETE",
@@ -15104,6 +15372,158 @@ async function detachStorage(apiUrl, token, projectId) {
15104
15372
  );
15105
15373
  }
15106
15374
  }
15375
+ async function listEnvVars(apiUrl, token, projectId, include = "all") {
15376
+ const res = await fetch(
15377
+ `${apiUrl}/projects/${encodeURIComponent(projectId)}/env?scope=${encodeURIComponent(include)}`,
15378
+ { headers: { authorization: `Bearer ${token}` } }
15379
+ );
15380
+ if (!res.ok) {
15381
+ throw new ApiError(
15382
+ `Listing environment variables failed (${res.status})`,
15383
+ await parseEnvelope(res),
15384
+ res.status
15385
+ );
15386
+ }
15387
+ const parsed = await res.json().catch(() => void 0);
15388
+ if (!parsed || !Array.isArray(parsed.vars)) {
15389
+ throw new ApiError("The API returned an unexpected environment response.");
15390
+ }
15391
+ return parsed.vars.map((raw) => {
15392
+ if (typeof raw !== "object" || raw === null) {
15393
+ throw new ApiError("The API returned an unexpected environment response.");
15394
+ }
15395
+ const entry = raw;
15396
+ if (typeof entry.key !== "string" || entry.key.length === 0 || typeof entry.source !== "string") {
15397
+ throw new ApiError("The API returned an unexpected environment response.");
15398
+ }
15399
+ return {
15400
+ key: entry.key,
15401
+ // Which environments hold an explicit value (KRYD-159). An api older than KRYD-159 omits it, and
15402
+ // the renderer reads a missing/empty list as "one value, all environments" — the correct reading
15403
+ // of its absence. Kept as strings without rejecting an unknown one, same tolerance as `source`.
15404
+ ...Array.isArray(entry.environments) ? {
15405
+ environments: entry.environments.filter(
15406
+ (e) => e === "production" || e === "preview"
15407
+ )
15408
+ } : {},
15409
+ // Deliberately NOT rejected when it is outside `SECRET_SOURCES`. `SecretSource` is branded
15410
+ // plain text precisely so the value set can grow without a migration, and the CLI is published
15411
+ // to npm — so an installed copy is routinely older than the api. Rejecting an unrecognised
15412
+ // source would make one new row break `kryd env list` entirely, including for the variables
15413
+ // the user set themselves. The renderer groups anything it does not recognise instead. The
15414
+ // malformed-body guard still holds: a proxy error page has no `vars` array at all.
15415
+ source: entry.source,
15416
+ // Same tolerance, same reason (KRYD-156): an api older than this CLI omits `scope` entirely,
15417
+ // and every row such an api can return IS a runtime var — so the absent field has one correct
15418
+ // reading. An unrecognised NEW scope is passed through rather than rejected, and the renderer
15419
+ // groups it under "other" instead of dropping the variable from the listing.
15420
+ scope: typeof entry.scope === "string" ? entry.scope : "runtime",
15421
+ // The contract types `updatedAt` as nullable precisely so a missing timestamp is rendered
15422
+ // rather than assumed — normalise anything non-string to `null` instead of failing.
15423
+ updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : null
15424
+ };
15425
+ });
15426
+ }
15427
+ async function pullEnvValues(apiUrl, token, projectId, environment = "production") {
15428
+ const query = environment === "production" ? "" : `?environment=${encodeURIComponent(environment)}`;
15429
+ const res = await fetch(
15430
+ `${apiUrl}/projects/${encodeURIComponent(projectId)}/env/values${query}`,
15431
+ { headers: { authorization: `Bearer ${token}` } }
15432
+ );
15433
+ if (!res.ok) {
15434
+ throw new ApiError(
15435
+ `Pulling environment values failed (${res.status})`,
15436
+ await parseEnvelope(res),
15437
+ res.status
15438
+ );
15439
+ }
15440
+ const parsed = await res.json().catch(() => void 0);
15441
+ if (!parsed || typeof parsed.values !== "object" || parsed.values === null || Array.isArray(parsed.values)) {
15442
+ throw new ApiError("The API returned an unexpected environment-values response.");
15443
+ }
15444
+ const values = {};
15445
+ for (const [key, value] of Object.entries(
15446
+ parsed.values
15447
+ )) {
15448
+ if (typeof value !== "string") {
15449
+ throw new ApiError("The API returned an unexpected environment-values response.");
15450
+ }
15451
+ values[key] = value;
15452
+ }
15453
+ return {
15454
+ environment: parsed.environment === "preview" ? "preview" : "production",
15455
+ values
15456
+ };
15457
+ }
15458
+ async function setEnvVar(apiUrl, token, projectId, key, value, scope = "runtime", environment = "production") {
15459
+ const res = await fetch(
15460
+ `${apiUrl}/projects/${encodeURIComponent(projectId)}/env`,
15461
+ {
15462
+ method: "PUT",
15463
+ headers: {
15464
+ "content-type": "application/json",
15465
+ authorization: `Bearer ${token}`
15466
+ },
15467
+ // `scope`/`environment` are sent only when NOT their defaults. An api older than KRYD-156/159
15468
+ // ignores unknown body fields, so this is belt-and-braces — but it keeps a plain runtime
15469
+ // production request byte-identical to the one KRYD-157 shipped.
15470
+ body: JSON.stringify({
15471
+ key,
15472
+ value,
15473
+ ...scope === "runtime" ? {} : { scope },
15474
+ ...environment === "production" ? {} : { environment }
15475
+ })
15476
+ }
15477
+ );
15478
+ if (!res.ok) {
15479
+ throw new ApiError(
15480
+ `Setting ${key} failed (${res.status})`,
15481
+ await parseEnvelope(res),
15482
+ res.status
15483
+ );
15484
+ }
15485
+ const parsed = await res.json().catch(() => void 0);
15486
+ const expected = scope === "build" ? "appliesAtNextBuild" : "appliesAtNextDeploy";
15487
+ if (!parsed || typeof parsed.key !== "string" || typeof parsed.created !== "boolean" || parsed[expected] !== true) {
15488
+ throw new ApiError(
15489
+ 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."
15490
+ );
15491
+ }
15492
+ return {
15493
+ key: parsed.key,
15494
+ created: parsed.created,
15495
+ appliesAtNextDeploy: scope !== "build",
15496
+ appliesAtNextBuild: scope === "build"
15497
+ };
15498
+ }
15499
+ async function removeEnvVar(apiUrl, token, projectId, key, scope = "runtime", environment = "production") {
15500
+ const params = new URLSearchParams();
15501
+ if (scope !== "runtime") params.set("scope", scope);
15502
+ if (environment !== "production") params.set("environment", environment);
15503
+ const query = params.toString() ? `?${params.toString()}` : "";
15504
+ const res = await fetch(
15505
+ `${apiUrl}/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(key)}${query}`,
15506
+ { method: "DELETE", headers: { authorization: `Bearer ${token}` } }
15507
+ );
15508
+ if (!res.ok) {
15509
+ throw new ApiError(
15510
+ `Removing ${key} failed (${res.status})`,
15511
+ await parseEnvelope(res),
15512
+ res.status
15513
+ );
15514
+ }
15515
+ const parsed = await res.json().catch(() => void 0);
15516
+ if (scope === "build" && parsed?.appliesAtNextBuild !== true) {
15517
+ throw new ApiError(
15518
+ `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.`
15519
+ );
15520
+ }
15521
+ return {
15522
+ key: typeof parsed?.key === "string" ? parsed.key : key,
15523
+ appliesAtNextDeploy: scope !== "build",
15524
+ appliesAtNextBuild: scope === "build"
15525
+ };
15526
+ }
15107
15527
  async function triggerDeploy(apiUrl, token, projectId) {
15108
15528
  const res = await fetch(`${apiUrl}/deployments`, {
15109
15529
  method: "POST",
@@ -15135,6 +15555,20 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
15135
15555
  }
15136
15556
  return await res.json();
15137
15557
  }
15558
+ async function redeployProject(apiUrl, token, projectId) {
15559
+ const res = await fetch(`${apiUrl}/deployments/redeploy`, {
15560
+ method: "POST",
15561
+ headers: {
15562
+ "content-type": "application/json",
15563
+ authorization: `Bearer ${token}`
15564
+ },
15565
+ body: JSON.stringify({ projectId })
15566
+ });
15567
+ if (!res.ok) {
15568
+ throw new ApiError(`Redeploy failed (${res.status})`, await parseEnvelope(res));
15569
+ }
15570
+ return await res.json();
15571
+ }
15138
15572
  async function listDeployments(apiUrl, token, projectId) {
15139
15573
  const url2 = new URL(`${apiUrl}/deployments`);
15140
15574
  if (projectId) url2.searchParams.set("projectId", projectId);
@@ -15196,14 +15630,14 @@ async function* readSseFrames(body) {
15196
15630
  }
15197
15631
  function abortableSleep(ms, signal) {
15198
15632
  if (signal?.aborted) return Promise.resolve();
15199
- return new Promise((resolve) => {
15633
+ return new Promise((resolve2) => {
15200
15634
  const onAbort = () => {
15201
15635
  clearTimeout(timer);
15202
- resolve();
15636
+ resolve2();
15203
15637
  };
15204
15638
  const timer = setTimeout(() => {
15205
15639
  signal?.removeEventListener("abort", onAbort);
15206
- resolve();
15640
+ resolve2();
15207
15641
  }, ms);
15208
15642
  signal?.addEventListener("abort", onAbort, { once: true });
15209
15643
  });
@@ -15246,13 +15680,14 @@ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15246
15680
  }
15247
15681
  return terminal;
15248
15682
  }
15249
- async function streamRuntime(apiUrl, token, projectId, handlers, opts = {}) {
15683
+ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15684
+ const basePath = opts.resource === "deployment" ? "deployments" : "projects";
15250
15685
  const doFetch = opts.fetchImpl ?? fetch;
15251
15686
  const reconnectMs = opts.reconnectMs ?? 1e3;
15252
15687
  let afterNs;
15253
15688
  while (!opts.signal?.aborted) {
15254
15689
  try {
15255
- const url2 = new URL(`${apiUrl}/projects/${projectId}/runtime-logs`);
15690
+ const url2 = new URL(`${apiUrl}/${basePath}/${id}/runtime-logs`);
15256
15691
  if (afterNs) url2.searchParams.set("after", afterNs);
15257
15692
  else if (opts.since) url2.searchParams.set("since", opts.since);
15258
15693
  const res = await doFetch(url2, {
@@ -15286,7 +15721,12 @@ async function streamRuntime(apiUrl, token, projectId, handlers, opts = {}) {
15286
15721
  // src/framework.ts
15287
15722
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
15288
15723
  import { basename, join as join2 } from "node:path";
15289
- var FRAMEWORKS = ["react-router", "nextjs", "vite-spa"];
15724
+ var FRAMEWORKS = [
15725
+ "react-router",
15726
+ "nextjs",
15727
+ "vite-spa",
15728
+ "node"
15729
+ ];
15290
15730
  function isFramework(value) {
15291
15731
  return FRAMEWORKS.includes(value);
15292
15732
  }
@@ -15300,6 +15740,7 @@ var VITE_META_FRAMEWORKS = [
15300
15740
  "@builder.io/qwik-city",
15301
15741
  "vike"
15302
15742
  ];
15743
+ var SERVER_FRAMEWORKS = ["hono", "express", "fastify", "koa"];
15303
15744
  function readPackageJson(cwd) {
15304
15745
  try {
15305
15746
  return JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
@@ -15315,6 +15756,7 @@ function frameworkFrom(pkg, cwd) {
15315
15756
  return "react-router";
15316
15757
  }
15317
15758
  if (VITE_META_FRAMEWORKS.some(has)) return null;
15759
+ if (SERVER_FRAMEWORKS.some(has)) return "node";
15318
15760
  if (has("vite")) return "vite-spa";
15319
15761
  return null;
15320
15762
  }
@@ -15326,6 +15768,94 @@ function inspectProject(cwd) {
15326
15768
  return { framework: frameworkFrom(pkg, cwd), name: nameFrom(pkg, cwd) };
15327
15769
  }
15328
15770
 
15771
+ // src/input.ts
15772
+ import { createInterface } from "node:readline";
15773
+ import { Writable } from "node:stream";
15774
+ function defaultPromptIO() {
15775
+ return {
15776
+ input: process.stdin,
15777
+ output: process.stderr,
15778
+ isTTY: Boolean(process.stdin.isTTY) && Boolean(process.stderr.isTTY)
15779
+ };
15780
+ }
15781
+ function isInteractive(io) {
15782
+ if (io.isTTY !== void 0) return io.isTTY;
15783
+ const output = io.output;
15784
+ return Boolean(io.input.isTTY) && Boolean(output.isTTY);
15785
+ }
15786
+ async function readAll(input, maxBytes) {
15787
+ const chunks = [];
15788
+ let bytes = 0;
15789
+ for await (const chunk of input) {
15790
+ const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
15791
+ bytes += buf.length;
15792
+ if (maxBytes !== void 0 && bytes > maxBytes) return { tooLarge: true };
15793
+ chunks.push(buf);
15794
+ }
15795
+ return { value: Buffer.concat(chunks).toString("utf8") };
15796
+ }
15797
+ function stripOneTrailingNewline(value) {
15798
+ if (value.endsWith("\r\n")) return value.slice(0, -2);
15799
+ if (value.endsWith("\n")) return value.slice(0, -1);
15800
+ return value;
15801
+ }
15802
+ async function promptHidden(question, io) {
15803
+ io.output.write(question);
15804
+ const restore = () => {
15805
+ const tty = io.input;
15806
+ if (tty.isTTY) tty.setRawMode?.(false);
15807
+ };
15808
+ process.once("SIGTERM", restore);
15809
+ process.once("SIGHUP", restore);
15810
+ const sink = new Writable({
15811
+ write(_chunk, _encoding, callback) {
15812
+ callback();
15813
+ }
15814
+ });
15815
+ const rl = createInterface({
15816
+ input: io.input,
15817
+ output: sink,
15818
+ terminal: true
15819
+ });
15820
+ try {
15821
+ return await new Promise((resolve2) => {
15822
+ rl.once("SIGINT", () => resolve2(null));
15823
+ rl.once("close", () => resolve2(null));
15824
+ rl.question("", (answer) => resolve2(answer));
15825
+ });
15826
+ } finally {
15827
+ rl.close();
15828
+ process.off("SIGTERM", restore);
15829
+ process.off("SIGHUP", restore);
15830
+ io.output.write("\n");
15831
+ }
15832
+ }
15833
+ async function promptLine(question, io) {
15834
+ const rl = createInterface({ input: io.input, output: io.output });
15835
+ try {
15836
+ return await new Promise((resolve2) => {
15837
+ rl.once("SIGINT", () => resolve2(null));
15838
+ rl.once("close", () => resolve2(null));
15839
+ rl.question(question, (answer) => resolve2(answer));
15840
+ });
15841
+ } finally {
15842
+ rl.close();
15843
+ }
15844
+ }
15845
+ async function promptConfirm(question, io) {
15846
+ const rl = createInterface({ input: io.input, output: io.output });
15847
+ try {
15848
+ const answer = await new Promise((resolve2) => {
15849
+ rl.once("SIGINT", () => resolve2(null));
15850
+ rl.once("close", () => resolve2(null));
15851
+ rl.question(`${question} [y/N] `, (a) => resolve2(a));
15852
+ });
15853
+ return answer !== null && /^y(es)?$/i.test(answer.trim());
15854
+ } finally {
15855
+ rl.close();
15856
+ }
15857
+ }
15858
+
15329
15859
  // src/git.ts
15330
15860
  import { execFileSync, spawnSync } from "node:child_process";
15331
15861
  function authenticatedRemoteUrl(cloneUrl, username, token) {
@@ -15467,7 +15997,7 @@ async function runInit(opts) {
15467
15997
  if (opts.framework) {
15468
15998
  if (!isFramework(opts.framework)) {
15469
15999
  process.stderr.write(
15470
- `Unknown framework "${opts.framework}" \u2014 expected react-router, nextjs, or vite-spa.
16000
+ `Unknown framework "${opts.framework}" \u2014 expected react-router, nextjs, vite-spa, or node.
15471
16001
  `
15472
16002
  );
15473
16003
  process.exitCode = 1;
@@ -15477,7 +16007,7 @@ async function runInit(opts) {
15477
16007
  } else {
15478
16008
  if (!detected.framework) {
15479
16009
  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"
16010
+ "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
16011
  );
15482
16012
  process.exitCode = 1;
15483
16013
  return;
@@ -15486,7 +16016,7 @@ async function runInit(opts) {
15486
16016
  }
15487
16017
  const name = opts.name ?? detected.name;
15488
16018
  try {
15489
- const { project, repo, pushToken } = await linkProject(apiUrl, token, {
16019
+ const { project: project2, repo, pushToken } = await linkProject(apiUrl, token, {
15490
16020
  name,
15491
16021
  framework,
15492
16022
  // First `kryd init` for the account claims the tenant slug (the vanity routing key in
@@ -15496,7 +16026,7 @@ async function runInit(opts) {
15496
16026
  });
15497
16027
  let linkNote = "";
15498
16028
  try {
15499
- saveProjectLink(cwd, { projectId: project.id });
16029
+ saveProjectLink(cwd, { projectId: project2.id });
15500
16030
  linkNote = "Linked this folder \u2192 .kryd/project.json \u2014 `kryd deploy` / `kryd logs` now work here with no id.\n";
15501
16031
  try {
15502
16032
  ensureKrydIgnored(cwd);
@@ -15508,7 +16038,7 @@ async function runInit(opts) {
15508
16038
  }
15509
16039
  } catch (linkErr) {
15510
16040
  process.stderr.write(
15511
- `warning: could not write .kryd/project.json (${linkErr instanceof Error ? linkErr.message : String(linkErr)}) \u2014 pass the project id explicitly, e.g. \`kryd deploy ${project.id}\`.
16041
+ `warning: could not write .kryd/project.json (${linkErr instanceof Error ? linkErr.message : String(linkErr)}) \u2014 pass the project id explicitly, e.g. \`kryd deploy ${project2.id}\`.
15512
16042
  `
15513
16043
  );
15514
16044
  }
@@ -15541,9 +16071,9 @@ Install git (https://git-scm.com/downloads), then re-run \`kryd init\` here.
15541
16071
  break;
15542
16072
  }
15543
16073
  process.stdout.write(
15544
- `Linked "${name}" (${project.framework}) \u2192 ${repo.htmlUrl}
15545
- Production URL: https://${project.subdomain}.${PRODUCTION_TLD}
15546
- Preview branches deploy to https://<branch>-<hash>-${project.subdomain}.${PREVIEW_TLD}
16074
+ `Linked "${name}" (${project2.framework}) \u2192 ${repo.htmlUrl}
16075
+ Production URL: https://${project2.subdomain}.${PRODUCTION_TLD}
16076
+ Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
15547
16077
  ${linkNote}
15548
16078
  ${nextStep}`
15549
16079
  );
@@ -15612,8 +16142,9 @@ async function runRuntimeLogs(opts) {
15612
16142
  process.exitCode = 1;
15613
16143
  return;
15614
16144
  }
15615
- const project = resolveProjectId(opts.project, opts.cwd);
15616
- if (!project) {
16145
+ const isDeployment = opts.project?.startsWith("dpl_") ?? false;
16146
+ const target = isDeployment ? opts.project : resolveProjectId(opts.project, opts.cwd);
16147
+ if (!target) {
15617
16148
  reportNotLinked("kryd logs <projectId> --runtime");
15618
16149
  return;
15619
16150
  }
@@ -15632,7 +16163,7 @@ async function runRuntimeLogs(opts) {
15632
16163
  await streamRuntime(
15633
16164
  apiUrl,
15634
16165
  token,
15635
- project,
16166
+ target,
15636
16167
  {
15637
16168
  onLine: ({ stream, line }) => {
15638
16169
  const out = stream === "stderr" ? process.stderr : process.stdout;
@@ -15640,7 +16171,11 @@ async function runRuntimeLogs(opts) {
15640
16171
  `);
15641
16172
  }
15642
16173
  },
15643
- { ...opts.since ? { since: opts.since } : {}, signal: controller.signal }
16174
+ {
16175
+ ...opts.since ? { since: opts.since } : {},
16176
+ signal: controller.signal,
16177
+ ...isDeployment ? { resource: "deployment" } : {}
16178
+ }
15644
16179
  );
15645
16180
  } catch (err) {
15646
16181
  reportError(err);
@@ -15656,13 +16191,13 @@ async function runDeploy(opts) {
15656
16191
  process.exitCode = 1;
15657
16192
  return;
15658
16193
  }
15659
- const project = resolveProjectId(opts.project, opts.cwd);
15660
- if (!project) {
16194
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16195
+ if (!project2) {
15661
16196
  reportNotLinked("kryd deploy <projectId>");
15662
16197
  return;
15663
16198
  }
15664
16199
  try {
15665
- const deploymentId = await triggerDeploy(apiUrl, token, project);
16200
+ const deploymentId = await triggerDeploy(apiUrl, token, project2);
15666
16201
  process.stdout.write(`Triggered deploy ${deploymentId}
15667
16202
  `);
15668
16203
  await followDeploy(apiUrl, token, deploymentId);
@@ -15807,8 +16342,8 @@ async function runRollback(opts) {
15807
16342
  process.exitCode = 1;
15808
16343
  return;
15809
16344
  }
15810
- const project = resolveProjectId(opts.project, opts.cwd);
15811
- if (!project) {
16345
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16346
+ if (!project2) {
15812
16347
  reportNotLinked("kryd rollback <projectId> [deployment]");
15813
16348
  return;
15814
16349
  }
@@ -15816,7 +16351,7 @@ async function runRollback(opts) {
15816
16351
  const { deploymentId, rolledBackTo, note } = await rollbackDeploy(
15817
16352
  apiUrl,
15818
16353
  token,
15819
- project,
16354
+ project2,
15820
16355
  opts.deployment
15821
16356
  );
15822
16357
  process.stdout.write(
@@ -15829,6 +16364,35 @@ async function runRollback(opts) {
15829
16364
  reportError(err);
15830
16365
  }
15831
16366
  }
16367
+ async function runRedeploy(opts) {
16368
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16369
+ const token = loadConfig().token;
16370
+ if (!token) {
16371
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16372
+ process.exitCode = 1;
16373
+ return;
16374
+ }
16375
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16376
+ if (!project2) {
16377
+ reportNotLinked("kryd redeploy <projectId>");
16378
+ return;
16379
+ }
16380
+ try {
16381
+ const { deploymentId, redeployedCommit, note } = await redeployProject(
16382
+ apiUrl,
16383
+ token,
16384
+ project2
16385
+ );
16386
+ process.stdout.write(
16387
+ `Redeploying ${redeployedCommit.slice(0, 7)} \u2192 ${deploymentId}
16388
+ ${note}
16389
+ `
16390
+ );
16391
+ await followDeploy(apiUrl, token, deploymentId);
16392
+ } catch (err) {
16393
+ reportError(err);
16394
+ }
16395
+ }
15832
16396
  async function runDbCreate(opts) {
15833
16397
  const apiUrl = resolveApiUrl(opts.apiUrl);
15834
16398
  const token = loadConfig().token;
@@ -15837,8 +16401,8 @@ async function runDbCreate(opts) {
15837
16401
  process.exitCode = 1;
15838
16402
  return;
15839
16403
  }
15840
- const project = resolveProjectId(opts.project, opts.cwd);
15841
- if (!project) {
16404
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16405
+ if (!project2) {
15842
16406
  reportNotLinked("kryd db create <projectId>");
15843
16407
  return;
15844
16408
  }
@@ -15866,29 +16430,29 @@ async function runDbCreate(opts) {
15866
16430
  }
15867
16431
  try {
15868
16432
  const db2 = await attachDatabase(apiUrl, token, {
15869
- projectId: project,
16433
+ projectId: project2,
15870
16434
  mode,
15871
16435
  ...connectionString ? { connectionString } : {}
15872
16436
  });
15873
16437
  if (db2.status === "ready") {
15874
16438
  process.stdout.write(
15875
- `Attached ${db2.mode} database ${db2.id} to ${project}.
16439
+ `Attached ${db2.mode} database ${db2.id} to ${project2}.
15876
16440
  The connection string will be injected as DATABASE_URL on your next deploy.
15877
16441
  `
15878
16442
  );
15879
16443
  return;
15880
16444
  }
15881
- process.stdout.write(`Provisioning a shared database for ${project}\u2026
16445
+ process.stdout.write(`Provisioning a shared database for ${project2}\u2026
15882
16446
  `);
15883
16447
  const result = await pollResourceUntilTerminal(
15884
- () => getDatabaseStatus(apiUrl, token, project)
16448
+ () => getDatabaseStatus(apiUrl, token, project2)
15885
16449
  );
15886
16450
  reportSettleResult(result, {
15887
- readyMsg: `Attached shared database ${db2.id} to ${project}.
16451
+ readyMsg: `Attached shared database ${db2.id} to ${project2}.
15888
16452
  The connection string will be injected as DATABASE_URL on your next deploy.
15889
16453
  `,
15890
16454
  failedLabel: "Database provisioning",
15891
- retryCmd: `kryd db create ${project}`
16455
+ retryCmd: `kryd db create ${project2}`
15892
16456
  });
15893
16457
  } catch (err) {
15894
16458
  reportError(err);
@@ -15918,24 +16482,24 @@ async function runStorageCreate(opts) {
15918
16482
  process.exitCode = 1;
15919
16483
  return;
15920
16484
  }
15921
- const project = resolveProjectId(opts.project, opts.cwd);
15922
- if (!project) {
16485
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16486
+ if (!project2) {
15923
16487
  reportNotLinked("kryd storage create <projectId>");
15924
16488
  return;
15925
16489
  }
15926
16490
  try {
15927
- const bucket = await createStorage(apiUrl, token, { projectId: project });
15928
- process.stdout.write(`Provisioning object storage for ${project}\u2026
16491
+ const bucket = await createStorage(apiUrl, token, { projectId: project2 });
16492
+ process.stdout.write(`Provisioning object storage for ${project2}\u2026
15929
16493
  `);
15930
16494
  const result = await pollResourceUntilTerminal(
15931
- () => getStorageStatus(apiUrl, token, project)
16495
+ () => getStorageStatus(apiUrl, token, project2)
15932
16496
  );
15933
16497
  reportSettleResult(result, {
15934
- readyMsg: `Created object storage ${bucket.id} for ${project}.
16498
+ readyMsg: `Created object storage ${bucket.id} for ${project2}.
15935
16499
  Its S3 credentials will be injected on your next deploy.
15936
16500
  `,
15937
16501
  failedLabel: "Storage provisioning",
15938
- retryCmd: `kryd storage create ${project}`
16502
+ retryCmd: `kryd storage create ${project2}`
15939
16503
  });
15940
16504
  } catch (err) {
15941
16505
  reportError(err);
@@ -15958,6 +16522,113 @@ function reportDetachResult(result, opts) {
15958
16522
  process.exitCode = 1;
15959
16523
  }
15960
16524
  }
16525
+ async function runProjectRemove(opts) {
16526
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16527
+ const token = loadConfig().token;
16528
+ if (!token) {
16529
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16530
+ process.exitCode = 1;
16531
+ return;
16532
+ }
16533
+ const projectId = resolveProjectId(opts.project, opts.cwd);
16534
+ if (!projectId) {
16535
+ reportNotLinked("kryd project rm <projectId>");
16536
+ return;
16537
+ }
16538
+ let project2;
16539
+ let resources;
16540
+ try {
16541
+ project2 = await getProject(apiUrl, token, projectId);
16542
+ if (!project2) {
16543
+ process.stderr.write(`No project ${projectId} on this account.
16544
+ `);
16545
+ process.exitCode = 1;
16546
+ return;
16547
+ }
16548
+ if (project2.status === "deleting") {
16549
+ process.stdout.write(`${project2.name} is already being deleted \u2014 following it.
16550
+ `);
16551
+ return followProjectDeletion(apiUrl, token, projectId, project2.name, opts);
16552
+ }
16553
+ resources = project2.status === "delete_failed" ? ["whatever the previous attempt did not manage to remove"] : await describeProjectResources(apiUrl, token, projectId);
16554
+ } catch (err) {
16555
+ reportError(err);
16556
+ return;
16557
+ }
16558
+ if (!opts.yes) {
16559
+ const io = opts.io ?? defaultPromptIO();
16560
+ if (!isInteractive(io)) {
16561
+ process.stderr.write(
16562
+ `Refusing to delete ${project2.name} without confirmation. Re-run with --yes to confirm non-interactively.
16563
+ `
16564
+ );
16565
+ process.exitCode = 1;
16566
+ return;
16567
+ }
16568
+ io.output.write(
16569
+ `This permanently deletes ${project2.name} and:
16570
+ ` + resources.map((r) => ` \u2022 ${r}
16571
+ `).join("") + "This cannot be undone.\n"
16572
+ );
16573
+ const typed = await promptLine(
16574
+ `Type ${project2.name} to confirm: `,
16575
+ io
16576
+ );
16577
+ if (typed !== project2.name) {
16578
+ process.stdout.write(`Left ${project2.name} in place.
16579
+ `);
16580
+ return;
16581
+ }
16582
+ }
16583
+ try {
16584
+ const accepted = await deleteProject(apiUrl, token, projectId);
16585
+ process.stdout.write(
16586
+ accepted.alreadyInProgress ? `${project2.name} was already being deleted \u2014 following it.
16587
+ ` : `Deleting ${project2.name}\u2026
16588
+ `
16589
+ );
16590
+ await followProjectDeletion(apiUrl, token, projectId, project2.name, opts);
16591
+ } catch (err) {
16592
+ reportError(err);
16593
+ }
16594
+ }
16595
+ async function followProjectDeletion(apiUrl, token, projectId, name, opts) {
16596
+ const result = await pollProjectDeleted(
16597
+ () => getProject(apiUrl, token, projectId),
16598
+ opts.pollOpts
16599
+ );
16600
+ if (result.gone) {
16601
+ const cleared = clearProjectLinkIfMatches(projectId, opts.cwd);
16602
+ process.stdout.write(
16603
+ `Deleted ${name}.
16604
+ ` + (cleared ? "Removed the local .kryd/project.json link.\n" : "")
16605
+ );
16606
+ return;
16607
+ }
16608
+ if (result.status === "delete_failed") {
16609
+ process.stderr.write(
16610
+ `Deleting ${name} failed part-way. Your project is still listed, but some of its resources may already be gone \u2014 the teardown removes them in order and stops where it failed.
16611
+ Re-run \`kryd project rm ${projectId}\` to resume from where it stopped.
16612
+ `
16613
+ );
16614
+ process.exitCode = 1;
16615
+ return;
16616
+ }
16617
+ process.stderr.write(
16618
+ `Could not confirm that ${name} is gone \u2014 it is still being deleted (or the worker stalled). Re-run \`kryd project rm ${projectId}\` to check.
16619
+ `
16620
+ );
16621
+ process.exitCode = 1;
16622
+ }
16623
+ async function describeProjectResources(apiUrl, token, projectId) {
16624
+ const out = ["its deployed app, its containers and its built images"];
16625
+ const db2 = await getDatabaseStatus(apiUrl, token, projectId).catch(() => null);
16626
+ if (db2) out.push("its Postgres database \u2014 and all the data in it");
16627
+ const storage2 = await getStorageStatus(apiUrl, token, projectId).catch(() => null);
16628
+ if (storage2) out.push("its object-storage bucket \u2014 and every object in it");
16629
+ out.push("its Git repository on the forge, and its push token");
16630
+ return out;
16631
+ }
15961
16632
  async function runDbDetach(opts) {
15962
16633
  const apiUrl = resolveApiUrl(opts.apiUrl);
15963
16634
  const token = loadConfig().token;
@@ -15966,23 +16637,23 @@ async function runDbDetach(opts) {
15966
16637
  process.exitCode = 1;
15967
16638
  return;
15968
16639
  }
15969
- const project = resolveProjectId(opts.project, opts.cwd);
15970
- if (!project) {
16640
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16641
+ if (!project2) {
15971
16642
  reportNotLinked("kryd db detach <projectId>");
15972
16643
  return;
15973
16644
  }
15974
16645
  try {
15975
- await detachDatabase(apiUrl, token, project);
15976
- process.stdout.write(`Detaching the database from ${project}\u2026
16646
+ await detachDatabase(apiUrl, token, project2);
16647
+ process.stdout.write(`Detaching the database from ${project2}\u2026
15977
16648
  `);
15978
16649
  const result = await pollResourceDetached(
15979
- () => getDatabaseStatus(apiUrl, token, project)
16650
+ () => getDatabaseStatus(apiUrl, token, project2)
15980
16651
  );
15981
16652
  reportDetachResult(result, {
15982
- goneMsg: `Detached the database from ${project}.
16653
+ goneMsg: `Detached the database from ${project2}.
15983
16654
  `,
15984
16655
  failedLabel: "Database detach",
15985
- retryCmd: `kryd db detach ${project}`
16656
+ retryCmd: `kryd db detach ${project2}`
15986
16657
  });
15987
16658
  } catch (err) {
15988
16659
  reportError(err);
@@ -15996,15 +16667,15 @@ async function runAiEnable(opts) {
15996
16667
  process.exitCode = 1;
15997
16668
  return;
15998
16669
  }
15999
- const project = resolveProjectId(opts.project, opts.cwd);
16000
- if (!project) {
16670
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16671
+ if (!project2) {
16001
16672
  reportNotLinked("kryd ai enable <projectId>");
16002
16673
  return;
16003
16674
  }
16004
16675
  try {
16005
- const status = await enableAi(apiUrl, token, { projectId: project });
16676
+ const status = await enableAi(apiUrl, token, { projectId: project2 });
16006
16677
  process.stdout.write(
16007
- `AI enabled for ${project}.
16678
+ `AI enabled for ${project2}.
16008
16679
  ` + (status.url ? `Endpoint: ${status.url} (OpenAI-compatible base URL \u2014 use it as-is)
16009
16680
  ` : "") + `AI_GATEWAY_URL + AI_GATEWAY_TOKEN will be injected on your next deploy (kryd deploy).
16010
16681
  `
@@ -16021,29 +16692,442 @@ async function runStorageDetach(opts) {
16021
16692
  process.exitCode = 1;
16022
16693
  return;
16023
16694
  }
16024
- const project = resolveProjectId(opts.project, opts.cwd);
16025
- if (!project) {
16695
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16696
+ if (!project2) {
16026
16697
  reportNotLinked("kryd storage detach <projectId>");
16027
16698
  return;
16028
16699
  }
16029
16700
  try {
16030
- await detachStorage(apiUrl, token, project);
16031
- process.stdout.write(`Detaching object storage from ${project}\u2026
16701
+ await detachStorage(apiUrl, token, project2);
16702
+ process.stdout.write(`Detaching object storage from ${project2}\u2026
16032
16703
  `);
16033
16704
  const result = await pollResourceDetached(
16034
- () => getStorageStatus(apiUrl, token, project)
16705
+ () => getStorageStatus(apiUrl, token, project2)
16035
16706
  );
16036
16707
  reportDetachResult(result, {
16037
- goneMsg: `Detached object storage from ${project}.
16708
+ goneMsg: `Detached object storage from ${project2}.
16038
16709
  `,
16039
16710
  failedLabel: "Storage detach",
16040
- retryCmd: `kryd storage detach ${project}`
16711
+ retryCmd: `kryd storage detach ${project2}`
16041
16712
  });
16042
16713
  } catch (err) {
16043
16714
  reportError(err);
16044
16715
  }
16045
16716
  }
16046
- var CLI_VERSION = true ? "0.2.0" : "0.0.0-dev";
16717
+ function splitKeyValue(spec) {
16718
+ const eq = spec.indexOf("=");
16719
+ if (eq === -1) return { key: spec };
16720
+ return { key: spec.slice(0, eq), value: spec.slice(eq + 1) };
16721
+ }
16722
+ 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";
16723
+ var NEXT_BUILD_NOTE = `${BUILD_ENV_REBUILD_NOTE}
16724
+ `;
16725
+ var ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T/;
16726
+ function isoDate(value) {
16727
+ if (!value || !ISO_TIMESTAMP.test(value)) return null;
16728
+ const date5 = new Date(value);
16729
+ if (Number.isNaN(date5.getTime())) return null;
16730
+ const pad = (n) => String(n).padStart(2, "0");
16731
+ return `${date5.getFullYear()}-${pad(date5.getMonth() + 1)}-${pad(date5.getDate())}`;
16732
+ }
16733
+ function envAnnotation(environments) {
16734
+ if (!environments || environments.length === 0) return "";
16735
+ const prod = environments.includes("production");
16736
+ const preview = environments.includes("preview");
16737
+ if (prod && preview) return " \u2192 preview override";
16738
+ if (preview && !prod) return " \u2192 preview only";
16739
+ return "";
16740
+ }
16741
+ function renderEnvGroup(heading, vars, opts) {
16742
+ const width = vars.reduce((max, v) => Math.max(max, v.key.length), 0);
16743
+ const dates = opts.showUpdated ? vars.map((v) => isoDate(v.updatedAt)) : [];
16744
+ const anyDate = dates.some((d) => d !== null);
16745
+ const rows = vars.map((v, i) => {
16746
+ const envNote = opts.showEnvironments ? envAnnotation(v.environments) : "";
16747
+ if (!anyDate) return ` ${v.key}${envNote}
16748
+ `;
16749
+ const updated = dates[i];
16750
+ return ` ${v.key.padEnd(width)} ${updated ? `updated ${updated}` : ""}${envNote}
16751
+ `.trimEnd() + "\n";
16752
+ }).join("");
16753
+ return `${heading}
16754
+ ${rows}`;
16755
+ }
16756
+ async function runEnvList(opts) {
16757
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16758
+ const token = loadConfig().token;
16759
+ if (!token) {
16760
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16761
+ process.exitCode = 1;
16762
+ return;
16763
+ }
16764
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16765
+ if (!project2) {
16766
+ reportNotLinked("kryd env list <projectId>");
16767
+ return;
16768
+ }
16769
+ try {
16770
+ const vars = await listEnvVars(apiUrl, token, project2);
16771
+ if (vars.length === 0) {
16772
+ process.stdout.write(
16773
+ `No environment variables for ${project2} yet.
16774
+ Set one with \`kryd env set KEY --stdin\`, or attach a database, storage or the AI gateway.
16775
+ `
16776
+ );
16777
+ return;
16778
+ }
16779
+ const byKey = (a, b) => a.key.localeCompare(b.key);
16780
+ const buildVars = vars.filter((v) => v.scope === "build").sort(byKey);
16781
+ const runtimeVars = vars.filter((v) => v.scope !== "build");
16782
+ const mine = runtimeVars.filter((v) => v.source === "customer").sort(byKey);
16783
+ const managed = runtimeVars.filter((v) => v.source === "managed").sort(byKey);
16784
+ const other = runtimeVars.filter((v) => v.source !== "customer" && v.source !== "managed").sort(byKey);
16785
+ let out = "";
16786
+ if (mine.length > 0) {
16787
+ out += renderEnvGroup(`Set by you (${mine.length})`, mine, {
16788
+ showUpdated: true,
16789
+ // Your customer vars can carry a preview override (runtime KRYD-159, build KRYD-215); annotate
16790
+ // which ones do.
16791
+ showEnvironments: true
16792
+ });
16793
+ }
16794
+ if (managed.length > 0) {
16795
+ if (out) out += "\n";
16796
+ out += renderEnvGroup(
16797
+ `Managed by Kryd (${managed.length}) \u2014 set when you attach a resource; not editable`,
16798
+ managed,
16799
+ // A managed row's timestamp is Kryd's own bookkeeping, not something the user did; showing
16800
+ // it invites "why did my DATABASE_URL change today?".
16801
+ { showUpdated: false }
16802
+ );
16803
+ }
16804
+ if (other.length > 0) {
16805
+ if (out) out += "\n";
16806
+ out += renderEnvGroup(
16807
+ `Other (${other.length}) \u2014 set by a newer version of Kryd; upgrade the CLI (\`npm install -g @kryd/cli\`) to see what they are`,
16808
+ other,
16809
+ { showUpdated: false }
16810
+ );
16811
+ }
16812
+ if (buildVars.length > 0) {
16813
+ if (out) out += "\n";
16814
+ out += renderEnvGroup(
16815
+ `Build-time (${buildVars.length}) \u2014 compiled into your public bundle`,
16816
+ buildVars,
16817
+ // Build vars are per-environment too now (KRYD-215) — annotate a preview override / preview-only
16818
+ // build var, exactly as the runtime group does.
16819
+ { showUpdated: true, showEnvironments: true }
16820
+ );
16821
+ out += `${BUILD_ENV_PUBLIC_WARNING}
16822
+ `;
16823
+ }
16824
+ 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");
16825
+ process.stdout.write(out);
16826
+ } catch (err) {
16827
+ reportError(err);
16828
+ }
16829
+ }
16830
+ function toDotenvLine(key, value) {
16831
+ const hasNewline = /[\n\r]/.test(value);
16832
+ const needsQuote = hasNewline || value === "" || value !== value.trim() || value.includes("#") || /^["'`]/.test(value);
16833
+ if (!needsQuote) return `${key}=${value}`;
16834
+ if (!hasNewline && !value.includes("'")) return `${key}='${value}'`;
16835
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
16836
+ return `${key}="${escaped}"`;
16837
+ }
16838
+ async function runEnvPull(opts) {
16839
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16840
+ const token = loadConfig().token;
16841
+ if (!token) {
16842
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16843
+ process.exitCode = 1;
16844
+ return;
16845
+ }
16846
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16847
+ if (!project2) {
16848
+ reportNotLinked("kryd env pull <projectId>");
16849
+ return;
16850
+ }
16851
+ const environment = opts.preview ? "preview" : "production";
16852
+ const cwd = opts.cwd ?? process.cwd();
16853
+ const root = findProjectLinkDir(cwd) ?? cwd;
16854
+ const outRel = opts.out ?? ".env.kryd";
16855
+ const outPath = resolve(root, outRel);
16856
+ if (opts.out !== void 0 && existsSync3(outPath) && !opts.force) {
16857
+ process.stderr.write(
16858
+ `Refusing to overwrite ${outRel} \u2014 pass --force to replace it (or omit --out to write .env.kryd).
16859
+ `
16860
+ );
16861
+ process.exitCode = 1;
16862
+ return;
16863
+ }
16864
+ try {
16865
+ const { values } = await pullEnvValues(apiUrl, token, project2, environment);
16866
+ const entries = Object.entries(values).sort(
16867
+ ([a], [b]) => a.localeCompare(b)
16868
+ );
16869
+ const header = `# Written by \`kryd env pull\` \u2014 your customer-set variables for ${project2} (${environment}).
16870
+ # Regenerate with \`kryd env pull\`. Do not commit this file.
16871
+ `;
16872
+ const body = entries.map(([k, v]) => toDotenvLine(k, v)).join("\n");
16873
+ const rel = relative(root, outPath);
16874
+ const insideRoot = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
16875
+ const added = insideRoot ? ensureIgnored(root, rel) : false;
16876
+ writeSecretFile(outPath, body.length > 0 ? `${header}
16877
+ ${body}
16878
+ ` : header);
16879
+ const count = entries.length;
16880
+ process.stdout.write(
16881
+ count === 0 ? `No customer environment variables for ${project2} (${environment}) \u2014 wrote an empty ${outRel}.
16882
+ ` : `Wrote ${count} variable${count === 1 ? "" : "s"} to ${outRel} (${environment}), mode 0600.
16883
+ `
16884
+ );
16885
+ if (added) process.stdout.write(`Added ${rel} to .gitignore.
16886
+ `);
16887
+ if (!insideRoot)
16888
+ process.stderr.write(
16889
+ `Warning: ${outRel} is outside the project root \u2014 NOT added to .gitignore. Make sure you don't commit it.
16890
+ `
16891
+ );
16892
+ process.stdout.write(
16893
+ "These are your own values, read back from Kryd. Managed values (DATABASE_URL, storage, the AI gateway) are never pulled.\n"
16894
+ );
16895
+ } catch (err) {
16896
+ reportError(err);
16897
+ }
16898
+ }
16899
+ async function resolveEnvValue(inline, opts) {
16900
+ if (opts.stdin && inline !== void 0) {
16901
+ return {
16902
+ error: "Pass the value with --stdin OR as KEY=VALUE, not both \u2014 it is ambiguous which one you meant."
16903
+ };
16904
+ }
16905
+ if (opts.stdin) {
16906
+ if (isInteractive(opts.io)) {
16907
+ return {
16908
+ 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.`
16909
+ };
16910
+ }
16911
+ const read = await readAll(opts.io.input, ENV_VAR_VALUE_MAX_BYTES + 1);
16912
+ if ("tooLarge" in read) {
16913
+ return {
16914
+ error: `That value is larger than the ${ENV_VAR_VALUE_MAX_BYTES}-byte limit for an environment variable.`
16915
+ };
16916
+ }
16917
+ return { value: stripOneTrailingNewline(read.value) };
16918
+ }
16919
+ if (inline !== void 0) {
16920
+ if (inline.length > 0) {
16921
+ process.stderr.write(
16922
+ `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.
16923
+ `
16924
+ );
16925
+ }
16926
+ return { value: inline };
16927
+ }
16928
+ if (!isInteractive(opts.io)) {
16929
+ return {
16930
+ error: `No value given for ${opts.key}, and there is no terminal to prompt on. Pipe it with \`kryd env set ${opts.key} --stdin\`.`
16931
+ };
16932
+ }
16933
+ const typed = await promptHidden(`Value for ${opts.key} (not echoed): `, opts.io);
16934
+ if (typed === null) return { aborted: true };
16935
+ return { value: typed };
16936
+ }
16937
+ async function runEnvSet(opts) {
16938
+ const scope = opts.build ? "build" : "runtime";
16939
+ const environment = opts.preview ? "preview" : "production";
16940
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16941
+ const token = loadConfig().token;
16942
+ if (!token) {
16943
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16944
+ process.exitCode = 1;
16945
+ return;
16946
+ }
16947
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16948
+ if (!project2) {
16949
+ reportNotLinked("kryd env set KEY=VALUE <projectId>");
16950
+ return;
16951
+ }
16952
+ if (!opts.spec) {
16953
+ process.stderr.write(
16954
+ "Which variable? e.g. `kryd env set MY_API_KEY --stdin` or `kryd env set FEATURE_X=on`.\n"
16955
+ );
16956
+ process.exitCode = 1;
16957
+ return;
16958
+ }
16959
+ const { key, value: inline } = splitKeyValue(opts.spec);
16960
+ const nameError = validateEnvVarName(key);
16961
+ if (nameError) {
16962
+ process.stderr.write(`${nameError}
16963
+ `);
16964
+ process.exitCode = 1;
16965
+ return;
16966
+ }
16967
+ if (scope === "build") {
16968
+ const buildNameError = validateBuildEnvVarName(key);
16969
+ if (buildNameError) {
16970
+ process.stderr.write(`${buildNameError}
16971
+ `);
16972
+ process.exitCode = 1;
16973
+ return;
16974
+ }
16975
+ }
16976
+ const io = opts.io ?? defaultPromptIO();
16977
+ const resolved = await resolveEnvValue(inline, {
16978
+ ...opts.stdin !== void 0 ? { stdin: opts.stdin } : {},
16979
+ io,
16980
+ key
16981
+ });
16982
+ if ("aborted" in resolved) {
16983
+ process.stderr.write(`Aborted \u2014 ${key} was not set.
16984
+ `);
16985
+ process.exitCode = 1;
16986
+ return;
16987
+ }
16988
+ if ("error" in resolved) {
16989
+ process.stderr.write(`${resolved.error}
16990
+ `);
16991
+ process.exitCode = 1;
16992
+ return;
16993
+ }
16994
+ const valueError = validateEnvVarValue(resolved.value);
16995
+ if (valueError) {
16996
+ process.stderr.write(`${valueError}
16997
+ `);
16998
+ process.exitCode = 1;
16999
+ return;
17000
+ }
17001
+ if (scope === "build") {
17002
+ const buildValueError = validateBuildEnvVarValue(resolved.value);
17003
+ if (buildValueError) {
17004
+ process.stderr.write(`${buildValueError}
17005
+ `);
17006
+ process.exitCode = 1;
17007
+ return;
17008
+ }
17009
+ }
17010
+ try {
17011
+ const written = await setEnvVar(
17012
+ apiUrl,
17013
+ token,
17014
+ project2,
17015
+ key,
17016
+ resolved.value,
17017
+ scope,
17018
+ environment
17019
+ );
17020
+ const envTag = environment === "preview" ? " (preview)" : "";
17021
+ process.stdout.write(
17022
+ `${written.created ? "Set" : "Updated"} ${written.key}${scope === "build" ? " (build-time)" : envTag} for ${project2}.
17023
+ ` + // The public-bundle warning comes FIRST, before the applies-when note. Someone who reads one
17024
+ // line of output should read the consequential one.
17025
+ (scope === "build" ? `${BUILD_ENV_PUBLIC_WARNING}
17026
+ ${NEXT_BUILD_NOTE}` : NEXT_DEPLOY_NOTE)
17027
+ );
17028
+ } catch (err) {
17029
+ reportError(err);
17030
+ }
17031
+ }
17032
+ async function runEnvRemove(opts) {
17033
+ const scope = opts.build ? "build" : "runtime";
17034
+ const environment = opts.preview ? "preview" : "production";
17035
+ const apiUrl = resolveApiUrl(opts.apiUrl);
17036
+ const token = loadConfig().token;
17037
+ if (!token) {
17038
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
17039
+ process.exitCode = 1;
17040
+ return;
17041
+ }
17042
+ const project2 = resolveProjectId(opts.project, opts.cwd);
17043
+ if (!project2) {
17044
+ reportNotLinked("kryd env rm KEY <projectId>");
17045
+ return;
17046
+ }
17047
+ const key = opts.key;
17048
+ if (!key) {
17049
+ process.stderr.write("Which variable? e.g. `kryd env rm MY_API_KEY`.\n");
17050
+ process.exitCode = 1;
17051
+ return;
17052
+ }
17053
+ if (!ENV_VAR_NAME_PATTERN.test(key)) {
17054
+ process.stderr.write(
17055
+ "Environment variable names must match ^[A-Za-z_][A-Za-z0-9_]*$ \u2014 a letter or underscore, then letters, digits or underscores.\n"
17056
+ );
17057
+ process.exitCode = 1;
17058
+ return;
17059
+ }
17060
+ if (isReservedEnvVarName(key)) {
17061
+ process.stderr.write(
17062
+ `${key} is reserved by Kryd and cannot be removed here.
17063
+ If it comes from a resource you attached, detach that instead (\`kryd db detach\`, \`kryd storage detach\`).
17064
+ `
17065
+ );
17066
+ process.exitCode = 1;
17067
+ return;
17068
+ }
17069
+ const nameError = validateEnvVarName(key);
17070
+ if (nameError) {
17071
+ process.stderr.write(`${nameError}
17072
+ `);
17073
+ process.exitCode = 1;
17074
+ return;
17075
+ }
17076
+ if (scope === "build") {
17077
+ try {
17078
+ const vars = await listEnvVars(apiUrl, token, project2, "build");
17079
+ if (!vars.some((v) => v.key === key && v.scope === "build")) {
17080
+ process.stderr.write(
17081
+ `${key} is not set as a build-time variable on ${project2}.
17082
+ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of that name, drop --build.)
17083
+ `
17084
+ );
17085
+ process.exitCode = 1;
17086
+ return;
17087
+ }
17088
+ } catch (err) {
17089
+ reportError(err);
17090
+ return;
17091
+ }
17092
+ }
17093
+ if (!opts.yes) {
17094
+ const io = opts.io ?? defaultPromptIO();
17095
+ if (!isInteractive(io)) {
17096
+ process.stderr.write(
17097
+ `Refusing to remove ${key} without confirmation. Re-run with --yes to confirm non-interactively.
17098
+ `
17099
+ );
17100
+ process.exitCode = 1;
17101
+ return;
17102
+ }
17103
+ const confirmed = await promptConfirm(
17104
+ scope === "build" ? `Remove the build-time variable ${key} from ${project2}? Your next build will not have it (the current image still does).` : environment === "preview" ? `Remove the preview override for ${key} on ${project2}? Previews will revert to inheriting the production value.` : `Remove ${key} from ${project2}? Its stored value (production and any preview override) is deleted and cannot be recovered.`,
17105
+ io
17106
+ );
17107
+ if (!confirmed) {
17108
+ process.stdout.write(`Left ${key} in place.
17109
+ `);
17110
+ return;
17111
+ }
17112
+ }
17113
+ try {
17114
+ const removed = await removeEnvVar(apiUrl, token, project2, key, scope, environment);
17115
+ const envTag = environment === "preview" ? " (preview override)" : "";
17116
+ process.stdout.write(
17117
+ `Removed ${removed.key}${scope === "build" ? " (build-time)" : envTag} from ${project2}.
17118
+ ` + // The build-scope warning is deliberately bleaker than the runtime one, because the truth is.
17119
+ // A runtime value is gone from the container at the next deploy. A build-time value is
17120
+ // COMPILED INTO the live image and into every bundle already downloaded from it — removing
17121
+ // it stops future builds baking it in and does nothing else. Someone removing a value they
17122
+ // now realise was sensitive must not read "removed" as "contained".
17123
+ (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\`.
17124
+ `)
17125
+ );
17126
+ } catch (err) {
17127
+ reportError(err);
17128
+ }
17129
+ }
17130
+ var CLI_VERSION = true ? "0.3.0" : "0.0.0-dev";
16047
17131
  var program = new Command();
16048
17132
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
16049
17133
  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 +17138,19 @@ program.command("whoami").description("Show the current account").option("--api-
16054
17138
  program.command("logout").description("Clear the stored token").action(() => runLogout());
16055
17139
  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
17140
  "--framework <framework>",
16057
- "react-router | nextjs | vite-spa (auto-detected if omitted)"
17141
+ "react-router | nextjs | vite-spa | node (auto-detected if omitted)"
16058
17142
  ).option(
16059
17143
  "--tenant <slug>",
16060
- "your tenant slug (claimed once on first init \u2014 appears in every deploy URL)"
17144
+ // KRYD-265 / KRYD-188: this used to say "claimed once on first init", which was false — signup
17145
+ // has always assigned a slug, so the flag could only agree with it or 409. It now changes the
17146
+ // slug, and only while nothing derives from it.
17147
+ "your tenant slug \u2014 appears in every deploy URL; changeable until your first project"
16061
17148
  ).option("--api-url <url>", "control-plane API base URL").action((opts) => runInit(opts));
16062
17149
  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)."
17150
+ "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
17151
  ).option(
16065
17152
  "--runtime",
16066
- "stream the live container's runtime stdout/stderr (the running app) instead of build/deploy logs"
17153
+ "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
17154
  ).option(
16068
17155
  "--since <dur>",
16069
17156
  "with --runtime: backfill this window before going live (e.g. 30m, 2h, 1d; max 7d)"
@@ -16075,21 +17162,58 @@ program.command("push [branch]").description(
16075
17162
  ).option("--api-url <url>", "control-plane API base URL").action(
16076
17163
  (branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
16077
17164
  );
16078
- 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 }));
17165
+ 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((project2, opts) => runDeploy({ ...opts, project: project2 }));
16079
17166
  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
- (project, deployment, opts) => runRollback({ ...opts, project, deployment })
17167
+ (project2, deployment, opts) => runRollback({ ...opts, project: project2, deployment })
16081
17168
  );
17169
+ program.command("redeploy [project]").description(
17170
+ "Redeploy the current live commit (no rebuild) to apply config/env changes, and follow it live"
17171
+ ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
17172
+ var project = program.command("project").description("Manage your projects");
17173
+ project.command("rm [project]").description(
17174
+ "Permanently delete a project and everything Kryd created for it (asks you to type its name)"
17175
+ ).option("--yes", "skip the confirmation (for non-interactive use)").option("--api-url <url>", "control-plane API base URL").action((projectArg, opts) => runProjectRemove({ ...opts, project: projectArg }));
16082
17176
  var db = program.command("db").description("Manage project databases");
16083
17177
  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
17178
  "--connection-string <url>",
16085
17179
  "attach an existing database (BYO) by pasting its connection string"
16086
- ).option("--api-url <url>", "control-plane API base URL").action((project, opts) => runDbCreate({ ...opts, project }));
16087
- db.command("detach [project]").description("Tear down the project's database and reclaim its credential").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runDbDetach({ ...opts, project }));
17180
+ ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbCreate({ ...opts, project: project2 }));
17181
+ db.command("detach [project]").description("Tear down the project's database and reclaim its credential").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbDetach({ ...opts, project: project2 }));
16088
17182
  var storage = program.command("storage").description("Manage project object storage");
16089
- 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
- 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 }));
17183
+ 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((project2, opts) => runStorageCreate({ ...opts, project: project2 }));
17184
+ 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((project2, opts) => runStorageDetach({ ...opts, project: project2 }));
17185
+ var env = program.command("env").description("Manage your app's environment variables");
17186
+ env.command("list [project]").description("List the project's environment variables (names and origin \u2014 never values)").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvList({ ...opts, project: project2 }));
17187
+ env.command("set <key> [project]").description(
17188
+ "Set one of your environment variables \u2014 `KEY=VALUE`, or `KEY` with --stdin / a prompt"
17189
+ ).option(
17190
+ "--stdin",
17191
+ "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"
17192
+ ).option(
17193
+ "--build",
17194
+ "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"
17195
+ ).option(
17196
+ "--preview",
17197
+ "set the value for PREVIEW deploys only \u2014 an override that shadows production for previews (without it, a set targets production, which previews inherit)"
17198
+ ).option("--api-url <url>", "control-plane API base URL").action((key, project2, opts) => runEnvSet({ ...opts, spec: key, project: project2 }));
17199
+ 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(
17200
+ "--build",
17201
+ "remove the build-time variable of this name rather than the runtime one (the same name can exist as both)"
17202
+ ).option(
17203
+ "--preview",
17204
+ "remove only the PREVIEW override, reverting previews to inheriting production (without it, the whole key is removed \u2014 production and any preview override)"
17205
+ ).option("--api-url <url>", "control-plane API base URL").action((key, project2, opts) => runEnvRemove({ ...opts, key, project: project2 }));
17206
+ env.command("pull [project]").description(
17207
+ "Write your customer-set environment variables (values included) to a local .env file for development"
17208
+ ).option(
17209
+ "--preview",
17210
+ "pull the values a PREVIEW deploy would see (preview overrides plus inherited production), instead of production"
17211
+ ).option(
17212
+ "--out <path>",
17213
+ "file to write, relative to the project root (default: .env.kryd \u2014 never your hand-maintained .env)"
17214
+ ).option("--force", "overwrite the --out file if it already exists").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvPull({ ...opts, project: project2 }));
16091
17215
  var ai = program.command("ai").description("Manage the app's EU AI gateway");
16092
- 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 }));
17216
+ 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((project2, opts) => runAiEnable({ ...opts, project: project2 }));
16093
17217
  if (import.meta.url === `file://${process.argv[1]}`) {
16094
17218
  program.parseAsync().catch((err) => {
16095
17219
  process.stderr.write(
@@ -16105,14 +17229,21 @@ export {
16105
17229
  runDbCreate,
16106
17230
  runDbDetach,
16107
17231
  runDeploy,
17232
+ runEnvList,
17233
+ runEnvPull,
17234
+ runEnvRemove,
17235
+ runEnvSet,
16108
17236
  runInit,
16109
17237
  runLogin,
16110
17238
  runLogout,
16111
17239
  runLogs,
17240
+ runProjectRemove,
16112
17241
  runPush,
17242
+ runRedeploy,
16113
17243
  runRollback,
16114
17244
  runRuntimeLogs,
16115
17245
  runStorageCreate,
16116
17246
  runStorageDetach,
16117
- runWhoami
17247
+ runWhoami,
17248
+ splitKeyValue
16118
17249
  };