@kryd/cli 0.8.0 → 0.8.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.
Files changed (2) hide show
  1. package/dist/index.js +272 -74
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -14855,6 +14855,10 @@ function validateBuildEnvVarValue(value) {
14855
14855
  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.";
14856
14856
  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.";
14857
14857
 
14858
+ // ../../packages/shared-types/dist/workflow-run.js
14859
+ var MAX_WEBHOOK_NAME = 32;
14860
+ var WEBHOOK_NAME_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_WEBHOOK_NAME - 2}}[a-z0-9])?$`);
14861
+
14858
14862
  // ../../packages/shared-types/dist/index.js
14859
14863
  var DEPLOY_STATES = [
14860
14864
  "queued",
@@ -14956,8 +14960,13 @@ function clearToken() {
14956
14960
  delete config2.token;
14957
14961
  writeConfig(config2);
14958
14962
  }
14963
+ var lastApiUrl = null;
14959
14964
  function resolveApiUrl(flag) {
14960
- return flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
14965
+ lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
14966
+ return lastApiUrl;
14967
+ }
14968
+ function currentApiUrl() {
14969
+ return lastApiUrl ?? resolveApiUrl();
14961
14970
  }
14962
14971
  var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
14963
14972
  function resolveDashboardUrl(flag) {
@@ -15046,9 +15055,33 @@ function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
15046
15055
  return false;
15047
15056
  }
15048
15057
  }
15058
+ function resolvedLink() {
15059
+ return lastLinkResolution;
15060
+ }
15061
+ var lastLinkResolution = null;
15049
15062
  function resolveProjectId(explicit, cwd = process.cwd()) {
15063
+ lastLinkResolution = null;
15050
15064
  if (explicit) return explicit;
15051
- return loadProjectLink(cwd)?.projectId ?? null;
15065
+ const root = findProjectLinkDir(cwd);
15066
+ if (!root) return null;
15067
+ const link = loadProjectLink(cwd);
15068
+ if (!link) return null;
15069
+ lastLinkResolution = { root, link };
15070
+ return link.projectId;
15071
+ }
15072
+ function rememberLinkAccount(root, account) {
15073
+ if (!account.accountId) return false;
15074
+ const link = directoryIsLinked(root);
15075
+ if (!link) return false;
15076
+ const slug = account.accountSlug ?? link.accountSlug;
15077
+ if (link.accountId === account.accountId && link.accountSlug === slug) return false;
15078
+ saveProjectLink(root, {
15079
+ ...link,
15080
+ accountId: account.accountId,
15081
+ // `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
15082
+ ...slug ? { accountSlug: slug } : {}
15083
+ });
15084
+ return true;
15052
15085
  }
15053
15086
 
15054
15087
  // src/browser-login.ts
@@ -15136,7 +15169,15 @@ Waiting for approval\u2026
15136
15169
  // src/client.ts
