@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.
- package/README.md +3 -2
- package/dist/index.js +1207 -76
- 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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
15633
|
+
return new Promise((resolve2) => {
|
|
15200
15634
|
const onAbort = () => {
|
|
15201
15635
|
clearTimeout(timer);
|
|
15202
|
-
|
|
15636
|
+
resolve2();
|
|
15203
15637
|
};
|
|
15204
15638
|
const timer = setTimeout(() => {
|
|
15205
15639
|
signal?.removeEventListener("abort", onAbort);
|
|
15206
|
-
|
|
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,
|
|
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}
|
|
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 = [
|
|
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,
|
|
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
|
|
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:
|
|
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 ${
|
|
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}" (${
|
|
15545
|
-
Production URL: https://${
|
|
15546
|
-
Preview branches deploy to https://<branch>-<hash>-${
|
|
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
|
|
15616
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
|
15660
|
-
if (!
|
|
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,
|
|
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
|
|
15811
|
-
if (!
|
|
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
|
-
|
|
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
|
|
15841
|
-
if (!
|
|
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:
|
|
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 ${
|
|
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 ${
|
|
16445
|
+
process.stdout.write(`Provisioning a shared database for ${project2}\u2026
|
|
15882
16446
|
`);
|
|
15883
16447
|
const result = await pollResourceUntilTerminal(
|
|
15884
|
-
() => getDatabaseStatus(apiUrl, token,
|
|
16448
|
+
() => getDatabaseStatus(apiUrl, token, project2)
|
|
15885
16449
|
);
|
|
15886
16450
|
reportSettleResult(result, {
|
|
15887
|
-
readyMsg: `Attached shared database ${db2.id} to ${
|
|
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 ${
|
|
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
|
|
15922
|
-
if (!
|
|
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:
|
|
15928
|
-
process.stdout.write(`Provisioning object storage for ${
|
|
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,
|
|
16495
|
+
() => getStorageStatus(apiUrl, token, project2)
|
|
15932
16496
|
);
|
|
15933
16497
|
reportSettleResult(result, {
|
|
15934
|
-
readyMsg: `Created object storage ${bucket.id} for ${
|
|
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 ${
|
|
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
|
|
15970
|
-
if (!
|
|
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,
|
|
15976
|
-
process.stdout.write(`Detaching the database from ${
|
|
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,
|
|
16650
|
+
() => getDatabaseStatus(apiUrl, token, project2)
|
|
15980
16651
|
);
|
|
15981
16652
|
reportDetachResult(result, {
|
|
15982
|
-
goneMsg: `Detached the database from ${
|
|
16653
|
+
goneMsg: `Detached the database from ${project2}.
|
|
15983
16654
|
`,
|
|
15984
16655
|
failedLabel: "Database detach",
|
|
15985
|
-
retryCmd: `kryd db detach ${
|
|
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
|
|
16000
|
-
if (!
|
|
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:
|
|
16676
|
+
const status = await enableAi(apiUrl, token, { projectId: project2 });
|
|
16006
16677
|
process.stdout.write(
|
|
16007
|
-
`AI enabled for ${
|
|
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
|
|
16025
|
-
if (!
|
|
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,
|
|
16031
|
-
process.stdout.write(`Detaching object storage from ${
|
|
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,
|
|
16705
|
+
() => getStorageStatus(apiUrl, token, project2)
|
|
16035
16706
|
);
|
|
16036
16707
|
reportDetachResult(result, {
|
|
16037
|
-
goneMsg: `Detached object storage from ${
|
|
16708
|
+
goneMsg: `Detached object storage from ${project2}.
|
|
16038
16709
|
`,
|
|
16039
16710
|
failedLabel: "Storage detach",
|
|
16040
|
-
retryCmd: `kryd storage detach ${
|
|
16711
|
+
retryCmd: `kryd storage detach ${project2}`
|
|
16041
16712
|
});
|
|
16042
16713
|
} catch (err) {
|
|
16043
16714
|
reportError(err);
|
|
16044
16715
|
}
|
|
16045
16716
|
}
|
|
16046
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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((
|
|
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
|
-
(
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
};
|