@kryd/cli 0.4.1 → 0.5.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 +4 -4
- package/dist/index.js +428 -27
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ Requires Node.js ≥ 22 and git (the CLI shells out to git for `kryd init` and `
|
|
|
17
17
|
```sh
|
|
18
18
|
kryd login # sign in via the browser
|
|
19
19
|
kryd init # link this folder to a Kryd project (+ a `kryd` git remote)
|
|
20
|
-
kryd push # push the current branch → build → deploy → live,
|
|
21
|
-
kryd logs #
|
|
20
|
+
kryd push # push the current branch → build → deploy → live, with live progress
|
|
21
|
+
kryd logs # the full build log of the latest deploy (kryd push shows progress, not log lines)
|
|
22
22
|
kryd logs <project> --runtime # tail the live app's stdout/stderr
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -30,9 +30,9 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
|
|
|
30
30
|
|---|---|
|
|
31
31
|
| `kryd login` / `logout` / `whoami` | Authenticate the CLI (browser flow); the token is stored in `~/.kryd`. |
|
|
32
32
|
| `kryd init` | Link the current repo to a Kryd project + register the deploy webhook + add a `kryd` git remote. |
|
|
33
|
-
| `kryd push [branch]` | Push to the `kryd` remote and follow the deploy it triggers (defaults to the current branch). |
|
|
33
|
+
| `kryd push [branch]` | Push to the `kryd` remote and follow the deploy it triggers (defaults to the current branch). Shows which step the deploy is on, how long it has taken and a progress bar; `--logs` streams the full build log instead. |
|
|
34
34
|
| `kryd deploy [project]` | Re-deploy the production-branch HEAD already on the forge — no new commit. |
|
|
35
|
-
| `kryd logs [target]` | Follow a deploy's build/deploy log (`--runtime` tails the live container instead). |
|
|
35
|
+
| `kryd logs [target]` | Follow a deploy's build/deploy log **in full** — this is where the build output lives (`--runtime` tails the live container instead). |
|
|
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. |
|
package/dist/index.js
CHANGED
|
@@ -14855,6 +14855,24 @@ var BUILD_ENV_PUBLIC_WARNING = "\u26A0 Build-time values are compiled into your
|
|
|
14855
14855
|
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.";
|
|
14856
14856
|
|
|
14857
14857
|
// ../../packages/shared-types/dist/index.js
|
|
14858
|
+
var DEPLOY_STATES = [
|
|
14859
|
+
"queued",
|
|
14860
|
+
"building",
|
|
14861
|
+
"deploying",
|
|
14862
|
+
// KRYD-360: the container exists, and the app is now being asked whether it answers. It is a
|
|
14863
|
+
// step of its own because it asks a different question than `deploying` does — that one is
|
|
14864
|
+
// Kryd's infrastructure creating compute, this one is the customer's app booting — and the
|
|
14865
|
+
// two were sharing a budget that the first one spent entirely. See the health gate section
|
|
14866
|
+
// in `packages/deploy-state-machine`.
|
|
14867
|
+
"verifying",
|
|
14868
|
+
"live",
|
|
14869
|
+
"failed",
|
|
14870
|
+
"superseded",
|
|
14871
|
+
// Compute reclaimed by the Story 3.10 cleanup job — the deploy's tenant container (and, for
|
|
14872
|
+
// a preview, its ephemeral host) no longer exists. Reached from live/failed/superseded via
|
|
14873
|
+
// the state machine's `TEARDOWN` event; fully terminal (a torn-down deploy is gone for good).
|
|
14874
|
+
"torn_down"
|
|
14875
|
+
];
|
|
14858
14876
|
var TERMINAL_DEPLOY_STATES = [
|
|
14859
14877
|
"live",
|
|
14860
14878
|
"failed",
|
|
@@ -14876,8 +14894,9 @@ function isTerminalResourceStatus(status) {
|
|
|
14876
14894
|
}
|
|
14877
14895
|
|
|
14878
14896
|
// src/index.ts
|
|
14879
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
14897
|
+
import { existsSync as existsSync3, realpathSync } from "node:fs";
|
|
14880
14898
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
14899
|
+
import { pathToFileURL } from "node:url";
|
|
14881
14900
|
|
|
14882
14901
|
// src/config.ts
|
|
14883
14902
|
import {
|
|
@@ -15046,6 +15065,7 @@ function browserLogin(dashboardUrl, opts = {}) {
|
|
|
15046
15065
|
fn();
|
|
15047
15066
|
};
|
|
15048
15067
|
const server = createServer((req, res) => {
|
|
15068
|
+
res.setHeader("connection", "close");
|
|
15049
15069
|
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
15050
15070
|
if (reqUrl.pathname !== "/callback") {
|
|
15051
15071
|
res.writeHead(404);
|
|
@@ -15057,20 +15077,26 @@ function browserLogin(dashboardUrl, opts = {}) {
|
|
|
15057
15077
|
const gotState = reqUrl.searchParams.get("state");
|
|
15058
15078
|
if (!token || gotState !== state) {
|
|
15059
15079
|
res.writeHead(400, { "content-type": "text/html" });
|
|
15060
|
-
res.end(FAIL_HTML)
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15080
|
+
res.end(FAIL_HTML, () => {
|
|
15081
|
+
settle(() => {
|
|
15082
|
+
shutdown();
|
|
15083
|
+
reject(new Error("Login failed \u2014 the approval didn't match this request. Try `kryd login` again."));
|
|
15084
|
+
});
|
|
15064
15085
|
});
|
|
15065
15086
|
return;
|
|
15066
15087
|
}
|
|
15067
15088
|
res.writeHead(200, { "content-type": "text/html" });
|
|
15068
|
-
res.end(SUCCESS_HTML)
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15089
|
+
res.end(SUCCESS_HTML, () => {
|
|
15090
|
+
settle(() => {
|
|
15091
|
+
shutdown();
|
|
15092
|
+
resolve2({ token, ...apiUrl ? { apiUrl } : {} });
|
|
15093
|
+
});
|
|
15072
15094
|
});
|
|
15073
15095
|
});
|
|
15096
|
+
const shutdown = () => {
|
|
15097
|
+
server.closeAllConnections();
|
|
15098
|
+
server.close();
|
|
15099
|
+
};
|
|
15074
15100
|
server.on("error", (err) => settle(() => reject(err)));
|
|
15075
15101
|
server.listen(0, "127.0.0.1", () => {
|
|
15076
15102
|
const port = server.address().port;
|
|
@@ -15087,7 +15113,7 @@ Waiting for approval\u2026
|
|
|
15087
15113
|
});
|
|
15088
15114
|
setTimeout(
|
|
15089
15115
|
() => settle(() => {
|
|
15090
|
-
|
|
15116
|
+
shutdown();
|
|
15091
15117
|
reject(new Error("Login timed out after 2 minutes."));
|
|
15092
15118
|
}),
|
|
15093
15119
|
timeoutMs
|
|
@@ -15743,6 +15769,20 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
|
|
|
15743
15769
|
}
|
|
15744
15770
|
}
|
|
15745
15771
|
}
|
|
15772
|
+
async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
|
|
15773
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
15774
|
+
const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/log`, {
|
|
15775
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15776
|
+
});
|
|
15777
|
+
if (res.status === 404) return null;
|
|
15778
|
+
if (!res.ok) {
|
|
15779
|
+
throw new ApiError(
|
|
15780
|
+
`Fetching the stored log failed (${res.status})`,
|
|
15781
|
+
await parseEnvelope(res)
|
|
15782
|
+
);
|
|
15783
|
+
}
|
|
15784
|
+
return (await res.json()).log;
|
|
15785
|
+
}
|
|
15746
15786
|
|
|
15747
15787
|
// src/framework.ts
|
|
15748
15788
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -15819,6 +15859,286 @@ function inspectProject(cwd) {
|
|
|
15819
15859
|
return { framework: frameworkFrom(pkg, cwd), name: nameFrom(pkg, cwd) };
|
|
15820
15860
|
}
|
|
15821
15861
|
|
|
15862
|
+
// src/progress.ts
|
|
15863
|
+
var STEPS = DEPLOY_STATES.filter(
|
|
15864
|
+
(s) => !TERMINAL_DEPLOY_STATES.includes(s)
|
|
15865
|
+
);
|
|
15866
|
+
var STEP_FLOOR = {
|
|
15867
|
+
queued: 0,
|
|
15868
|
+
building: 0.04,
|
|
15869
|
+
deploying: 0.42,
|
|
15870
|
+
verifying: 0.88,
|
|
15871
|
+
live: 1,
|
|
15872
|
+
failed: 1,
|
|
15873
|
+
superseded: 1,
|
|
15874
|
+
torn_down: 1
|
|
15875
|
+
};
|
|
15876
|
+
var STEP_TYPICAL_MS = {
|
|
15877
|
+
queued: 3e3,
|
|
15878
|
+
building: 12e4,
|
|
15879
|
+
deploying: 15e4,
|
|
15880
|
+
verifying: 2e4,
|
|
15881
|
+
live: 0,
|
|
15882
|
+
failed: 0,
|
|
15883
|
+
superseded: 0,
|
|
15884
|
+
torn_down: 0
|
|
15885
|
+
};
|
|
15886
|
+
var MAX_ESTIMATED_FILL = 0.95;
|
|
15887
|
+
var TAIL_LINES = 50;
|
|
15888
|
+
var ESTIMATE_SAMPLE = 5;
|
|
15889
|
+
var FRAME_MS = 80;
|
|
15890
|
+
var LABEL_WIDTH = 7;
|
|
15891
|
+
var NAME_WIDTH = 12;
|
|
15892
|
+
function formatClock(ms) {
|
|
15893
|
+
const total = Math.max(0, Math.round(ms / 1e3));
|
|
15894
|
+
const minutes = Math.floor(total / 60);
|
|
15895
|
+
const seconds = total % 60;
|
|
15896
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
15897
|
+
}
|
|
15898
|
+
function estimateTotalMs(deployments) {
|
|
15899
|
+
const durations = deployments.filter((d) => d.status === "live").slice(0, ESTIMATE_SAMPLE).map((d) => Date.parse(d.statusChangedAt) - Date.parse(d.createdAt)).filter((ms) => Number.isFinite(ms) && ms > 0).sort((a, b) => a - b);
|
|
15900
|
+
if (durations.length === 0) return null;
|
|
15901
|
+
return durations[Math.floor(durations.length / 2)] ?? null;
|
|
15902
|
+
}
|
|
15903
|
+
function resolveRenderMode(input) {
|
|
15904
|
+
if (input.want === "lines") return "lines";
|
|
15905
|
+
if (!input.isTTY) return "lines";
|
|
15906
|
+
if (input.env.NO_COLOR !== void 0) return "lines";
|
|
15907
|
+
if (input.env.CI !== void 0 && input.env.CI !== "") return "lines";
|
|
15908
|
+
return "progress";
|
|
15909
|
+
}
|
|
15910
|
+
function resolveColourMode(input) {
|
|
15911
|
+
if (!input.isTTY) return "none";
|
|
15912
|
+
if (input.env.NO_COLOR !== void 0) return "none";
|
|
15913
|
+
const colorterm = input.env.COLORTERM ?? "";
|
|
15914
|
+
if (colorterm === "truecolor" || colorterm === "24bit") return "truecolor";
|
|
15915
|
+
return "basic";
|
|
15916
|
+
}
|
|
15917
|
+
function supportsUnicode(env2) {
|
|
15918
|
+
const locale = env2.LC_ALL ?? env2.LC_CTYPE ?? env2.LANG ?? "";
|
|
15919
|
+
return /utf-?8/i.test(locale);
|
|
15920
|
+
}
|
|
15921
|
+
function usableColumns(columns) {
|
|
15922
|
+
return columns !== void 0 && columns > 0 ? columns : 80;
|
|
15923
|
+
}
|
|
15924
|
+
function defaultProgressIO() {
|
|
15925
|
+
const stdout = process.stdout;
|
|
15926
|
+
const isTTY = Boolean(stdout.isTTY);
|
|
15927
|
+
return {
|
|
15928
|
+
out: stdout,
|
|
15929
|
+
isTTY,
|
|
15930
|
+
now: () => Date.now(),
|
|
15931
|
+
columns: usableColumns(stdout.columns),
|
|
15932
|
+
colour: resolveColourMode({ isTTY, env: process.env }),
|
|
15933
|
+
unicode: supportsUnicode(process.env)
|
|
15934
|
+
};
|
|
15935
|
+
}
|
|
15936
|
+
var TRUECOLOR = {
|
|
15937
|
+
done: "38;2;46;125;87",
|
|
15938
|
+
// --status-live #2e7d57
|
|
15939
|
+
busy: "38;2;217;145;0",
|
|
15940
|
+
// --status-building #d99100
|
|
15941
|
+
bad: "38;2;192;69;60"
|
|
15942
|
+
// --status-failed #c0453c
|
|
15943
|
+
};
|
|
15944
|
+
var BASIC = { done: "32", busy: "33", bad: "31" };
|
|
15945
|
+
var CSI = "\x1B[";
|
|
15946
|
+
var CLEAR_LINE = `${CSI}2K`;
|
|
15947
|
+
var CURSOR_UP = `${CSI}1A`;
|
|
15948
|
+
var RESET = `${CSI}0m`;
|
|
15949
|
+
var BLOCKS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
|
|
15950
|
+
var ELLIPSIS_FRAMES = [" ", ". ", ".. ", "..."];
|
|
15951
|
+
var ELLIPSIS_WIDTH = 3;
|
|
15952
|
+
var ELLIPSIS_MS = 420;
|
|
15953
|
+
function trimEllipsis(line) {
|
|
15954
|
+
return line.replace(/(\.{3}|…)\s*$/u, "");
|
|
15955
|
+
}
|
|
15956
|
+
function createDeployProgress(io) {
|
|
15957
|
+
const unicode = io.unicode ?? true;
|
|
15958
|
+
const columns = usableColumns(io.columns);
|
|
15959
|
+
const startedAt = io.now();
|
|
15960
|
+
const tail = [];
|
|
15961
|
+
let current = null;
|
|
15962
|
+
let currentStartedAt = startedAt;
|
|
15963
|
+
let activity = "";
|
|
15964
|
+
let estimateMs = null;
|
|
15965
|
+
let deploymentId = "";
|
|
15966
|
+
let highWaterFill = 0;
|
|
15967
|
+
let drawnRows = 0;
|
|
15968
|
+
let timer = null;
|
|
15969
|
+
let finished = false;
|
|
15970
|
+
const paint = (role, text) => {
|
|
15971
|
+
if (io.colour === "none") return text;
|
|
15972
|
+
if (role === "muted") return `${CSI}2m${text}${RESET}`;
|
|
15973
|
+
return `${CSI}${io.colour === "truecolor" ? TRUECOLOR[role] : BASIC[role]}m${text}${RESET}`;
|
|
15974
|
+
};
|
|
15975
|
+
const write = (s) => {
|
|
15976
|
+
io.out.write(s);
|
|
15977
|
+
};
|
|
15978
|
+
const clip = (s, max) => {
|
|
15979
|
+
if (max <= 0) return "";
|
|
15980
|
+
if (s.length <= max) return s;
|
|
15981
|
+
return max <= 1 ? s.slice(0, max) : `${s.slice(0, max - 1)}\u2026`;
|
|
15982
|
+
};
|
|
15983
|
+
const row = (word, role, name, right) => {
|
|
15984
|
+
const left = word.padStart(LABEL_WIDTH);
|
|
15985
|
+
const middle = name.padEnd(NAME_WIDTH);
|
|
15986
|
+
const budget = columns - LABEL_WIDTH - 1;
|
|
15987
|
+
const body = clip(`${middle} ${right}`.trimEnd(), Math.max(0, budget));
|
|
15988
|
+
const pad = Math.max(0, budget - body.length);
|
|
15989
|
+
const spaced = pad > 0 && right.length > 0 ? `${middle}${" ".repeat(pad + 1)}${right}` : body;
|
|
15990
|
+
return `${paint(role, left)} ${clip(spaced, Math.max(0, budget))}`;
|
|
15991
|
+
};
|
|
15992
|
+
const eraseAnimated = () => {
|
|
15993
|
+
if (drawnRows === 0) return;
|
|
15994
|
+
write(`\r${CLEAR_LINE}`);
|
|
15995
|
+
for (let i = 1; i < drawnRows; i++) write(`${CURSOR_UP}\r${CLEAR_LINE}`);
|
|
15996
|
+
drawnRows = 0;
|
|
15997
|
+
};
|
|
15998
|
+
const ellipsis = () => {
|
|
15999
|
+
if (activity.length === 0) return "";
|
|
16000
|
+
const frame = Math.floor((io.now() - startedAt) / ELLIPSIS_MS) % ELLIPSIS_FRAMES.length;
|
|
16001
|
+
return ELLIPSIS_FRAMES[frame] ?? "";
|
|
16002
|
+
};
|
|
16003
|
+
const ceilingAfter = (step) => {
|
|
16004
|
+
const next = STEPS[STEPS.indexOf(step) + 1];
|
|
16005
|
+
return next ? STEP_FLOOR[next] : MAX_ESTIMATED_FILL;
|
|
16006
|
+
};
|
|
16007
|
+
const withinStep = (step, elapsedInStep) => {
|
|
16008
|
+
const typical = STEP_TYPICAL_MS[step];
|
|
16009
|
+
if (typical <= 0) return 0;
|
|
16010
|
+
const progress = 1 - Math.exp(-Math.max(0, elapsedInStep) / typical);
|
|
16011
|
+
return STEP_FLOOR[step] + (ceilingAfter(step) - STEP_FLOOR[step]) * progress;
|
|
16012
|
+
};
|
|
16013
|
+
const fill = () => {
|
|
16014
|
+
const now = io.now();
|
|
16015
|
+
const stepwise = current ? withinStep(current, now - currentStartedAt) : 0;
|
|
16016
|
+
const estimated = estimateMs !== null && estimateMs > 0 ? (now - startedAt) / estimateMs : 0;
|
|
16017
|
+
highWaterFill = Math.max(highWaterFill, stepwise, estimated);
|
|
16018
|
+
return Math.min(highWaterFill, MAX_ESTIMATED_FILL);
|
|
16019
|
+
};
|
|
16020
|
+
const bar = (fraction, width) => {
|
|
16021
|
+
if (width <= 0) return "";
|
|
16022
|
+
if (!unicode) {
|
|
16023
|
+
const filled = Math.max(0, Math.min(width, Math.round(fraction * width)));
|
|
16024
|
+
const head = filled > 0 && filled < width ? ">" : "";
|
|
16025
|
+
return `${"=".repeat(Math.max(0, filled - head.length))}${head}${" ".repeat(width - filled)}`;
|
|
16026
|
+
}
|
|
16027
|
+
const exact = Math.max(0, Math.min(width, fraction * width));
|
|
16028
|
+
const full = Math.floor(exact);
|
|
16029
|
+
const partial2 = BLOCKS[Math.floor((exact - full) * 8)] ?? "";
|
|
16030
|
+
const used = full + (partial2 ? 1 : 0);
|
|
16031
|
+
return `${"\u2588".repeat(full)}${partial2}${"\u2591".repeat(Math.max(0, width - used))}`;
|
|
16032
|
+
};
|
|
16033
|
+
const draw = () => {
|
|
16034
|
+
if (finished || current === null || !io.isTTY) return;
|
|
16035
|
+
eraseAnimated();
|
|
16036
|
+
const elapsed = formatClock(io.now() - startedAt);
|
|
16037
|
+
const counter = estimateMs !== null ? `${elapsed} / ~${formatClock(estimateMs)}` : elapsed;
|
|
16038
|
+
const prefixWidth = LABEL_WIDTH + 1 + NAME_WIDTH + 1;
|
|
16039
|
+
const barWidth = columns - prefixWidth - counter.length - 3;
|
|
16040
|
+
const barPart = current !== "queued" && barWidth >= 8 ? `[${bar(fill(), barWidth)}] ` : "";
|
|
16041
|
+
write(`\r${CLEAR_LINE}${row("Loading", "busy", current, `${barPart}${counter}`)}
|
|
16042
|
+
`);
|
|
16043
|
+
const indent = " ".repeat(LABEL_WIDTH + 1);
|
|
16044
|
+
const room = Math.max(0, columns - indent.length - 2 - ELLIPSIS_WIDTH);
|
|
16045
|
+
write(
|
|
16046
|
+
`\r${CLEAR_LINE}${paint("muted", `${indent}\u2514 ${clip(trimEllipsis(activity), room)}${ellipsis()}`)}`
|
|
16047
|
+
);
|
|
16048
|
+
drawnRows = 2;
|
|
16049
|
+
};
|
|
16050
|
+
const startTimer = () => {
|
|
16051
|
+
if (timer !== null || !io.isTTY) return;
|
|
16052
|
+
timer = setInterval(draw, FRAME_MS);
|
|
16053
|
+
timer.unref?.();
|
|
16054
|
+
};
|
|
16055
|
+
const stopTimer = () => {
|
|
16056
|
+
if (timer === null) return;
|
|
16057
|
+
clearInterval(timer);
|
|
16058
|
+
timer = null;
|
|
16059
|
+
};
|
|
16060
|
+
const closeCurrent = (role) => {
|
|
16061
|
+
if (current === null) return;
|
|
16062
|
+
eraseAnimated();
|
|
16063
|
+
const step = { status: current, ms: io.now() - currentStartedAt, role };
|
|
16064
|
+
const word = role === "bad" ? "Failed" : "Success";
|
|
16065
|
+
write(`${row(word, role, step.status, formatClock(step.ms))}
|
|
16066
|
+
`);
|
|
16067
|
+
current = null;
|
|
16068
|
+
};
|
|
16069
|
+
return {
|
|
16070
|
+
onLog(line) {
|
|
16071
|
+
tail.push(line);
|
|
16072
|
+
if (tail.length > TAIL_LINES) tail.shift();
|
|
16073
|
+
activity = line;
|
|
16074
|
+
draw();
|
|
16075
|
+
},
|
|
16076
|
+
onStatus(event) {
|
|
16077
|
+
if (TERMINAL_DEPLOY_STATES.includes(event.status)) return;
|
|
16078
|
+
if (event.status === current) return;
|
|
16079
|
+
const incoming = STEPS.indexOf(event.status);
|
|
16080
|
+
const running = current === null ? -1 : STEPS.indexOf(current);
|
|
16081
|
+
if (incoming !== -1 && incoming < running) return;
|
|
16082
|
+
closeCurrent("done");
|
|
16083
|
+
current = event.status;
|
|
16084
|
+
currentStartedAt = io.now();
|
|
16085
|
+
activity = "";
|
|
16086
|
+
startTimer();
|
|
16087
|
+
draw();
|
|
16088
|
+
},
|
|
16089
|
+
finish(terminal, outcome) {
|
|
16090
|
+
stopTimer();
|
|
16091
|
+
const failed = terminal === "failed";
|
|
16092
|
+
closeCurrent(failed ? "bad" : "done");
|
|
16093
|
+
finished = true;
|
|
16094
|
+
const total = formatClock(io.now() - startedAt);
|
|
16095
|
+
if (failed) {
|
|
16096
|
+
write("\n");
|
|
16097
|
+
if (tail.length > 0) {
|
|
16098
|
+
write(`${paint("muted", `--- last ${tail.length} log line${tail.length === 1 ? "" : "s"} ---`)}
|
|
16099
|
+
`);
|
|
16100
|
+
for (const line of tail) write(`${line}
|
|
16101
|
+
`);
|
|
16102
|
+
write(`${paint("muted", "---")}
|
|
16103
|
+
|
|
16104
|
+
`);
|
|
16105
|
+
} else {
|
|
16106
|
+
write(`${paint("muted", "No log lines arrived before this failed.")}
|
|
16107
|
+
|
|
16108
|
+
`);
|
|
16109
|
+
}
|
|
16110
|
+
write(`${paint("bad", "Failed".padStart(LABEL_WIDTH))} ${outcome.failure?.reason ?? "the deploy failed"}
|
|
16111
|
+
`);
|
|
16112
|
+
if (deploymentId) {
|
|
16113
|
+
write(`${paint("muted", `${" ".repeat(LABEL_WIDTH + 1)}Full log: kryd logs ${deploymentId}`)}
|
|
16114
|
+
`);
|
|
16115
|
+
}
|
|
16116
|
+
return;
|
|
16117
|
+
}
|
|
16118
|
+
if (terminal === "live") {
|
|
16119
|
+
write(`${row("Live", "done", outcome.url ?? "(no url reported)", total)}
|
|
16120
|
+
`);
|
|
16121
|
+
return;
|
|
16122
|
+
}
|
|
16123
|
+
const text = terminal === "superseded" ? "superseded by a newer push" : "torn down \u2014 this deploy's compute was reclaimed by cleanup";
|
|
16124
|
+
write(`${row("Done", "muted", text, "")}
|
|
16125
|
+
`);
|
|
16126
|
+
},
|
|
16127
|
+
setEstimate(ms) {
|
|
16128
|
+
estimateMs = ms !== null && ms > 0 ? ms : null;
|
|
16129
|
+
},
|
|
16130
|
+
setDeploymentId(id) {
|
|
16131
|
+
deploymentId = id;
|
|
16132
|
+
},
|
|
16133
|
+
tick() {
|
|
16134
|
+
draw();
|
|
16135
|
+
},
|
|
16136
|
+
isAnimating() {
|
|
16137
|
+
return timer !== null;
|
|
16138
|
+
}
|
|
16139
|
+
};
|
|
16140
|
+
}
|
|
16141
|
+
|
|
15822
16142
|
// src/input.ts
|
|
15823
16143
|
import { createInterface } from "node:readline";
|
|
15824
16144
|
import { Writable } from "node:stream";
|
|
@@ -16165,19 +16485,82 @@ function formatStatus(event) {
|
|
|
16165
16485
|
return "\u2298 superseded by a newer push";
|
|
16166
16486
|
case "torn_down":
|
|
16167
16487
|
return "\u232B torn down \u2014 this deploy's compute was reclaimed by cleanup";
|
|
16488
|
+
// KRYD-360: the container exists and the app is being asked whether it answers. Named, rather
|
|
16489
|
+
// than falling through to the generic `→ verifying`, because this is the step where a customer's
|
|
16490
|
+
// own app is on the clock and the next line they see may be about their crash.
|
|
16491
|
+
case "verifying":
|
|
16492
|
+
return "\u2192 verifying the app answers\u2026";
|
|
16168
16493
|
default:
|
|
16169
16494
|
return `\u2192 ${event.status}`;
|
|
16170
16495
|
}
|
|
16171
16496
|
}
|
|
16172
|
-
async function followDeploy(apiUrl, token, deploymentId) {
|
|
16173
|
-
|
|
16174
|
-
|
|
16175
|
-
|
|
16176
|
-
|
|
16177
|
-
|
|
16497
|
+
async function followDeploy(apiUrl, token, deploymentId, opts = {}) {
|
|
16498
|
+
let streamedLines = 0;
|
|
16499
|
+
const mode = resolveRenderMode({
|
|
16500
|
+
want: opts.render ?? "lines",
|
|
16501
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
16502
|
+
env: process.env
|
|
16178
16503
|
});
|
|
16504
|
+
let terminal;
|
|
16505
|
+
if (mode === "lines") {
|
|
16506
|
+
terminal = await streamDeploy(apiUrl, token, deploymentId, {
|
|
16507
|
+
onLog: (line) => {
|
|
16508
|
+
streamedLines++;
|
|
16509
|
+
process.stdout.write(`${line}
|
|
16510
|
+
`);
|
|
16511
|
+
},
|
|
16512
|
+
onStatus: (event) => process.stdout.write(`${formatStatus(event)}
|
|
16513
|
+
`)
|
|
16514
|
+
});
|
|
16515
|
+
} else {
|
|
16516
|
+
const progress = createDeployProgress(defaultProgressIO());
|
|
16517
|
+
progress.setDeploymentId(deploymentId);
|
|
16518
|
+
progress.setEstimate(estimateTotalMs(opts.estimateFrom ?? []));
|
|
16519
|
+
let last;
|
|
16520
|
+
try {
|
|
16521
|
+
terminal = await streamDeploy(apiUrl, token, deploymentId, {
|
|
16522
|
+
onLog: (line) => {
|
|
16523
|
+
streamedLines++;
|
|
16524
|
+
progress.onLog(line);
|
|
16525
|
+
},
|
|
16526
|
+
onStatus: (event) => {
|
|
16527
|
+
last = event;
|
|
16528
|
+
progress.onStatus(event);
|
|
16529
|
+
}
|
|
16530
|
+
});
|
|
16531
|
+
} catch (err) {
|
|
16532
|
+
progress.finish("failed", { failure: { code: "streamFailed", reason: "the log stream ended" } });
|
|
16533
|
+
throw err;
|
|
16534
|
+
}
|
|
16535
|
+
progress.finish(terminal, {
|
|
16536
|
+
url: last?.url,
|
|
16537
|
+
failure: last?.failure
|
|
16538
|
+
});
|
|
16539
|
+
}
|
|
16540
|
+
if (streamedLines === 0) {
|
|
16541
|
+
await replayStoredLog(apiUrl, token, deploymentId);
|
|
16542
|
+
}
|
|
16179
16543
|
if (terminal !== "live" && terminal !== "torn_down") process.exitCode = 1;
|
|
16180
16544
|
}
|
|
16545
|
+
async function replayStoredLog(apiUrl, token, deploymentId) {
|
|
16546
|
+
try {
|
|
16547
|
+
const log = await fetchDeployLog(apiUrl, token, deploymentId);
|
|
16548
|
+
if (log === null) {
|
|
16549
|
+
process.stderr.write(
|
|
16550
|
+
`No stored log for ${deploymentId} \u2014 it finished before its log was persisted.
|
|
16551
|
+
`
|
|
16552
|
+
);
|
|
16553
|
+
return;
|
|
16554
|
+
}
|
|
16555
|
+
process.stdout.write(log.endsWith("\n") ? log : `${log}
|
|
16556
|
+
`);
|
|
16557
|
+
} catch (err) {
|
|
16558
|
+
process.stderr.write(
|
|
16559
|
+
`Could not read the stored log (${err instanceof Error ? err.message : String(err)}). It is kept for later viewing \u2014 try \`kryd logs ${deploymentId}\` again.
|
|
16560
|
+
`
|
|
16561
|
+
);
|
|
16562
|
+
}
|
|
16563
|
+
}
|
|
16181
16564
|
async function runLogs(opts) {
|
|
16182
16565
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
16183
16566
|
const token = loadConfig().token;
|
|
@@ -16274,11 +16657,15 @@ async function runDeploy(opts) {
|
|
|
16274
16657
|
const deploymentId = await triggerDeploy(apiUrl, token, project2);
|
|
16275
16658
|
process.stdout.write(`Triggered deploy ${deploymentId}
|
|
16276
16659
|
`);
|
|
16277
|
-
await followDeploy(apiUrl, token, deploymentId);
|
|
16660
|
+
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16278
16661
|
} catch (err) {
|
|
16279
16662
|
reportError(err);
|
|
16280
16663
|
}
|
|
16281
16664
|
}
|
|
16665
|
+
function renderFor(logs) {
|
|
16666
|
+
return logs ? "lines" : "progress";
|
|
16667
|
+
}
|
|
16668
|
+
var LOGS_FLAG_HELP = "stream the full build log line by line instead of the progress display";
|
|
16282
16669
|
async function runPush(opts) {
|
|
16283
16670
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
16284
16671
|
const token = loadConfig().token;
|
|
@@ -16363,8 +16750,10 @@ async function runPush(opts) {
|
|
|
16363
16750
|
}
|
|
16364
16751
|
const fetchDeployments = () => listDeployments(apiUrl, token, projectId ?? void 0);
|
|
16365
16752
|
let known = null;
|
|
16753
|
+
let recent = [];
|
|
16366
16754
|
try {
|
|
16367
|
-
|
|
16755
|
+
recent = await fetchDeployments();
|
|
16756
|
+
known = findDeploymentsForCommit(recent, commitSha, branch);
|
|
16368
16757
|
} catch {
|
|
16369
16758
|
known = null;
|
|
16370
16759
|
}
|
|
@@ -16403,7 +16792,10 @@ async function runPush(opts) {
|
|
|
16403
16792
|
}
|
|
16404
16793
|
return;
|
|
16405
16794
|
}
|
|
16406
|
-
await followDeploy(apiUrl, token, deployment.id
|
|
16795
|
+
await followDeploy(apiUrl, token, deployment.id, {
|
|
16796
|
+
render: renderFor(opts.logs),
|
|
16797
|
+
estimateFrom: recent
|
|
16798
|
+
});
|
|
16407
16799
|
} catch (err) {
|
|
16408
16800
|
reportError(err);
|
|
16409
16801
|
}
|
|
@@ -16433,7 +16825,7 @@ async function runRollback(opts) {
|
|
|
16433
16825
|
\u26A0 ${note}
|
|
16434
16826
|
`
|
|
16435
16827
|
);
|
|
16436
|
-
await followDeploy(apiUrl, token, deploymentId);
|
|
16828
|
+
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16437
16829
|
} catch (err) {
|
|
16438
16830
|
reportError(err);
|
|
16439
16831
|
}
|
|
@@ -16462,7 +16854,7 @@ async function runRedeploy(opts) {
|
|
|
16462
16854
|
${note}
|
|
16463
16855
|
`
|
|
16464
16856
|
);
|
|
16465
|
-
await followDeploy(apiUrl, token, deploymentId);
|
|
16857
|
+
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16466
16858
|
} catch (err) {
|
|
16467
16859
|
reportError(err);
|
|
16468
16860
|
}
|
|
@@ -17274,7 +17666,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
17274
17666
|
reportError(err);
|
|
17275
17667
|
}
|
|
17276
17668
|
}
|
|
17277
|
-
var CLI_VERSION = true ? "0.
|
|
17669
|
+
var CLI_VERSION = true ? "0.5.1" : "0.0.0-dev";
|
|
17278
17670
|
var program = new Command();
|
|
17279
17671
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
17280
17672
|
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(
|
|
@@ -17310,16 +17702,16 @@ program.command("logs [target]").description(
|
|
|
17310
17702
|
);
|
|
17311
17703
|
program.command("push [branch]").description(
|
|
17312
17704
|
"Push to the kryd remote and follow the deploy it triggers (defaults to the current branch)"
|
|
17313
|
-
).option("--api-url <url>", "control-plane API base URL").action(
|
|
17705
|
+
).option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action(
|
|
17314
17706
|
(branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
|
|
17315
17707
|
);
|
|
17316
|
-
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 }));
|
|
17317
|
-
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(
|
|
17708
|
+
program.command("deploy [project]").description("Trigger a deploy of the project's production branch and follow it live").option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDeploy({ ...opts, project: project2 }));
|
|
17709
|
+
program.command("rollback [project] [deployment]").description("Roll back to a previous successful deploy (no rebuild) and follow it live").option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action(
|
|
17318
17710
|
(project2, deployment, opts) => runRollback({ ...opts, project: project2, deployment })
|
|
17319
17711
|
);
|
|
17320
17712
|
program.command("redeploy [project]").description(
|
|
17321
17713
|
"Redeploy the current live commit (no rebuild) to apply config/env changes, and follow it live"
|
|
17322
|
-
).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
|
|
17714
|
+
).option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
|
|
17323
17715
|
var project = program.command("project").description("Manage your projects");
|
|
17324
17716
|
project.command("list").description("List your projects \u2014 name, subdomain, status and id").option("--api-url <url>", "control-plane API base URL").action((opts) => runProjectList(opts));
|
|
17325
17717
|
project.command("rm [project]").description(
|
|
@@ -17366,7 +17758,16 @@ env.command("pull [project]").description(
|
|
|
17366
17758
|
).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 }));
|
|
17367
17759
|
var ai = program.command("ai").description("Manage the app's EU AI gateway");
|
|
17368
17760
|
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 }));
|
|
17369
|
-
|
|
17761
|
+
function invokedDirectly() {
|
|
17762
|
+
const entry = process.argv[1];
|
|
17763
|
+
if (!entry) return false;
|
|
17764
|
+
try {
|
|
17765
|
+
return import.meta.url === pathToFileURL(realpathSync(entry)).href;
|
|
17766
|
+
} catch {
|
|
17767
|
+
return false;
|
|
17768
|
+
}
|
|
17769
|
+
}
|
|
17770
|
+
if (invokedDirectly()) {
|
|
17370
17771
|
program.parseAsync().catch((err) => {
|
|
17371
17772
|
process.stderr.write(
|
|
17372
17773
|
`${err instanceof Error ? err.message : String(err)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kryd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
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",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
59
|
"lint": "eslint src",
|
|
60
60
|
"test": "vitest run",
|
|
61
|
+
"smoke": "bash scripts/smoke-package.sh",
|
|
61
62
|
"dev": "tsx src/index.ts"
|
|
62
63
|
}
|
|
63
64
|
}
|