15137
15170
  var ApiError = class extends Error {
15138
15171
  envelope;
15139
- /** The HTTP status of the failed response, when it came from one (used to detect a 404 = gone). */
15172
+ /**
15173
+ * The HTTP status of the failed response, when it came from one (used to detect a 404 = gone).
15174
+ *
15175
+ * ⚠️ Still optional because an `ApiError` is also thrown for a well-formed response the CLI
15176
+ * cannot use ("the API returned an unexpected …") and for a stream's own error frame — neither
15177
+ * has a status. Every throw that DOES hold a `Response` now passes it (KRYD-481): the 404
15178
+ * diagnosis keys on the status, and a third of them used to omit it, which made the same
15179
+ * failure explainable or unexplainable depending on which verb the customer typed.
15180
+ */
15140
15181
  status;
15141
15182
  constructor(message, envelope, status) {
15142
15183
  super(message);
@@ -15161,7 +15202,8 @@ async function login(apiUrl, creds) {
15161
15202
  if (!res.ok) {
15162
15203
  throw new ApiError(
15163
15204
  `Login failed (${res.status})`,
15164
- await parseEnvelope(res)
15205
+ await parseEnvelope(res),
15206
+ res.status
15165
15207
  );
15166
15208
  }
15167
15209
  const token = res.headers.get("set-auth-token");
@@ -15177,7 +15219,8 @@ async function whoami(apiUrl, token) {
15177
15219
  if (!res.ok) {
15178
15220
  throw new ApiError(
15179
15221
  `Request failed (${res.status})`,
15180
- await parseEnvelope(res)
15222
+ await parseEnvelope(res),
15223
+ res.status
15181
15224
  );
15182
15225
  }
15183
15226
  return await res.json();
@@ -15194,7 +15237,8 @@ async function linkProject(apiUrl, token, input) {
15194
15237
  if (!res.ok) {
15195
15238
  throw new ApiError(
15196
15239
  `Repo linking failed (${res.status})`,
15197
- await parseEnvelope(res)
15240
+ await parseEnvelope(res),
15241
+ res.status
15198
15242
  );
15199
15243
  }
15200
15244
  const parsed = await res.json().catch(() => void 0);
@@ -15222,7 +15266,8 @@ async function attachDatabase(apiUrl, token, input) {
15222
15266
  if (!res.ok) {
15223
15267
  throw new ApiError(
15224
15268
  `Database attach failed (${res.status})`,
15225
- await parseEnvelope(res)
15269
+ await parseEnvelope(res),
15270
+ res.status
15226
15271
  );
15227
15272
  }
15228
15273
  const parsed = await res.json().catch(() => void 0);
@@ -15247,7 +15292,8 @@ async function createStorage(apiUrl, token, input) {
15247
15292
  if (!res.ok) {
15248
15293
  throw new ApiError(
15249
15294
  `Storage create failed (${res.status})`,
15250
- await parseEnvelope(res)
15295
+ await parseEnvelope(res),
15296
+ res.status
15251
15297
  );
15252
15298
  }
15253
15299
  const parsed = await res.json().catch(() => void 0);
@@ -15449,7 +15495,8 @@ async function detachDatabase(apiUrl, token, projectId) {
15449
15495
  if (!res.ok) {
15450
15496
  throw new ApiError(
15451
15497
  `Database detach failed (${res.status})`,
15452
- await parseEnvelope(res)
15498
+ await parseEnvelope(res),
15499
+ res.status
15453
15500
  );
15454
15501
  }
15455
15502
  }
@@ -15459,7 +15506,7 @@ async function getProject(apiUrl, token, projectId) {
15459
15506
  });
15460
15507
  if (res.status === 404) return null;
15461
15508
  if (!res.ok) {
15462
- throw new ApiError(`Reading the project failed (${res.status})`, await parseEnvelope(res));
15509
+ throw new ApiError(`Reading the project failed (${res.status})`, await parseEnvelope(res), res.status);
15463
15510
  }
15464
15511
  return await res.json();
15465
15512
  }
@@ -15468,7 +15515,7 @@ async function listProjects(apiUrl, token) {
15468
15515
  headers: { authorization: `Bearer ${token}` }
15469
15516
  });
15470
15517
  if (!res.ok) {
15471
- throw new ApiError(`Listing projects failed (${res.status})`, await parseEnvelope(res));
15518
+ throw new ApiError(`Listing projects failed (${res.status})`, await parseEnvelope(res), res.status);
15472
15519
  }
15473
15520
  return (await res.json()).items;
15474
15521
  }
@@ -15478,7 +15525,7 @@ async function deleteProject(apiUrl, token, projectId) {
15478
15525
  headers: { authorization: `Bearer ${token}` }
15479
15526
  });
15480
15527
  if (!res.ok) {
15481
- throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res));
15528
+ throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res), res.status);
15482
15529
  }
15483
15530
  return await res.json();
15484
15531
  }
@@ -15513,7 +15560,8 @@ async function detachStorage(apiUrl, token, projectId) {
15513
15560
  if (!res.ok) {
15514
15561
  throw new ApiError(
15515
15562
  `Storage detach failed (${res.status})`,
15516
- await parseEnvelope(res)
15563
+ await parseEnvelope(res),
15564
+ res.status
15517
15565
  );
15518
15566
  }
15519
15567
  }
@@ -15679,7 +15727,7 @@ async function triggerDeploy(apiUrl, token, projectId) {
15679
15727
  body: JSON.stringify({ projectId })
15680
15728
  });
15681
15729
  if (!res.ok) {
15682
- throw new ApiError(`Deploy trigger failed (${res.status})`, await parseEnvelope(res));
15730
+ throw new ApiError(`Deploy trigger failed (${res.status})`, await parseEnvelope(res), res.status);
15683
15731
  }
15684
15732
  return (await res.json()).deploymentId;
15685
15733
  }
@@ -15696,7 +15744,7 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
15696
15744
  })
15697
15745
  });
15698
15746
  if (!res.ok) {
15699
- throw new ApiError(`Rollback failed (${res.status})`, await parseEnvelope(res));
15747
+ throw new ApiError(`Rollback failed (${res.status})`, await parseEnvelope(res), res.status);
15700
15748
  }
15701
15749
  return await res.json();
15702
15750
  }
@@ -15710,7 +15758,7 @@ async function redeployProject(apiUrl, token, projectId) {
15710
15758
  body: JSON.stringify({ projectId })
15711
15759
  });
15712
15760
  if (!res.ok) {
15713
- throw new ApiError(`Redeploy failed (${res.status})`, await parseEnvelope(res));
15761
+ throw new ApiError(`Redeploy failed (${res.status})`, await parseEnvelope(res), res.status);
15714
15762
  }
