@kryd/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -4
  2. package/dist/index.js +207 -7
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -10,15 +10,15 @@ npm install -g @kryd/cli # then use the `kryd` command
10
10
  npx @kryd/cli <command>
11
11
  ```
12
12
 
13
- Requires Node.js ≥ 22.
13
+ Requires Node.js ≥ 22 and git (the CLI shells out to git for `kryd init` and `kryd push`).
14
14
 
15
15
  ## Quickstart
16
16
 
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
- git push kryd main # build → deploy → live, with a streamed log
21
- kryd logs # follow the linked project's latest deploy
20
+ kryd push # push the current branch → build → deploy → live, streamed
21
+ kryd logs # follow the linked project's latest deploy (kryd push already does)
22
22
  kryd logs <project> --runtime # tail the live app's stdout/stderr
23
23
  ```
24
24
 
@@ -30,7 +30,8 @@ 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 deploy [project]` | Trigger a deploy of the production branch and follow it live. |
33
+ | `kryd push [branch]` | Push to the `kryd` remote and follow the deploy it triggers (defaults to the current branch). |
34
+ | `kryd deploy [project]` | Re-deploy the production-branch HEAD already on the forge — no new commit. |
34
35
  | `kryd logs [target]` | Follow a deploy's build/deploy log (`--runtime` tails the live container instead). |
35
36
  | `kryd rollback [project] [deployment]` | Roll back to a previous successful deploy — no rebuild. |
36
37
  | `kryd db create \| detach [project]` | Attach / tear down managed Postgres (shared or bring-your-own). |
package/dist/index.js CHANGED
@@ -15144,6 +15144,32 @@ async function listDeployments(apiUrl, token, projectId) {
15144
15144
  }
15145
15145
  return (await res.json()).items;
15146
15146
  }
15147
+ function findDeploymentsForCommit(deployments, commitSha, branch) {
15148
+ return deployments.filter((d) => d.commitSha === commitSha && d.branch === branch);
15149
+ }
15150
+ async function pollForNewDeployment(fetchDeployments, match, opts) {
15151
+ const intervalMs = opts?.intervalMs ?? 2e3;
15152
+ const timeoutMs = opts?.timeoutMs ?? 6e4;
15153
+ const maxConsecutiveErrors = opts?.maxConsecutiveErrors ?? 5;
15154
+ const sleep = opts?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
15155
+ const started = Date.now();
15156
+ let consecutiveErrors = 0;
15157
+ for (; ; ) {
15158
+ try {
15159
+ const items = await fetchDeployments();
15160
+ consecutiveErrors = 0;
15161
+ const fresh = findDeploymentsForCommit(items, match.commitSha, match.branch).filter(
15162
+ (d) => !match.knownIds.has(d.id)
15163
+ );
15164
+ const found = fresh[fresh.length - 1];
15165
+ if (found) return found;
15166
+ } catch (err) {
15167
+ if (++consecutiveErrors > maxConsecutiveErrors) throw err;
15168
+ }
15169
+ if (Date.now() - started >= timeoutMs) return null;
15170
+ await sleep(intervalMs);
15171
+ }
15172
+ }
15147
15173
  function dataFromFrame(frame) {
15148
15174
  const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
15149
15175
  return data.length > 0 ? data : void 0;
@@ -15301,7 +15327,7 @@ function inspectProject(cwd) {
15301
15327
  }
15302
15328
 
15303
15329
  // src/git.ts
15304
- import { execFileSync } from "node:child_process";
15330
+ import { execFileSync, spawnSync } from "node:child_process";
15305
15331
  function authenticatedRemoteUrl(cloneUrl, username, token) {
15306
15332
  const prefix = "https://";
15307
15333
  if (!cloneUrl.startsWith(prefix)) return cloneUrl;
@@ -15330,6 +15356,43 @@ function configureGitRemote(cwd, remote, url2) {
15330
15356
  return { status: "unavailable" };
15331
15357
  }
15332
15358
  }
15359
+ function probe(cwd, args) {
15360
+ try {
15361
+ const value = execFileSync("git", args, {
15362
+ cwd,
15363
+ stdio: ["ignore", "pipe", "ignore"]
15364
+ }).toString().trim();
15365
+ return { status: "ok", value };
15366
+ } catch (err) {
15367
+ if (err.code === "ENOENT") return { status: "no-git" };
15368
+ return { status: "failed" };
15369
+ }
15370
+ }
15371
+ function gitAvailable(cwd) {
15372
+ const res = probe(cwd, ["rev-parse", "--is-inside-work-tree"]);
15373
+ if (res.status === "no-git") return res;
15374
+ if (res.status !== "ok" || res.value !== "true") return { status: "not-a-repo" };
15375
+ return { status: "ok" };
15376
+ }
15377
+ function currentBranch(cwd) {
15378
+ return probe(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
15379
+ }
15380
+ function headSha(cwd, ref) {
15381
+ return probe(cwd, ["rev-parse", ref]);
15382
+ }
15383
+ function hasRemote(cwd, remote) {
15384
+ const res = probe(cwd, ["remote"]);
15385
+ if (res.status !== "ok") return res;
15386
+ return { status: "ok", value: res.value.split(/\s+/).filter(Boolean).includes(remote) };
15387
+ }
15388
+ function pushBranch(cwd, remote, branch, extraArgs) {
15389
+ const res = spawnSync("git", ["push", remote, branch, ...extraArgs], {
15390
+ cwd,
15391
+ stdio: "inherit"
15392
+ });
15393
+ if (res.status === 0) return { status: "ok" };
15394
+ return { status: "failed", code: res.status ?? 1 };
15395
+ }
15333
15396
 
15334
15397
  // src/index.ts
15335
15398
  function reportError(err) {
@@ -15458,20 +15521,22 @@ async function runInit(opts) {
15458
15521
  case "added":
15459
15522
  case "updated":
15460
15523
  nextStep = `${remote.status === "added" ? "Added" : "Updated"} git remote "${remote.remote}" (with your push credential).
