@kryd/cli 0.4.0 → 0.5.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 +4 -4
- package/dist/index.js +470 -24
- package/package.json +1 -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
|
@@ -14666,6 +14666,23 @@ function date4(params) {
|
|
|
14666
14666
|
config(en_default());
|
|
14667
14667
|
|
|
14668
14668
|
// ../../packages/shared-types/dist/usage.js
|
|
14669
|
+
var USAGE_OUTCOME = {
|
|
14670
|
+
ok: "ok",
|
|
14671
|
+
/** Refused by the request-rate limiter. */
|
|
14672
|
+
throttledRate: "throttled_rate",
|
|
14673
|
+
/** Refused by the daily token cap. */
|
|
14674
|
+
throttledCap: "throttled_cap"
|
|
14675
|
+
};
|
|
14676
|
+
var USAGE_OUTCOMES = [
|
|
14677
|
+
USAGE_OUTCOME.ok,
|
|
14678
|
+
USAGE_OUTCOME.throttledRate,
|
|
14679
|
+
USAGE_OUTCOME.throttledCap
|
|
14680
|
+
];
|
|
14681
|
+
var STORAGE_FRESHNESS_MS = 3 * 60 * 60 * 1e3;
|
|
14682
|
+
var USAGE_THROTTLED_OUTCOMES = [
|
|
14683
|
+
USAGE_OUTCOME.throttledRate,
|
|
14684
|
+
USAGE_OUTCOME.throttledCap
|
|
14685
|
+
];
|
|
14669
14686
|
var usageEventSchema = external_exports.object({
|
|
14670
14687
|
// Gateway-assigned id (stable across POST retries) → the ingest inserts `onConflictDoNothing` on
|
|
14671
14688
|
// it, so a re-sent batch after a lost ACK never double-counts (KRYD-35 review).
|
|
@@ -14688,7 +14705,7 @@ var usageEventSchema = external_exports.object({
|
|
|
14688
14705
|
// fail the whole batch — so one malformed outcome can't discard up to 999 valid usage events in the
|
|
14689
14706
|
// same POST (the batch-loss failure mode the FK-free `account_id` decision already guards, KRYD-35).
|
|
14690
14707
|
// Because only in-set values are ever persisted, the DB column needs no CHECK constraint.
|
|
14691
|
-
outcome: external_exports.enum(
|
|
14708
|
+
outcome: external_exports.enum(USAGE_OUTCOMES).default(USAGE_OUTCOME.ok).catch(USAGE_OUTCOME.ok)
|
|
14692
14709
|
});
|
|
14693
14710
|
var usageIngestBatchSchema = external_exports.object({
|
|
14694
14711
|
events: external_exports.array(usageEventSchema).min(1).max(1e3)
|
|
@@ -14838,6 +14855,24 @@ var BUILD_ENV_PUBLIC_WARNING = "\u26A0 Build-time values are compiled into your
|
|
|
14838
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.";
|
|
14839
14856
|
|
|
14840
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
|
+
];
|
|
14841
14876
|
var TERMINAL_DEPLOY_STATES = [
|
|
14842
14877
|
"live",
|
|
14843
14878
|
"failed",
|
|
@@ -15726,6 +15761,20 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
|
|
|
15726
15761
|
}
|
|
15727
15762
|
}
|
|
15728
15763
|
}
|
|
15764
|
+
async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
|
|
15765
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
15766
|
+
const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/log`, {
|
|
15767
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15768
|
+
});
|
|
15769
|
+
if (res.status === 404) return null;
|
|
15770
|
+
if (!res.ok) {
|
|
15771
|
+
throw new ApiError(
|
|
15772
|
+
`Fetching the stored log failed (${res.status})`,
|
|
15773
|
+
await parseEnvelope(res)
|
|
15774
|
+
);
|
|
15775
|
+
}
|
|
15776
|
+
return (await res.json()).log;
|
|
15777
|
+
}
|
|
15729
15778
|
|
|
15730
15779
|
// src/framework.ts
|
|
15731
15780
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -15750,6 +15799,29 @@ var VITE_META_FRAMEWORKS = [
|
|
|
15750
15799
|
"vike"
|
|
15751
15800
|
];
|
|
15752
15801
|
var SERVER_FRAMEWORKS = ["hono", "express", "fastify", "koa"];
|
|
15802
|
+
var NON_NODE_START_TOOLS = [
|
|
15803
|
+
"vite",
|
|
15804
|
+
"next",
|
|
15805
|
+
"astro",
|
|
15806
|
+
"nuxt",
|
|
15807
|
+
"remix",
|
|
15808
|
+
"react-router",
|
|
15809
|
+
"svelte",
|
|
15810
|
+
"solid-start",
|
|
15811
|
+
"qwik",
|
|
15812
|
+
"vike",
|
|
15813
|
+
"parcel",
|
|
15814
|
+
"webpack",
|
|
15815
|
+
"rollup",
|
|
15816
|
+
"serve",
|
|
15817
|
+
"http-server"
|
|
15818
|
+
];
|
|
15819
|
+
function startScriptLooksLikePlainNode(start) {
|
|
15820
|
+
const s = start.toLowerCase();
|
|
15821
|
+
return !NON_NODE_START_TOOLS.some(
|
|
15822
|
+
(tool) => new RegExp(`(^|[\\s/"'\`=])${tool}([\\s@/"'\`]|$)`).test(s)
|
|
15823
|
+
);
|
|
15824
|
+
}
|
|
15753
15825
|
function readPackageJson(cwd) {
|
|
15754
15826
|
try {
|
|
15755
15827
|
return JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
|
|
@@ -15767,6 +15839,8 @@ function frameworkFrom(pkg, cwd) {
|
|
|
15767
15839
|
if (VITE_META_FRAMEWORKS.some(has)) return null;
|
|
15768
15840
|
if (SERVER_FRAMEWORKS.some(has)) return "node";
|
|
15769
15841
|
if (has("vite")) return "vite-spa";
|
|
15842
|
+
const start = pkg?.scripts?.start?.trim();
|
|
15843
|
+
if (start && startScriptLooksLikePlainNode(start)) return "node";
|
|
15770
15844
|
return null;
|
|
15771
15845
|
}
|
|
15772
15846
|
function nameFrom(pkg, cwd) {
|
|
@@ -15777,6 +15851,286 @@ function inspectProject(cwd) {
|
|
|
15777
15851
|
return { framework: frameworkFrom(pkg, cwd), name: nameFrom(pkg, cwd) };
|
|
15778
15852
|
}
|
|
15779
15853
|
|
|
15854
|
+
// src/progress.ts
|
|
15855
|
+
var STEPS = DEPLOY_STATES.filter(
|
|
15856
|
+
(s) => !TERMINAL_DEPLOY_STATES.includes(s)
|
|
15857
|
+
);
|
|
15858
|
+
var STEP_FLOOR = {
|
|
15859
|
+
queued: 0,
|
|
15860
|
+
building: 0.04,
|
|
15861
|
+
deploying: 0.42,
|
|
15862
|
+
verifying: 0.88,
|
|
15863
|
+
live: 1,
|
|
15864
|
+
failed: 1,
|
|
15865
|
+
superseded: 1,
|
|
15866
|
+
torn_down: 1
|
|
15867
|
+
};
|
|
15868
|
+
var STEP_TYPICAL_MS = {
|
|
15869
|
+
queued: 3e3,
|
|
15870
|
+
building: 12e4,
|
|
15871
|
+
deploying: 15e4,
|
|
15872
|
+
verifying: 2e4,
|
|
15873
|
+
live: 0,
|
|
15874
|
+
failed: 0,
|
|
15875
|
+
superseded: 0,
|
|
15876
|
+
torn_down: 0
|
|
15877
|
+
};
|
|
15878
|
+
var MAX_ESTIMATED_FILL = 0.95;
|
|
15879
|
+
var TAIL_LINES = 50;
|
|
15880
|
+
var ESTIMATE_SAMPLE = 5;
|
|
15881
|
+
var FRAME_MS = 80;
|
|
15882
|
+
var LABEL_WIDTH = 7;
|
|
15883
|
+
var NAME_WIDTH = 12;
|
|
15884
|
+
function formatClock(ms) {
|
|
15885
|
+
const total = Math.max(0, Math.round(ms / 1e3));
|
|
15886
|
+
const minutes = Math.floor(total / 60);
|
|
15887
|
+
const seconds = total % 60;
|
|
15888
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
15889
|
+
}
|
|
15890
|
+
function estimateTotalMs(deployments) {
|
|
15891
|
+
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);
|
|
15892
|
+
if (durations.length === 0) return null;
|
|
15893
|
+
return durations[Math.floor(durations.length / 2)] ?? null;
|
|
15894
|
+
}
|
|
15895
|
+
function resolveRenderMode(input) {
|
|
15896
|
+
if (input.want === "lines") return "lines";
|
|
15897
|
+
if (!input.isTTY) return "lines";
|
|
15898
|
+
if (input.env.NO_COLOR !== void 0) return "lines";
|
|
15899
|
+
if (input.env.CI !== void 0 && input.env.CI !== "") return "lines";
|
|
15900
|
+
return "progress";
|
|
15901
|
+
}
|
|
15902
|
+
function resolveColourMode(input) {
|
|
15903
|
+
if (!input.isTTY) return "none";
|
|
15904
|
+
if (input.env.NO_COLOR !== void 0) return "none";
|
|
15905
|
+
const colorterm = input.env.COLORTERM ?? "";
|
|
15906
|
+
if (colorterm === "truecolor" || colorterm === "24bit") return "truecolor";
|
|
15907
|
+
return "basic";
|
|
15908
|
+
}
|
|
15909
|
+
function supportsUnicode(env2) {
|
|
15910
|
+
const locale = env2.LC_ALL ?? env2.LC_CTYPE ?? env2.LANG ?? "";
|
|
15911
|
+
return /utf-?8/i.test(locale);
|
|
15912
|
+
}
|
|
15913
|
+
function usableColumns(columns) {
|
|
15914
|
+
return columns !== void 0 && columns > 0 ? columns : 80;
|
|
15915
|
+
}
|
|
15916
|
+
function defaultProgressIO() {
|
|
15917
|
+
const stdout = process.stdout;
|
|
15918
|
+
const isTTY = Boolean(stdout.isTTY);
|
|
15919
|
+
return {
|
|
15920
|
+
out: stdout,
|
|
15921
|
+
isTTY,
|
|
15922
|
+
now: () => Date.now(),
|
|
15923
|
+
columns: usableColumns(stdout.columns),
|
|
15924
|
+
colour: resolveColourMode({ isTTY, env: process.env }),
|
|
15925
|
+
unicode: supportsUnicode(process.env)
|
|
15926
|
+
};
|
|
15927
|
+
}
|
|
15928
|
+
var TRUECOLOR = {
|
|
15929
|
+
done: "38;2;46;125;87",
|
|
15930
|
+
// --status-live #2e7d57
|
|
15931
|
+
busy: "38;2;217;145;0",
|
|
15932
|
+
// --status-building #d99100
|
|
15933
|
+
bad: "38;2;192;69;60"
|
|
15934
|
+
// --status-failed #c0453c
|
|
15935
|
+
};
|
|
15936
|
+
var BASIC = { done: "32", busy: "33", bad: "31" };
|
|
15937
|
+
var CSI = "\x1B[";
|
|
15938
|
+
var CLEAR_LINE = `${CSI}2K`;
|
|
15939
|
+
var CURSOR_UP = `${CSI}1A`;
|
|
15940
|
+
var RESET = `${CSI}0m`;
|
|
15941
|
+
var BLOCKS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
|
|
15942
|
+
var ELLIPSIS_FRAMES = [" ", ". ", ".. ", "..."];
|
|
15943
|
+
var ELLIPSIS_WIDTH = 3;
|
|
15944
|
+
var ELLIPSIS_MS = 420;
|
|
15945
|
+
function trimEllipsis(line) {
|
|
15946
|
+
return line.replace(/(\.{3}|…)\s*$/u, "");
|
|
15947
|
+
}
|
|
15948
|
+
function createDeployProgress(io) {
|
|
15949
|
+
const unicode = io.unicode ?? true;
|
|
15950
|
+
const columns = usableColumns(io.columns);
|
|
15951
|
+
const startedAt = io.now();
|
|
15952
|
+
const tail = [];
|
|
15953
|
+
let current = null;
|
|
15954
|
+
let currentStartedAt = startedAt;
|
|
15955
|
+
let activity = "";
|
|
15956
|
+
let estimateMs = null;
|
|
15957
|
+
let deploymentId = "";
|
|
15958
|
+
let highWaterFill = 0;
|
|
15959
|
+
let drawnRows = 0;
|
|
15960
|
+
let timer = null;
|
|
15961
|
+
let finished = false;
|
|
15962
|
+
const paint = (role, text) => {
|
|
15963
|
+
if (io.colour === "none") return text;
|
|
15964
|
+
if (role === "muted") return `${CSI}2m${text}${RESET}`;
|
|
15965
|
+
return `${CSI}${io.colour === "truecolor" ? TRUECOLOR[role] : BASIC[role]}m${text}${RESET}`;
|
|
15966
|
+
};
|
|
15967
|
+
const write = (s) => {
|
|
15968
|
+
io.out.write(s);
|
|
15969
|
+
};
|
|
15970
|
+
const clip = (s, max) => {
|
|
15971
|
+
if (max <= 0) return "";
|
|
15972
|
+
if (s.length <= max) return s;
|
|
15973
|
+
return max <= 1 ? s.slice(0, max) : `${s.slice(0, max - 1)}\u2026`;
|
|
15974
|
+
};
|
|
15975
|
+
const row = (word, role, name, right) => {
|
|
15976
|
+
const left = word.padStart(LABEL_WIDTH);
|
|
15977
|
+
const middle = name.padEnd(NAME_WIDTH);
|
|
15978
|
+
const budget = columns - LABEL_WIDTH - 1;
|
|
15979
|
+
const body = clip(`${middle} ${right}`.trimEnd(), Math.max(0, budget));
|
|
15980
|
+
const pad = Math.max(0, budget - body.length);
|
|
15981
|
+
const spaced = pad > 0 && right.length > 0 ? `${middle}${" ".repeat(pad + 1)}${right}` : body;
|
|
15982
|
+
return `${paint(role, left)} ${clip(spaced, Math.max(0, budget))}`;
|
|
15983
|
+
};
|
|
15984
|
+
const eraseAnimated = () => {
|
|
15985
|
+
if (drawnRows === 0) return;
|
|
15986
|
+
write(`\r${CLEAR_LINE}`);
|
|
15987
|
+
for (let i = 1; i < drawnRows; i++) write(`${CURSOR_UP}\r${CLEAR_LINE}`);
|
|
15988
|
+
drawnRows = 0;
|
|
15989
|
+
};
|
|
15990
|
+
const ellipsis = () => {
|
|
15991
|
+
if (activity.length === 0) return "";
|
|
15992
|
+
const frame = Math.floor((io.now() - startedAt) / ELLIPSIS_MS) % ELLIPSIS_FRAMES.length;
|
|
15993
|
+
return ELLIPSIS_FRAMES[frame] ?? "";
|
|
15994
|
+
};
|
|
15995
|
+
const ceilingAfter = (step) => {
|
|
15996
|
+
const next = STEPS[STEPS.indexOf(step) + 1];
|
|
15997
|
+
return next ? STEP_FLOOR[next] : MAX_ESTIMATED_FILL;
|
|
15998
|
+
};
|
|
15999
|
+
const withinStep = (step, elapsedInStep) => {
|
|
16000
|
+
const typical = STEP_TYPICAL_MS[step];
|
|
16001
|
+
if (typical <= 0) return 0;
|
|
16002
|
+
const progress = 1 - Math.exp(-Math.max(0, elapsedInStep) / typical);
|
|
16003
|
+
return STEP_FLOOR[step] + (ceilingAfter(step) - STEP_FLOOR[step]) * progress;
|
|
16004
|
+
};
|
|
16005
|
+
const fill = () => {
|
|
16006
|
+
const now = io.now();
|
|
16007
|
+
const stepwise = current ? withinStep(current, now - currentStartedAt) : 0;
|
|
16008
|
+
const estimated = estimateMs !== null && estimateMs > 0 ? (now - startedAt) / estimateMs : 0;
|
|
16009
|
+
highWaterFill = Math.max(highWaterFill, stepwise, estimated);
|
|
16010
|
+
return Math.min(highWaterFill, MAX_ESTIMATED_FILL);
|
|
16011
|
+
};
|
|
16012
|
+
const bar = (fraction, width) => {
|
|
16013
|
+
if (width <= 0) return "";
|
|
16014
|
+
if (!unicode) {
|
|
16015
|
+
const filled = Math.max(0, Math.min(width, Math.round(fraction * width)));
|
|
16016
|
+
const head = filled > 0 && filled < width ? ">" : "";
|
|
16017
|
+
return `${"=".repeat(Math.max(0, filled - head.length))}${head}${" ".repeat(width - filled)}`;
|
|
16018
|
+
}
|
|
16019
|
+
const exact = Math.max(0, Math.min(width, fraction * width));
|
|
16020
|
+
const full = Math.floor(exact);
|
|
16021
|
+
const partial2 = BLOCKS[Math.floor((exact - full) * 8)] ?? "";
|
|
16022
|
+
const used = full + (partial2 ? 1 : 0);
|
|
16023
|
+
return `${"\u2588".repeat(full)}${partial2}${"\u2591".repeat(Math.max(0, width - used))}`;
|
|
16024
|
+
};
|
|
16025
|
+
const draw = () => {
|
|
16026
|
+
if (finished || current === null || !io.isTTY) return;
|
|
16027
|
+
eraseAnimated();
|
|
16028
|
+
const elapsed = formatClock(io.now() - startedAt);
|
|
16029
|
+
const counter = estimateMs !== null ? `${elapsed} / ~${formatClock(estimateMs)}` : elapsed;
|
|
16030
|
+
const prefixWidth = LABEL_WIDTH + 1 + NAME_WIDTH + 1;
|
|
16031
|
+
const barWidth = columns - prefixWidth - counter.length - 3;
|
|
16032
|
+
const barPart = current !== "queued" && barWidth >= 8 ? `[${bar(fill(), barWidth)}] ` : "";
|
|
16033
|
+
write(`\r${CLEAR_LINE}${row("Loading", "busy", current, `${barPart}${counter}`)}
|
|
16034
|
+
`);
|
|
16035
|
+
const indent = " ".repeat(LABEL_WIDTH + 1);
|
|
16036
|
+
const room = Math.max(0, columns - indent.length - 2 - ELLIPSIS_WIDTH);
|
|
16037
|
+
write(
|
|
16038
|
+
`\r${CLEAR_LINE}${paint("muted", `${indent}\u2514 ${clip(trimEllipsis(activity), room)}${ellipsis()}`)}`
|
|
16039
|
+
);
|
|
16040
|
+
drawnRows = 2;
|
|
16041
|
+
};
|
|
16042
|
+
const startTimer = () => {
|
|
16043
|
+
if (timer !== null || !io.isTTY) return;
|
|
16044
|
+
timer = setInterval(draw, FRAME_MS);
|
|
16045
|
+
timer.unref?.();
|
|
16046
|
+
};
|
|
16047
|
+
const stopTimer = () => {
|
|
16048
|
+
if (timer === null) return;
|
|
16049
|
+
clearInterval(timer);
|
|
16050
|
+
timer = null;
|
|
16051
|
+
};
|
|
16052
|
+
const closeCurrent = (role) => {
|
|
16053
|
+
if (current === null) return;
|
|
16054
|
+
eraseAnimated();
|
|
16055
|
+
const step = { status: current, ms: io.now() - currentStartedAt, role };
|
|
16056
|
+
const word = role === "bad" ? "Failed" : "Success";
|
|
16057
|
+
write(`${row(word, role, step.status, formatClock(step.ms))}
|
|
16058
|
+
`);
|
|
16059
|
+
current = null;
|
|
16060
|
+
};
|
|
16061
|
+
return {
|
|
16062
|
+
onLog(line) {
|
|
16063
|
+
tail.push(line);
|
|
16064
|
+
if (tail.length > TAIL_LINES) tail.shift();
|
|
16065
|
+
activity = line;
|
|
16066
|
+
draw();
|
|
16067
|
+
},
|
|
16068
|
+
onStatus(event) {
|
|
16069
|
+
if (TERMINAL_DEPLOY_STATES.includes(event.status)) return;
|
|
16070
|
+
if (event.status === current) return;
|
|
16071
|
+
const incoming = STEPS.indexOf(event.status);
|
|
16072
|
+
const running = current === null ? -1 : STEPS.indexOf(current);
|
|
16073
|
+
if (incoming !== -1 && incoming < running) return;
|
|
16074
|
+
closeCurrent("done");
|
|
16075
|
+
current = event.status;
|
|
16076
|
+
currentStartedAt = io.now();
|
|
16077
|
+
activity = "";
|
|
16078
|
+
startTimer();
|
|
16079
|
+
draw();
|
|
16080
|
+
},
|
|
16081
|
+
finish(terminal, outcome) {
|
|
16082
|
+
stopTimer();
|
|
16083
|
+
const failed = terminal === "failed";
|
|
16084
|
+
closeCurrent(failed ? "bad" : "done");
|
|
16085
|
+
finished = true;
|
|
16086
|
+
const total = formatClock(io.now() - startedAt);
|
|
16087
|
+
if (failed) {
|
|
16088
|
+
write("\n");
|
|
16089
|
+
if (tail.length > 0) {
|
|
16090
|
+
write(`${paint("muted", `--- last ${tail.length} log line${tail.length === 1 ? "" : "s"} ---`)}
|
|
16091
|
+
`);
|
|
16092
|
+
for (const line of tail) write(`${line}
|
|
16093
|
+
`);
|
|
16094
|
+
write(`${paint("muted", "---")}
|
|
16095
|
+
|
|
16096
|
+
`);
|
|
16097
|
+
} else {
|
|
16098
|
+
write(`${paint("muted", "No log lines arrived before this failed.")}
|
|
16099
|
+
|
|
16100
|
+
`);
|
|
16101
|
+
}
|
|
16102
|
+
write(`${paint("bad", "Failed".padStart(LABEL_WIDTH))} ${outcome.failure?.reason ?? "the deploy failed"}
|
|
16103
|
+
`);
|
|
16104
|
+
if (deploymentId) {
|
|
16105
|
+
write(`${paint("muted", `${" ".repeat(LABEL_WIDTH + 1)}Full log: kryd logs ${deploymentId}`)}
|
|
16106
|
+
`);
|
|
16107
|
+
}
|
|
16108
|
+
return;
|
|
16109
|
+
}
|
|
16110
|
+
if (terminal === "live") {
|
|
16111
|
+
write(`${row("Live", "done", outcome.url ?? "(no url reported)", total)}
|
|
16112
|
+
`);
|
|
16113
|
+
return;
|
|
16114
|
+
}
|
|
16115
|
+
const text = terminal === "superseded" ? "superseded by a newer push" : "torn down \u2014 this deploy's compute was reclaimed by cleanup";
|
|
16116
|
+
write(`${row("Done", "muted", text, "")}
|
|
16117
|
+
`);
|
|
16118
|
+
},
|
|
16119
|
+
setEstimate(ms) {
|
|
16120
|
+
estimateMs = ms !== null && ms > 0 ? ms : null;
|
|
16121
|
+
},
|
|
16122
|
+
setDeploymentId(id) {
|
|
16123
|
+
deploymentId = id;
|
|
16124
|
+
},
|
|
16125
|
+
tick() {
|
|
16126
|
+
draw();
|
|
16127
|
+
},
|
|
16128
|
+
isAnimating() {
|
|
16129
|
+
return timer !== null;
|
|
16130
|
+
}
|
|
16131
|
+
};
|
|
16132
|
+
}
|
|
16133
|
+
|
|
15780
16134
|
// src/input.ts
|
|
15781
16135
|
import { createInterface } from "node:readline";
|
|
15782
16136
|
import { Writable } from "node:stream";
|
|
@@ -15885,16 +16239,34 @@ function configureGitRemote(cwd, remote, url2) {
|
|
|
15885
16239
|
}
|
|
15886
16240
|
try {
|
|
15887
16241
|
const remotes = run(["remote"]).split(/\s+/).filter(Boolean);
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
}
|
|
15892
|
-
run(["remote", "add", remote, url2]);
|
|
15893
|
-
return { status: "added", remote };
|
|
16242
|
+
const status = remotes.includes(remote) ? "updated" : "added";
|
|
16243
|
+
if (status === "updated") run(["remote", "set-url", remote, url2]);
|
|
16244
|
+
else run(["remote", "add", remote, url2]);
|
|
16245
|
+
return { status, remote, tracking: setUpstreamIfUnset(run, remote) };
|
|
15894
16246
|
} catch {
|
|
15895
16247
|
return { status: "unavailable" };
|
|
15896
16248
|
}
|
|
15897
16249
|
}
|
|
16250
|
+
function setUpstreamIfUnset(run, remote) {
|
|
16251
|
+
let branch;
|
|
16252
|
+
try {
|
|
16253
|
+
branch = run(["symbolic-ref", "--short", "HEAD"]);
|
|
16254
|
+
} catch {
|
|
16255
|
+
return "detached";
|
|
16256
|
+
}
|
|
16257
|
+
if (!branch) return "detached";
|
|
16258
|
+
try {
|
|
16259
|
+
if (run(["config", "--get", `branch.${branch}.remote`])) return "kept-existing";
|
|
16260
|
+
} catch {
|
|
16261
|
+
}
|
|
16262
|
+
try {
|
|
16263
|
+
run(["config", `branch.${branch}.remote`, remote]);
|
|
16264
|
+
run(["config", `branch.${branch}.merge`, `refs/heads/${branch}`]);
|
|
16265
|
+
return "set";
|
|
16266
|
+
} catch {
|
|
16267
|
+
return "unavailable";
|
|
16268
|
+
}
|
|
16269
|
+
}
|
|
15898
16270
|
function probe(cwd, args) {
|
|
15899
16271
|
try {
|
|
15900
16272
|
const value = execFileSync("git", args, {
|
|
@@ -16063,7 +16435,9 @@ async function runInit(opts) {
|
|
|
16063
16435
|
case "added":
|
|
16064
16436
|
case "updated":
|
|
16065
16437
|
nextStep = `${remote.status === "added" ? "Added" : "Updated"} git remote "${remote.remote}" (with your push credential).
|
|
16066
|
-
|
|
16438
|
+
` + (remote.tracking === "set" ? `Tracking set: \`git push\` and \`kryd push\` both deploy this branch.
|
|
16439
|
+
` : remote.tracking === "kept-existing" ? `This branch already tracks another remote, so it was left alone \u2014 deploy with \`kryd push\`.
|
|
16440
|
+
` : "") + `Next: kryd push # push to deploy (same as \`git push ${remote.remote} ${branch}\`)
|
|
16067
16441
|
`;
|
|
16068
16442
|
break;
|
|
16069
16443
|
case "not-a-repo":
|
|
@@ -16103,19 +16477,82 @@ function formatStatus(event) {
|
|
|
16103
16477
|
return "\u2298 superseded by a newer push";
|
|
16104
16478
|
case "torn_down":
|
|
16105
16479
|
return "\u232B torn down \u2014 this deploy's compute was reclaimed by cleanup";
|
|
16480
|
+
// KRYD-360: the container exists and the app is being asked whether it answers. Named, rather
|
|
16481
|
+
// than falling through to the generic `→ verifying`, because this is the step where a customer's
|
|
16482
|
+
// own app is on the clock and the next line they see may be about their crash.
|
|
16483
|
+
case "verifying":
|
|
16484
|
+
return "\u2192 verifying the app answers\u2026";
|
|
16106
16485
|
default:
|
|
16107
16486
|
return `\u2192 ${event.status}`;
|
|
16108
16487
|
}
|
|
16109
16488
|
}
|
|
16110
|
-
async function followDeploy(apiUrl, token, deploymentId) {
|
|
16111
|
-
|
|
16112
|
-
|
|
16113
|
-
|
|
16114
|
-
|
|
16115
|
-
|
|
16489
|
+
async function followDeploy(apiUrl, token, deploymentId, opts = {}) {
|
|
16490
|
+
let streamedLines = 0;
|
|
16491
|
+
const mode = resolveRenderMode({
|
|
16492
|
+
want: opts.render ?? "lines",
|
|
16493
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
16494
|
+
env: process.env
|
|
16116
16495
|
});
|
|
16496
|
+
let terminal;
|
|
16497
|
+
if (mode === "lines") {
|
|
16498
|
+
terminal = await streamDeploy(apiUrl, token, deploymentId, {
|
|
16499
|
+
onLog: (line) => {
|
|
16500
|
+
streamedLines++;
|
|
16501
|
+
process.stdout.write(`${line}
|
|
16502
|
+
`);
|
|
16503
|
+
},
|
|
16504
|
+
onStatus: (event) => process.stdout.write(`${formatStatus(event)}
|
|
16505
|
+
`)
|
|
16506
|
+
});
|
|
16507
|
+
} else {
|
|
16508
|
+
const progress = createDeployProgress(defaultProgressIO());
|
|
16509
|
+
progress.setDeploymentId(deploymentId);
|
|
16510
|
+
progress.setEstimate(estimateTotalMs(opts.estimateFrom ?? []));
|
|
16511
|
+
let last;
|
|
16512
|
+
try {
|
|
16513
|
+
terminal = await streamDeploy(apiUrl, token, deploymentId, {
|
|
16514
|
+
onLog: (line) => {
|
|
16515
|
+
streamedLines++;
|
|
16516
|
+
progress.onLog(line);
|
|
16517
|
+
},
|
|
16518
|
+
onStatus: (event) => {
|
|
16519
|
+
last = event;
|
|
16520
|
+
progress.onStatus(event);
|
|
16521
|
+
}
|
|
16522
|
+
});
|
|
16523
|
+
} catch (err) {
|
|
16524
|
+
progress.finish("failed", { failure: { code: "streamFailed", reason: "the log stream ended" } });
|
|
16525
|
+
throw err;
|
|
16526
|
+
}
|
|
16527
|
+
progress.finish(terminal, {
|
|
16528
|
+
url: last?.url,
|
|
16529
|
+
failure: last?.failure
|
|
16530
|
+
});
|
|
16531
|
+
}
|
|
16532
|
+
if (streamedLines === 0) {
|
|
16533
|
+
await replayStoredLog(apiUrl, token, deploymentId);
|
|
16534
|
+
}
|
|
16117
16535
|
if (terminal !== "live" && terminal !== "torn_down") process.exitCode = 1;
|
|
16118
16536
|
}
|
|
16537
|
+
async function replayStoredLog(apiUrl, token, deploymentId) {
|
|
16538
|
+
try {
|
|
16539
|
+
const log = await fetchDeployLog(apiUrl, token, deploymentId);
|
|
16540
|
+
if (log === null) {
|
|
16541
|
+
process.stderr.write(
|
|
16542
|
+
`No stored log for ${deploymentId} \u2014 it finished before its log was persisted.
|
|
16543
|
+
`
|
|
16544
|
+
);
|
|
16545
|
+
return;
|
|
16546
|
+
}
|
|
16547
|
+
process.stdout.write(log.endsWith("\n") ? log : `${log}
|
|
16548
|
+
`);
|
|
16549
|
+
} catch (err) {
|
|
16550
|
+
process.stderr.write(
|
|
16551
|
+
`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.
|
|
16552
|
+
`
|
|
16553
|
+
);
|
|
16554
|
+
}
|
|
16555
|
+
}
|
|
16119
16556
|
async function runLogs(opts) {
|
|
16120
16557
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
16121
16558
|
const token = loadConfig().token;
|
|
@@ -16212,11 +16649,15 @@ async function runDeploy(opts) {
|
|
|
16212
16649
|
const deploymentId = await triggerDeploy(apiUrl, token, project2);
|
|
16213
16650
|
process.stdout.write(`Triggered deploy ${deploymentId}
|
|
16214
16651
|
`);
|
|
16215
|
-
await followDeploy(apiUrl, token, deploymentId);
|
|
16652
|
+
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16216
16653
|
} catch (err) {
|
|
16217
16654
|
reportError(err);
|
|
16218
16655
|
}
|
|
16219
16656
|
}
|
|
16657
|
+
function renderFor(logs) {
|
|
16658
|
+
return logs ? "lines" : "progress";
|
|
16659
|
+
}
|
|
16660
|
+
var LOGS_FLAG_HELP = "stream the full build log line by line instead of the progress display";
|
|
16220
16661
|
async function runPush(opts) {
|
|
16221
16662
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
16222
16663
|
const token = loadConfig().token;
|
|
@@ -16301,8 +16742,10 @@ async function runPush(opts) {
|
|
|
16301
16742
|
}
|
|
16302
16743
|
const fetchDeployments = () => listDeployments(apiUrl, token, projectId ?? void 0);
|
|
16303
16744
|
let known = null;
|
|
16745
|
+
let recent = [];
|
|
16304
16746
|
try {
|
|
16305
|
-
|
|
16747
|
+
recent = await fetchDeployments();
|
|
16748
|
+
known = findDeploymentsForCommit(recent, commitSha, branch);
|
|
16306
16749
|
} catch {
|
|
16307
16750
|
known = null;
|
|
16308
16751
|
}
|
|
@@ -16341,7 +16784,10 @@ async function runPush(opts) {
|
|
|
16341
16784
|
}
|
|
16342
16785
|
return;
|
|
16343
16786
|
}
|
|
16344
|
-
await followDeploy(apiUrl, token, deployment.id
|
|
16787
|
+
await followDeploy(apiUrl, token, deployment.id, {
|
|
16788
|
+
render: renderFor(opts.logs),
|
|
16789
|
+
estimateFrom: recent
|
|
16790
|
+
});
|
|
16345
16791
|
} catch (err) {
|
|
16346
16792
|
reportError(err);
|
|
16347
16793
|
}
|
|
@@ -16371,7 +16817,7 @@ async function runRollback(opts) {
|
|
|
16371
16817
|
\u26A0 ${note}
|
|
16372
16818
|
`
|
|
16373
16819
|
);
|
|
16374
|
-
await followDeploy(apiUrl, token, deploymentId);
|
|
16820
|
+
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16375
16821
|
} catch (err) {
|
|
16376
16822
|
reportError(err);
|
|
16377
16823
|
}
|
|
@@ -16400,7 +16846,7 @@ async function runRedeploy(opts) {
|
|
|
16400
16846
|
${note}
|
|
16401
16847
|
`
|
|
16402
16848
|
);
|
|
16403
|
-
await followDeploy(apiUrl, token, deploymentId);
|
|
16849
|
+
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16404
16850
|
} catch (err) {
|
|
16405
16851
|
reportError(err);
|
|
16406
16852
|
}
|
|
@@ -17212,7 +17658,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
17212
17658
|
reportError(err);
|
|
17213
17659
|
}
|
|
17214
17660
|
}
|
|
17215
|
-
var CLI_VERSION = true ? "0.
|
|
17661
|
+
var CLI_VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
17216
17662
|
var program = new Command();
|
|
17217
17663
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
17218
17664
|
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(
|
|
@@ -17248,16 +17694,16 @@ program.command("logs [target]").description(
|
|
|
17248
17694
|
);
|
|
17249
17695
|
program.command("push [branch]").description(
|
|
17250
17696
|
"Push to the kryd remote and follow the deploy it triggers (defaults to the current branch)"
|
|
17251
|
-
).option("--api-url <url>", "control-plane API base URL").action(
|
|
17697
|
+
).option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action(
|
|
17252
17698
|
(branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
|
|
17253
17699
|
);
|
|
17254
|
-
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 }));
|
|
17255
|
-
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(
|
|
17700
|
+
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 }));
|
|
17701
|
+
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(
|
|
17256
17702
|
(project2, deployment, opts) => runRollback({ ...opts, project: project2, deployment })
|
|
17257
17703
|
);
|
|
17258
17704
|
program.command("redeploy [project]").description(
|
|
17259
17705
|
"Redeploy the current live commit (no rebuild) to apply config/env changes, and follow it live"
|
|
17260
|
-
).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
|
|
17706
|
+
).option("--logs", LOGS_FLAG_HELP).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
|
|
17261
17707
|
var project = program.command("project").description("Manage your projects");
|
|
17262
17708
|
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));
|
|
17263
17709
|
project.command("rm [project]").description(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kryd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|