15715
15763
  return await res.json();
15716
15764
  }
@@ -15719,7 +15767,7 @@ async function listDeployments(apiUrl, token, projectId) {
15719
15767
  if (projectId) url2.searchParams.set("projectId", projectId);
15720
15768
  const res = await fetch(url2, { headers: { authorization: `Bearer ${token}` } });
15721
15769
  if (!res.ok) {
15722
- throw new ApiError(`Listing deploys failed (${res.status})`, await parseEnvelope(res));
15770
+ throw new ApiError(`Listing deploys failed (${res.status})`, await parseEnvelope(res), res.status);
15723
15771
  }
15724
15772
  return (await res.json()).items;
15725
15773
  }
@@ -15798,7 +15846,7 @@ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15798
15846
  headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
15799
15847
  });
15800
15848
  if (!res.ok) {
15801
- throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res));
15849
+ throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res), res.status);
15802
15850
  }
15803
15851
  if (!res.body) throw new Error("The log stream returned no body.");
15804
15852
  for await (const data of readSseFrames(res.body)) {
@@ -15842,7 +15890,8 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15842
15890
  if (!res.ok) {
15843
15891
  throw new ApiError(
15844
15892
  `Runtime log stream failed (${res.status})`,
15845
- await parseEnvelope(res)
15893
+ await parseEnvelope(res),
15894
+ res.status
15846
15895
  );
15847
15896
  }
15848
15897
  if (!res.body) throw new Error("The runtime log stream returned no body.");
@@ -15871,11 +15920,59 @@ async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
15871
15920
  if (!res.ok) {
15872
15921
  throw new ApiError(
15873
15922
  `Fetching the stored log failed (${res.status})`,
15874
- await parseEnvelope(res)
15923
+ await parseEnvelope(res),
15924
+ res.status
15875
15925
  );
15876
15926
  }
15877
15927
  return (await res.json()).log;
15878
15928
  }
15929
+ async function fetchPushCredential(apiUrl, token, projectId) {
15930
+ const res = await fetch(
15931
+ `${apiUrl}/repos/${encodeURIComponent(projectId)}/push-credential`,
15932
+ { headers: { authorization: `Bearer ${token}` } }
15933
+ );
15934
+ if (!res.ok) {
15935
+ throw new ApiError(
15936
+ `Could not fetch the push credential (${res.status})`,
15937
+ await parseEnvelope(res),
15938
+ res.status
15939
+ );
15940
+ }
15941
+ const parsed = await res.json().catch(() => void 0);
15942
+ if (!parsed || typeof parsed.username !== "string" || typeof parsed.password !== "string" || parsed.password.length === 0) {
15943
+ throw new ApiError("The API returned an unexpected push-credential response.");
15944
+ }
15945
+ return { username: parsed.username, password: parsed.password };
15946
+ }
15947
+
15948
+ // src/git-credential.ts
15949
+ async function runGitCredential(operation, opts = {}) {
15950
+ if (operation !== "get") return;
15951
+ const input = parseCredentialInput(opts.stdin ?? "");
15952
+ if (input.protocol && input.protocol !== "https") return;
15953
+ const cwd = opts.cwd ?? process.cwd();
15954
+ const token = loadConfig().token;
15955
+ if (!token) return;
15956
+ const projectId = resolveProjectId(void 0, cwd);
15957
+ if (!projectId) return;
15958
+ try {
15959
+ const cred = await fetchPushCredential(resolveApiUrl(opts.apiUrl), token, projectId);
15960
+ process.stdout.write(`username=${cred.username}
15961
+ password=${cred.password}
15962
+ `);
15963
+ } catch {
15964
+ }
15965
+ }
15966
+ function parseCredentialInput(raw) {
15967
+ const out = {};
15968
+ for (const line of raw.split("\n")) {
15969
+ if (line === "") continue;
15970
+ const eq = line.indexOf("=");
15971
+ if (eq <= 0) continue;
15972
+ out[line.slice(0, eq)] = line.slice(eq + 1);
15973
+ }
15974
+ return out;
15975
+ }
15879
15976
 
15880
15977
  // src/framework.ts
