@kryd/cli 0.12.1 → 0.13.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 +834 -510
  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,398 +14936,141 @@ 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 login(apiUrl, creds) {
14967
+ const res = await fetch(`${apiUrl}/api/auth/sign-in/email`, {
14968
+ method: "POST",
14969
+ headers: { "content-type": "application/json", origin: apiUrl },
14970
+ body: JSON.stringify(creds)
14971
+ });
14972
+ if (!res.ok) {
14973
+ throw new ApiError(
14974
+ `Login failed (${res.status})`,
14975
+ await parseEnvelope(res),
14976
+ res.status
14977
+ );
14978
+ }
14979
+ const token = res.headers.get("set-auth-token");
14980
+ if (!token) {
14981
+ throw new ApiError("Login succeeded but the API returned no token.");
14996
14982
  }
14983
+ return token;
14997
14984
  }
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;
14985
+ async function whoami(apiUrl, token) {
14986
+ const res = await fetch(`${apiUrl}/me`, {
14987
+ headers: { authorization: `Bearer ${token}` }
14988
+ });
14989
+ if (!res.ok) {
14990
+ throw new ApiError(
14991
+ `Request failed (${res.status})`,
14992
+ await parseEnvelope(res),
14993
+ res.status
14994
+ );
15007
14995
  }
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;
14996
+ return await res.json();
14997
+ }
14998
+ async function linkProject(apiUrl, token, input) {
14999
+ const res = await fetch(`${apiUrl}/repos`, {
15000
+ method: "POST",
15001
+ headers: {
15002
+ "content-type": "application/json",
15003
+ authorization: `Bearer ${token}`
15004
+ },
15005
+ body: JSON.stringify(input)
15006
+ });
15007
+ if (!res.ok) {
15008
+ throw new ApiError(
15009
+ `Repo linking failed (${res.status})`,
15010
+ await parseEnvelope(res),
15011
+ res.status
15012
+ );
15015
15013
  }
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;
15014
+ const parsed = await res.json().catch(() => void 0);
15015
+ 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 —
15016
+ // a 200 missing it must raise the friendly ApiError, not a raw TypeError downstream.
15017
+ typeof parsed.repo?.fullName !== "string" || parsed.repo.fullName.length === 0) {
15018
+ throw new ApiError("The API returned an unexpected repo-link response.");
15020
15019
  }
15021
15020
  return parsed;
15022
15021
  }
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 {
15022
+ async function attachDatabase(apiUrl, token, input) {
15023
+ const res = await fetch(`${apiUrl}/databases`, {
15024
+ method: "POST",
15025
+ headers: {
15026
+ "content-type": "application/json",
15027
+ authorization: `Bearer ${token}`
15028
+ },
15029
+ body: JSON.stringify({
15030
+ projectId: input.projectId,
15031
+ mode: input.mode,
15032
+ // Conditional (exactOptionalPropertyTypes) — only BYO carries a connection string.
15033
+ ...input.connectionString ? { connectionString: input.connectionString } : {}
15034
+ })
15035
+ });
15036
+ if (!res.ok) {
15037
+ throw new ApiError(
15038
+ `Database attach failed (${res.status})`,
15039
+ await parseEnvelope(res),
15040
+ res.status
15041
+ );
15036
15042
  }
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);
15051
- }
15052
- function writeSecretFile(path, contents) {
15053
- mkdirSync(dirname(path), { recursive: true });
15054
- writeFileSync(path, contents, { mode: 384 });
15055
- chmodSync(path, 384);
15056
- }
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;
15043
+ const parsed = await res.json().catch(() => void 0);
15044
+ if (!parsed || typeof parsed.id !== "string" || parsed.id.length === 0 || !DATABASE_MODES.includes(parsed.mode) || !RESOURCE_STATUSES.includes(parsed.status)) {
15045
+ throw new ApiError("The API returned an unexpected database response.");
15067
15046
  }
