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