15881
15978
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
@@ -16863,12 +16960,6 @@ async function promptConfirm(question, io) {
16863
16960
  // src/git.ts
16864
16961
  import { execFileSync, spawnSync } from "node:child_process";
16865
16962
  import { resolve } from "node:path";
16866
- function authenticatedRemoteUrl(cloneUrl, username, token) {
16867
- const prefix = "https://";
16868
- if (!cloneUrl.startsWith(prefix)) return cloneUrl;
16869
- const creds = `${encodeURIComponent(username)}:${encodeURIComponent(token)}@`;
16870
- return prefix + creds + cloneUrl.slice(prefix.length);
16871
- }
16872
16963
  function configureGitRemote(cwd, remote, url2) {
16873
16964
  const run = (args) => execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
16874
16965
  try {
@@ -16961,9 +17052,32 @@ function pushBranch(cwd, remote, branch, extraArgs) {
16961
17052
  if (res.status === 0) return { status: "ok" };
16962
17053
  return { status: "failed", code: res.status ?? 1 };
16963
17054
  }
17055
+ function configureCredentialHelper(cwd, cloneUrl) {
17056
+ const origin = originOf(cloneUrl);
17057
+ if (!origin) return "unavailable";
17058
+ const run = (args) => {
17059
+ try {
17060
+ execFileSync("git", args, { cwd, stdio: ["ignore", "ignore", "ignore"] });
17061
+ return true;
17062
+ } catch {
17063
+ return false;
17064
+ }
17065
+ };
17066
+ const ok = run(["config", `credential.${origin}.helper`, ""]) && run(["config", "--add", `credential.${origin}.helper`, "!kryd git-credential"]) && run(["config", `credential.${origin}.useHttpPath`, "true"]);
17067
+ return ok ? "set" : "unavailable";
17068
+ }
17069
+ function originOf(url2) {
17070
+ try {
17071
+ const parsed = new URL(url2);
17072
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
17073
+ return `${parsed.protocol}//${parsed.host}`;
17074
+ } catch {
17075
+ return null;
17076
+ }
17077
+ }
16964
17078
 
16965
17079
  // src/index.ts
16966
- function reportError(err) {
17080
+ async function reportError(err) {
16967
17081
  if (err instanceof ApiError && err.envelope) {
16968
17082
  process.stderr.write(
16969
17083
  `${err.envelope.error.code}: ${err.envelope.error.message}
@@ -16974,6 +17088,63 @@ function reportError(err) {
16974
17088
  `);
16975
17089
  }
16976
17090
  process.exitCode = 1;
17091
+ if (err instanceof ApiError) {
17092
+ process.stderr.write(await explainMissingProject(err));
17093
+ }
17094
+ }
17095
+ async function projectIsVisible(token, projectId) {
17096
+ try {
17097
+ return await getProject(currentApiUrl(), token, projectId) !== null;
17098
+ } catch {
17099
+ return null;
17100
+ }
17101
+ }
17102
+ function accountLabel(slug, id) {
17103
+ return slug ?? id;
17104
+ }
17105
+ async function explainMissingProject(err) {
17106
+ if (err.status !== 404) return "";
17107
+ const resolution = resolvedLink();
17108
+ if (!resolution) return "";
17109
+ const token = loadConfig().token;
17110
+ if (!token) return "";
17111
+ if (await projectIsVisible(token, resolution.link.projectId) !== false) return "";
17112
+ let session;
17113
+ try {
17114
+ session = await whoami(currentApiUrl(), token);
17115
+ } catch {
17116
+ return "";
17117
+ }
17118
+ if (!session.accountId) return "";
17119
+ const signedInAs = accountLabel(session.accountSlug, session.accountId);
17120
+ const linkedAccount = resolution.link.accountId;
17121
+ if (!linkedAccount) {
17122
+ return `This folder is linked to project ${resolution.link.projectId}, and you are signed in as ${signedInAs}. That project is not on this account \u2014 if it belongs to another one, switch with \`kryd login\` and run this again.
17123
+ `;
17124
+ }
17125
+ if (linkedAccount === session.accountId) {
17126
+ return `This folder is linked to project ${resolution.link.projectId} on ${signedInAs}, which is the account you are signed in as \u2014 so that project no longer exists.
17127
+ `;
17128
+ }
17129
+ return `This folder is linked to a project of account ${accountLabel(resolution.link.accountSlug, linkedAccount)}, but you are signed in as ${signedInAs}. Switch with \`kryd login\` and run this again.
17130
+ `;
17131
+ }
17132
+ async function backfillLinkAccount() {
17133
+ if (process.exitCode) return;
17134
+ const resolution = resolvedLink();
17135
+ if (!resolution) return;
17136
+ if (resolution.link.accountId && resolution.link.accountSlug) return;
17137
+ const token = loadConfig().token;
17138
+ if (!token) return;
17139
+ if (await projectIsVisible(token, resolution.link.projectId) !== true) return;
17140
+ try {
17141
+ const session = await whoami(currentApiUrl(), token);
17142
+ rememberLinkAccount(resolution.root, {
17143
+ accountId: session.accountId,
17144
+ accountSlug: session.accountSlug
17145
+ });
17146
+ } catch {
17147
+ }
16977
17148
  }
16978
17149
  function reportNotLinked(example) {
16979
17150
  process.stderr.write(
@@ -17014,7 +17185,7 @@ async function runWhoami(opts = {}) {
17014
17185
  `
17015
17186
  );
17016
17187
  } catch (err) {
17017
- reportError(err);
17188
+ await reportError(err);
17018
17189
  }
17019
17190
  }
17020
17191
  function runLogout() {
@@ -17073,7 +17244,7 @@ To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete
17073
17244
  }
17074
17245
  const name = opts.name ?? detected.name;
17075
17246
  try {
17076
- const { project: project2, repo, pushToken } = await linkProject(apiUrl, token, {
17247
+ const { project: project2, repo } = await linkProject(apiUrl, token, {
17077
17248
  name,
17078
17249
  framework,
17079
17250
  // First `kryd init` for the account claims the tenant slug (the vanity routing key in
@@ -17087,6 +17258,14 @@ To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete
17087
17258
  let linkNote = "";
17088
17259
  try {
17089
17260
  saveProjectLink(cwd, { projectId: project2.id });
17261
+ try {
17262
+ const session = await whoami(apiUrl, token);
17263
+ rememberLinkAccount(cwd, {
17264
+ accountId: session.accountId,
17265
+ accountSlug: session.accountSlug
17266
+ });
17267
+ } catch {
17268
+ }
17090
17269
  linkNote = "Linked this folder \u2192 .kryd/project.json \u2014 `kryd deploy` / `kryd logs` now work here with no id.\n";
17091
17270
  try {
17092
17271
  ensureKrydIgnored(cwd);
@@ -17103,44 +17282,49 @@ To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete
17103
17282
  );
17104
17283
  }
17105
17284
  const branch = repo.defaultBranch;
17106
- const slug = repo.fullName.split("/")[0] ?? "kryd";
17107
- const authedUrl = authenticatedRemoteUrl(repo.cloneUrl, slug, pushToken);
17108
- const remote = configureGitRemote(cwd, "kryd", authedUrl);
17285
+ const remote = configureGitRemote(cwd, "kryd", repo.cloneUrl);
17286
+ const helper = configureCredentialHelper(cwd, repo.cloneUrl);
17109
17287
  let nextStep;
17110
17288
  switch (remote.status) {
17111
17289
  case "added":
17112
17290
  case "updated":
17113
- nextStep = `${remote.status === "added" ? "Added" : "Updated"} git remote "${remote.remote}" (with your push credential).
17114
- ` + (remote.tracking === "set" ? `Tracking set: \`git push\` and \`kryd push\` both deploy this branch.
17291
+ nextStep = `${remote.status === "added" ? "Added" : "Updated"} git remote "${remote.remote}".
17292
+ ` + (helper === "set" ? "" : `\u26A0\uFE0F Could not register the credential helper, so git will ask for a password on push.
17293
+ Run \`kryd login\` and re-run \`kryd init\` here to fix it.
17294
+ `) + (remote.tracking === "set" ? `Tracking set: \`git push\` and \`kryd push\` both deploy this branch.
17115
17295
  ` : remote.tracking === "kept-existing" ? `This branch already tracks another remote, so it was left alone \u2014 deploy with \`kryd push\`.
17116
17296
  ` : "") + `Next: kryd push # push to deploy (same as \`git push ${remote.remote} ${branch}\`)
17117
17297
  `;
17118
17298
  break;
17119
17299
  case "not-a-repo":
17120
- nextStep = `No git repo here yet. To deploy (the URL carries your push token \u2014 keep it private):
17300
+ nextStep = `No git repo here yet. To deploy:
17121
17301
  git init && git add -A && git commit -m "init"
17122
- git remote add kryd ${authedUrl}
17302
+ git remote add kryd ${repo.cloneUrl}
17303
+ kryd init # again, to register the credential helper
17123
17304
  kryd push
17124
17305
  `;
17125
17306
  break;
17126
17307
  case "unavailable":
17127
17308
  nextStep = gitAvailable(cwd).status === "no-git" ? `git is not installed, so the deploy remote could not be configured.
17128
17309
  Install git (https://git-scm.com/downloads), then re-run \`kryd init\` here.
17129
- ` : `Add the remote, then push to deploy (the URL carries your push token \u2014 keep it private):
17130
- git remote add kryd ${authedUrl}
17310
+ ` : `Add the remote, then push to deploy:
17311
+ git remote add kryd ${repo.cloneUrl}
17131
17312
  kryd push
17132
17313
  `;
17133
17314
  break;
17134
17315
  }
17316
+ const routing = project2.framework === "workflow" ? `No public URL: a workflow worker dials out to the engine and serves nothing.
17317
+ Scheduled and triggered work runs on your production deploy; preview branches build but do not start a worker.
17318
+ ` : `Production URL: https://${project2.subdomain}.${PRODUCTION_TLD}
17319
+ Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
17320
+ `;
17135
17321
  process.stdout.write(
17136
17322
  `Linked "${name}" (${project2.framework}) \u2192 ${repo.htmlUrl}
17137
- Production URL: https://${project2.subdomain}.${PRODUCTION_TLD}
17138
- Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
17139
- ${linkNote}
17323
+ ` + routing + `${linkNote}
17140
17324
  ${nextStep}`
17141
17325
  );
17142
17326
  } catch (err) {
17143
- reportError(err);
17327
+ await reportError(err);
17144
17328
  }
17145
17329
  }
17146
17330
  function formatStatus(event) {
@@ -17256,7 +17440,7 @@ async function runLogs(opts) {
17256
17440
  }
17257
17441
  await followDeploy(apiUrl, token, deploymentId);
17258
17442
  } catch (err) {
17259
- reportError(err);
17443
+ await reportError(err);
17260
17444
  }
17261
17445
  }
17262
17446
  async function runRuntimeLogs(opts) {
@@ -17303,7 +17487,7 @@ async function runRuntimeLogs(opts) {
17303
17487
  }
17304
17488
  );
17305
17489
  } catch (err) {
17306
- reportError(err);
17490
+ await reportError(err);
17307
17491
  } finally {
17308
17492
  process.off("SIGINT", onSigint);
17309
17493
  }
@@ -17327,7 +17511,7 @@ async function runDeploy(opts) {
17327
17511
  `);
17328
17512
  await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
17329
17513
  } catch (err) {
17330
- reportError(err);
17514
+ await reportError(err);
17331
17515
  }
17332
17516
  }
17333
17517
  function renderFor(logs) {
@@ -17465,7 +17649,7 @@ async function runPush(opts) {
17465
17649
  estimateFrom: recent
17466
17650
  });
17467
17651
  } catch (err) {
17468
- reportError(err);
17652
+ await reportError(err);
17469
17653
  }
17470
17654
  }
17471
17655
  async function runRollback(opts) {
@@ -17495,7 +17679,7 @@ async function runRollback(opts) {
17495
17679
  );
17496
17680
  await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
17497
17681
  } catch (err) {
17498
- reportError(err);
17682
+ await reportError(err);
17499
17683
  }
17500
17684
  }
17501
17685
  async function runRedeploy(opts) {
@@ -17524,7 +17708,7 @@ ${note}
17524
17708
  );
17525
17709
  await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
17526
17710
  } catch (err) {
17527
- reportError(err);
17711
+ await reportError(err);
17528
17712
  }