15047
+ return {
15048
+ id: parsed.id,
15049
+ mode: parsed.mode,
15050
+ status: parsed.status
15051
+ };
15068
15052
  }
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`, {
15228
- headers: { authorization: `Bearer ${token}` }
15229
- });
15230
- if (!res.ok) {
15231
- 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})`,
15251
- await parseEnvelope(res),
15252
- res.status
15253
- );
15254
- }
15255
- 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.");
15260
- }
15261
- return parsed;
15262
- }
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
- })
15276
- });
15277
- if (!res.ok) {
15278
- throw new ApiError(
15279
- `Database attach failed (${res.status})`,
15280
- await parseEnvelope(res),
15281
- res.status
15282
- );
15283
- }
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.");
15287
- }
15288
- return {
15289
- id: parsed.id,
15290
- mode: parsed.mode,
15291
- status: parsed.status
15292
- };
15293
- }
15294
- async function createStorage(apiUrl, token, input) {
15295
- const res = await fetch(`${apiUrl}/storage`, {
15296
- method: "POST",
15297
- headers: {
15298
- "content-type": "application/json",
15299
- authorization: `Bearer ${token}`
15300
- },
15301
- body: JSON.stringify({ projectId: input.projectId })
15302
- });
15303
- if (!res.ok) {
15304
- throw new ApiError(
15305
- `Storage create failed (${res.status})`,
15306
- await parseEnvelope(res),
15307
- res.status
15308
- );
15309
- }
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 };
15053
+ async function createStorage(apiUrl, token, input) {
15054
+ const res = await fetch(`${apiUrl}/storage`, {
15055
+ method: "POST",
15056
+ headers: {
15057
+ "content-type": "application/json",
15058
+ authorization: `Bearer ${token}`
15059
+ },
15060
+ body: JSON.stringify({ projectId: input.projectId })
15061
+ });
15062
+ if (!res.ok) {
15063
+ throw new ApiError(
15064
+ `Storage create failed (${res.status})`,
15065
+ await parseEnvelope(res),
15066
+ res.status
15067
+ );
15068
+ }
15069
+ const parsed = await res.json().catch(() => void 0);
15070
+ if (!parsed || typeof parsed.id !== "string" || parsed.id.length === 0 || !RESOURCE_STATUSES.includes(parsed.status)) {
15071
+ throw new ApiError("The API returned an unexpected storage response.");
15072
+ }
15073
+ return { id: parsed.id, status: parsed.status };
15315
15074
  }
