hngr 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/bin/hngr.js +12 -0
- package/bin/node-gate.d.ts +5 -0
- package/bin/node-gate.js +18 -0
- package/dist/auth-BT8HuCif.mjs +327 -0
- package/dist/catalog-jU0-wzh3.mjs +3184 -0
- package/dist/commands-BchmocR_.mjs +13 -0
- package/dist/http-2HWxLIOq.mjs +459 -0
- package/dist/index.mjs +7155 -0
- package/dist/mcp-DeWHHgQK.mjs +610 -0
- package/dist/pagination-BbKLKSw3.mjs +32 -0
- package/dist/publish-D7zaQl9x.mjs +512 -0
- package/dist/skills-DwoDzsXd.mjs +871 -0
- package/package.json +46 -16
- package/dist/index.js +0 -4
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { t as commandCatalog } from "./catalog-jU0-wzh3.mjs";
|
|
2
|
+
// src/commands/commands.ts
|
|
3
|
+
function run(ctx) {
|
|
4
|
+
const catalog = commandCatalog();
|
|
5
|
+
if (ctx.io.stdout.isTTY && !ctx.flags.json) {
|
|
6
|
+
const width = Math.max(...catalog.map((entry) => entry.name.length));
|
|
7
|
+
for (const entry of catalog) ctx.io.stdout.write(`${entry.name.padEnd(width)} ${entry.summary}\n`);
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
ctx.writePayload(catalog);
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
export { run };
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { chmod, lstat, mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var HngrError = class extends Error {
|
|
7
|
+
body;
|
|
8
|
+
exitCode;
|
|
9
|
+
constructor(body, exitCode = 1) {
|
|
10
|
+
super(body.title);
|
|
11
|
+
this.name = "HngrError";
|
|
12
|
+
this.body = body;
|
|
13
|
+
this.exitCode = exitCode;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Interactive prompt cancelled via Ctrl+C / Esc; maps to exit code 130. */
|
|
17
|
+
var CancelledError = class extends Error {
|
|
18
|
+
constructor(message = "cancelled") {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "CancelledError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function bodyOf(err) {
|
|
24
|
+
if (err instanceof HngrError) return {
|
|
25
|
+
body: err.body,
|
|
26
|
+
exitCode: err.exitCode
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
body: {
|
|
30
|
+
type: "io",
|
|
31
|
+
title: err instanceof Error ? err.message : String(err)
|
|
32
|
+
},
|
|
33
|
+
exitCode: 1
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Renders an error per the output contract: human text on a TTY (unless
|
|
38
|
+
* --json), otherwise a single-line JSON object on stderr. Returns the exit
|
|
39
|
+
* code. Never prints tokens or passwords; bodies must be pre-redacted.
|
|
40
|
+
*/
|
|
41
|
+
function renderError(io, options, err) {
|
|
42
|
+
if (err instanceof CancelledError) {
|
|
43
|
+
if (io.stderr.isTTY && !options.json) io.stderr.write(`${options.colors.dim(err.message)}\n`);
|
|
44
|
+
else io.stderr.write(`${JSON.stringify({ error: {
|
|
45
|
+
type: "usage",
|
|
46
|
+
title: err.message
|
|
47
|
+
} })}\n`);
|
|
48
|
+
return 130;
|
|
49
|
+
}
|
|
50
|
+
const { body, exitCode } = bodyOf(err);
|
|
51
|
+
if (io.stderr.isTTY && !options.json) {
|
|
52
|
+
io.stderr.write(`${options.colors.red(`error: ${body.title}`)}\n`);
|
|
53
|
+
if (body.detail) io.stderr.write(`${options.colors.dim(body.detail)}\n`);
|
|
54
|
+
} else io.stderr.write(`${JSON.stringify({ error: body })}\n`);
|
|
55
|
+
return exitCode;
|
|
56
|
+
}
|
|
57
|
+
// src/atomic.ts
|
|
58
|
+
/**
|
|
59
|
+
* Writes a file through a temp file plus rename, so readers see either the old
|
|
60
|
+
* content or the complete new content.
|
|
61
|
+
*/
|
|
62
|
+
async function writeFileAtomically(path, content, options = {}) {
|
|
63
|
+
const dir = dirname(path);
|
|
64
|
+
const temp = resolve(dir, `.${basename(path)}.${randomUUID()}.tmp`);
|
|
65
|
+
try {
|
|
66
|
+
const existed = await stat(dir).then((stats) => stats.isDirectory(), () => false);
|
|
67
|
+
if (options.dirMode === void 0) await mkdir(dir, { recursive: true });
|
|
68
|
+
else {
|
|
69
|
+
await mkdir(dir, {
|
|
70
|
+
recursive: true,
|
|
71
|
+
mode: options.dirMode
|
|
72
|
+
});
|
|
73
|
+
if (!existed) await chmod(dir, options.dirMode);
|
|
74
|
+
}
|
|
75
|
+
await writeFile(temp, content, { mode: options.mode ?? 420 });
|
|
76
|
+
await rename(temp, path);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
await unlink(temp).catch(() => {});
|
|
79
|
+
if (err instanceof HngrError) throw err;
|
|
80
|
+
throw new HngrError({
|
|
81
|
+
type: "io",
|
|
82
|
+
title: `Could not write ${options.label ?? path}`,
|
|
83
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Writes a directory through "temp → rename": the destination is either
|
|
89
|
+
* absent or complete, never half-written. With `force`, the old directory is
|
|
90
|
+
* moved aside before the rename so a failed swap puts it back.
|
|
91
|
+
*/
|
|
92
|
+
async function writeDirectoryAtomically(dest, writeTemp, options) {
|
|
93
|
+
const parent = dirname(dest);
|
|
94
|
+
await mkdir(parent, { recursive: true });
|
|
95
|
+
const temp = resolve(parent, `.hngr-${basename(dest)}-${randomUUID()}.tmp`);
|
|
96
|
+
try {
|
|
97
|
+
await writeTemp(temp);
|
|
98
|
+
const exists = await lstat(dest).then(() => true, () => false);
|
|
99
|
+
if (exists && !options.force) throw options.existsError();
|
|
100
|
+
if (!exists) {
|
|
101
|
+
await rename(temp, dest);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const backup = resolve(parent, `.hngr-${basename(dest)}-${randomUUID()}.bak`);
|
|
105
|
+
await rename(dest, backup);
|
|
106
|
+
try {
|
|
107
|
+
await rename(temp, dest);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
await rename(backup, dest).catch(() => {});
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
await rm(backup, {
|
|
113
|
+
recursive: true,
|
|
114
|
+
force: true
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
await rm(temp, {
|
|
118
|
+
recursive: true,
|
|
119
|
+
force: true
|
|
120
|
+
}).catch(() => {});
|
|
121
|
+
if (err instanceof HngrError) throw err;
|
|
122
|
+
throw new HngrError({
|
|
123
|
+
type: "io",
|
|
124
|
+
title: `Could not install into ${options.label ?? dest}`,
|
|
125
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// src/config.ts
|
|
130
|
+
/** `$XDG_CONFIG_HOME/hngr/config.json`, falling back to `~/.config/hngr/config.json`. */
|
|
131
|
+
function configFilePath(env) {
|
|
132
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
133
|
+
const home = env.HOME ?? homedir();
|
|
134
|
+
const base = xdg && xdg.length > 0 ? xdg : home ? join(home, ".config") : void 0;
|
|
135
|
+
return base ? join(base, "hngr", "config.json") : void 0;
|
|
136
|
+
}
|
|
137
|
+
/** Reads the stored config; a missing file is not an error, invalid JSON is. */
|
|
138
|
+
async function readStoredConfig(env) {
|
|
139
|
+
const path = configFilePath(env);
|
|
140
|
+
if (!path) return;
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = await readFile(path, "utf8");
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err.code === "ENOENT") return;
|
|
146
|
+
throw new HngrError({
|
|
147
|
+
type: "config",
|
|
148
|
+
title: `Could not read ${path}`,
|
|
149
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(raw);
|
|
154
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("expected a JSON object");
|
|
155
|
+
return parsed;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new HngrError({
|
|
158
|
+
type: "config",
|
|
159
|
+
title: `Invalid config file ${path}`,
|
|
160
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The instance the CLI talks to when `--server`, `HANGAR_URL` and the config
|
|
166
|
+
* file are all absent. Point a deployment elsewhere with any of those.
|
|
167
|
+
*/
|
|
168
|
+
const DEFAULT_SERVER_URL = "http://10.199.100.24:26434";
|
|
169
|
+
const INVALID_SERVER_DETAIL = "Pass --server <url>, set HANGAR_URL, or run `hngr auth login` to store one in the config file.";
|
|
170
|
+
/** Parses and normalizes a server URL (no trailing slash); invalid input is a config error. */
|
|
171
|
+
function normalizeServerUrl(server) {
|
|
172
|
+
let url;
|
|
173
|
+
try {
|
|
174
|
+
url = new URL(server);
|
|
175
|
+
} catch {
|
|
176
|
+
throw new HngrError({
|
|
177
|
+
type: "config",
|
|
178
|
+
title: `Invalid server URL: ${redactUrl(server)}`,
|
|
179
|
+
detail: INVALID_SERVER_DETAIL
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return url.toString().replace(/\/+$/, "");
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Atomically writes the config file: a `0600` temp file renamed into place
|
|
186
|
+
* inside a `0700` directory. Returns the config path. Never logs the content.
|
|
187
|
+
*/
|
|
188
|
+
async function writeStoredConfig(env, config) {
|
|
189
|
+
const path = configFilePath(env);
|
|
190
|
+
if (!path) throw new HngrError({
|
|
191
|
+
type: "config",
|
|
192
|
+
title: "No config location available",
|
|
193
|
+
detail: "Set HOME or XDG_CONFIG_HOME so hngr can store credentials."
|
|
194
|
+
});
|
|
195
|
+
await writeFileAtomically(path, `${JSON.stringify(config, null, 2)}\n`, {
|
|
196
|
+
mode: 384,
|
|
197
|
+
dirMode: 448
|
|
198
|
+
});
|
|
199
|
+
return path;
|
|
200
|
+
}
|
|
201
|
+
/** Deletes the stored config file; a missing file is not an error. */
|
|
202
|
+
async function deleteStoredConfig(env) {
|
|
203
|
+
const path = configFilePath(env);
|
|
204
|
+
if (!path) return;
|
|
205
|
+
try {
|
|
206
|
+
await unlink(path);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (err.code !== "ENOENT") throw new HngrError({
|
|
209
|
+
type: "io",
|
|
210
|
+
title: `Could not delete ${path}`,
|
|
211
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Resolves the server and credentials for one call: `--server` beats
|
|
217
|
+
* `HANGAR_URL` beats the config file beats the built-in default; `HANGAR_TOKEN`
|
|
218
|
+
* beats a stored token.
|
|
219
|
+
*/
|
|
220
|
+
async function resolveConnection(flags, env) {
|
|
221
|
+
const config = await readStoredConfig(env);
|
|
222
|
+
const server = flags.server ?? env.HANGAR_URL ?? config?.server ?? "http://10.199.100.24:26434";
|
|
223
|
+
const token = env.HANGAR_TOKEN ?? config?.token;
|
|
224
|
+
return {
|
|
225
|
+
server: normalizeServerUrl(server),
|
|
226
|
+
token
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** Strips any embedded credentials so a URL is safe to print. */
|
|
230
|
+
function redactUrl(raw) {
|
|
231
|
+
try {
|
|
232
|
+
const url = new URL(raw);
|
|
233
|
+
url.username = "";
|
|
234
|
+
url.password = "";
|
|
235
|
+
return url.toString();
|
|
236
|
+
} catch {
|
|
237
|
+
return raw;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Removes any credentials embedded in `server` from free-form error text. */
|
|
241
|
+
function scrubCredentials(text, server) {
|
|
242
|
+
try {
|
|
243
|
+
const url = new URL(server);
|
|
244
|
+
for (const secret of [url.username, url.password]) if (secret) text = text.split(secret).join("<redacted>");
|
|
245
|
+
} catch {}
|
|
246
|
+
return text;
|
|
247
|
+
}
|
|
248
|
+
// ../client/src/problem.ts
|
|
249
|
+
/** A typed transport failure. `detail` is developer text, never the primary message. */
|
|
250
|
+
var ApiError = class ApiError extends Error {
|
|
251
|
+
status;
|
|
252
|
+
type;
|
|
253
|
+
title;
|
|
254
|
+
detail;
|
|
255
|
+
requestId;
|
|
256
|
+
retryAfter;
|
|
257
|
+
problem;
|
|
258
|
+
constructor(init) {
|
|
259
|
+
super(init.title ?? (init.status === 0 ? "网络连接失败" : "请求失败"));
|
|
260
|
+
this.name = "ApiError";
|
|
261
|
+
this.status = init.status;
|
|
262
|
+
this.type = init.type;
|
|
263
|
+
this.title = init.title;
|
|
264
|
+
this.detail = init.detail;
|
|
265
|
+
this.requestId = init.requestId;
|
|
266
|
+
this.retryAfter = init.retryAfter;
|
|
267
|
+
this.problem = init.problem;
|
|
268
|
+
}
|
|
269
|
+
static network() {
|
|
270
|
+
return new ApiError({
|
|
271
|
+
status: 0,
|
|
272
|
+
type: "network"
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
/** Narrows a decoded body to a problem document; anything else maps to about:blank. */
|
|
277
|
+
function asProblemDetails(body) {
|
|
278
|
+
if (body === null || typeof body !== "object") return;
|
|
279
|
+
return body;
|
|
280
|
+
}
|
|
281
|
+
/** Parses the `Retry-After` delta-seconds header; absent or invalid means no hint. */
|
|
282
|
+
function parseRetryAfter(header) {
|
|
283
|
+
if (header === null) return;
|
|
284
|
+
const value = Number.parseInt(header, 10);
|
|
285
|
+
return Number.isFinite(value) ? value : void 0;
|
|
286
|
+
}
|
|
287
|
+
/** Maps a decoded problem+json body onto an `ApiError`. */
|
|
288
|
+
function apiErrorFromProblem(init) {
|
|
289
|
+
const problem = asProblemDetails(init.body);
|
|
290
|
+
return new ApiError({
|
|
291
|
+
status: init.status,
|
|
292
|
+
type: problem?.type ?? "about:blank",
|
|
293
|
+
title: problem?.title,
|
|
294
|
+
detail: problem?.detail,
|
|
295
|
+
requestId: init.requestId,
|
|
296
|
+
retryAfter: parseRetryAfter(init.retryAfter ?? null),
|
|
297
|
+
problem
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/** Maps a failed HTTP response onto an `ApiError`; non-JSON bodies degrade to status only. */
|
|
301
|
+
async function apiErrorFromResponse(response, requestId) {
|
|
302
|
+
let body;
|
|
303
|
+
if ((response.headers.get("Content-Type") ?? "").includes("json")) try {
|
|
304
|
+
body = await response.json();
|
|
305
|
+
} catch {
|
|
306
|
+
body = void 0;
|
|
307
|
+
}
|
|
308
|
+
return apiErrorFromProblem({
|
|
309
|
+
status: response.status,
|
|
310
|
+
body,
|
|
311
|
+
retryAfter: response.headers.get("Retry-After"),
|
|
312
|
+
requestId
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// ../client/src/query.ts
|
|
316
|
+
/**
|
|
317
|
+
* Serializes query parameters with `URLSearchParams` semantics (spaces become
|
|
318
|
+
* `+`), dropping `undefined`, `null` and empty-string values. Returns the empty
|
|
319
|
+
* string when nothing survives, so callers can skip the `?` separator.
|
|
320
|
+
*/
|
|
321
|
+
function serializeQueryParams(query = {}) {
|
|
322
|
+
const params = new URLSearchParams();
|
|
323
|
+
for (const [key, value] of Object.entries(query)) {
|
|
324
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
325
|
+
params.set(key, String(value));
|
|
326
|
+
}
|
|
327
|
+
return params.toString();
|
|
328
|
+
}
|
|
329
|
+
// src/redact.ts
|
|
330
|
+
const REDACTED = "<redacted>";
|
|
331
|
+
/**
|
|
332
|
+
* One redaction rule for every human-visible representation of a request:
|
|
333
|
+
* a key is sensitive when it contains password, token, secret, authorization
|
|
334
|
+
* or cookie, case-insensitively.
|
|
335
|
+
*/
|
|
336
|
+
const SENSITIVE_KEY = /password|token|secret|authorization|cookie/i;
|
|
337
|
+
function isSensitiveKey(key) {
|
|
338
|
+
return SENSITIVE_KEY.test(key);
|
|
339
|
+
}
|
|
340
|
+
/** Recursively replaces sensitive object values with `<redacted>`. */
|
|
341
|
+
function redactSensitive(value) {
|
|
342
|
+
if (Array.isArray(value)) return value.map((item) => redactSensitive(item));
|
|
343
|
+
if (value === null || typeof value !== "object") return value;
|
|
344
|
+
const redacted = {};
|
|
345
|
+
for (const [key, inner] of Object.entries(value)) redacted[key] = isSensitiveKey(key) ? REDACTED : redactSensitive(inner);
|
|
346
|
+
return redacted;
|
|
347
|
+
}
|
|
348
|
+
/** Replaces the value of every sensitive query parameter. */
|
|
349
|
+
function redactQuery(query) {
|
|
350
|
+
const redacted = {};
|
|
351
|
+
for (const [key, value] of Object.entries(query)) redacted[key] = isSensitiveKey(key) ? REDACTED : value;
|
|
352
|
+
return redacted;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* A URL safe to print: embedded credentials are removed and sensitive query
|
|
356
|
+
* values are redacted, matching the object redaction rule.
|
|
357
|
+
*/
|
|
358
|
+
function redactUrlForDisplay(raw) {
|
|
359
|
+
let url;
|
|
360
|
+
try {
|
|
361
|
+
url = new URL(raw);
|
|
362
|
+
} catch {
|
|
363
|
+
return raw;
|
|
364
|
+
}
|
|
365
|
+
url.username = "";
|
|
366
|
+
url.password = "";
|
|
367
|
+
for (const key of [...url.searchParams.keys()]) if (isSensitiveKey(key)) url.searchParams.set(key, REDACTED);
|
|
368
|
+
return url.toString();
|
|
369
|
+
}
|
|
370
|
+
// src/http.ts
|
|
371
|
+
const AUTH_REMEDIATION = "Run `hngr auth login` or set HANGAR_TOKEN.";
|
|
372
|
+
/** Builds the absolute request URL; the redacted form is safe for dry-run and errors. */
|
|
373
|
+
function buildRequestUrl(connection, path, query) {
|
|
374
|
+
const qs = serializeQueryParams(query);
|
|
375
|
+
const url = `${connection.server}${path}${qs ? `?${qs}` : ""}`;
|
|
376
|
+
return {
|
|
377
|
+
url,
|
|
378
|
+
redactedUrl: redactUrlForDisplay(url)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/** Maps a failed response or transport error onto the CLI error contract. */
|
|
382
|
+
function toHngrError(err) {
|
|
383
|
+
if (err.status === 401) return new HngrError({
|
|
384
|
+
type: err.type === "about:blank" ? "auth" : err.type,
|
|
385
|
+
title: err.title ?? "Unauthenticated",
|
|
386
|
+
status: err.status,
|
|
387
|
+
requestId: err.requestId,
|
|
388
|
+
detail: err.detail ? `${err.detail} ${AUTH_REMEDIATION}` : AUTH_REMEDIATION
|
|
389
|
+
}, 4);
|
|
390
|
+
let detail = err.detail;
|
|
391
|
+
if (err.retryAfter !== void 0) {
|
|
392
|
+
const hint = `retry after ${err.retryAfter}s`;
|
|
393
|
+
detail = detail ? `${detail} (${hint})` : hint;
|
|
394
|
+
}
|
|
395
|
+
return new HngrError({
|
|
396
|
+
type: err.type,
|
|
397
|
+
title: err.title ?? `Request failed with status ${err.status}`,
|
|
398
|
+
status: err.status,
|
|
399
|
+
requestId: err.requestId,
|
|
400
|
+
detail
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Sends one request and returns the raw Response. A Bearer token goes out
|
|
405
|
+
* when the connection has one; non-2xx responses and transport errors become
|
|
406
|
+
* HngrErrors through the same mapping as everything else.
|
|
407
|
+
*/
|
|
408
|
+
async function apiRequest(connection, options) {
|
|
409
|
+
const { url } = buildRequestUrl(connection, options.path, options.query);
|
|
410
|
+
const headers = {
|
|
411
|
+
Accept: "application/json",
|
|
412
|
+
...options.headers
|
|
413
|
+
};
|
|
414
|
+
if (connection.token) headers.Authorization = `Bearer ${connection.token}`;
|
|
415
|
+
const init = {
|
|
416
|
+
method: options.method,
|
|
417
|
+
headers,
|
|
418
|
+
signal: options.signal
|
|
419
|
+
};
|
|
420
|
+
if (options.rawBody !== void 0) init.body = options.rawBody;
|
|
421
|
+
else if (options.body !== void 0) {
|
|
422
|
+
if (!("Content-Type" in headers)) headers["Content-Type"] = "application/json";
|
|
423
|
+
init.body = JSON.stringify(options.body);
|
|
424
|
+
}
|
|
425
|
+
let response;
|
|
426
|
+
try {
|
|
427
|
+
response = await fetch(url, init);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const origin = redactUrl(connection.server);
|
|
430
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
431
|
+
throw new HngrError({
|
|
432
|
+
type: "network",
|
|
433
|
+
title: `Could not reach the Hangar server at ${origin}`,
|
|
434
|
+
detail: scrubCredentials(cause, connection.server)
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (!response.ok) throw toHngrError(await apiErrorFromResponse(response, response.headers.get("X-Request-ID") ?? void 0));
|
|
438
|
+
return response;
|
|
439
|
+
}
|
|
440
|
+
/** GETs a JSON resource; anonymous reads work when no token is configured. */
|
|
441
|
+
async function apiGet(connection, path, query, options) {
|
|
442
|
+
return (await apiRequest(connection, {
|
|
443
|
+
method: "GET",
|
|
444
|
+
path,
|
|
445
|
+
query,
|
|
446
|
+
signal: options?.signal
|
|
447
|
+
})).json();
|
|
448
|
+
}
|
|
449
|
+
/** Sends a non-GET JSON request and returns the raw Response (status, headers). */
|
|
450
|
+
async function apiFetch(connection, options) {
|
|
451
|
+
return apiRequest(connection, {
|
|
452
|
+
method: options.method,
|
|
453
|
+
path: options.path,
|
|
454
|
+
body: options.body,
|
|
455
|
+
headers: options.headers,
|
|
456
|
+
signal: options.signal
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
export { renderError as C, HngrError as S, scrubCredentials as _, toHngrError as a, writeFileAtomically as b, redactUrlForDisplay as c, configFilePath as d, deleteStoredConfig as f, resolveConnection as g, redactUrl as h, buildRequestUrl as i, apiErrorFromResponse as l, readStoredConfig as m, apiGet as n, redactQuery as o, normalizeServerUrl as p, apiRequest as r, redactSensitive as s, apiFetch as t, DEFAULT_SERVER_URL as u, writeStoredConfig as v, CancelledError as x, writeDirectoryAtomically as y };
|