17529
17713
  }
17530
17714
  async function runDbAdd(opts) {
@@ -17589,7 +17773,7 @@ The connection string will be injected as DATABASE_URL on your next deploy.
17589
17773
  retryCmd: `kryd db status ${project2}`
17590
17774
  });
17591
17775
  } catch (err) {
17592
- reportError(err);
17776
+ await reportError(err);
17593
17777
  }
17594
17778
  }
17595
17779
  function reportSettleResult(result, opts) {
@@ -17636,7 +17820,7 @@ Its S3 credentials will be injected on your next deploy.
17636
17820
  retryCmd: `kryd storage status ${project2}`
17637
17821
  });
17638
17822
  } catch (err) {
17639
- reportError(err);
17823
+ await reportError(err);
17640
17824
  }
17641
17825
  }
17642
17826
  function reportDetachResult(result, opts) {
@@ -17679,7 +17863,7 @@ async function resolveProjectIdByName(apiUrl, token, name) {
17679
17863
  try {
17680
17864
  projects = await listProjects(apiUrl, token);
17681
17865
  } catch (err) {
17682
- reportError(err);
17866
+ await reportError(err);
17683
17867
  return null;
17684
17868
  }
17685
17869
  const matches = projects.filter((p) => p.name === name);
@@ -17740,7 +17924,7 @@ async function runProjectRemove(opts) {
17740
17924
  }
17741
17925
  resources = project2.status === "delete_failed" ? ["whatever the previous attempt did not manage to remove"] : await describeProjectResources(apiUrl, token, projectId);
17742
17926
  } catch (err) {
17743
- reportError(err);
17927
+ await reportError(err);
17744
17928
  return;
17745
17929
  }
17746
17930
  if (!opts.yes) {
@@ -17777,7 +17961,7 @@ async function runProjectRemove(opts) {
17777
17961
  );
17778
17962
  await followProjectDeletion(apiUrl, token, projectId, project2.name, opts);
17779
17963
  } catch (err) {
17780
- reportError(err);
17964
+ await reportError(err);
17781
17965
  }
17782
17966
  }
