@kryd/cli 0.12.1 → 0.14.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 +8 -0
  2. package/dist/index.js +869 -574
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -14859,6 +14859,22 @@ function validateBuildEnvVarValue(value) {
14859
14859
  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.";
14860
14860
  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.";
14861
14861
 
14862
+ // ../../packages/shared-types/dist/tickets.js
14863
+ var TICKET_STATUSES = ["open", "waiting", "solved"];
14864
+ var TICKET_TYPES = ["question", "bug", "request"];
14865
+ function formatTicketNumber(n) {
14866
+ return `T-${n}`;
14867
+ }
14868
+ function parseTicketNumber(raw) {
14869
+ const m = /^(?:[tT]-?)?(\d{1,9})$/.exec(raw.trim());
14870
+ if (!m)
14871
+ return null;
14872
+ const n = Number(m[1]);
14873
+ return Number.isSafeInteger(n) && n > 0 ? n : null;
14874
+ }
14875
+ var TICKET_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
14876
+ var TICKET_ATTACHMENT_MAX_SIZE_LABEL = `${TICKET_ATTACHMENT_MAX_BYTES / (1024 * 1024)} MB`;
14877
+
14862
14878
  // ../../packages/shared-types/dist/workflow-run.js
14863
14879
  var MAX_WEBHOOK_NAME = 32;
14864
14880
  var WEBHOOK_NAME_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_WEBHOOK_NAME - 2}}[a-z0-9])?$`);
@@ -14920,379 +14936,184 @@ import {
14920
14936
  import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "node:path";
14921
14937
  import { pathToFileURL } from "node:url";
14922
14938
 
14923
- // src/config.ts
14924
- import {
14925
- appendFileSync,
14926
- chmodSync,
14927
- existsSync,
14928
- mkdirSync,
14929
- readFileSync,
14930
- rmSync,
14931
- writeFileSync
14932
- } from "node:fs";
14933
- import { homedir } from "node:os";
14934
- import { dirname, join } from "node:path";
14935
- var DEFAULT_API_URL = "http://localhost:3000";
14936
- function configPath() {
14937
- const dir = process.env.KRYD_CONFIG_DIR ?? join(homedir(), ".kryd");
14938
- return join(dir, "config.json");
14939
- }
14940
- function loadConfig() {
14941
- let raw;
14942
- try {
14943
- raw = readFileSync(configPath(), "utf8");
14944
- } catch {
14945
- return {};
14939
+ // src/client.ts
14940
+ var ApiError = class extends Error {
14941
+ envelope;
14942
+ /**
14943
+ * The HTTP status of the failed response, when it came from one (used to detect a 404 = gone).
14944
+ *
14945
+ * ⚠️ Still optional because an `ApiError` is also thrown for a well-formed response the CLI
14946
+ * cannot use ("the API returned an unexpected …") and for a stream's own error frame — neither
14947
+ * has a status. Every throw that DOES hold a `Response` now passes it (KRYD-481): the 404
14948
+ * diagnosis keys on the status, and a third of them used to omit it, which made the same
14949
+ * failure explainable or unexplainable depending on which verb the customer typed.
14950
+ */
14951
+ status;
14952
+ constructor(message, envelope, status) {
14953
+ super(message);
14954
+ this.name = "ApiError";
14955
+ this.envelope = envelope;
14956
+ this.status = status;
14946
14957
  }
14958
+ };
14959
+ async function parseEnvelope(res) {
14947
14960
  try {
14948
- return JSON.parse(raw);
14961
+ return await res.json();
14949
14962
  } catch {
14950
- process.stderr.write(
14951
- `warning: ${configPath()} is not valid JSON \u2014 ignoring it.
14952
- `
14953
- );
14954
- return {};
14963
+ return void 0;
14955
14964
  }
14956
14965
  }
14957
- function writeConfig(config2) {
14958
- const path = configPath();
14959
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
14960
- writeFileSync(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
14961
- chmodSync(path, 384);
14962
- }
14963
- function saveToken(token, apiUrl) {
14964
- const config2 = loadConfig();
14965
- config2.token = token;
14966
- if (apiUrl) config2.apiUrl = apiUrl;
14967
- writeConfig(config2);
14968
- }
14969
- function clearToken() {
14970
- const config2 = loadConfig();
14971
- delete config2.token;
14972
- writeConfig(config2);
14973
- }
14974
- var lastApiUrl = null;
14975
- function resolveApiUrl(flag) {
14976
- lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
14977
- return lastApiUrl;
14978
- }
14979
- function currentApiUrl() {
14980
- return lastApiUrl ?? resolveApiUrl();
14981
- }
14982
- var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
14983
- function resolveDashboardUrl(flag) {
14984
- return flag ?? process.env.KRYD_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
14985
- }
14986
- var PROJECT_LINK_DIR = ".kryd";
14987
- var PROJECT_LINK_FILE = "project.json";
14988
- function findProjectLinkDir(cwd = process.cwd()) {
14989
- let dir = cwd;
14990
- for (; ; ) {
14991
- if (existsSync(join(dir, PROJECT_LINK_DIR, PROJECT_LINK_FILE))) return dir;
14992
- if (existsSync(join(dir, ".git"))) return null;
14993
- const parent = dirname(dir);
14994
- if (parent === dir) return null;
14995
- dir = parent;
14966
+ async function whoami(apiUrl, token) {
14967
+ const res = await fetch(`${apiUrl}/me`, {
14968
+ headers: { authorization: `Bearer ${token}` }
14969
+ });
14970
+ if (!res.ok) {
14971
+ throw new ApiError(
14972
+ `Request failed (${res.status})`,
14973
+ await parseEnvelope(res),
14974
+ res.status
14975
+ );
14996
14976
  }
14977
+ return await res.json();
14997
14978
  }
14998
- function loadProjectLink(cwd = process.cwd()) {
14999
- const root = findProjectLinkDir(cwd);
15000
- if (!root) return null;
15001
- const path = join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15002
- let raw;
15003
- try {
15004
- raw = readFileSync(path, "utf8");
15005
- } catch {
15006
- return null;
15007
- }
15008
- let parsed;
15009
- try {
15010
- parsed = JSON.parse(raw);
15011
- } catch {
15012
- process.stderr.write(`warning: ${path} is not valid JSON \u2014 ignoring it.
15013
- `);
15014
- return null;
14979
+ async function linkProject(apiUrl, token, input) {
14980
+ const res = await fetch(`${apiUrl}/repos`, {
14981
+ method: "POST",
14982
+ headers: {
14983
+ "content-type": "application/json",
14984
+ authorization: `Bearer ${token}`
14985
+ },
14986
+ body: JSON.stringify(input)
14987
+ });
14988
+ if (!res.ok) {
14989
+ throw new ApiError(
14990
+ `Repo linking failed (${res.status})`,
14991
+ await parseEnvelope(res),
14992
+ res.status
14993
+ );
15015
14994
  }
15016
- if (!parsed || typeof parsed.projectId !== "string" || parsed.projectId.length === 0) {
15017
- process.stderr.write(`warning: ${path} has no projectId \u2014 ignoring it.
15018
- `);
15019
- return null;
14995
+ const parsed = await res.json().catch(() => void 0);
14996
+ if (!parsed || typeof parsed.pushToken !== "string" || parsed.pushToken.length === 0 || typeof parsed.repo?.cloneUrl !== "string" || // `runInit` derives the owner slug from `fullName` (`.split("/")`), so guard it here too —
14997
+ // a 200 missing it must raise the friendly ApiError, not a raw TypeError downstream.
14998
+ typeof parsed.repo?.fullName !== "string" || parsed.repo.fullName.length === 0) {
14999
+ throw new ApiError("The API returned an unexpected repo-link response.");
15020
15000
  }
15021
15001
  return parsed;
15022
15002
  }
15023
- function saveProjectLink(cwd, link) {
15024
- const dir = join(cwd, PROJECT_LINK_DIR);
15025
- mkdirSync(dir, { recursive: true });
15026
- const path = join(dir, PROJECT_LINK_FILE);
15027
- writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
15028
- return path;
15029
- }
15030
- function ensureIgnored(cwd, entry, aliases = []) {
15031
- const path = join(cwd, ".gitignore");
15032
- let current = "";
15033
- try {
15034
- current = readFileSync(path, "utf8");
15035
- } catch {
15003
+ async function attachDatabase(apiUrl, token, input) {
15004
+ const res = await fetch(`${apiUrl}/databases`, {
15005
+ method: "POST",
15006
+ headers: {
15007
+ "content-type": "application/json",
15008
+ authorization: `Bearer ${token}`
15009
+ },
15010
+ body: JSON.stringify({
15011
+ projectId: input.projectId,
15012
+ mode: input.mode,
15013
+ // Conditional (exactOptionalPropertyTypes) — only BYO carries a connection string.
15014
+ ...input.connectionString ? { connectionString: input.connectionString } : {}
15015
+ })
15016
+ });
15017
+ if (!res.ok) {
15018
+ throw new ApiError(
15019
+ `Database attach failed (${res.status})`,
15020
+ await parseEnvelope(res),
15021
+ res.status
15022
+ );
15036
15023
  }
15037
- const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
15038
- if (present) return false;
15039
- const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
15040
- appendFileSync(path, `${prefix}${entry}
15041
- `);
15042
- return true;
15043
- }
15044
- function ensureKrydIgnored(cwd) {
15045
- ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
15046
- }
15047
- function directoryIsLinked(cwd) {
15048
- const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15049
- if (!existsSync(path)) return null;
15050
- return loadProjectLink(cwd);
15024
+ const parsed = await res.json().catch(() => void 0);
15025
+ if (!parsed || typeof parsed.id !== "string" || parsed.id.length === 0 || !DATABASE_MODES.includes(parsed.mode) || !RESOURCE_STATUSES.includes(parsed.status)) {
15026
+ throw new ApiError("The API returned an unexpected database response.");
15027
+ }
15028
+ return {
15029
+ id: parsed.id,
15030
+ mode: parsed.mode,
15031
+ status: parsed.status
15032
+ };
15051
15033
  }
15052
- function writeSecretFile(path, contents) {
15053
- mkdirSync(dirname(path), { recursive: true });
15054
- writeFileSync(path, contents, { mode: 384 });
15055
- chmodSync(path, 384);
15034
+ async function createStorage(apiUrl, token, input) {
15035
+ const res = await fetch(`${apiUrl}/storage`, {
15036
+ method: "POST",
15037
+ headers: {
15038
+ "content-type": "application/json",
15039
+ authorization: `Bearer ${token}`
15040
+ },
15041
+ body: JSON.stringify({ projectId: input.projectId })
15042
+ });
15043
+ if (!res.ok) {
15044
+ throw new ApiError(
15045
+ `Storage create failed (${res.status})`,
15046
+ await parseEnvelope(res),
15047
+ res.status
15048
+ );
15049
+ }
15050
+ const parsed = await res.json().catch(() => void 0);
15051
+ if (!parsed || typeof parsed.id !== "string" || parsed.id.length === 0 || !RESOURCE_STATUSES.includes(parsed.status)) {
15052
+ throw new ApiError("The API returned an unexpected storage response.");
15053
+ }
15054
+ return { id: parsed.id, status: parsed.status };
15056
15055
  }
15057
- function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
15058
- const root = findProjectLinkDir(cwd);
15059
- if (!root) return false;
15060
- const link = loadProjectLink(cwd);
15061
- if (link?.projectId !== projectId) return false;
15062
- try {
15063
- rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
15064
- return true;
15065
- } catch {
15066
- return false;
15056
+ async function enableAi(apiUrl, token, input) {
15057
+ const res = await fetch(`${apiUrl}/ai/enable`, {
15058
+ method: "POST",
15059
+ headers: {
15060
+ "content-type": "application/json",
15061
+ authorization: `Bearer ${token}`
15062
+ },
15063
+ body: JSON.stringify({ projectId: input.projectId })
15064
+ });
15065
+ if (!res.ok) {
15066
+ throw new ApiError(
15067
+ `AI enable failed (${res.status})`,
15068
+ await parseEnvelope(res),
15069
+ res.status
15070
+ );
15071
+ }
15072
+ const parsed = await res.json().catch(() => void 0);
15073
+ if (!parsed || parsed.enabled !== true || typeof parsed.url !== "string" && parsed.url !== null) {
15074
+ throw new ApiError("The API returned an unexpected AI-enable response.");
15067
15075
  }
15076
+ return { enabled: true, url: parsed.url };
15068
15077
  }
15069
- function resolvedLink() {
15070
- return lastLinkResolution;
15071
- }
15072
- var lastLinkResolution = null;
15073
- function resolveProjectId(explicit, cwd = process.cwd()) {
15074
- lastLinkResolution = null;
15075
- if (explicit) return explicit;
15076
- const root = findProjectLinkDir(cwd);
15077
- if (!root) return null;
15078
- const link = loadProjectLink(cwd);
15079
- if (!link) return null;
15080
- lastLinkResolution = { root, link };
15081
- return link.projectId;
15082
- }
15083
- function rememberLinkAccount(root, account) {
15084
- if (!account.accountId) return false;
15085
- const link = directoryIsLinked(root);
15086
- if (!link) return false;
15087
- const slug = account.accountSlug ?? link.accountSlug;
15088
- if (link.accountId === account.accountId && link.accountSlug === slug) return false;
15089
- saveProjectLink(root, {
15090
- ...link,
15091
- accountId: account.accountId,
15092
- // `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
15093
- ...slug ? { accountSlug: slug } : {}
15094
- });
15095
- return true;
15096
- }
15097
-
15098
- // src/browser-login.ts
15099
- import { spawn } from "node:child_process";
15100
- import { randomUUID } from "node:crypto";
15101
- import { createServer } from "node:http";
15102
- var page = (title, body) => `<!doctype html><html><head><meta charset="utf-8"><title>Kryd CLI</title></head><body style="font-family:system-ui,-apple-system,sans-serif;text-align:center;padding:3rem;color:#1a2634"><h1>${title}</h1><p>${body}</p></body></html>`;
15103
- var SUCCESS_HTML = page("\u2713 Logged in to Kryd", "You can close this tab and return to your terminal.");
15104
- var FAIL_HTML = page("Login failed", "Please return to your terminal and run <code>kryd login</code> again.");
15105
- function openBrowser(url2) {
15106
- const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : ["xdg-open", [url2]];
15107
- try {
15108
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
15109
- } catch {
15110
- }
15111
- }
15112
- function browserLogin(dashboardUrl, opts = {}) {
15113
- const state = randomUUID();
15114
- const timeoutMs = opts.timeoutMs ?? 12e4;
15115
- const shouldOpen = opts.open ?? true;
15116
- return new Promise((resolve3, reject) => {
15117
- let settled = false;
15118
- const settle = (fn) => {
15119
- if (settled) return;
15120
- settled = true;
15121
- fn();
15122
- };
15123
- const server = createServer((req, res) => {
15124
- res.setHeader("connection", "close");
15125
- const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
15126
- if (reqUrl.pathname !== "/callback") {
15127
- res.writeHead(404);
15128
- res.end();
15129
- return;
15130
- }
15131
- const token = reqUrl.searchParams.get("token");
15132
- const apiUrl = reqUrl.searchParams.get("apiUrl") ?? void 0;
15133
- const gotState = reqUrl.searchParams.get("state");
15134
- if (!token || gotState !== state) {
15135
- res.writeHead(400, { "content-type": "text/html" });
15136
- res.end(FAIL_HTML, () => {
15137
- settle(() => {
15138
- shutdown();
15139
- reject(new Error("Login failed \u2014 the approval didn't match this request. Try `kryd login` again."));
15140
- });
15141
- });
15142
- return;
15143
- }
15144
- res.writeHead(200, { "content-type": "text/html" });
15145
- res.end(SUCCESS_HTML, () => {
15146
- settle(() => {
15147
- shutdown();
15148
- resolve3({ token, ...apiUrl ? { apiUrl } : {} });
15149
- });
15150
- });
15151
- });
15152
- const shutdown = () => {
15153
- server.closeAllConnections();
15154
- server.close();
15155
- };
15156
- server.on("error", (err) => settle(() => reject(err)));
15157
- server.listen(0, "127.0.0.1", () => {
15158
- const port = server.address().port;
15159
- const authorizeUrl = `${dashboardUrl.replace(/\/$/, "")}/cli/authorize?port=${port}&state=${state}`;
15160
- process.stdout.write(
15161
- `
15162
- To sign in, approve the CLI in your browser:
15163
- ${authorizeUrl}
15164
-
15165
- Waiting for approval\u2026
15166
- `
15167
- );
15168
- if (shouldOpen) openBrowser(authorizeUrl);
15169
- });
15170
- setTimeout(
15171
- () => settle(() => {
15172
- shutdown();
15173
- reject(new Error("Login timed out after 2 minutes."));
15174
- }),
15175
- timeoutMs
15176
- ).unref();
15177
- });
15178
- }
15179
-
15180
- // src/client.ts
15181
- var ApiError = class extends Error {
15182
- envelope;
15183
- /**
15184
- * The HTTP status of the failed response, when it came from one (used to detect a 404 = gone).
15185
- *
15186
- * ⚠️ Still optional because an `ApiError` is also thrown for a well-formed response the CLI
15187
- * cannot use ("the API returned an unexpected …") and for a stream's own error frame — neither
15188
- * has a status. Every throw that DOES hold a `Response` now passes it (KRYD-481): the 404
15189
- * diagnosis keys on the status, and a third of them used to omit it, which made the same
15190
- * failure explainable or unexplainable depending on which verb the customer typed.
15191
- */
15192
- status;
15193
- constructor(message, envelope, status) {
15194
- super(message);
15195
- this.name = "ApiError";
15196
- this.envelope = envelope;
15197
- this.status = status;
15198
- }
15199
- };
15200
- async function parseEnvelope(res) {
15201
- try {
15202
- return await res.json();
15203
- } catch {
15204
- return void 0;
15205
- }
15206
- }
15207
- async function login(apiUrl, creds) {
15208
- const res = await fetch(`${apiUrl}/api/auth/sign-in/email`, {
15209
- method: "POST",
15210
- headers: { "content-type": "application/json", origin: apiUrl },
15211
- body: JSON.stringify(creds)
15212
- });
15213
- if (!res.ok) {
15214
- throw new ApiError(
15215
- `Login failed (${res.status})`,
15216
- await parseEnvelope(res),
15217
- res.status
15218
- );
15219
- }
15220
- const token = res.headers.get("set-auth-token");
15221
- if (!token) {
15222
- throw new ApiError("Login succeeded but the API returned no token.");
15223
- }
15224
- return token;
15225
- }
15226
- async function whoami(apiUrl, token) {
15227
- const res = await fetch(`${apiUrl}/me`, {
15078
+ async function getAiStatus(apiUrl, token, projectId) {
15079
+ const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15228
15080
  headers: { authorization: `Bearer ${token}` }
15229
15081
  });
15230
15082
  if (!res.ok) {
15231
15083
  throw new ApiError(
15232
- `Request failed (${res.status})`,
15233
- await parseEnvelope(res),
15234
- res.status
15235
- );
15236
- }
15237
- return await res.json();
15238
- }
15239
- async function linkProject(apiUrl, token, input) {
15240
- const res = await fetch(`${apiUrl}/repos`, {
15241
- method: "POST",
15242
- headers: {
15243
- "content-type": "application/json",
15244
- authorization: `Bearer ${token}`
15245
- },
15246
- body: JSON.stringify(input)
15247
- });
15248
- if (!res.ok) {
15249
- throw new ApiError(
15250
- `Repo linking failed (${res.status})`,
15084
+ `AI status check failed (${res.status})`,
15251
15085
  await parseEnvelope(res),
15252
15086
  res.status
15253
15087
  );
15254
15088
  }
15255
15089
  const parsed = await res.json().catch(() => void 0);
15256
- if (!parsed || typeof parsed.pushToken !== "string" || parsed.pushToken.length === 0 || typeof parsed.repo?.cloneUrl !== "string" || // `runInit` derives the owner slug from `fullName` (`.split("/")`), so guard it here too —
15257
- // a 200 missing it must raise the friendly ApiError, not a raw TypeError downstream.
15258
- typeof parsed.repo?.fullName !== "string" || parsed.repo.fullName.length === 0) {
15259
- throw new ApiError("The API returned an unexpected repo-link response.");
15090
+ if (!parsed || typeof parsed.enabled !== "boolean" || typeof parsed.url !== "string" && parsed.url !== null) {
15091
+ throw new ApiError("The API returned an unexpected AI-status response.");
15260
15092
  }
15261
- return parsed;
15093
+ return { enabled: parsed.enabled, url: parsed.url };
15262
15094
  }
15263
- async function attachDatabase(apiUrl, token, input) {
15264
- const res = await fetch(`${apiUrl}/databases`, {
15265
- method: "POST",
15266
- headers: {
15267
- "content-type": "application/json",
15268
- authorization: `Bearer ${token}`
15269
- },
15270
- body: JSON.stringify({
15271
- projectId: input.projectId,
15272
- mode: input.mode,
15273
- // Conditional (exactOptionalPropertyTypes) — only BYO carries a connection string.
15274
- ...input.connectionString ? { connectionString: input.connectionString } : {}
15275
- })
15095
+ async function removeAi(apiUrl, token, projectId) {
15096
+ const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15097
+ method: "DELETE",
15098
+ headers: { authorization: `Bearer ${token}` }
15276
15099
  });
15277
15100
  if (!res.ok) {
15278
15101
  throw new ApiError(
15279
- `Database attach failed (${res.status})`,
15102
+ `AI removal failed (${res.status})`,
15280
15103
  await parseEnvelope(res),
15281
15104
  res.status
15282
15105
  );
15283
15106
  }
15284
- const parsed = await res.json().catch(() => void 0);
15285
- if (!parsed || typeof parsed.id !== "string" || parsed.id.length === 0 || !DATABASE_MODES.includes(parsed.mode) || !RESOURCE_STATUSES.includes(parsed.status)) {
15286
- throw new ApiError("The API returned an unexpected database response.");
15107
+ }
15108
+ function parseWorkflowStatus(body, label) {
15109
+ const parsed = body;
15110
+ if (!parsed || typeof parsed.enabled !== "boolean" || typeof parsed.tokenExpiresAt !== "string" && parsed.tokenExpiresAt !== null) {
15111
+ throw new ApiError(`The API returned an unexpected ${label} response.`);
15287
15112
  }
15288
- return {
15289
- id: parsed.id,
15290
- mode: parsed.mode,
15291
- status: parsed.status
15292
- };
15113
+ return { enabled: parsed.enabled, tokenExpiresAt: parsed.tokenExpiresAt };
15293
15114
  }
15294
- async function createStorage(apiUrl, token, input) {
15295
- const res = await fetch(`${apiUrl}/storage`, {
15115
+ async function addWorkflows(apiUrl, token, input) {
15116
+ const res = await fetch(`${apiUrl}/workflows`, {
15296
15117
  method: "POST",
15297
15118
  headers: {
15298
15119
  "content-type": "application/json",
@@ -15302,115 +15123,34 @@ async function createStorage(apiUrl, token, input) {
15302
15123
  });
15303
15124
  if (!res.ok) {
15304
15125
  throw new ApiError(
15305
- `Storage create failed (${res.status})`,
15126
+ `Workflow add failed (${res.status})`,
15306
15127
  await parseEnvelope(res),
15307
15128
  res.status
15308
15129
  );
15309
15130
  }
15310
- const parsed = await res.json().catch(() => void 0);
15311
- if (!parsed || typeof parsed.id !== "string" || parsed.id.length === 0 || !RESOURCE_STATUSES.includes(parsed.status)) {
15312
- throw new ApiError("The API returned an unexpected storage response.");
15313
- }
15314
- return { id: parsed.id, status: parsed.status };
15131
+ return parseWorkflowStatus(await res.json().catch(() => void 0), "workflow-add");
15315
15132
  }
15316
- async function enableAi(apiUrl, token, input) {
15317
- const res = await fetch(`${apiUrl}/ai/enable`, {
15318
- method: "POST",
15319
- headers: {
15320
- "content-type": "application/json",
15321
- authorization: `Bearer ${token}`
15322
- },
15323
- body: JSON.stringify({ projectId: input.projectId })
15133
+ async function getWorkflowStatus(apiUrl, token, projectId) {
15134
+ const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
15135
+ headers: { authorization: `Bearer ${token}` }
15324
15136
  });
15325
15137
  if (!res.ok) {
15326
15138
  throw new ApiError(
15327
- `AI enable failed (${res.status})`,
15139
+ `Workflow status check failed (${res.status})`,
15328
15140
  await parseEnvelope(res),
15329
15141
  res.status
15330
15142
  );
15331
15143
  }
15332
- const parsed = await res.json().catch(() => void 0);
15333
- if (!parsed || parsed.enabled !== true || typeof parsed.url !== "string" && parsed.url !== null) {
15334
- throw new ApiError("The API returned an unexpected AI-enable response.");
15335
- }
15336
- return { enabled: true, url: parsed.url };
15144
+ return parseWorkflowStatus(await res.json().catch(() => void 0), "workflow-status");
15337
15145
  }
15338
- async function getAiStatus(apiUrl, token, projectId) {
15339
- const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15146
+ async function removeWorkflows(apiUrl, token, projectId) {
15147
+ const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
15148
+ method: "DELETE",
15340
15149
  headers: { authorization: `Bearer ${token}` }
15341
15150
  });
15342
15151
  if (!res.ok) {
15343
15152
  throw new ApiError(
15344
- `AI status check failed (${res.status})`,
15345
- await parseEnvelope(res),
15346
- res.status
15347
- );
15348
- }
15349
- const parsed = await res.json().catch(() => void 0);
15350
- if (!parsed || typeof parsed.enabled !== "boolean" || typeof parsed.url !== "string" && parsed.url !== null) {
15351
- throw new ApiError("The API returned an unexpected AI-status response.");
15352
- }
15353
- return { enabled: parsed.enabled, url: parsed.url };
15354
- }
15355
- async function removeAi(apiUrl, token, projectId) {
15356
- const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15357
- method: "DELETE",
15358
- headers: { authorization: `Bearer ${token}` }
15359
- });
15360
- if (!res.ok) {
15361
- throw new ApiError(
15362
- `AI removal failed (${res.status})`,
15363
- await parseEnvelope(res),
15364
- res.status
15365
- );
15366
- }
15367
- }
15368
- function parseWorkflowStatus(body, label) {
15369
- const parsed = body;
15370
- if (!parsed || typeof parsed.enabled !== "boolean" || typeof parsed.tokenExpiresAt !== "string" && parsed.tokenExpiresAt !== null) {
15371
- throw new ApiError(`The API returned an unexpected ${label} response.`);
15372
- }
15373
- return { enabled: parsed.enabled, tokenExpiresAt: parsed.tokenExpiresAt };
15374
- }
15375
- async function addWorkflows(apiUrl, token, input) {
15376
- const res = await fetch(`${apiUrl}/workflows`, {
15377
- method: "POST",
15378
- headers: {
15379
- "content-type": "application/json",
15380
- authorization: `Bearer ${token}`
15381
- },
15382
- body: JSON.stringify({ projectId: input.projectId })
15383
- });
15384
- if (!res.ok) {
15385
- throw new ApiError(
15386
- `Workflow add failed (${res.status})`,
15387
- await parseEnvelope(res),
15388
- res.status
15389
- );
15390
- }
15391
- return parseWorkflowStatus(await res.json().catch(() => void 0), "workflow-add");
15392
- }
15393
- async function getWorkflowStatus(apiUrl, token, projectId) {
15394
- const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
15395
- headers: { authorization: `Bearer ${token}` }
15396
- });
15397
- if (!res.ok) {
15398
- throw new ApiError(
15399
- `Workflow status check failed (${res.status})`,
15400
- await parseEnvelope(res),
15401
- res.status
15402
- );
15403
- }
15404
- return parseWorkflowStatus(await res.json().catch(() => void 0), "workflow-status");
15405
- }
15406
- async function removeWorkflows(apiUrl, token, projectId) {
15407
- const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
15408
- method: "DELETE",
15409
- headers: { authorization: `Bearer ${token}` }
15410
- });
15411
- if (!res.ok) {
15412
- throw new ApiError(
15413
- `Workflow removal failed (${res.status})`,
15153
+ `Workflow removal failed (${res.status})`,
15414
15154
  await parseEnvelope(res),
15415
15155
  res.status
15416
15156
  );
@@ -15945,153 +15685,709 @@ async function pollForNewDeployments(fetchDeployments, match, opts) {
15945
15685
  await sleep(intervalMs);
15946
15686
  }
15947
15687
  }
15948
- function dataFromFrame(frame) {
15949
- const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
15950
- return data.length > 0 ? data : void 0;
15688
+ function dataFromFrame(frame) {
15689
+ const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
15690
+ return data.length > 0 ? data : void 0;
15691
+ }
15692
+ async function* readSseFrames(body) {
15693
+ const reader = body.getReader();
15694
+ const decoder = new TextDecoder();
15695
+ let buffer = "";
15696
+ try {
15697
+ for (; ; ) {
15698
+ const { value, done } = await reader.read();
15699
+ if (done) return;
15700
+ buffer += decoder.decode(value, { stream: true });
15701
+ let sep;
15702
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
15703
+ const data = dataFromFrame(buffer.slice(0, sep));
15704
+ buffer = buffer.slice(sep + 2);
15705
+ if (data !== void 0) yield data;
15706
+ }
15707
+ }
15708
+ } finally {
15709
+ await reader.cancel().catch(() => void 0);
15710
+ }
15711
+ }
15712
+ function abortableSleep(ms, signal) {
15713
+ if (signal?.aborted) return Promise.resolve();
15714
+ return new Promise((resolve3) => {
15715
+ const onAbort = () => {
15716
+ clearTimeout(timer);
15717
+ resolve3();
15718
+ };
15719
+ const timer = setTimeout(() => {
15720
+ signal?.removeEventListener("abort", onAbort);
15721
+ resolve3();
15722
+ }, ms);
15723
+ signal?.addEventListener("abort", onAbort, { once: true });
15724
+ });
15725
+ }
15726
+ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15727
+ const doFetch = opts.fetchImpl ?? fetch;
15728
+ const reconnectMs = opts.reconnectMs ?? 1e3;
15729
+ let terminal;
15730
+ while (terminal === void 0) {
15731
+ let dropped = false;
15732
+ try {
15733
+ const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/logs`, {
15734
+ headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
15735
+ });
15736
+ if (!res.ok) {
15737
+ throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res), res.status);
15738
+ }
15739
+ if (!res.body) throw new Error("The log stream returned no body.");
15740
+ for await (const data of readSseFrames(res.body)) {
15741
+ const event = parseDeployEvent(data);
15742
+ if (!event) continue;
15743
+ if (event.kind === "log") {
15744
+ handlers.onLog(event.line);
15745
+ } else {
15746
+ handlers.onStatus(event);
15747
+ if (isTerminalDeployState(event.status)) {
15748
+ terminal = event.status;
15749
+ break;
15750
+ }
15751
+ }
15752
+ }
15753
+ if (terminal === void 0) dropped = true;
15754
+ } catch (err) {
15755
+ if (err instanceof ApiError) throw err;
15756
+ dropped = true;
15757
+ }
15758
+ if (terminal === void 0 && dropped) {
15759
+ await new Promise((r) => setTimeout(r, reconnectMs));
15760
+ }
15761
+ }
15762
+ return terminal;
15763
+ }
15764
+ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15765
+ const path = opts.resource === "deployment" ? `deployments/${id}/runtime-logs` : opts.resource === "worker" ? `projects/${id}/workflow-logs` : `projects/${id}/runtime-logs`;
15766
+ const doFetch = opts.fetchImpl ?? fetch;
15767
+ const reconnectMs = opts.reconnectMs ?? 1e3;
15768
+ let afterNs;
15769
+ while (!opts.signal?.aborted) {
15770
+ try {
15771
+ const url2 = new URL(`${apiUrl}/${path}`);
15772
+ if (opts.app && (opts.resource ?? "project") === "project") url2.searchParams.set("app", opts.app);
15773
+ if (afterNs) url2.searchParams.set("after", afterNs);
15774
+ else if (opts.since) url2.searchParams.set("since", opts.since);
15775
+ const res = await doFetch(url2, {
15776
+ headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" },
15777
+ ...opts.signal ? { signal: opts.signal } : {}
15778
+ });
15779
+ if (!res.ok) {
15780
+ throw new ApiError(
15781
+ `Runtime log stream failed (${res.status})`,
15782
+ await parseEnvelope(res),
15783
+ res.status
15784
+ );
15785
+ }
15786
+ if (!res.body) throw new Error("The runtime log stream returned no body.");
15787
+ for await (const data of readSseFrames(res.body)) {
15788
+ const frame = parseRuntimeStreamFrame(data);
15789
+ if (!frame) continue;
15790
+ if (frame.kind === "error") throw new ApiError(frame.message);
15791
+ handlers.onLine({ stream: frame.stream, line: frame.line });
15792
+ if (frame.ts) afterNs = frame.ts;
15793
+ }
15794
+ } catch (err) {
15795
+ if (opts.signal?.aborted) return;
15796
+ if (err instanceof ApiError) throw err;
15797
+ }
15798
+ if (!opts.signal?.aborted) {
15799
+ await abortableSleep(reconnectMs, opts.signal);
15800
+ }
15801
+ }
15802
+ }
15803
+ async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
15804
+ const doFetch = opts.fetchImpl ?? fetch;
15805
+ const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/log`, {
15806
+ headers: { authorization: `Bearer ${token}` }
15807
+ });
15808
+ if (res.status === 404) return null;
15809
+ if (!res.ok) {
15810
+ throw new ApiError(
15811
+ `Fetching the stored log failed (${res.status})`,
15812
+ await parseEnvelope(res),
15813
+ res.status
15814
+ );
15815
+ }
15816
+ return (await res.json()).log;
15817
+ }
15818
+ async function fetchPushCredential(apiUrl, token, projectId) {
15819
+ const res = await fetch(
15820
+ `${apiUrl}/repos/${encodeURIComponent(projectId)}/push-credential`,
15821
+ { headers: { authorization: `Bearer ${token}` } }
15822
+ );
15823
+ if (!res.ok) {
15824
+ throw new ApiError(
15825
+ `Could not fetch the push credential (${res.status})`,
15826
+ await parseEnvelope(res),
15827
+ res.status
15828
+ );
15829
+ }
15830
+ const parsed = await res.json().catch(() => void 0);
15831
+ if (!parsed || typeof parsed.username !== "string" || typeof parsed.password !== "string" || parsed.password.length === 0) {
15832
+ throw new ApiError("The API returned an unexpected push-credential response.");
15833
+ }
15834
+ return { username: parsed.username, password: parsed.password };
15835
+ }
15836
+ function isSupportTicket(v) {
15837
+ const t = v;
15838
+ return typeof t === "object" && t !== null && typeof t.ref === "string" && typeof t.subject === "string" && typeof t.status === "string" && typeof t.type === "string" && (t.project === null || t.project === void 0 || typeof t.project?.name === "string");
15839
+ }
15840
+ function isSupportDetail(v) {
15841
+ return isSupportTicket(v) && Array.isArray(v.messages) && v.messages.every(
15842
+ (m) => typeof m?.text === "string" && typeof m?.side === "string" && typeof m?.createdAt === "string"
15843
+ );
15844
+ }
15845
+ var unexpected = (what) => new ApiError(`The API returned ${what} in a shape this CLI does not recognise`);
15846
+ async function supportFetch(apiUrl, token, path, what, init = {}) {
15847
+ const res = await fetch(`${apiUrl}${path}`, {
15848
+ method: init.method ?? "GET",
15849
+ headers: {
15850
+ authorization: `Bearer ${token}`,
15851
+ ...init.body !== void 0 ? { "content-type": "application/json" } : {}
15852
+ },
15853
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
15854
+ });
15855
+ if (!res.ok) {
15856
+ throw new ApiError(`${what} failed (${res.status})`, await parseEnvelope(res), res.status);
15857
+ }
15858
+ try {
15859
+ return await res.json();
15860
+ } catch {
15861
+ throw unexpected(`an answer to "${what.toLowerCase()}"`);
15862
+ }
15863
+ }
15864
+ async function createSupportTicket(apiUrl, token, input) {
15865
+ const body = await supportFetch(apiUrl, token, "/support/tickets", "Opening the ticket", {
15866
+ method: "POST",
15867
+ body: {
15868
+ type: input.type,
15869
+ subject: input.subject,
15870
+ text: input.text,
15871
+ ...input.projectId ? { projectId: input.projectId } : {},
15872
+ via: "cli"
15873
+ }
15874
+ });
15875
+ if (!isSupportTicket(body)) throw unexpected("the new ticket");
15876
+ return body;
15877
+ }
15878
+ async function listSupportTickets(apiUrl, token) {
15879
+ const body = await supportFetch(apiUrl, token, "/support/tickets", "Listing the tickets");
15880
+ if (!Array.isArray(body?.items) || !body.items.every(isSupportTicket)) throw unexpected("the tickets");
15881
+ return body.items;
15882
+ }
15883
+ async function getSupportTicket(apiUrl, token, ref) {
15884
+ const body = await supportFetch(apiUrl, token, `/support/tickets/${encodeURIComponent(ref)}`, "Reading the ticket");
15885
+ if (!isSupportDetail(body)) throw unexpected("the ticket");
15886
+ return body;
15887
+ }
15888
+ async function replySupportTicket(apiUrl, token, ref, text) {
15889
+ const body = await supportFetch(
15890
+ apiUrl,
15891
+ token,
15892
+ `/support/tickets/${encodeURIComponent(ref)}/messages`,
15893
+ "Sending the reply",
15894
+ { method: "POST", body: { text } }
15895
+ );
15896
+ if (!isSupportDetail(body)) throw unexpected("the ticket");
15897
+ return body;
15898
+ }
15899
+
15900
+ // src/config.ts
15901
+ import {
15902
+ appendFileSync,
15903
+ chmodSync,
15904
+ existsSync,
15905
+ mkdirSync,
15906
+ readFileSync,
15907
+ rmSync,
15908
+ writeFileSync
15909
+ } from "node:fs";
15910
+ import { homedir } from "node:os";
15911
+ import { dirname, join } from "node:path";
15912
+ var DEFAULT_API_URL = "http://localhost:3000";
15913
+ function configPath() {
15914
+ const dir = process.env.KRYD_CONFIG_DIR ?? join(homedir(), ".kryd");
15915
+ return join(dir, "config.json");
15916
+ }
15917
+ function loadConfig() {
15918
+ let raw;
15919
+ try {
15920
+ raw = readFileSync(configPath(), "utf8");
15921
+ } catch {
15922
+ return {};
15923
+ }
15924
+ try {
15925
+ return JSON.parse(raw);
15926
+ } catch {
15927
+ process.stderr.write(
15928
+ `warning: ${configPath()} is not valid JSON \u2014 ignoring it.
15929
+ `
15930
+ );
15931
+ return {};
15932
+ }
15933
+ }
15934
+ function writeConfig(config2) {
15935
+ const path = configPath();
15936
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
15937
+ writeFileSync(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
15938
+ chmodSync(path, 384);
15939
+ }
15940
+ function saveToken(token, apiUrl) {
15941
+ const config2 = loadConfig();
15942
+ config2.token = token;
15943
+ if (apiUrl) config2.apiUrl = apiUrl;
15944
+ writeConfig(config2);
15945
+ }
15946
+ function clearToken() {
15947
+ const config2 = loadConfig();
15948
+ delete config2.token;
15949
+ writeConfig(config2);
15950
+ }
15951
+ var lastApiUrl = null;
15952
+ function resolveApiUrl(flag) {
15953
+ lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
15954
+ return lastApiUrl;
15955
+ }
15956
+ function currentApiUrl() {
15957
+ return lastApiUrl ?? resolveApiUrl();
15958
+ }
15959
+ var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
15960
+ function resolveDashboardUrl(flag) {
15961
+ return flag ?? process.env.KRYD_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
15962
+ }
15963
+ var PROJECT_LINK_DIR = ".kryd";
15964
+ var PROJECT_LINK_FILE = "project.json";
15965
+ function findProjectLinkDir(cwd = process.cwd()) {
15966
+ let dir = cwd;
15967
+ for (; ; ) {
15968
+ if (existsSync(join(dir, PROJECT_LINK_DIR, PROJECT_LINK_FILE))) return dir;
15969
+ if (existsSync(join(dir, ".git"))) return null;
15970
+ const parent = dirname(dir);
15971
+ if (parent === dir) return null;
15972
+ dir = parent;
15973
+ }
15974
+ }
15975
+ function loadProjectLink(cwd = process.cwd()) {
15976
+ const root = findProjectLinkDir(cwd);
15977
+ if (!root) return null;
15978
+ const path = join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15979
+ let raw;
15980
+ try {
15981
+ raw = readFileSync(path, "utf8");
15982
+ } catch {
15983
+ return null;
15984
+ }
15985
+ let parsed;
15986
+ try {
15987
+ parsed = JSON.parse(raw);
15988
+ } catch {
15989
+ process.stderr.write(`warning: ${path} is not valid JSON \u2014 ignoring it.
15990
+ `);
15991
+ return null;
15992
+ }
15993
+ if (!parsed || typeof parsed.projectId !== "string" || parsed.projectId.length === 0) {
15994
+ process.stderr.write(`warning: ${path} has no projectId \u2014 ignoring it.
15995
+ `);
15996
+ return null;
15997
+ }
15998
+ return parsed;
15999
+ }
16000
+ function saveProjectLink(cwd, link) {
16001
+ const dir = join(cwd, PROJECT_LINK_DIR);
16002
+ mkdirSync(dir, { recursive: true });
16003
+ const path = join(dir, PROJECT_LINK_FILE);
16004
+ writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
16005
+ return path;
16006
+ }
16007
+ function ensureIgnored(cwd, entry, aliases = []) {
16008
+ const path = join(cwd, ".gitignore");
16009
+ let current = "";
16010
+ try {
16011
+ current = readFileSync(path, "utf8");
16012
+ } catch {
16013
+ }
16014
+ const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
16015
+ if (present) return false;
16016
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
16017
+ appendFileSync(path, `${prefix}${entry}
16018
+ `);
16019
+ return true;
16020
+ }
16021
+ function ensureKrydIgnored(cwd) {
16022
+ ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
16023
+ }
16024
+ function directoryIsLinked(cwd) {
16025
+ const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
16026
+ if (!existsSync(path)) return null;
16027
+ return loadProjectLink(cwd);
16028
+ }
16029
+ function writeSecretFile(path, contents) {
16030
+ mkdirSync(dirname(path), { recursive: true });
16031
+ writeFileSync(path, contents, { mode: 384 });
16032
+ chmodSync(path, 384);
16033
+ }
16034
+ function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
16035
+ const root = findProjectLinkDir(cwd);
16036
+ if (!root) return false;
16037
+ const link = loadProjectLink(cwd);
16038
+ if (link?.projectId !== projectId) return false;
16039
+ try {
16040
+ rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
16041
+ return true;
16042
+ } catch {
16043
+ return false;
16044
+ }
16045
+ }
16046
+ function resolvedLink() {
16047
+ return lastLinkResolution;
16048
+ }
16049
+ var lastLinkResolution = null;
16050
+ function resolveProjectId(explicit, cwd = process.cwd()) {
16051
+ lastLinkResolution = null;
16052
+ if (explicit) return explicit;
16053
+ const root = findProjectLinkDir(cwd);
16054
+ if (!root) return null;
16055
+ const link = loadProjectLink(cwd);
16056
+ if (!link) return null;
16057
+ lastLinkResolution = { root, link };
16058
+ return link.projectId;
16059
+ }
16060
+ function rememberLinkAccount(root, account) {
16061
+ if (!account.accountId) return false;
16062
+ const link = directoryIsLinked(root);
16063
+ if (!link) return false;
16064
+ const slug = account.accountSlug ?? link.accountSlug;
16065
+ if (link.accountId === account.accountId && link.accountSlug === slug) return false;
16066
+ saveProjectLink(root, {
16067
+ ...link,
16068
+ accountId: account.accountId,
16069
+ // `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
16070
+ ...slug ? { accountSlug: slug } : {}
16071
+ });
16072
+ return true;
16073
+ }
16074
+
16075
+ // src/support.ts
16076
+ var SUPPORT_TYPES = TICKET_TYPES;
16077
+ var MESSAGE_MAX = 2e4;
16078
+ var SUBJECT_MAX = 200;
16079
+ var LOG_TAIL_LINES = 40;
16080
+ var CONTEXT_SEPARATOR = "\n\n---\n";
16081
+ var URL_CREDENTIAL = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s:@/]+):([^\s@/]+)@/gi;
16082
+ var SECRET_ASSIGNMENT = /\b([A-Za-z0-9_.-]*(?:TOKEN|SECRET|PASSWORD|PASSWD|KEY|DSN|DATABASE_URL|CREDENTIAL)[A-Za-z0-9_.-]*)(\s*[=:]\s*)(["']?)([^\s"']+)/gi;
16083
+ function maskSecrets(line) {
16084
+ return line.replace(URL_CREDENTIAL, "$1$2:***@").replace(SECRET_ASSIGNMENT, "$1$2$3***");
16085
+ }
16086
+ function logTail(log, n = LOG_TAIL_LINES) {
16087
+ const lines = log.split(/\r?\n/);
16088
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
16089
+ return lines.slice(-n).map(maskSecrets);
16090
+ }
16091
+ function fenceFor(lines) {
16092
+ const longest = Math.max(0, ...lines.map((l) => Math.max(0, ...(l.match(/`+/g) ?? []).map((r) => r.length))));
16093
+ return "`".repeat(Math.max(3, longest + 1));
16094
+ }
16095
+ function deployLine(deploy) {
16096
+ if (deploy === "unreadable") return "Last production deploy: could not be read";
16097
+ if (deploy === null) return "Last production deploy: none yet";
16098
+ return `Last production deploy: ${deploy.id} (${deploy.type}, app ${deploy.app ?? "web"}) \u2014 ${deploy.status}, branch ${deploy.branch}, commit ${deploy.commitSha.slice(0, 7)}, ${deploy.statusChangedAt}`;
16099
+ }
16100
+ function buildContext(input, room) {
16101
+ if (!input.project) return null;
16102
+ const head = [
16103
+ "Context added by the Kryd CLI (kryd support new \u2014 leave it out with --no-context)",
16104
+ `Project: ${input.project.name} (${input.project.id})`,
16105
+ deployLine(input.deploy)
16106
+ ].join("\n");
16107
+ if (head.length > room) return null;
16108
+ let lines = input.log ? logTail(input.log) : [];
16109
+ const withLog = (ls) => {
16110
+ const fence = fenceFor(ls);
16111
+ return `${head}
16112
+ Last ${ls.length} lines of its log (secret-looking values masked):
16113
+ ${fence}
16114
+ ${ls.join("\n")}
16115
+ ${fence}`;
16116
+ };
16117
+ while (lines.length > 0 && withLog(lines).length > room) lines = lines.slice(1);
16118
+ return lines.length > 0 ? withLog(lines) : head;
16119
+ }
16120
+ function composeMessage(text, context) {
16121
+ return context ? `${text}${CONTEXT_SEPARATOR}${context}` : text;
16122
+ }
16123
+ function contextRoom(text) {
16124
+ return MESSAGE_MAX - text.length - CONTEXT_SEPARATOR.length;
16125
+ }
16126
+ var STATUS_TEXT = {
16127
+ open: "open \u2014 Kryd support is on it",
16128
+ waiting: "waiting on you",
16129
+ solved: "solved"
16130
+ };
16131
+ var statusText = (s) => TICKET_STATUSES.includes(s) ? STATUS_TEXT[s] : s;
16132
+ function printJson(value) {
16133
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
16134
+ `);
16135
+ }
16136
+ function usage(message) {
16137
+ process.stderr.write(`${message}
16138
+ `);
16139
+ process.exitCode = 1;
16140
+ }
16141
+ function issueMessages(details) {
16142
+ const list = Array.isArray(details) ? details : Array.isArray(details?.issues) ? details.issues : [];
16143
+ return list.map((i) => i?.message).filter((m) => typeof m === "string" && m.length > 0);
16144
+ }
16145
+ function reportSupportError(err) {
16146
+ if (err instanceof ApiError && err.status === 401) {
16147
+ process.stderr.write("Not logged in, or the session expired \u2014 run `kryd login`.\n");
16148
+ } else if (err instanceof ApiError && err.envelope?.error) {
16149
+ const issues = issueMessages(err.envelope.error.details);
16150
+ process.stderr.write(`${issues.length > 0 ? issues.join("\n") : err.envelope.error.message}
16151
+ `);
16152
+ } else {
16153
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
16154
+ `);
16155
+ }
16156
+ process.exitCode = 1;
16157
+ }
16158
+ function requireToken() {
16159
+ const token = loadConfig().token;
16160
+ if (!token) usage("Not logged in \u2014 run `kryd login` first.");
16161
+ return token ?? null;
15951
16162
  }
15952
- async function* readSseFrames(body) {
15953
- const reader = body.getReader();
15954
- const decoder = new TextDecoder();
15955
- let buffer = "";
15956
- try {
15957
- for (; ; ) {
15958
- const { value, done } = await reader.read();
15959
- if (done) return;
15960
- buffer += decoder.decode(value, { stream: true });
15961
- let sep;
15962
- while ((sep = buffer.indexOf("\n\n")) !== -1) {
15963
- const data = dataFromFrame(buffer.slice(0, sep));
15964
- buffer = buffer.slice(sep + 2);
15965
- if (data !== void 0) yield data;
15966
- }
15967
- }
15968
- } finally {
15969
- await reader.cancel().catch(() => void 0);
15970
- }
16163
+ function ticketRef(raw) {
16164
+ const n = raw ? parseTicketNumber(raw) : null;
16165
+ return n === null ? null : formatTicketNumber(n);
15971
16166
  }
15972
- function abortableSleep(ms, signal) {
15973
- if (signal?.aborted) return Promise.resolve();
15974
- return new Promise((resolve3) => {
15975
- const onAbort = () => {
15976
- clearTimeout(timer);
15977
- resolve3();
15978
- };
15979
- const timer = setTimeout(() => {
15980
- signal?.removeEventListener("abort", onAbort);
15981
- resolve3();
15982
- }, ms);
15983
- signal?.addEventListener("abort", onAbort, { once: true });
15984
- });
16167
+ function printTicketLine(t) {
16168
+ process.stdout.write(`${t.ref} ${statusText(t.status).padEnd(30)} ${t.type.padEnd(8)} ${t.subject}
16169
+ `);
15985
16170
  }
15986
- async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15987
- const doFetch = opts.fetchImpl ?? fetch;
15988
- const reconnectMs = opts.reconnectMs ?? 1e3;
15989
- let terminal;
15990
- while (terminal === void 0) {
15991
- let dropped = false;
15992
- try {
15993
- const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/logs`, {
15994
- headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
15995
- });
15996
- if (!res.ok) {
15997
- throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res), res.status);
16171
+ function printDetail(t) {
16172
+ process.stdout.write(`${t.ref} \u2014 ${t.subject}
16173
+ `);
16174
+ process.stdout.write(`Status: ${statusText(t.status)} \xB7 ${t.type}${t.project ? ` \xB7 ${t.project.name}` : ""}
16175
+ `);
16176
+ for (const m of t.messages) {
16177
+ const who = m.side === "reply" ? "Kryd support" : m.author?.name ?? m.author?.email ?? "you";
16178
+ process.stdout.write(`
16179
+ \u2014 ${who}, ${m.createdAt}
16180
+ ${m.text}
16181
+ `);
16182
+ }
16183
+ }
16184
+ var errStatus = (err) => err instanceof ApiError && err.status ? String(err.status) : err instanceof Error ? err.message : "error";
16185
+ async function runSupportNew(opts) {
16186
+ const type = opts.type?.trim().toLowerCase();
16187
+ const subject = opts.subject?.trim();
16188
+ const text = opts.message?.trim();
16189
+ if (!type || !SUPPORT_TYPES.includes(type)) {
16190
+ return usage(`--type is required: one of ${SUPPORT_TYPES.join(", ")}.`);
16191
+ }
16192
+ if (!subject) return usage("--subject is required.");
16193
+ if (subject.length > SUBJECT_MAX) return usage(`--subject is at most ${SUBJECT_MAX} characters (this one is ${subject.length}).`);
16194
+ if (!text) return usage("--message is required.");
16195
+ if (text.length > MESSAGE_MAX) return usage(`--message is at most ${MESSAGE_MAX} characters (this one is ${text.length}).`);
16196
+ const token = requireToken();
16197
+ if (!token) return;
16198
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16199
+ const explicit = opts.project?.trim() || void 0;
16200
+ const target = resolveProjectId(explicit, opts.cwd);
16201
+ const notes = [];
16202
+ const flushNotes = () => {
16203
+ for (const n of notes) process.stderr.write(`note: ${n}
16204
+ `);
16205
+ };
16206
+ let project2 = null;
16207
+ let context = null;
16208
+ try {
16209
+ if (target) {
16210
+ let found;
16211
+ try {
16212
+ found = await getProject(apiUrl, token, target);
16213
+ } catch (err) {
16214
+ if (explicit) return reportSupportError(err);
16215
+ notes.push(`could not read the linked project ${target} (${errStatus(err)}); the ticket is opened without it`);
15998
16216
  }
15999
- if (!res.body) throw new Error("The log stream returned no body.");
16000
- for await (const data of readSseFrames(res.body)) {
16001
- const event = parseDeployEvent(data);
16002
- if (!event) continue;
16003
- if (event.kind === "log") {
16004
- handlers.onLog(event.line);
16005
- } else {
16006
- handlers.onStatus(event);
16007
- if (isTerminalDeployState(event.status)) {
16008
- terminal = event.status;
16009
- break;
16010
- }
16011
- }
16217
+ if (found === null) {
16218
+ if (explicit) return usage(`--project ${target} is not a project in your account. Nothing was sent.`);
16219
+ notes.push(`this folder is linked to ${target}, which is not in your account; the ticket is opened without a project`);
16012
16220
  }
16013
- if (terminal === void 0) dropped = true;
16014
- } catch (err) {
16015
- if (err instanceof ApiError) throw err;
16016
- dropped = true;
16221
+ if (found) project2 = { id: found.id, name: found.name };
16017
16222
  }
16018
- if (terminal === void 0 && dropped) {
16019
- await new Promise((r) => setTimeout(r, reconnectMs));
16223
+ if (project2 && opts.context !== false) {
16224
+ let deploy;
16225
+ try {
16226
+ deploy = (await listDeployments(apiUrl, token, project2.id)).find((d) => d.type === "production") ?? null;
16227
+ } catch (err) {
16228
+ deploy = "unreadable";
16229
+ notes.push(`could not read the project's deploys (${errStatus(err)}); the context says so`);
16230
+ }
16231
+ let log = null;
16232
+ if (deploy && deploy !== "unreadable" && deploy.status === "failed") {
16233
+ try {
16234
+ log = await fetchDeployLog(apiUrl, token, deploy.id);
16235
+ } catch (err) {
16236
+ notes.push(`could not read the failed deploy's log (${errStatus(err)}); the context leaves it out`);
16237
+ }
16238
+ }
16239
+ context = buildContext({ project: project2, deploy, log }, contextRoom(text));
16240
+ if (context === null) notes.push("the context did not fit next to your message; it was left out");
16020
16241
  }
16242
+ const ticket = await createSupportTicket(apiUrl, token, {
16243
+ type,
16244
+ subject,
16245
+ text: composeMessage(text, context),
16246
+ projectId: project2?.id ?? null
16247
+ });
16248
+ flushNotes();
16249
+ if (opts.json) return printJson({ ...ticket, contextIncluded: context !== null, notes });
16250
+ process.stdout.write(`Opened ${ticket.ref} \u2014 ${ticket.subject}
16251
+ `);
16252
+ process.stdout.write(`Status: ${statusText(ticket.status)}
16253
+ `);
16254
+ process.stdout.write(
16255
+ context ? `Context: added (${project2.name})
16256
+ ` : project2 ? "Context: none\n" : target ? "Opened without a project (see the note above).\n" : "No linked project here \u2014 opened without project context.\n"
16257
+ );
16258
+ process.stdout.write(`Follow it with \`kryd support show ${ticket.ref}\`.
16259
+ `);
16260
+ } catch (err) {
16261
+ flushNotes();
16262
+ reportSupportError(err);
16021
16263
  }
16022
- return terminal;
16023
16264
  }
16024
- async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
16025
- const path = opts.resource === "deployment" ? `deployments/${id}/runtime-logs` : opts.resource === "worker" ? `projects/${id}/workflow-logs` : `projects/${id}/runtime-logs`;
16026
- const doFetch = opts.fetchImpl ?? fetch;
16027
- const reconnectMs = opts.reconnectMs ?? 1e3;
16028
- let afterNs;
16029
- while (!opts.signal?.aborted) {
16030
- try {
16031
- const url2 = new URL(`${apiUrl}/${path}`);
16032
- if (opts.app && (opts.resource ?? "project") === "project") url2.searchParams.set("app", opts.app);
16033
- if (afterNs) url2.searchParams.set("after", afterNs);
16034
- else if (opts.since) url2.searchParams.set("since", opts.since);
16035
- const res = await doFetch(url2, {
16036
- headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" },
16037
- ...opts.signal ? { signal: opts.signal } : {}
16038
- });
16039
- if (!res.ok) {
16040
- throw new ApiError(
16041
- `Runtime log stream failed (${res.status})`,
16042
- await parseEnvelope(res),
16043
- res.status
16044
- );
16045
- }
16046
- if (!res.body) throw new Error("The runtime log stream returned no body.");
16047
- for await (const data of readSseFrames(res.body)) {
16048
- const frame = parseRuntimeStreamFrame(data);
16049
- if (!frame) continue;
16050
- if (frame.kind === "error") throw new ApiError(frame.message);
16051
- handlers.onLine({ stream: frame.stream, line: frame.line });
16052
- if (frame.ts) afterNs = frame.ts;
16053
- }
16054
- } catch (err) {
16055
- if (opts.signal?.aborted) return;
16056
- if (err instanceof ApiError) throw err;
16057
- }
16058
- if (!opts.signal?.aborted) {
16059
- await abortableSleep(reconnectMs, opts.signal);
16265
+ async function runSupportList(opts) {
16266
+ const token = requireToken();
16267
+ if (!token) return;
16268
+ try {
16269
+ const items = await listSupportTickets(resolveApiUrl(opts.apiUrl), token);
16270
+ if (opts.json) return printJson({ items });
16271
+ if (items.length === 0) {
16272
+ process.stdout.write("No tickets yet. Open one with `kryd support new`.\n");
16273
+ return;
16060
16274
  }
16275
+ for (const t of items) printTicketLine(t);
16276
+ } catch (err) {
16277
+ reportSupportError(err);
16061
16278
  }
16062
16279
  }
16063
- async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
16064
- const doFetch = opts.fetchImpl ?? fetch;
16065
- const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/log`, {
16066
- headers: { authorization: `Bearer ${token}` }
16067
- });
16068
- if (res.status === 404) return null;
16069
- if (!res.ok) {
16070
- throw new ApiError(
16071
- `Fetching the stored log failed (${res.status})`,
16072
- await parseEnvelope(res),
16073
- res.status
16074
- );
16280
+ async function runSupportShow(raw, opts) {
16281
+ const ref = ticketRef(raw);
16282
+ if (!ref) return usage("Which ticket? Give its number, e.g. `kryd support show T-1042`.");
16283
+ const token = requireToken();
16284
+ if (!token) return;
16285
+ try {
16286
+ const t = await getSupportTicket(resolveApiUrl(opts.apiUrl), token, ref);
16287
+ if (opts.json) return printJson(t);
16288
+ printDetail(t);
16289
+ } catch (err) {
16290
+ reportSupportError(err);
16075
16291
  }
16076
- return (await res.json()).log;
16077
16292
  }
16078
- async function fetchPushCredential(apiUrl, token, projectId) {
16079
- const res = await fetch(
16080
- `${apiUrl}/repos/${encodeURIComponent(projectId)}/push-credential`,
16081
- { headers: { authorization: `Bearer ${token}` } }
16082
- );
16083
- if (!res.ok) {
16084
- throw new ApiError(
16085
- `Could not fetch the push credential (${res.status})`,
16086
- await parseEnvelope(res),
16087
- res.status
16088
- );
16293
+ async function runSupportReply(raw, opts) {
16294
+ const ref = ticketRef(raw);
16295
+ if (!ref) return usage("Which ticket? Give its number, e.g. `kryd support reply T-1042 --message \u2026`.");
16296
+ const text = opts.message?.trim();
16297
+ if (!text) return usage("--message is required.");
16298
+ if (text.length > MESSAGE_MAX) return usage(`--message is at most ${MESSAGE_MAX} characters (this one is ${text.length}).`);
16299
+ const token = requireToken();
16300
+ if (!token) return;
16301
+ try {
16302
+ const t = await replySupportTicket(resolveApiUrl(opts.apiUrl), token, ref, text);
16303
+ if (opts.json) return printJson(t);
16304
+ process.stdout.write(`Sent. ${t.ref} is now ${statusText(t.status)}.
16305
+ `);
16306
+ } catch (err) {
16307
+ reportSupportError(err);
16089
16308
  }
16090
- const parsed = await res.json().catch(() => void 0);
16091
- if (!parsed || typeof parsed.username !== "string" || typeof parsed.password !== "string" || parsed.password.length === 0) {
16092
- throw new ApiError("The API returned an unexpected push-credential response.");
16309
+ }
16310
+
16311
+ // src/browser-login.ts
16312
+ import { spawn } from "node:child_process";
16313
+ import { randomUUID } from "node:crypto";
16314
+ import { createServer } from "node:http";
16315
+ var page = (title, body) => `<!doctype html><html><head><meta charset="utf-8"><title>Kryd CLI</title></head><body style="font-family:system-ui,-apple-system,sans-serif;text-align:center;padding:3rem;color:#1a2634"><h1>${title}</h1><p>${body}</p></body></html>`;
16316
+ var SUCCESS_HTML = page("\u2713 Logged in to Kryd", "You can close this tab and return to your terminal.");
16317
+ var FAIL_HTML = page("Login failed", "Please return to your terminal and run <code>kryd login</code> again.");
16318
+ function openBrowser(url2) {
16319
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : ["xdg-open", [url2]];
16320
+ try {
16321
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
16322
+ } catch {
16093
16323
  }
16094
- return { username: parsed.username, password: parsed.password };
16324
+ }
16325
+ function browserLogin(dashboardUrl, opts = {}) {
16326
+ const state = randomUUID();
16327
+ const timeoutMs = opts.timeoutMs ?? 12e4;
16328
+ const shouldOpen = opts.open ?? true;
16329
+ return new Promise((resolve3, reject) => {
16330
+ let settled = false;
16331
+ const settle = (fn) => {
16332
+ if (settled) return;
16333
+ settled = true;
16334
+ fn();
16335
+ };
16336
+ const server = createServer((req, res) => {
16337
+ res.setHeader("connection", "close");
16338
+ const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
16339
+ if (reqUrl.pathname !== "/callback") {
16340
+ res.writeHead(404);
16341
+ res.end();
16342
+ return;
16343
+ }
16344
+ const token = reqUrl.searchParams.get("token");
16345
+ const apiUrl = reqUrl.searchParams.get("apiUrl") ?? void 0;
16346
+ const gotState = reqUrl.searchParams.get("state");
16347
+ if (!token || gotState !== state) {
16348
+ res.writeHead(400, { "content-type": "text/html" });
16349
+ res.end(FAIL_HTML, () => {
16350
+ settle(() => {
16351
+ shutdown();
16352
+ reject(new Error("Login failed \u2014 the approval didn't match this request. Try `kryd login` again."));
16353
+ });
16354
+ });
16355
+ return;
16356
+ }
16357
+ res.writeHead(200, { "content-type": "text/html" });
16358
+ res.end(SUCCESS_HTML, () => {
16359
+ settle(() => {
16360
+ shutdown();
16361
+ resolve3({ token, ...apiUrl ? { apiUrl } : {} });
16362
+ });
16363
+ });
16364
+ });
16365
+ const shutdown = () => {
16366
+ server.closeAllConnections();
16367
+ server.close();
16368
+ };
16369
+ server.on("error", (err) => settle(() => reject(err)));
16370
+ server.listen(0, "127.0.0.1", () => {
16371
+ const port = server.address().port;
16372
+ const authorizeUrl = `${dashboardUrl.replace(/\/$/, "")}/cli/authorize?port=${port}&state=${state}`;
16373
+ process.stdout.write(
16374
+ `
16375
+ To sign in, approve the CLI in your browser:
16376
+ ${authorizeUrl}
16377
+
16378
+ Waiting for approval\u2026
16379
+ `
16380
+ );
16381
+ if (shouldOpen) openBrowser(authorizeUrl);
16382
+ });
16383
+ setTimeout(
16384
+ () => settle(() => {
16385
+ shutdown();
16386
+ reject(new Error("Login timed out after 2 minutes."));
16387
+ }),
16388
+ timeoutMs
16389
+ ).unref();
16390
+ });
16095
16391
  }
16096
16392
 
16097
16393
  // src/git-credential.ts
@@ -17366,16 +17662,6 @@ async function runLogin(opts) {
17366
17662
  process.stdout.write("Token saved.\n");
17367
17663
  return;
17368
17664
  }
17369
- if (opts.email && opts.password) {
17370
- const token2 = await login(apiUrl, { email: opts.email, password: opts.password });
17371
- saveToken(token2, apiUrl);
17372
- process.stdout.write(`Logged in as ${opts.email}.
17373
- `);
17374
- return;
17375
- }
17376
- if (opts.email || opts.password) {
17377
- throw new Error("Provide BOTH --email and --password, or omit them to log in via the browser.");
17378
- }
17379
17665
  const { token, apiUrl: learnedApiUrl } = await browserLogin(resolveDashboardUrl(opts.dashboardUrl));
17380
17666
  saveToken(token, learnedApiUrl ?? apiUrl);
17381
17667
  process.stdout.write("Logged in.\n");
@@ -18613,7 +18899,7 @@ function formatDomainRecords(records) {
18613
18899
  }
18614
18900
  return out;
18615
18901
  }
18616
- function domainContext(opts, usage) {
18902
+ function domainContext(opts, usage2) {
18617
18903
  const apiUrl = resolveApiUrl(opts.apiUrl);
18618
18904
  const token = loadConfig().token;
18619
18905
  if (!token) {
@@ -18623,7 +18909,7 @@ function domainContext(opts, usage) {
18623
18909
  }
18624
18910
  const project2 = resolveProjectId(opts.project, opts.cwd);
18625
18911
  if (!project2) {
18626
- reportNotLinked(usage);
18912
+ reportNotLinked(usage2);
18627
18913
  return null;
18628
18914
  }
18629
18915
  return { apiUrl, token, project: project2 };
@@ -19508,13 +19794,13 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
19508
19794
  await reportError(err);
19509
19795
  }
19510
19796
  }
19511
- var CLI_VERSION = true ? "0.12.1" : "0.0.0-dev";
19797
+ var CLI_VERSION = true ? "0.14.0" : "0.0.0-dev";
19512
19798
  var program = new Command();
19513
19799
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
19514
19800
  program.hook("postAction", async () => {
19515
19801
  await backfillLinkAccount();
19516
19802
  });
19517
- 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(
19803
+ program.command("login").description("Sign in via the browser (default) and store a token").option(
19518
19804
  "--token <token>",
19519
19805
  "supply a token instead of the browser flow (prefer $KRYD_TOKEN \u2014 flags are visible in `ps`)"
19520
19806
  ).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));
@@ -19647,6 +19933,15 @@ domain2.command("add <host>").description("Add a custom domain and print the DNS
19647
19933
  ).option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainAdd({ ...opts, host }));
19648
19934
  domain2.command("remove <host>").description("Remove a custom domain \u2014 its route and certificate go (asks for confirmation)").option("--project <projectId>", "the project (default: the one linked here)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainRemove({ ...opts, host }));
19649
19935
  domain2.command("status [host]").description("Show the project's custom domains, or one in detail with what is missing").option("--project <projectId>", "the project (default: the one linked here)").option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainStatus({ ...opts, host }));
19936
+ var support = program.command("support").description(
19937
+ "Ask Kryd support a question, report a bug or a wish, and read the answer \u2014 non-interactive, with --json for agents"
19938
+ );
19939
+ support.command("new").description(
19940
+ "Open a ticket. In a linked folder it adds the project and its last production deploy; if that deploy failed, the last lines of its log are sent to Kryd support too (secret-looking values masked). --no-context leaves all of it out"
19941
+ ).option("--type <type>", "question | bug | request (required)").option("--subject <text>", "one line: what it is about (required)").option("--message <text>", "the question or the bug report, in full (required)").option("--project <projectId>", "the project it is about (default: the one linked here)").option("--no-context", "leave out the project, deploy and log context").option("--json", "print the ticket as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((opts) => runSupportNew(opts));
19942
+ support.command("list").description("Your account's tickets, with their status").option("--json", "print { items } as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((opts) => runSupportList(opts));
19943
+ support.command("show <ticket>").description("One ticket and its conversation, e.g. `kryd support show T-1042`").option("--json", "print the ticket and its messages as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((ticket, opts) => runSupportShow(ticket, opts));
19944
+ support.command("reply <ticket>").description("Answer in a ticket's conversation \u2014 the ticket goes back to Kryd support (status open)").option("--message <text>", "your reply (required)").option("--json", "print the updated ticket as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((ticket, opts) => runSupportReply(ticket, opts));
19650
19945
  function invokedDirectly() {
19651
19946
  const entry = process.argv[1];
19652
19947
  if (!entry) return false;