15316
15075
  async function enableAi(apiUrl, token, input) {
15317
15076
  const res = await fetch(`${apiUrl}/ai/enable`, {
@@ -15945,153 +15704,709 @@ async function pollForNewDeployments(fetchDeployments, match, opts) {
15945
15704
  await sleep(intervalMs);
15946
15705
  }
15947
15706
  }
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;
15707
+ function dataFromFrame(frame) {
15708
+ const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).replace(/^ /, "")).join("\n");
15709
+ return data.length > 0 ? data : void 0;
15710
+ }
15711
+ async function* readSseFrames(body) {
15712
+ const reader = body.getReader();
15713
+ const decoder = new TextDecoder();
15714
+ let buffer = "";
15715
+ try {
15716
+ for (; ; ) {
15717
+ const { value, done } = await reader.read();
15718
+ if (done) return;
15719
+ buffer += decoder.decode(value, { stream: true });
15720
+ let sep;
15721
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
15722
+ const data = dataFromFrame(buffer.slice(0, sep));
15723
+ buffer = buffer.slice(sep + 2);
15724
+ if (data !== void 0) yield data;
15725
+ }
15726
+ }
15727
+ } finally {
15728
+ await reader.cancel().catch(() => void 0);
15729
+ }
15730
+ }
15731
+ function abortableSleep(ms, signal) {
15732
+ if (signal?.aborted) return Promise.resolve();
15733
+ return new Promise((resolve3) => {
15734
+ const onAbort = () => {
15735
+ clearTimeout(timer);
15736
+ resolve3();
15737
+ };
15738
+ const timer = setTimeout(() => {
15739
+ signal?.removeEventListener("abort", onAbort);
15740
+ resolve3();
15741
+ }, ms);
15742
+ signal?.addEventListener("abort", onAbort, { once: true });
15743
+ });
15744
+ }
15745
+ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15746
+ const doFetch = opts.fetchImpl ?? fetch;
15747
+ const reconnectMs = opts.reconnectMs ?? 1e3;
15748
+ let terminal;
15749
+ while (terminal === void 0) {
15750
+ let dropped = false;
15751
+ try {
15752
+ const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/logs`, {
15753
+ headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
15754
+ });
15755
+ if (!res.ok) {
15756
+ throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res), res.status);
15757
+ }
15758
+ if (!res.body) throw new Error("The log stream returned no body.");
15759
+ for await (const data of readSseFrames(res.body)) {
15760
+ const event = parseDeployEvent(data);
15761
+ if (!event) continue;
15762
+ if (event.kind === "log") {
15763
+ handlers.onLog(event.line);
15764
+ } else {
15765
+ handlers.onStatus(event);
15766
+ if (isTerminalDeployState(event.status)) {
15767
+ terminal = event.status;
15768
+ break;
15769
+ }
15770
+ }
15771
+ }
15772
+ if (terminal === void 0) dropped = true;
15773
+ } catch (err) {
15774
+ if (err instanceof ApiError) throw err;
15775
+ dropped = true;
15776
+ }
15777
+ if (terminal === void 0 && dropped) {
15778
+ await new Promise((r) => setTimeout(r, reconnectMs));
15779
+ }
15780
+ }
15781
+ return terminal;
15782
+ }
15783
+ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15784
+ const path = opts.resource === "deployment" ? `deployments/${id}/runtime-logs` : opts.resource === "worker" ? `projects/${id}/workflow-logs` : `projects/${id}/runtime-logs`;
15785
+ const doFetch = opts.fetchImpl ?? fetch;
15786
+ const reconnectMs = opts.reconnectMs ?? 1e3;
15787
+ let afterNs;
15788
+ while (!opts.signal?.aborted) {
15789
+ try {
15790
+ const url2 = new URL(`${apiUrl}/${path}`);
15791
+ if (opts.app && (opts.resource ?? "project") === "project") url2.searchParams.set("app", opts.app);
15792
+ if (afterNs) url2.searchParams.set("after", afterNs);
15793
+ else if (opts.since) url2.searchParams.set("since", opts.since);
15794
+ const res = await doFetch(url2, {
15795
+ headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" },
15796
+ ...opts.signal ? { signal: opts.signal } : {}
15797
+ });
15798
+ if (!res.ok) {
15799
+ throw new ApiError(
15800
+ `Runtime log stream failed (${res.status})`,
15801
+ await parseEnvelope(res),
15802
+ res.status
15803
+ );
15804
+ }
15805
+ if (!res.body) throw new Error("The runtime log stream returned no body.");
15806
+ for await (const data of readSseFrames(res.body)) {
15807
+ const frame = parseRuntimeStreamFrame(data);
15808
+ if (!frame) continue;
15809
+ if (frame.kind === "error") throw new ApiError(frame.message);
15810
+ handlers.onLine({ stream: frame.stream, line: frame.line });
15811
+ if (frame.ts) afterNs = frame.ts;
15812
+ }
15813
+ } catch (err) {
15814
+ if (opts.signal?.aborted) return;
15815
+ if (err instanceof ApiError) throw err;
15816
+ }
15817
+ if (!opts.signal?.aborted) {
15818
+ await abortableSleep(reconnectMs, opts.signal);
15819
+ }
15820
+ }
15821
+ }
15822
+ async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
15823
+ const doFetch = opts.fetchImpl ?? fetch;
15824
+ const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/log`, {
15825
+ headers: { authorization: `Bearer ${token}` }
15826
+ });
15827
+ if (res.status === 404) return null;
15828
+ if (!res.ok) {
15829
+ throw new ApiError(
15830
+ `Fetching the stored log failed (${res.status})`,
15831
+ await parseEnvelope(res),
15832
+ res.status
15833
+ );
15834
+ }
15835
+ return (await res.json()).log;
15836
+ }
15837
+ async function fetchPushCredential(apiUrl, token, projectId) {
15838
+ const res = await fetch(
15839
+ `${apiUrl}/repos/${encodeURIComponent(projectId)}/push-credential`,
15840
+ { headers: { authorization: `Bearer ${token}` } }
15841
+ );
15842
+ if (!res.ok) {
15843
+ throw new ApiError(
15844
+ `Could not fetch the push credential (${res.status})`,
15845
+ await parseEnvelope(res),
15846
+ res.status
15847
+ );
15848
+ }
15849
+ const parsed = await res.json().catch(() => void 0);
15850
+ if (!parsed || typeof parsed.username !== "string" || typeof parsed.password !== "string" || parsed.password.length === 0) {
15851
+ throw new ApiError("The API returned an unexpected push-credential response.");
15852
+ }
15853
+ return { username: parsed.username, password: parsed.password };
15854
+ }
15855
+ function isSupportTicket(v) {
15856
+ const t = v;
15857
+ 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");
15858
+ }
15859
+ function isSupportDetail(v) {
15860
+ return isSupportTicket(v) && Array.isArray(v.messages) && v.messages.every(
15861
+ (m) => typeof m?.text === "string" && typeof m?.side === "string" && typeof m?.createdAt === "string"
15862
+ );
15863
+ }
15864
+ var unexpected = (what) => new ApiError(`The API returned ${what} in a shape this CLI does not recognise`);
15865
+ async function supportFetch(apiUrl, token, path, what, init = {}) {
15866
+ const res = await fetch(`${apiUrl}${path}`, {
15867
+ method: init.method ?? "GET",
15868
+ headers: {
15869
+ authorization: `Bearer ${token}`,
15870
+ ...init.body !== void 0 ? { "content-type": "application/json" } : {}
15871
+ },
15872
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
15873
+ });
15874
+ if (!res.ok) {
15875
+ throw new ApiError(`${what} failed (${res.status})`, await parseEnvelope(res), res.status);
15876
+ }
15877
+ try {
15878
+ return await res.json();
15879
+ } catch {
15880
+ throw unexpected(`an answer to "${what.toLowerCase()}"`);
15881
+ }
15882
+ }
15883
+ async function createSupportTicket(apiUrl, token, input) {
15884
+ const body = await supportFetch(apiUrl, token, "/support/tickets", "Opening the ticket", {
15885
+ method: "POST",
15886
+ body: {
15887
+ type: input.type,
15888
+ subject: input.subject,
15889
+ text: input.text,
15890
+ ...input.projectId ? { projectId: input.projectId } : {},
15891
+ via: "cli"
15892
+ }
15893
+ });
15894
+ if (!isSupportTicket(body)) throw unexpected("the new ticket");
15895
+ return body;
15896
+ }
15897
+ async function listSupportTickets(apiUrl, token) {
15898
+ const body = await supportFetch(apiUrl, token, "/support/tickets", "Listing the tickets");
15899
+ if (!Array.isArray(body?.items) || !body.items.every(isSupportTicket)) throw unexpected("the tickets");
15900
+ return body.items;
15901
+ }
15902
+ async function getSupportTicket(apiUrl, token, ref) {
15903
+ const body = await supportFetch(apiUrl, token, `/support/tickets/${encodeURIComponent(ref)}`, "Reading the ticket");
15904
+ if (!isSupportDetail(body)) throw unexpected("the ticket");
15905
+ return body;
15906
+ }
15907
+ async function replySupportTicket(apiUrl, token, ref, text) {
15908
+ const body = await supportFetch(
15909
+ apiUrl,
15910
+ token,
15911
+ `/support/tickets/${encodeURIComponent(ref)}/messages`,
15912
+ "Sending the reply",
15913
+ { method: "POST", body: { text } }
15914
+ );
15915
+ if (!isSupportDetail(body)) throw unexpected("the ticket");
15916
+ return body;
15917
+ }
15918
+
15919
+ // src/config.ts
15920
+ import {
15921
+ appendFileSync,
15922
+ chmodSync,
15923
+ existsSync,
15924
+ mkdirSync,
15925
+ readFileSync,
15926
+ rmSync,
15927
+ writeFileSync
15928
+ } from "node:fs";
15929
+ import { homedir } from "node:os";
15930
+ import { dirname, join } from "node:path";
15931
+ var DEFAULT_API_URL = "http://localhost:3000";
15932
+ function configPath() {
15933
+ const dir = process.env.KRYD_CONFIG_DIR ?? join(homedir(), ".kryd");
15934
+ return join(dir, "config.json");
15935
+ }
15936
+ function loadConfig() {
15937
+ let raw;
15938
+ try {
15939
+ raw = readFileSync(configPath(), "utf8");
15940
+ } catch {
15941
+ return {};
15942
+ }
15943
+ try {
15944
+ return JSON.parse(raw);
15945
+ } catch {
15946
+ process.stderr.write(
15947
+ `warning: ${configPath()} is not valid JSON \u2014 ignoring it.
15948
+ `
15949
+ );
15950
+ return {};
15951
+ }
15952
+ }
15953
+ function writeConfig(config2) {
15954
+ const path = configPath();
15955
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
15956
+ writeFileSync(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
15957
+ chmodSync(path, 384);
15958
+ }
15959
+ function saveToken(token, apiUrl) {
15960
+ const config2 = loadConfig();
15961
+ config2.token = token;
15962
+ if (apiUrl) config2.apiUrl = apiUrl;
15963
+ writeConfig(config2);
15964
+ }
15965
+ function clearToken() {
15966
+ const config2 = loadConfig();
15967
+ delete config2.token;
15968
+ writeConfig(config2);
15969
+ }
15970
+ var lastApiUrl = null;
15971
+ function resolveApiUrl(flag) {
15972
+ lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
15973
+ return lastApiUrl;
15974
+ }
15975
+ function currentApiUrl() {
15976
+ return lastApiUrl ?? resolveApiUrl();
15977
+ }
15978
+ var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
15979
+ function resolveDashboardUrl(flag) {
15980
+ return flag ?? process.env.KRYD_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
15981
+ }
15982
+ var PROJECT_LINK_DIR = ".kryd";
15983
+ var PROJECT_LINK_FILE = "project.json";
15984
+ function findProjectLinkDir(cwd = process.cwd()) {
15985
+ let dir = cwd;
15986
+ for (; ; ) {
15987
+ if (existsSync(join(dir, PROJECT_LINK_DIR, PROJECT_LINK_FILE))) return dir;
15988
+ if (existsSync(join(dir, ".git"))) return null;
15989
+ const parent = dirname(dir);
15990
+ if (parent === dir) return null;
15991
+ dir = parent;
15992
+ }
15993
+ }
15994
+ function loadProjectLink(cwd = process.cwd()) {
15995
+ const root = findProjectLinkDir(cwd);
15996
+ if (!root) return null;
15997
+ const path = join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15998
+ let raw;
15999
+ try {
16000
+ raw = readFileSync(path, "utf8");
16001
+ } catch {
16002
+ return null;
16003
+ }
16004
+ let parsed;
16005
+ try {
16006
+ parsed = JSON.parse(raw);
16007
+ } catch {
16008
+ process.stderr.write(`warning: ${path} is not valid JSON \u2014 ignoring it.
16009
+ `);
16010
+ return null;
16011
+ }
16012
+ if (!parsed || typeof parsed.projectId !== "string" || parsed.projectId.length === 0) {
16013
+ process.stderr.write(`warning: ${path} has no projectId \u2014 ignoring it.
16014
+ `);
16015
+ return null;
16016
+ }
16017
+ return parsed;
16018
+ }
16019
+ function saveProjectLink(cwd, link) {
16020
+ const dir = join(cwd, PROJECT_LINK_DIR);
16021
+ mkdirSync(dir, { recursive: true });
16022
+ const path = join(dir, PROJECT_LINK_FILE);
16023
+ writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
16024
+ return path;
16025
+ }
16026
+ function ensureIgnored(cwd, entry, aliases = []) {
16027
+ const path = join(cwd, ".gitignore");
16028
+ let current = "";
16029
+ try {
16030
+ current = readFileSync(path, "utf8");
16031
+ } catch {
16032
+ }
16033
+ const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
16034
+ if (present) return false;
16035
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
16036
+ appendFileSync(path, `${prefix}${entry}
16037
+ `);
16038
+ return true;
16039
+ }
16040
+ function ensureKrydIgnored(cwd) {
16041
+ ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
16042
+ }
16043
+ function directoryIsLinked(cwd) {
16044
+ const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
16045
+ if (!existsSync(path)) return null;
16046
+ return loadProjectLink(cwd);
16047
+ }
16048
+ function writeSecretFile(path, contents) {
16049
+ mkdirSync(dirname(path), { recursive: true });
16050
+ writeFileSync(path, contents, { mode: 384 });
16051
+ chmodSync(path, 384);
16052
+ }
16053
+ function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
16054
+ const root = findProjectLinkDir(cwd);
16055
+ if (!root) return false;
16056
+ const link = loadProjectLink(cwd);
16057
+ if (link?.projectId !== projectId) return false;
16058
+ try {
16059
+ rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
16060
+ return true;
16061
+ } catch {
16062
+ return false;
16063
+ }
16064
+ }
16065
+ function resolvedLink() {
16066
+ return lastLinkResolution;
16067
+ }
16068
+ var lastLinkResolution = null;
16069
+ function resolveProjectId(explicit, cwd = process.cwd()) {
16070
+ lastLinkResolution = null;
16071
+ if (explicit) return explicit;
16072
+ const root = findProjectLinkDir(cwd);
16073
+ if (!root) return null;
16074
+ const link = loadProjectLink(cwd);
16075
+ if (!link) return null;
16076
+ lastLinkResolution = { root, link };
16077
+ return link.projectId;
16078
+ }
16079
+ function rememberLinkAccount(root, account) {
16080
+ if (!account.accountId) return false;
16081
+ const link = directoryIsLinked(root);
16082
+ if (!link) return false;
16083
+ const slug = account.accountSlug ?? link.accountSlug;
16084
+ if (link.accountId === account.accountId && link.accountSlug === slug) return false;
16085
+ saveProjectLink(root, {
16086
+ ...link,
16087
+ accountId: account.accountId,
16088
+ // `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
16089
+ ...slug ? { accountSlug: slug } : {}
16090
+ });
16091
+ return true;
16092
+ }
16093
+
16094
+ // src/support.ts
16095
+ var SUPPORT_TYPES = TICKET_TYPES;
16096
+ var MESSAGE_MAX = 2e4;
16097
+ var SUBJECT_MAX = 200;
16098
+ var LOG_TAIL_LINES = 40;
16099
+ var CONTEXT_SEPARATOR = "\n\n---\n";
16100
+ var URL_CREDENTIAL = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s:@/]+):([^\s@/]+)@/gi;
16101
+ 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;
16102
+ function maskSecrets(line) {
16103
+ return line.replace(URL_CREDENTIAL, "$1$2:***@").replace(SECRET_ASSIGNMENT, "$1$2$3***");
16104
+ }
16105
+ function logTail(log, n = LOG_TAIL_LINES) {
16106
+ const lines = log.split(/\r?\n/);
16107
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
16108
+ return lines.slice(-n).map(maskSecrets);
16109
+ }
16110
+ function fenceFor(lines) {
16111
+ const longest = Math.max(0, ...lines.map((l) => Math.max(0, ...(l.match(/`+/g) ?? []).map((r) => r.length))));
16112
+ return "`".repeat(Math.max(3, longest + 1));
16113
+ }
16114
+ function deployLine(deploy) {
16115
+ if (deploy === "unreadable") return "Last production deploy: could not be read";
16116
+ if (deploy === null) return "Last production deploy: none yet";
16117
+ 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}`;
16118
+ }
16119
+ function buildContext(input, room) {
16120
+ if (!input.project) return null;
16121
+ const head = [
16122
+ "Context added by the Kryd CLI (kryd support new \u2014 leave it out with --no-context)",
16123
+ `Project: ${input.project.name} (${input.project.id})`,
16124
+ deployLine(input.deploy)
16125
+ ].join("\n");
16126
+ if (head.length > room) return null;
16127
+ let lines = input.log ? logTail(input.log) : [];
16128
+ const withLog = (ls) => {
16129
+ const fence = fenceFor(ls);
16130
+ return `${head}
16131
+ Last ${ls.length} lines of its log (secret-looking values masked):
16132
+ ${fence}
16133
+ ${ls.join("\n")}
16134
+ ${fence}`;
16135
+ };
16136
+ while (lines.length > 0 && withLog(lines).length > room) lines = lines.slice(1);
16137
+ return lines.length > 0 ? withLog(lines) : head;
16138
+ }
16139
+ function composeMessage(text, context) {
16140
+ return context ? `${text}${CONTEXT_SEPARATOR}${context}` : text;
16141
+ }
16142
+ function contextRoom(text) {
16143
+ return MESSAGE_MAX - text.length - CONTEXT_SEPARATOR.length;
16144
+ }
16145
+ var STATUS_TEXT = {
16146
+ open: "open \u2014 Kryd support is on it",
16147
+ waiting: "waiting on you",
16148
+ solved: "solved"
16149
+ };
16150
+ var statusText = (s) => TICKET_STATUSES.includes(s) ? STATUS_TEXT[s] : s;
16151
+ function printJson(value) {
16152
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
16153
+ `);
16154
+ }
16155
+ function usage(message) {
16156
+ process.stderr.write(`${message}
16157
+ `);
16158
+ process.exitCode = 1;
16159
+ }
16160
+ function issueMessages(details) {
16161
+ const list = Array.isArray(details) ? details : Array.isArray(details?.issues) ? details.issues : [];
16162
+ return list.map((i) => i?.message).filter((m) => typeof m === "string" && m.length > 0);
15951
16163
  }
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);
16164
+ function reportSupportError(err) {
16165
+ if (err instanceof ApiError && err.status === 401) {
16166
+ process.stderr.write("Not logged in, or the session expired \u2014 run `kryd login`.\n");
16167
+ } else if (err instanceof ApiError && err.envelope?.error) {
16168
+ const issues = issueMessages(err.envelope.error.details);
16169
+ process.stderr.write(`${issues.length > 0 ? issues.join("\n") : err.envelope.error.message}
16170
+ `);
16171
+ } else {
16172
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
16173
+ `);
15970
16174
  }
16175
+ process.exitCode = 1;
15971
16176
  }
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
- });
16177
+ function requireToken() {
16178
+ const token = loadConfig().token;
16179
+ if (!token) usage("Not logged in \u2014 run `kryd login` first.");
16180
+ return token ?? null;
15985
16181
  }
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);
16182
+ function ticketRef(raw) {
16183
+ const n = raw ? parseTicketNumber(raw) : null;
16184
+ return n === null ? null : formatTicketNumber(n);
16185
+ }
16186
+ function printTicketLine(t) {
16187
+ process.stdout.write(`${t.ref} ${statusText(t.status).padEnd(30)} ${t.type.padEnd(8)} ${t.subject}
16188
+ `);
16189
+ }
16190
+ function printDetail(t) {
16191
+ process.stdout.write(`${t.ref} \u2014 ${t.subject}
16192
+ `);
16193
+ process.stdout.write(`Status: ${statusText(t.status)} \xB7 ${t.type}${t.project ? ` \xB7 ${t.project.name}` : ""}
16194
+ `);
16195
+ for (const m of t.messages) {
16196
+ const who = m.side === "reply" ? "Kryd support" : m.author?.name ?? m.author?.email ?? "you";
16197
+ process.stdout.write(`
16198
+ \u2014 ${who}, ${m.createdAt}
16199
+ ${m.text}
16200
+ `);
16201
+ }
16202
+ }
16203
+ var errStatus = (err) => err instanceof ApiError && err.status ? String(err.status) : err instanceof Error ? err.message : "error";
16204
+ async function runSupportNew(opts) {
16205
+ const type = opts.type?.trim().toLowerCase();
16206
+ const subject = opts.subject?.trim();
16207
+ const text = opts.message?.trim();
16208
+ if (!type || !SUPPORT_TYPES.includes(type)) {
16209
+ return usage(`--type is required: one of ${SUPPORT_TYPES.join(", ")}.`);
16210
+ }
16211
+ if (!subject) return usage("--subject is required.");
16212
+ if (subject.length > SUBJECT_MAX) return usage(`--subject is at most ${SUBJECT_MAX} characters (this one is ${subject.length}).`);
16213
+ if (!text) return usage("--message is required.");
16214
+ if (text.length > MESSAGE_MAX) return usage(`--message is at most ${MESSAGE_MAX} characters (this one is ${text.length}).`);
16215
+ const token = requireToken();
16216
+ if (!token) return;
16217
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16218
+ const explicit = opts.project?.trim() || void 0;
16219
+ const target = resolveProjectId(explicit, opts.cwd);
16220
+ const notes = [];
16221
+ const flushNotes = () => {
16222
+ for (const n of notes) process.stderr.write(`note: ${n}
16223
+ `);
16224
+ };
16225
+ let project2 = null;
16226
+ let context = null;
16227
+ try {
16228
+ if (target) {
16229
+ let found;
16230
+ try {
16231
+ found = await getProject(apiUrl, token, target);
16232
+ } catch (err) {
16233
+ if (explicit) return reportSupportError(err);
16234
+ notes.push(`could not read the linked project ${target} (${errStatus(err)}); the ticket is opened without it`);
15998
16235
  }
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
- }
16236
+ if (found === null) {
16237
+ if (explicit) return usage(`--project ${target} is not a project in your account. Nothing was sent.`);
16238
+ notes.push(`this folder is linked to ${target}, which is not in your account; the ticket is opened without a project`);
16012
16239
  }
16013
- if (terminal === void 0) dropped = true;
16014
- } catch (err) {
16015
- if (err instanceof ApiError) throw err;
16016
- dropped = true;
16240
+ if (found) project2 = { id: found.id, name: found.name };
16017
16241
  }
16018
- if (terminal === void 0 && dropped) {
16019
- await new Promise((r) => setTimeout(r, reconnectMs));
16242
+ if (project2 && opts.context !== false) {
16243
+ let deploy;
16244
+ try {
16245
+ deploy = (await listDeployments(apiUrl, token, project2.id)).find((d) => d.type === "production") ?? null;
16246
+ } catch (err) {
16247
+ deploy = "unreadable";
16248
+ notes.push(`could not read the project's deploys (${errStatus(err)}); the context says so`);
16249
+ }
16250
+ let log = null;
16251
+ if (deploy && deploy !== "unreadable" && deploy.status === "failed") {
16252
+ try {
16253
+ log = await fetchDeployLog(apiUrl, token, deploy.id);
16254
+ } catch (err) {
16255
+ notes.push(`could not read the failed deploy's log (${errStatus(err)}); the context leaves it out`);
16256
+ }
16257
+ }
16258
+ context = buildContext({ project: project2, deploy, log }, contextRoom(text));
16259
+ if (context === null) notes.push("the context did not fit next to your message; it was left out");
16020
16260
  }
16261
+ const ticket = await createSupportTicket(apiUrl, token, {
16262
+ type,
16263
+ subject,
16264
+ text: composeMessage(text, context),
16265
+ projectId: project2?.id ?? null
16266
+ });
16267
+ flushNotes();
16268
+ if (opts.json) return printJson({ ...ticket, contextIncluded: context !== null, notes });
16269
+ process.stdout.write(`Opened ${ticket.ref} \u2014 ${ticket.subject}
16270
+ `);
16271
+ process.stdout.write(`Status: ${statusText(ticket.status)}
16272
+ `);
16273
+ process.stdout.write(
16274
+ context ? `Context: added (${project2.name})
16275
+ ` : project2 ? "Context: none\n" : target ? "Opened without a project (see the note above).\n" : "No linked project here \u2014 opened without project context.\n"
16276
+ );
16277
+ process.stdout.write(`Follow it with \`kryd support show ${ticket.ref}\`.
16278
+ `);
16279
+ } catch (err) {
16280
+ flushNotes();
16281
+ reportSupportError(err);
16021
16282
  }
16022
- return terminal;
16023
16283
  }
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);
16284
+ async function runSupportList(opts) {
16285
+ const token = requireToken();
16286
+ if (!token) return;
16287
+ try {
16288
+ const items = await listSupportTickets(resolveApiUrl(opts.apiUrl), token);
16289
+ if (opts.json) return printJson({ items });
16290
+ if (items.length === 0) {
16291
+ process.stdout.write("No tickets yet. Open one with `kryd support new`.\n");
16292
+ return;
16060
16293
  }
16294
+ for (const t of items) printTicketLine(t);
16295
+ } catch (err) {
16296
+ reportSupportError(err);
16061
16297
  }
16062
16298
  }
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
- );
16299
+ async function runSupportShow(raw, opts) {
16300
+ const ref = ticketRef(raw);
16301
+ if (!ref) return usage("Which ticket? Give its number, e.g. `kryd support show T-1042`.");
16302
+ const token = requireToken();
16303
+ if (!token) return;
16304
+ try {
16305
+ const t = await getSupportTicket(resolveApiUrl(opts.apiUrl), token, ref);
16306
+ if (opts.json) return printJson(t);
16307
+ printDetail(t);
16308
+ } catch (err) {
16309
+ reportSupportError(err);
16075
16310
  }
16076
- return (await res.json()).log;
16077
16311
  }
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
- );
16312
+ async function runSupportReply(raw, opts) {
16313
+ const ref = ticketRef(raw);
16314
+ if (!ref) return usage("Which ticket? Give its number, e.g. `kryd support reply T-1042 --message \u2026`.");
16315
+ const text = opts.message?.trim();
16316
+ if (!text) return usage("--message is required.");
16317
+ if (text.length > MESSAGE_MAX) return usage(`--message is at most ${MESSAGE_MAX} characters (this one is ${text.length}).`);
16318
+ const token = requireToken();
16319
+ if (!token) return;
16320
+ try {
16321
+ const t = await replySupportTicket(resolveApiUrl(opts.apiUrl), token, ref, text);
16322
+ if (opts.json) return printJson(t);
16323
+ process.stdout.write(`Sent. ${t.ref} is now ${statusText(t.status)}.
16324
+ `);
16325
+ } catch (err) {
16326
+ reportSupportError(err);
16089
16327
  }
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.");
16328
+ }
16329
+
16330
+ // src/browser-login.ts
16331
+ import { spawn } from "node:child_process";
16332
+ import { randomUUID } from "node:crypto";
16333
+ import { createServer } from "node:http";
16334
+ 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>`;
16335
+ var SUCCESS_HTML = page("\u2713 Logged in to Kryd", "You can close this tab and return to your terminal.");
16336
+ var FAIL_HTML = page("Login failed", "Please return to your terminal and run <code>kryd login</code> again.");
16337
+ function openBrowser(url2) {
16338
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : ["xdg-open", [url2]];
16339
+ try {
16340
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
16341
+ } catch {
16093
16342
  }
16094
- return { username: parsed.username, password: parsed.password };
16343
+ }
16344
+ function browserLogin(dashboardUrl, opts = {}) {
16345
+ const state = randomUUID();
16346
+ const timeoutMs = opts.timeoutMs ?? 12e4;
16347
+ const shouldOpen = opts.open ?? true;
16348
+ return new Promise((resolve3, reject) => {
16349
+ let settled = false;
16350
+ const settle = (fn) => {
16351
+ if (settled) return;
16352
+ settled = true;
16353
+ fn();
16354
+ };
16355
+ const server = createServer((req, res) => {
16356
+ res.setHeader("connection", "close");
16357
+ const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
16358
+ if (reqUrl.pathname !== "/callback") {
16359
+ res.writeHead(404);
16360
+ res.end();
16361
+ return;
16362
+ }
16363
+ const token = reqUrl.searchParams.get("token");
16364
+ const apiUrl = reqUrl.searchParams.get("apiUrl") ?? void 0;
16365
+ const gotState = reqUrl.searchParams.get("state");
16366
+ if (!token || gotState !== state) {
16367
+ res.writeHead(400, { "content-type": "text/html" });
16368
+ res.end(FAIL_HTML, () => {
16369
+ settle(() => {
16370
+ shutdown();
16371
+ reject(new Error("Login failed \u2014 the approval didn't match this request. Try `kryd login` again."));
16372
+ });
16373
+ });
16374
+ return;
16375
+ }
16376
+ res.writeHead(200, { "content-type": "text/html" });
16377
+ res.end(SUCCESS_HTML, () => {
16378
+ settle(() => {
16379
+ shutdown();
16380
+ resolve3({ token, ...apiUrl ? { apiUrl } : {} });
16381
+ });
16382
+ });
16383
+ });
16384
+ const shutdown = () => {
16385
+ server.closeAllConnections();
16386
+ server.close();
16387
+ };
16388
+ server.on("error", (err) => settle(() => reject(err)));
16389
+ server.listen(0, "127.0.0.1", () => {
16390
+ const port = server.address().port;
16391
+ const authorizeUrl = `${dashboardUrl.replace(/\/$/, "")}/cli/authorize?port=${port}&state=${state}`;
16392
+ process.stdout.write(
16393
+ `
16394
+ To sign in, approve the CLI in your browser:
16395
+ ${authorizeUrl}
16396
+
16397
+ Waiting for approval\u2026
16398
+ `
16399
+ );
16400
+ if (shouldOpen) openBrowser(authorizeUrl);
16401
+ });
16402
+ setTimeout(
16403
+ () => settle(() => {
16404
+ shutdown();
16405
+ reject(new Error("Login timed out after 2 minutes."));
16406
+ }),
16407
+ timeoutMs
16408
+ ).unref();
16409
+ });
16095
16410
  }
16096
16411
 
16097
16412
  // src/git-credential.ts
@@ -18613,7 +18928,7 @@ function formatDomainRecords(records) {
18613
18928
  }
18614
18929
  return out;
18615
18930
  }
18616
- function domainContext(opts, usage) {
18931
+ function domainContext(opts, usage2) {
18617
18932
  const apiUrl = resolveApiUrl(opts.apiUrl);
18618
18933
  const token = loadConfig().token;
18619
18934
  if (!token) {
@@ -18623,7 +18938,7 @@ function domainContext(opts, usage) {
18623
18938
  }
18624
18939
  const project2 = resolveProjectId(opts.project, opts.cwd);
18625
18940
  if (!project2) {
18626
- reportNotLinked(usage);
18941
+ reportNotLinked(usage2);
18627
18942
  return null;
18628
18943
  }
18629
18944
  return { apiUrl, token, project: project2 };
@@ -19508,7 +19823,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
19508
19823
  await reportError(err);
19509
19824
  }
19510
19825
  }
19511
- var CLI_VERSION = true ? "0.12.1" : "0.0.0-dev";
19826
+ var CLI_VERSION = true ? "0.13.0" : "0.0.0-dev";
19512
19827
  var program = new Command();
19513
19828
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
19514
19829
  program.hook("postAction", async () => {
@@ -19647,6 +19962,15 @@ domain2.command("add <host>").description("Add a custom domain and print the DNS
19647
19962
  ).option("--api-url <url>", "control-plane API base URL").action((host, opts) => runDomainAdd({ ...opts, host }));
19648
19963
  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
19964
  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 }));
19965
+ var support = program.command("support").description(
19966
+ "Ask Kryd support a question, report a bug or a wish, and read the answer \u2014 non-interactive, with --json for agents"
19967
+ );
19968
+ support.command("new").description(
19969
+ "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"
19970
+ ).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));
19971
+ 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));
19972
+ 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));
19973
+ 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
19974
  function invokedDirectly() {
19651
19975
  const entry = process.argv[1];
19652
19976
  if (!entry) return false;