15461
- Next: git push ${remote.remote} ${branch} # push to deploy
15524
+ Next: kryd push # push to deploy (same as \`git push ${remote.remote} ${branch}\`)
15462
15525
  `;
15463
15526
  break;
15464
15527
  case "not-a-repo":
15465
15528
  nextStep = `No git repo here yet. To deploy (the URL carries your push token \u2014 keep it private):
15466
15529
  git init && git add -A && git commit -m "init"
15467
15530
  git remote add kryd ${authedUrl}
15468
- git push kryd ${branch}
15531
+ kryd push
15469
15532
  `;
15470
15533
  break;
15471
15534
  case "unavailable":
15472
- nextStep = `Add the remote, then push to deploy (the URL carries your push token \u2014 keep it private):
15535
+ nextStep = gitAvailable(cwd).status === "no-git" ? `git is not installed, so the deploy remote could not be configured.
15536
+ Install git (https://git-scm.com/downloads), then re-run \`kryd init\` here.
15537
+ ` : `Add the remote, then push to deploy (the URL carries your push token \u2014 keep it private):
15473
15538
  git remote add kryd ${authedUrl}
15474
- git push kryd ${branch}
15539
+ kryd push
15475
15540
  `;
15476
15541
  break;
15477
15542
  }
@@ -15605,6 +15670,135 @@ async function runDeploy(opts) {
15605
15670
  reportError(err);
15606
15671
  }
15607
15672
  }