17783
17967
  async function followProjectDeletion(apiUrl, token, projectId, name, opts) {
@@ -17853,7 +18037,7 @@ async function runDbRemove(opts) {
17853
18037
  retryCmd: `kryd db remove ${project2}`
17854
18038
  });
17855
18039
  } catch (err) {
17856
- reportError(err);
18040
+ await reportError(err);
17857
18041
  }
17858
18042
  }
17859
18043
  async function runAiAdd(opts) {
@@ -17878,7 +18062,7 @@ async function runAiAdd(opts) {
17878
18062
  `
17879
18063
  );
17880
18064
  } catch (err) {
17881
- reportError(err);
18065
+ await reportError(err);
17882
18066
  }
17883
18067
  }
17884
18068
  async function runAiRemove(opts) {
@@ -17898,7 +18082,7 @@ async function runAiRemove(opts) {
17898
18082
  try {
17899
18083
  current = await getAiStatus(apiUrl, token, project2);
17900
18084
  } catch (err) {
17901
- reportError(err);
18085
+ await reportError(err);
17902
18086
  return;
17903
18087
  }
17904
18088
  if (!current.enabled) {
@@ -17923,7 +18107,7 @@ AI_GATEWAY_URL and AI_GATEWAY_TOKEN stop being injected from your next deploy (k
17923
18107
  `
17924
18108
  );
17925
18109
  } catch (err) {
17926
- reportError(err);
18110
+ await reportError(err);
17927
18111
  }
