greybull 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/dist/index.js +143 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ var ApiError = class extends Error {
|
|
|
94
94
|
this.status = status;
|
|
95
95
|
}
|
|
96
96
|
};
|
|
97
|
-
var VERSION = "0.
|
|
97
|
+
var VERSION = "0.2.0";
|
|
98
98
|
function buildRequest(opts) {
|
|
99
99
|
const url = new URL(opts.apiUrl.replace(/\/+$/, "") + opts.path);
|
|
100
100
|
if (opts.query) {
|
|
@@ -148,13 +148,21 @@ async function apiRequest(opts) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// src/output.ts
|
|
151
|
-
function formatTable(headers, rows) {
|
|
151
|
+
function formatTable(headers, rows, opts = {}) {
|
|
152
152
|
if (rows.length === 0) return "(none)";
|
|
153
|
+
const max = opts.max ?? [];
|
|
154
|
+
const clip = (s, i) => {
|
|
155
|
+
const w = max[i];
|
|
156
|
+
return w && s.length > w ? s.slice(0, w - 1) + "\u2026" : s;
|
|
157
|
+
};
|
|
158
|
+
const hdr = headers.map((h, i) => clip(h, i));
|
|
159
|
+
const cells = rows.map((r) => headers.map((_, i) => clip(String(r[i] ?? ""), i)));
|
|
160
|
+
const last = headers.length - 1;
|
|
153
161
|
const widths = headers.map(
|
|
154
|
-
(
|
|
162
|
+
(_, i) => Math.max(hdr[i].length, ...cells.map((r) => r[i].length))
|
|
155
163
|
);
|
|
156
|
-
const line = (
|
|
157
|
-
return [line(
|
|
164
|
+
const line = (row) => row.map((c, i) => i === last ? c : c.padEnd(widths[i])).join(" ").trimEnd();
|
|
165
|
+
return [line(hdr), ...cells.map(line)].join("\n");
|
|
158
166
|
}
|
|
159
167
|
function fail(error, opts) {
|
|
160
168
|
let message = error instanceof Error ? error.message : String(error);
|
|
@@ -300,6 +308,10 @@ function page(title, body) {
|
|
|
300
308
|
}
|
|
301
309
|
|
|
302
310
|
// src/commands/dns.ts
|
|
311
|
+
import fs2 from "fs";
|
|
312
|
+
import os3 from "os";
|
|
313
|
+
import path2 from "path";
|
|
314
|
+
import { spawnSync } from "child_process";
|
|
303
315
|
var RECORD_TYPES = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV"];
|
|
304
316
|
function buildListRecords(apiUrl, zone, opts) {
|
|
305
317
|
return buildRequest({
|
|
@@ -368,11 +380,11 @@ async function listCmd(ctx2, zone, opts) {
|
|
|
368
380
|
const rows = res.data.map((r) => [
|
|
369
381
|
r.name,
|
|
370
382
|
r.type,
|
|
371
|
-
r.content,
|
|
372
383
|
String(r.ttl),
|
|
373
|
-
r.priority !== void 0 ? String(r.priority) : ""
|
|
384
|
+
r.priority !== void 0 ? String(r.priority) : "",
|
|
385
|
+
r.content
|
|
374
386
|
]);
|
|
375
|
-
process.stdout.write(formatTable(["NAME", "TYPE", "
|
|
387
|
+
process.stdout.write(formatTable(["NAME", "TYPE", "TTL", "PRIO", "CONTENT"], rows, { max: [48] }) + "\n");
|
|
376
388
|
}
|
|
377
389
|
async function addCmd(ctx2, zone, type, name, content, opts) {
|
|
378
390
|
const upperType = assertType(type, ctx2);
|
|
@@ -424,6 +436,125 @@ async function deleteCmd(ctx2, zone, type, name, content, opts) {
|
|
|
424
436
|
function runSpec(spec, ctx2) {
|
|
425
437
|
return performRequest(spec, ctx2.apiKey).catch((err) => fail(err, ctx2));
|
|
426
438
|
}
|
|
439
|
+
var ERR_PREFIX = "; !! ";
|
|
440
|
+
function renderZoneFile(zone, records) {
|
|
441
|
+
const rows = records.map((r) => {
|
|
442
|
+
const content = r.type === "MX" && r.priority !== void 0 ? `${r.priority} ${r.content}` : r.content;
|
|
443
|
+
return { name: r.name, ttl: String(r.ttl), type: r.type, content };
|
|
444
|
+
}).sort((a, b) => a.name.localeCompare(b.name) || a.type.localeCompare(b.type));
|
|
445
|
+
const nameW = Math.max(1, ...rows.map((r) => r.name.length));
|
|
446
|
+
const ttlW = Math.max(3, ...rows.map((r) => r.ttl.length));
|
|
447
|
+
const typeW = Math.max(4, ...rows.map((r) => r.type.length));
|
|
448
|
+
const header = [
|
|
449
|
+
`; Zone: ${zone}`,
|
|
450
|
+
"; Edit records below, then save and close your editor to apply.",
|
|
451
|
+
"; Format: NAME TTL TYPE CONTENT (use @ for the zone apex)",
|
|
452
|
+
"; TXT values keep their quotes; MX/SRV keep priority in CONTENT.",
|
|
453
|
+
"; SOA and the apex NS records are managed for you and not shown.",
|
|
454
|
+
";"
|
|
455
|
+
];
|
|
456
|
+
const body = rows.map(
|
|
457
|
+
(r) => `${r.name.padEnd(nameW)} ${r.ttl.padEnd(ttlW)} ${r.type.padEnd(typeW)} ${r.content}`
|
|
458
|
+
);
|
|
459
|
+
return [...header, ...body].join("\n") + "\n";
|
|
460
|
+
}
|
|
461
|
+
function parseZoneFile(text) {
|
|
462
|
+
const recs = [];
|
|
463
|
+
const lines = text.split("\n");
|
|
464
|
+
for (let i = 0; i < lines.length; i++) {
|
|
465
|
+
const trimmed = lines[i].trim();
|
|
466
|
+
if (!trimmed || trimmed.startsWith(";")) continue;
|
|
467
|
+
const m = trimmed.match(/^(\S+)\s+(\d+)\s+(?:IN\s+)?([A-Za-z]+)\s+(.+)$/i);
|
|
468
|
+
if (!m) {
|
|
469
|
+
throw new Error(`line ${i + 1}: could not parse "${trimmed}" (expected: NAME TTL TYPE CONTENT)`);
|
|
470
|
+
}
|
|
471
|
+
const [, name, ttlStr, typeRaw, content] = m;
|
|
472
|
+
const type = typeRaw.toUpperCase();
|
|
473
|
+
if (type === "SOA") throw new Error(`line ${i + 1}: SOA is managed automatically \u2014 remove it`);
|
|
474
|
+
if (!RECORD_TYPES.includes(type)) throw new Error(`line ${i + 1}: unknown record type "${typeRaw}"`);
|
|
475
|
+
recs.push({ name, type, content: content.trim(), ttl: parseInt(ttlStr) });
|
|
476
|
+
}
|
|
477
|
+
return recs;
|
|
478
|
+
}
|
|
479
|
+
function normalizeBody(text) {
|
|
480
|
+
return text.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith(";")).map((l) => l.replace(/\s+/g, " ")).sort().join("\n");
|
|
481
|
+
}
|
|
482
|
+
function stripErrorLines(text) {
|
|
483
|
+
return text.split("\n").filter((l) => !l.startsWith(ERR_PREFIX)).join("\n");
|
|
484
|
+
}
|
|
485
|
+
function openInEditor(zone, content) {
|
|
486
|
+
const file = path2.join(os3.tmpdir(), `greybull-${zone}-${process.pid}.zone`);
|
|
487
|
+
fs2.writeFileSync(file, content);
|
|
488
|
+
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
|
|
489
|
+
const [cmd, ...args] = editor.split(" ");
|
|
490
|
+
try {
|
|
491
|
+
const r = spawnSync(cmd, [...args, file], { stdio: "inherit" });
|
|
492
|
+
if (r.error) return null;
|
|
493
|
+
return fs2.readFileSync(file, "utf8");
|
|
494
|
+
} finally {
|
|
495
|
+
try {
|
|
496
|
+
fs2.unlinkSync(file);
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function editCmd(ctx2, zone) {
|
|
502
|
+
const res = await apiRequest({
|
|
503
|
+
apiUrl: ctx2.apiUrl,
|
|
504
|
+
apiKey: ctx2.apiKey,
|
|
505
|
+
method: "GET",
|
|
506
|
+
path: `/api/v1/dns/zones/${encodeURIComponent(zone)}/records`
|
|
507
|
+
}).catch((err) => fail(err, ctx2));
|
|
508
|
+
const original = renderZoneFile(zone, res.data);
|
|
509
|
+
let buffer = original;
|
|
510
|
+
let banner = "";
|
|
511
|
+
let lastTried = null;
|
|
512
|
+
for (; ; ) {
|
|
513
|
+
const edited = openInEditor(zone, banner + buffer);
|
|
514
|
+
if (edited === null) fail(new Error("Could not launch editor. Set $EDITOR or $VISUAL."), ctx2);
|
|
515
|
+
buffer = stripErrorLines(edited);
|
|
516
|
+
if (normalizeBody(buffer) === normalizeBody(original)) {
|
|
517
|
+
process.stdout.write("No changes.\n");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (lastTried !== null && normalizeBody(buffer) === normalizeBody(lastTried)) {
|
|
521
|
+
fail(new Error("Aborted \u2014 the reported problem was not fixed."), ctx2);
|
|
522
|
+
}
|
|
523
|
+
let records;
|
|
524
|
+
try {
|
|
525
|
+
records = parseZoneFile(buffer);
|
|
526
|
+
} catch (err) {
|
|
527
|
+
banner = errorBanner(err.message);
|
|
528
|
+
lastTried = buffer;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const put = await apiRequest({
|
|
533
|
+
apiUrl: ctx2.apiUrl,
|
|
534
|
+
apiKey: ctx2.apiKey,
|
|
535
|
+
method: "PUT",
|
|
536
|
+
path: `/api/v1/dns/zones/${encodeURIComponent(zone)}`,
|
|
537
|
+
body: { records }
|
|
538
|
+
});
|
|
539
|
+
process.stdout.write(`Applied ${put.data.applied} record(s) to ${zone}.
|
|
540
|
+
`);
|
|
541
|
+
return;
|
|
542
|
+
} catch (err) {
|
|
543
|
+
if (err instanceof ApiError && (err.status === 422 || err.status === 400)) {
|
|
544
|
+
banner = errorBanner(err.message);
|
|
545
|
+
lastTried = buffer;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
fail(err, ctx2);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function errorBanner(message) {
|
|
553
|
+
return `${ERR_PREFIX}${message}
|
|
554
|
+
${ERR_PREFIX}Fix and save again, or exit without changes to abort.
|
|
555
|
+
;
|
|
556
|
+
`;
|
|
557
|
+
}
|
|
427
558
|
|
|
428
559
|
// src/commands/domains.ts
|
|
429
560
|
async function domainsListCmd(ctx2) {
|
|
@@ -464,7 +595,7 @@ async function domainsCheckCmd(ctx2, query, opts) {
|
|
|
464
595
|
|
|
465
596
|
// src/index.ts
|
|
466
597
|
var program = new Command();
|
|
467
|
-
program.name("greybull").description("Manage Greybull DNS records and domains from the command line").version("0.
|
|
598
|
+
program.name("greybull").description("Manage Greybull DNS records and domains from the command line").version("0.2.0").option("--json", "output raw JSON", false).option("--api-url <url>", "portal base URL (or set GREYBULL_API_URL)");
|
|
468
599
|
function ctx(requireAuth = true) {
|
|
469
600
|
const opts = program.opts();
|
|
470
601
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
@@ -506,6 +637,9 @@ dns.command("update <zone> <type> <name> <old-content> <new-content>").descripti
|
|
|
506
637
|
dns.command("delete <zone> <type> <name> <content>").description("Delete one specific record (content required \u2014 sibling-safe)").option("--priority <n>", "priority (MX/SRV)").action(async (zone, type, name, content, opts) => {
|
|
507
638
|
await deleteCmd(ctx(), zone, type, name, content, opts);
|
|
508
639
|
});
|
|
640
|
+
dns.command("edit <zone>").description("Edit the whole zone in $EDITOR (like pdnsutil edit-zone)").action(async (zone) => {
|
|
641
|
+
await editCmd(ctx(), zone);
|
|
642
|
+
});
|
|
509
643
|
var domains = program.command("domains").description("Manage domains");
|
|
510
644
|
domains.command("list").description("List your registered domains").action(async () => {
|
|
511
645
|
await domainsListCmd(ctx());
|