@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.
- package/README.md +8 -0
- package/dist/index.js +869 -574
- 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/
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
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
|
|
14961
|
+
return await res.json();
|
|
14949
14962
|
} catch {
|
|
14950
|
-
|
|
14951
|
-
`warning: ${configPath()} is not valid JSON \u2014 ignoring it.
|
|
14952
|
-
`
|
|
14953
|
-
);
|
|
14954
|
-
return {};
|
|
14963
|
+
return void 0;
|
|
14955
14964
|
}
|
|
14956
14965
|
}
|
|
14957
|
-
function
|
|
14958
|
-
const
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
14962
|
-
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
|
|
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
|
|
14999
|
-
const
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
|
|
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
|
-
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
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
|
|
15024
|
-
const
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
}
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
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
|
|
15038
|
-
if (
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
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
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
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
|
|
15058
|
-
const
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
}
|
|
15066
|
-
|
|
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
|
|
15070
|
-
|
|
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
|
-
`
|
|
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.
|
|
15257
|
-
|
|
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
|
|
15264
|
-
const res = await fetch(`${apiUrl}/
|
|
15265
|
-
method: "
|
|
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
|
-
`
|
|
15102
|
+
`AI removal failed (${res.status})`,
|
|
15280
15103
|
await parseEnvelope(res),
|
|
15281
15104
|
res.status
|
|
15282
15105
|
);
|
|
15283
15106
|
}
|
|
15284
|
-
|
|
15285
|
-
|
|
15286
|
-
|
|
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
|
|
15295
|
-
const res = await fetch(`${apiUrl}/
|
|
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
|
-
`
|
|
15126
|
+
`Workflow add failed (${res.status})`,
|
|
15306
15127
|
await parseEnvelope(res),
|
|
15307
15128
|
res.status
|
|
15308
15129
|
);
|
|
15309
15130
|
}
|
|
15310
|
-
|
|
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
|
|
15317
|
-
const res = await fetch(`${apiUrl}/
|
|
15318
|
-
|
|
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
|
-
`
|
|
15139
|
+
`Workflow status check failed (${res.status})`,
|
|
15328
15140
|
await parseEnvelope(res),
|
|
15329
15141
|
res.status
|
|
15330
15142
|
);
|
|
15331
15143
|
}
|
|
15332
|
-
|
|
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
|
|
15339
|
-
const res = await fetch(`${apiUrl}/
|
|
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
|
-
`
|
|
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
|
-
|
|
15953
|
-
const
|
|
15954
|
-
|
|
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
|
|
15973
|
-
|
|
15974
|
-
|
|
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
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
|
|
15990
|
-
|
|
15991
|
-
|
|
15992
|
-
|
|
15993
|
-
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
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 (
|
|
16000
|
-
|
|
16001
|
-
|
|
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 (
|
|
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 (
|
|
16019
|
-
|
|
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
|
|
16025
|
-
const
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
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
|
|
16064
|
-
const
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
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
|
|
16079
|
-
const
|
|
16080
|
-
|
|
16081
|
-
|
|
16082
|
-
);
|
|
16083
|
-
if (
|
|
16084
|
-
|
|
16085
|
-
|
|
16086
|
-
|
|
16087
|
-
|
|
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
|
-
|
|
16091
|
-
|
|
16092
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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.
|
|
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(
|
|
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;
|