17928
18112
  }
17929
18113
  async function reportResourceStatus(opts) {
@@ -17940,12 +18124,12 @@ Remove it before attaching another.
17940
18124
  process.stdout.write(`${opts.project}: ${opts.noun} ${result.status}.
17941
18125
  `);
17942
18126
  } catch (err) {
17943
- if (err instanceof ApiError && err.status === 404) {
18127
+ if (err instanceof ApiError && err.status === 404 && await projectIsVisible(opts.token, opts.project) === true) {
17944
18128
  process.stdout.write(`${opts.project}: no ${opts.noun} attached.
17945
18129
  `);
17946
18130
  return;
17947
18131
  }
17948
- reportError(err);
18132
+ await reportError(err);
17949
18133
  }
17950
18134
  }
17951
18135
  async function runDbStatus(opts) {
@@ -17964,7 +18148,8 @@ async function runDbStatus(opts) {
17964
18148
  await reportResourceStatus({
17965
18149
  read: () => getDatabaseStatus(apiUrl, token, project2),
17966
18150
  noun: "database",
17967
- project: project2
18151
+ project: project2,
18152
+ token
17968
18153
  });
17969
18154
  }
17970
18155
  async function runStorageStatus(opts) {
@@ -17983,7 +18168,8 @@ async function runStorageStatus(opts) {
17983
18168
  await reportResourceStatus({
17984
18169
  read: () => getStorageStatus(apiUrl, token, project2),
17985
18170
  noun: "object storage",
17986
- project: project2
18171
+ project: project2,
18172
+ token
17987
18173
  });
17988
18174
  }
17989
18175
  async function runAiStatus(opts) {
@@ -18012,7 +18198,7 @@ async function runAiStatus(opts) {
18012
18198
  `
18013
18199
  );
18014
18200
  } catch (err) {
18015
- reportError(err);
18201
+ await reportError(err);
18016
18202
  }
18017
18203
  }
18018
18204
  function isoDay(iso) {
@@ -18052,7 +18238,7 @@ HATCHET_CLIENT_TOKEN will be injected on your next deploy (kryd deploy)` + (stat
18052
18238
  ` : ".\n")
18053
18239
  );
18054
18240
  } catch (err) {
18055
- reportError(err);
18241
+ await reportError(err);
18056
18242
  }
18057
18243
  }
18058
18244
  async function runWorkflowRemove(opts) {
@@ -18072,7 +18258,7 @@ async function runWorkflowRemove(opts) {
18072
18258
  try {
18073
18259
  current = await getWorkflowStatus(apiUrl, token, project2);
18074
18260
  } catch (err) {
18075
- reportError(err);
18261
+ await reportError(err);
18076
18262
  return;
18077
18263
  }
18078
18264
  if (!current.enabled) {
@@ -18097,7 +18283,7 @@ Workflow history, crons and schedules stay until the project is deleted.
18097
18283
  `
18098
18284
  );
18099
18285
  } catch (err) {
18100
- reportError(err);
18286
+ await reportError(err);
18101
18287
  }
18102
18288
  }
18103
18289
  async function runWorkflowStatus(opts) {
@@ -18137,7 +18323,7 @@ async function runWorkflowStatus(opts) {
18137
18323
  );
18138
18324
  }
18139
18325
  } catch (err) {
18140
- reportError(err);
18326
+ await reportError(err);
18141
18327
  }
18142
18328
  }