15673
+ async function runPush(opts) {
15674
+ const apiUrl = resolveApiUrl(opts.apiUrl);
15675
+ const token = loadConfig().token;
15676
+ if (!token) {
15677
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
15678
+ process.exitCode = 1;
15679
+ return;
15680
+ }
15681
+ const cwd = opts.cwd ?? process.cwd();
15682
+ const available = gitAvailable(cwd);
15683
+ if (available.status === "no-git") {
15684
+ process.stderr.write(
15685
+ "git is not installed, and `kryd push` needs it to deploy.\nInstall git (https://git-scm.com/downloads), then try again.\n"
15686
+ );
15687
+ process.exitCode = 1;
15688
+ return;
15689
+ }
15690
+ if (available.status !== "ok") {
15691
+ process.stderr.write(
15692
+ "Not a git repository \u2014 run `kryd push` from the root of your app's repo.\n"
15693
+ );
15694
+ process.exitCode = 1;
15695
+ return;
15696
+ }
15697
+ const remote = hasRemote(cwd, "kryd");
15698
+ if (remote.status !== "ok" || !remote.value) {
15699
+ process.stderr.write(
15700
+ 'No "kryd" git remote here. Run `kryd init` to link this project and configure it.\n'
15701
+ );
15702
+ process.exitCode = 1;
15703
+ return;
15704
+ }
15705
+ if (opts.branch?.startsWith("-")) {
15706
+ process.stderr.write(
15707
+ `"${opts.branch}" is not a branch. Pass the branch before \`--\`, e.g. \`kryd push main -- ${opts.branch}\`.
15708
+ `
15709
+ );
15710
+ process.exitCode = 1;
15711
+ return;
15712
+ }
15713
+ let branch = opts.branch;
15714
+ if (!branch) {
15715
+ const current = currentBranch(cwd);
15716
+ if (current.status !== "ok") {
15717
+ process.stderr.write("Could not determine the current branch \u2014 pass one, e.g. `kryd push main`.\n");
15718
+ process.exitCode = 1;
15719
+ return;
15720
+ }
15721
+ if (current.value === "HEAD") {
15722
+ process.stderr.write(
15723
+ "You are on a detached HEAD \u2014 pass the branch to push explicitly, e.g. `kryd push main`.\n"
15724
+ );
15725
+ process.exitCode = 1;
15726
+ return;
15727
+ }
15728
+ branch = current.value;
15729
+ }
15730
+ const gitArgs = opts.gitArgs ?? [];
15731
+ if (branch.includes(":") || branch.startsWith("+")) {
15732
+ const pushed = pushBranch(cwd, "kryd", branch, gitArgs);
15733
+ if (pushed.status === "failed") {
15734
+ process.exitCode = pushed.code;
15735
+ return;
15736
+ }
15737
+ process.stdout.write(
15738
+ "Pushed. `kryd push` does not follow refspec pushes \u2014 use `kryd logs` to watch a deploy.\n"
15739
+ );
15740
+ return;
15741
+ }
15742
+ try {
15743
+ const sha = headSha(cwd, branch);
15744
+ if (sha.status !== "ok") {
15745
+ process.stderr.write(`Unknown branch "${branch}" \u2014 nothing to push.
15746
+ `);
15747
+ process.exitCode = 1;
15748
+ return;
15749
+ }
15750
+ const commitSha = sha.value;
15751
+ const projectId = resolveProjectId(void 0, opts.cwd);
15752
+ if (!projectId) {
15753
+ process.stderr.write("tip: run `kryd init` here to scope `kryd push` to this project.\n");
15754
+ }
15755
+ const fetchDeployments = () => listDeployments(apiUrl, token, projectId ?? void 0);
15756
+ let known = null;
15757
+ try {
15758
+ known = findDeploymentsForCommit(await fetchDeployments(), commitSha, branch);
15759
+ } catch {
15760
+ known = null;
15761
+ }
15762
+ const pushed = pushBranch(cwd, "kryd", branch, gitArgs);
15763
+ if (pushed.status === "failed") {
15764
+ process.exitCode = pushed.code;
15765
+ return;
15766
+ }
15767
+ if (known === null) {
15768
+ process.stdout.write(
15769
+ "Pushed. Could not reach the API to follow the deploy \u2014 try `kryd logs`.\n"
15770
+ );
15771
+ return;
15772
+ }
15773
+ process.stdout.write("Waiting for the deploy to start\u2026\n");
15774
+ const deployment = await pollForNewDeployment(
15775
+ fetchDeployments,
15776
+ { commitSha, branch, knownIds: new Set(known.map((d) => d.id)) },
15777
+ {
15778
+ ...opts.sleep ? { sleep: opts.sleep } : {},
15779
+ ...opts.pollTimeoutMs !== void 0 ? { timeoutMs: opts.pollTimeoutMs } : {}
15780
+ }
15781
+ );
15782
+ if (!deployment) {
15783
+ const previous = known[0];
15784
+ if (previous) {
15785
+ const outcome = previous.status === "live" ? "is already deployed" : `has already been deployed once (that deploy ended \`${previous.status}\`)`;
15786
+ process.stdout.write(
15787
+ `Nothing new to push \u2014 ${commitSha.slice(0, 7)} ${outcome} (\`kryd logs ${previous.id}\` to review it).
15788
+ `
15789
+ );
15790
+ } else {
15791
+ process.stdout.write(
15792
+ "Pushed, but no deploy has started yet. Follow it with `kryd logs`.\n" + (projectId ? "" : "If this keeps happening, re-run `kryd init` to link this folder.\n")
15793
+ );
15794
+ }
15795
+ return;
15796
+ }
15797
+ await followDeploy(apiUrl, token, deployment.id);
15798
+ } catch (err) {
15799
+ reportError(err);
15800
+ }
15801
+ }
15608
15802
  async function runRollback(opts) {
15609
15803
  const apiUrl = resolveApiUrl(opts.apiUrl);
15610
15804
  const token = loadConfig().token;
@@ -15811,7 +16005,7 @@ async function runAiEnable(opts) {
15811
16005
  const status = await enableAi(apiUrl, token, { projectId: project });
15812
16006
  process.stdout.write(
15813
16007
  `AI enabled for ${project}.
15814
- ` + (status.url ? `Endpoint: ${status.url}
16008
+ ` + (status.url ? `Endpoint: ${status.url} (OpenAI-compatible base URL \u2014 use it as-is)
15815
16009
  ` : "") + `AI_GATEWAY_URL + AI_GATEWAY_TOKEN will be injected on your next deploy (kryd deploy).
15816
16010
  `
15817
16011
  );
@@ -15849,7 +16043,7 @@ async function runStorageDetach(opts) {
15849
16043
  reportError(err);
15850
16044
  }
15851
16045
  }
15852
- var CLI_VERSION = true ? "0.1.0" : "0.0.0-dev";
16046
+ var CLI_VERSION = true ? "0.2.0" : "0.0.0-dev";
15853
16047
  var program = new Command();
15854
16048
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
15855
16049
  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(
@@ -15876,6 +16070,11 @@ program.command("logs [target]").description(
15876
16070
  ).option("--api-url <url>", "control-plane API base URL").action(
15877
16071
  (target, opts) => opts.runtime ? runRuntimeLogs({ project: target, since: opts.since, apiUrl: opts.apiUrl }) : runLogs({ apiUrl: opts.apiUrl, deployment: target })
15878
16072
  );
16073
+ program.command("push [branch]").description(
16074
+ "Push to the kryd remote and follow the deploy it triggers (defaults to the current branch)"
16075
+ ).option("--api-url <url>", "control-plane API base URL").action(
16076
+ (branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
16077
+ );
15879
16078
  program.command("deploy [project]").description("Trigger a deploy of the project's production branch and follow it live").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runDeploy({ ...opts, project }));
15880
16079
  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(
15881
16080
  (project, deployment, opts) => runRollback({ ...opts, project, deployment })
@@ -15910,6 +16109,7 @@ export {
15910
16109
  runLogin,
15911
16110
  runLogout,
15912
16111
  runLogs,
16112
+ runPush,
15913
16113
  runRollback,
15914
16114
  runRuntimeLogs,
15915
16115
  runStorageCreate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kryd/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Kryd CLI — push a React / Vite / Next.js app to the effortless EU-sovereign deploy stack on Scaleway: git push → live with SSL, one-click managed Postgres & object storage, and an EU-hosted AI gateway already wired in.",
5
5
  "keywords": [
6
6
  "kryd",
@@ -51,8 +51,8 @@
51
51
  "typescript": "^5.6.3",
52
52
  "vitest": "^2.1.8",
53
53
  "@kryd/shared-types": "0.0.0",
54
- "@kryd/config-ts": "0.0.0",
55
- "@kryd/config-eslint": "0.0.0"
54
+ "@kryd/config-eslint": "0.0.0",
55
+ "@kryd/config-ts": "0.0.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --external:commander --define:__KRYD_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/index.js",