18143
18329
  async function runWorkflowInit(opts) {
@@ -18290,7 +18476,7 @@ async function runStorageRemove(opts) {
18290
18476
  retryCmd: `kryd storage remove ${project2}`
18291
18477
  });
18292
18478
  } catch (err) {
18293
- reportError(err);
18479
+ await reportError(err);
18294
18480
  }
18295
18481
  }
18296
18482
  function splitKeyValue(spec) {
@@ -18329,7 +18515,7 @@ async function runProjectList(opts) {
18329
18515
  try {
18330
18516
  projects = await listProjects(apiUrl, token);
18331
18517
  } catch (err) {
18332
- reportError(err);
18518
+ await reportError(err);
18333
18519
  return;
18334
18520
  }
18335
18521
  if (projects.length === 0) {
@@ -18437,7 +18623,7 @@ Set one with \`kryd env set KEY --stdin\`, or attach a database, storage or the
18437
18623
  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");
18438
18624
  process.stdout.write(out);
18439
18625
  } catch (err) {
18440
- reportError(err);
18626
+ await reportError(err);
18441
18627
  }
18442
18628
  }
18443
18629
  function toDotenvLine(key, value) {
@@ -18506,7 +18692,7 @@ ${body}
18506
18692
  "These are your own values, read back from Kryd. Managed values (DATABASE_URL, storage, the AI gateway) are never pulled.\n"
18507
18693
  );
18508
18694
  } catch (err) {
18509
- reportError(err);
18695
+ await reportError(err);
18510
18696
  }
18511
18697
  }
18512
18698
  async function resolveEnvValue(inline, opts) {
@@ -18639,7 +18825,7 @@ async function runEnvSet(opts) {
18639
18825
  ${NEXT_BUILD_NOTE}` : NEXT_DEPLOY_NOTE)
18640
18826
  );
18641
18827
  } catch (err) {
18642
- reportError(err);
18828
+ await reportError(err);
18643
18829
  }
18644
18830
  }
18645
18831
  async function runEnvRemove(opts) {
@@ -18699,7 +18885,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
18699
18885
  return;
18700
18886
  }
18701
18887
  } catch (err) {
18702
- reportError(err);
18888
+ await reportError(err);
18703
18889
  return;
18704
18890
  }
18705
18891
  }
@@ -18737,18 +18923,29 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
18737
18923
  `)
18738
18924
  );
18739
18925
  } catch (err) {
18740
- reportError(err);
18926
+ await reportError(err);
18741
18927
  }
18742
18928
  }
18743
- var CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
18929
+ var CLI_VERSION = true ? "0.8.1" : "0.0.0-dev";
18744
18930
  var program = new Command();
18745
18931
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
18932
+ program.hook("postAction", async () => {
18933
+ await backfillLinkAccount();
18934
+ });
18746
18935
  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(
18747
18936
  "--token <token>",
18748
18937
  "supply a token instead of the browser flow (prefer $KRYD_TOKEN \u2014 flags are visible in `ps`)"
18749
18938
  ).option("--api-url <url>", "control-plane API base URL").option("--dashboard-url <url>", "dashboard base URL for the browser approve page (default app.kryd.eu)").action((opts) => runLogin(opts));
18750
18939
  program.command("whoami").description("Show the current account").option("--api-url <url>", "control-plane API base URL").action((opts) => runWhoami(opts));
18751
18940
  program.command("logout").description("Clear the stored token").action(() => runLogout());
18941
+ program.command("git-credential <operation>", { hidden: true }).description("git credential helper (invoked by git, not by you)").option("--api-url <url>", "control-plane API base URL").action(async (operation, opts) => {
18942
+ const chunks = [];
18943
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
18944
+ await runGitCredential(operation, {
18945
+ stdin: Buffer.concat(chunks).toString("utf8"),
18946
+ ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
18947
+ });
18948
+ });
18752
18949
  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(
18753
18950
  "--framework <framework>",
18754
18951
  "react-router | nextjs | vite-spa | node (auto-detected if omitted; `workflow` comes from a committed kryd.json)"
@@ -18865,6 +19062,7 @@ if (invokedDirectly()) {
18865
19062
  });
18866
19063
  }
18867
19064
  export {
19065
+ backfillLinkAccount,
18868
19066
  program,
18869
19067
  runAiAdd,
18870
19068
  runAiRemove,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kryd/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.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",
@@ -50,8 +50,8 @@
50
50
  "typescript": "^5.6.3",
51
51
  "vitest": "^2.1.8",
52
52
  "@kryd/shared-types": "0.0.0",
53
- "@kryd/config-eslint": "0.0.0",
54
- "@kryd/config-ts": "0.0.0"
53
+ "@kryd/config-ts": "0.0.0",
54
+ "@kryd/config-eslint": "0.0.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --external:commander --define:__KRYD_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/index.js",