@hasna/shortlinks 0.2.5 → 0.2.7

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/cli/index.js CHANGED
@@ -33,7 +33,7 @@ var __toESM = (mod, isNodeMode, target) => {
33
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
34
  var __require = import.meta.require;
35
35
 
36
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/error.js
36
+ // node_modules/commander/lib/error.js
37
37
  var require_error = __commonJS((exports) => {
38
38
  class CommanderError extends Error {
39
39
  constructor(exitCode, code, message) {
@@ -57,7 +57,7 @@ var require_error = __commonJS((exports) => {
57
57
  exports.InvalidArgumentError = InvalidArgumentError;
58
58
  });
59
59
 
60
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/argument.js
60
+ // node_modules/commander/lib/argument.js
61
61
  var require_argument = __commonJS((exports) => {
62
62
  var { InvalidArgumentError } = require_error();
63
63
 
@@ -136,7 +136,7 @@ var require_argument = __commonJS((exports) => {
136
136
  exports.humanReadableArgName = humanReadableArgName;
137
137
  });
138
138
 
139
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/help.js
139
+ // node_modules/commander/lib/help.js
140
140
  var require_help = __commonJS((exports) => {
141
141
  var { humanReadableArgName } = require_argument();
142
142
 
@@ -486,7 +486,7 @@ ${itemIndentStr}`);
486
486
  exports.stripColor = stripColor;
487
487
  });
488
488
 
489
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/option.js
489
+ // node_modules/commander/lib/option.js
490
490
  var require_option = __commonJS((exports) => {
491
491
  var { InvalidArgumentError } = require_error();
492
492
 
@@ -664,7 +664,7 @@ var require_option = __commonJS((exports) => {
664
664
  exports.DualOptions = DualOptions;
665
665
  });
666
666
 
667
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/suggestSimilar.js
667
+ // node_modules/commander/lib/suggestSimilar.js
668
668
  var require_suggestSimilar = __commonJS((exports) => {
669
669
  var maxDistance = 3;
670
670
  function editDistance(a, b) {
@@ -737,7 +737,7 @@ var require_suggestSimilar = __commonJS((exports) => {
737
737
  exports.suggestSimilar = suggestSimilar;
738
738
  });
739
739
 
740
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/command.js
740
+ // node_modules/commander/lib/command.js
741
741
  var require_command = __commonJS((exports) => {
742
742
  var EventEmitter = __require("events").EventEmitter;
743
743
  var childProcess = __require("child_process");
@@ -979,7 +979,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
979
979
  this._exitCallback = (err) => {
980
980
  if (err.code !== "commander.executeSubCommandAsync") {
981
981
  throw err;
982
- } else {}
982
+ }
983
983
  };
984
984
  }
985
985
  return this;
@@ -2047,7 +2047,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2047
2047
  exports.useColor = useColor;
2048
2048
  });
2049
2049
 
2050
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/index.js
2050
+ // node_modules/commander/index.js
2051
2051
  var require_commander = __commonJS((exports) => {
2052
2052
  var { Argument } = require_argument();
2053
2053
  var { Command } = require_command();
@@ -2894,7 +2894,7 @@ async function upsertCloudflareDnsRecord(options) {
2894
2894
  return { id: created.id, action: "created" };
2895
2895
  }
2896
2896
 
2897
- // node_modules/@hasna/events/dist/commander.js
2897
+ // node_modules/@hasna/events/dist/index.js
2898
2898
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
2899
2899
  import { existsSync as existsSync3 } from "fs";
2900
2900
  import { homedir as homedir2 } from "os";
@@ -3413,172 +3413,8 @@ function normalizeRetryPolicy(policy) {
3413
3413
  multiplier: Math.max(1, policy?.multiplier ?? 2)
3414
3414
  };
3415
3415
  }
3416
- function parseJsonObject2(value, fallback) {
3417
- if (!value)
3418
- return fallback;
3419
- const parsed = JSON.parse(value);
3420
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3421
- throw new Error("Expected a JSON object");
3422
- }
3423
- return parsed;
3424
- }
3425
- function parseHeaders(values) {
3426
- if (!values?.length)
3427
- return;
3428
- const headers = {};
3429
- for (const value of values) {
3430
- const separator = value.indexOf("=");
3431
- if (separator === -1)
3432
- throw new Error(`Invalid header, expected name=value: ${value}`);
3433
- headers[value.slice(0, separator)] = value.slice(separator + 1);
3434
- }
3435
- return headers;
3436
- }
3437
- function parseFilter(options) {
3438
- const filter2 = {};
3439
- if (options.source)
3440
- filter2.source = options.source;
3441
- if (options.type)
3442
- filter2.type = options.type;
3443
- if (options.subject)
3444
- filter2.subject = options.subject;
3445
- if (options.severity)
3446
- filter2.severity = options.severity;
3447
- return Object.keys(filter2).length > 0 ? [filter2] : undefined;
3448
- }
3449
- function createClient(options) {
3450
- if (options.createClient)
3451
- return options.createClient();
3452
- return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
3453
- }
3454
- function print(value, json2, text) {
3455
- if (json2)
3456
- console.log(JSON.stringify(value, null, 2));
3457
- else
3458
- console.log(text);
3459
- }
3460
- function hasJsonOption(options) {
3461
- return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
3462
- }
3463
- function wantsJson(actionOptions, command) {
3464
- return hasJsonOption(actionOptions) || hasJsonOption(command);
3465
- }
3466
- function registerWebhookCommands(program, options) {
3467
- const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
3468
- webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
3469
- const timestamp = new Date().toISOString();
3470
- const channel = {
3471
- id: actionOptions.id,
3472
- name: actionOptions.name,
3473
- enabled: !actionOptions.disabled,
3474
- transport: actionOptions.transport,
3475
- filters: parseFilter(actionOptions),
3476
- retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
3477
- redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
3478
- createdAt: timestamp,
3479
- updatedAt: timestamp
3480
- };
3481
- if (actionOptions.transport === "webhook") {
3482
- channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
3483
- } else if (actionOptions.transport === "command") {
3484
- channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
3485
- } else {
3486
- throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
3487
- }
3488
- const saved = await createClient(options).addChannel(channel);
3489
- print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
3490
- });
3491
- webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
3492
- const channels = await createClient(options).listChannels();
3493
- if (wantsJson(actionOptions, command)) {
3494
- console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
3495
- return;
3496
- }
3497
- if (!channels.length) {
3498
- console.log("No channels configured.");
3499
- return;
3500
- }
3501
- for (const channel of channels) {
3502
- console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
3503
- }
3504
- });
3505
- webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
3506
- const removed = await createClient(options).removeChannel(id);
3507
- print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
3508
- });
3509
- webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
3510
- const result = await createClient(options).testChannel(id, {
3511
- source: options.source,
3512
- type: actionOptions.type,
3513
- subject: actionOptions.subject ?? id,
3514
- message: actionOptions.message,
3515
- data: parseJsonObject2(actionOptions.data, { test: true })
3516
- });
3517
- print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
3518
- });
3519
- return webhooks;
3520
- }
3521
- function registerEventCommands(program, options) {
3522
- const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
3523
- events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions, command) => {
3524
- const result = await createClient(options).emit({
3525
- source: actionOptions.source ?? options.source,
3526
- type,
3527
- subject: actionOptions.subject,
3528
- severity: actionOptions.severity,
3529
- message: actionOptions.message,
3530
- dedupeKey: actionOptions.dedupeKey,
3531
- data: parseJsonObject2(actionOptions.data, {}),
3532
- metadata: parseJsonObject2(actionOptions.metadata, {})
3533
- }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
3534
- print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
3535
- });
3536
- events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
3537
- let rows = await createClient(options).listEvents();
3538
- if (actionOptions.source)
3539
- rows = rows.filter((event) => event.source === actionOptions.source);
3540
- if (actionOptions.type)
3541
- rows = rows.filter((event) => event.type === actionOptions.type);
3542
- if (actionOptions.limit)
3543
- rows = rows.slice(-actionOptions.limit);
3544
- if (wantsJson(actionOptions, command)) {
3545
- console.log(JSON.stringify(rows, null, 2));
3546
- return;
3547
- }
3548
- if (!rows.length) {
3549
- console.log("No events recorded.");
3550
- return;
3551
- }
3552
- for (const event of rows)
3553
- console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
3554
- });
3555
- events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
3556
- const result = await createClient(options).replay({
3557
- eventId: actionOptions.id,
3558
- source: actionOptions.source,
3559
- type: actionOptions.type,
3560
- dryRun: actionOptions.dryRun
3561
- });
3562
- print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
3563
- });
3564
- return events;
3565
- }
3566
- function registerEventsCommands(program, options) {
3567
- registerWebhookCommands(program, options);
3568
- registerEventCommands(program, options);
3569
- }
3570
- function parseNumber(value) {
3571
- const parsed = Number(value);
3572
- if (!Number.isFinite(parsed))
3573
- throw new Error(`Expected a number, got ${value}`);
3574
- return parsed;
3575
- }
3576
- function collectValues(value, previous) {
3577
- previous.push(value);
3578
- return previous;
3579
- }
3580
3416
 
3581
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
3417
+ // node_modules/commander/esm.mjs
3582
3418
  var import__ = __toESM(require_commander(), 1);
3583
3419
  var {
3584
3420
  program,
@@ -3594,7 +3430,7 @@ var {
3594
3430
  Help
3595
3431
  } = import__.default;
3596
3432
 
3597
- // node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
3433
+ // node_modules/chalk/source/vendor/ansi-styles/index.js
3598
3434
  var ANSI_BACKGROUND_OFFSET = 10;
3599
3435
  var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
3600
3436
  var wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`;
@@ -3771,7 +3607,7 @@ function assembleStyles() {
3771
3607
  var ansiStyles = assembleStyles();
3772
3608
  var ansi_styles_default = ansiStyles;
3773
3609
 
3774
- // node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/supports-color/index.js
3610
+ // node_modules/chalk/source/vendor/supports-color/index.js
3775
3611
  import process2 from "process";
3776
3612
  import os from "os";
3777
3613
  import tty from "tty";
@@ -3903,7 +3739,7 @@ var supportsColor = {
3903
3739
  };
3904
3740
  var supports_color_default = supportsColor;
3905
3741
 
3906
- // node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/utilities.js
3742
+ // node_modules/chalk/source/utilities.js
3907
3743
  function stringReplaceAll(string, substring, replacer) {
3908
3744
  let index = string.indexOf(substring);
3909
3745
  if (index === -1) {
@@ -3936,7 +3772,7 @@ function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
3936
3772
  return returnValue;
3937
3773
  }
3938
3774
 
3939
- // node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/index.js
3775
+ // node_modules/chalk/source/index.js
3940
3776
  var { stdout: stdoutColor, stderr: stderrColor } = supports_color_default;
3941
3777
  var GENERATOR = Symbol("GENERATOR");
3942
3778
  var STYLER = Symbol("STYLER");
@@ -4089,7 +3925,7 @@ import { dirname as dirname3, join as join6 } from "path";
4089
3925
  import { fileURLToPath } from "url";
4090
3926
  import { spawnSync as spawnSync3 } from "child_process";
4091
3927
 
4092
- // node_modules/.pnpm/@hasna+contracts@0.5.2/node_modules/@hasna/contracts/dist/client/storage.js
3928
+ // node_modules/@hasna/contracts/dist/client/storage.js
4093
3929
  var __defProp2 = Object.defineProperty;
4094
3930
  var __returnValue = (v) => v;
4095
3931
  function __exportSetter(name, newValue) {
@@ -10797,7 +10633,7 @@ var program2 = new Command;
10797
10633
  function useJson(localOpts) {
10798
10634
  return Boolean(localOpts?.json || program2.opts().json);
10799
10635
  }
10800
- function print2(data, localOpts, human) {
10636
+ function print(data, localOpts, human) {
10801
10637
  if (useJson(localOpts)) {
10802
10638
  console.log(JSON.stringify(data, null, 2));
10803
10639
  return;
@@ -10822,13 +10658,335 @@ async function withRuntimeStore(fn) {
10822
10658
  await store.close();
10823
10659
  }
10824
10660
  }
10825
- function formatLink(link) {
10826
- return `${source_default.green(link.short_url || `${link.hostname}/${link.slug}`)} ${source_default.dim("->")} ${link.destination_url}`;
10827
- }
10828
10661
  function commandExists(command) {
10829
10662
  const result = spawnSync3("which", [command], { encoding: "utf-8" });
10830
10663
  return result.status === 0;
10831
10664
  }
10665
+ var DEFAULT_HUMAN_LIMIT = 20;
10666
+ var DEFAULT_JSON_LIMIT = 100;
10667
+ var TEXT_LIMIT = 88;
10668
+ var EXTERNAL_OUTPUT_LIMIT = 20;
10669
+ var EXTERNAL_OUTPUT_WIDTH = 120;
10670
+ function parseLimit(value, fallback, label = "--limit") {
10671
+ if (value === undefined || value === null || value === "")
10672
+ return fallback;
10673
+ const parsed = Number(value);
10674
+ if (!Number.isInteger(parsed) || parsed <= 0)
10675
+ throw new Error(`${label} must be a positive integer.`);
10676
+ return parsed;
10677
+ }
10678
+ function humanLimit(opts) {
10679
+ return parseLimit(opts.limit, DEFAULT_HUMAN_LIMIT);
10680
+ }
10681
+ function jsonLimit(opts) {
10682
+ return opts.limit === undefined ? undefined : parseLimit(opts.limit, DEFAULT_JSON_LIMIT);
10683
+ }
10684
+ function truncateText(value, max = TEXT_LIMIT) {
10685
+ const text = value || "";
10686
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text;
10687
+ }
10688
+ function yesNo(value) {
10689
+ return value ? "yes" : "no";
10690
+ }
10691
+ function printHint(message) {
10692
+ console.log(source_default.dim(message));
10693
+ }
10694
+ function printVerbose(data, opts) {
10695
+ if (!opts.verbose)
10696
+ return false;
10697
+ console.log(JSON.stringify(data, null, 2));
10698
+ return true;
10699
+ }
10700
+ function formatLink(link, maxDestinationLength = 72) {
10701
+ return `${source_default.green(link.short_url || `${link.hostname}/${link.slug}`)} ${source_default.dim("->")} ${truncateText(link.destination_url, maxDestinationLength)}`;
10702
+ }
10703
+ function printBoundedTextOutput(text, stream) {
10704
+ const lines = text.trim().split(/\r?\n/).filter(Boolean);
10705
+ let truncated = lines.length > EXTERNAL_OUTPUT_LIMIT;
10706
+ for (const line of lines.slice(0, EXTERNAL_OUTPUT_LIMIT)) {
10707
+ const output = truncateText(line, EXTERNAL_OUTPUT_WIDTH);
10708
+ if (output !== line)
10709
+ truncated = true;
10710
+ if (stream === "stderr")
10711
+ console.error(output);
10712
+ else
10713
+ console.log(output);
10714
+ }
10715
+ return truncated;
10716
+ }
10717
+ function printExternalCommandResult(result, opts, label) {
10718
+ if (opts.verbose) {
10719
+ if (result.stdout.trim())
10720
+ console.log(result.stdout.trim());
10721
+ if (result.stderr.trim())
10722
+ console.error(result.stderr.trim());
10723
+ return;
10724
+ }
10725
+ const stdoutTruncated = result.stdout.trim() ? printBoundedTextOutput(result.stdout, "stdout") : false;
10726
+ const stderrTruncated = result.stderr.trim() ? printBoundedTextOutput(result.stderr, "stderr") : false;
10727
+ if (stdoutTruncated || stderrTruncated)
10728
+ printHint(`Use --verbose or --json for full ${label} command output.`);
10729
+ }
10730
+ function printDomainSummary(domain) {
10731
+ console.log(`${domain.default_domain ? "*" : " "} ${domain.hostname} ${source_default.dim(domain.provider)} default=${yesNo(domain.default_domain)}`);
10732
+ if (domain.origin_url)
10733
+ console.log(` origin: ${truncateText(domain.origin_url)}`);
10734
+ if (domain.cloudflare_worker_name)
10735
+ console.log(` worker: ${domain.cloudflare_worker_name}`);
10736
+ printHint("Use `shortlinks domain get <hostname> --verbose` or `--json` for full details.");
10737
+ }
10738
+ function printLinkSummary(link) {
10739
+ console.log(formatLink(link));
10740
+ console.log(` slug: ${link.slug}`);
10741
+ console.log(` domain: ${link.hostname}`);
10742
+ console.log(` active: ${yesNo(link.active)}`);
10743
+ if (link.title)
10744
+ console.log(` title: ${truncateText(link.title)}`);
10745
+ if (link.expires_at)
10746
+ console.log(` expires: ${link.expires_at}`);
10747
+ printHint("Use `shortlinks link get <slug> --verbose` or `--json` for full details.");
10748
+ }
10749
+ function printStatsSummary(stats) {
10750
+ if ("link" in stats) {
10751
+ console.log(`${stats.link.short_url || `${stats.link.hostname}/${stats.link.slug}`} clicks=${stats.clicks}`);
10752
+ console.log(` destination: ${truncateText(stats.link.destination_url)}`);
10753
+ console.log(` last clicked: ${stats.last_clicked_at || "never"}`);
10754
+ const topReferrer = stats.top_referrers[0];
10755
+ const topAgent = stats.top_user_agents[0];
10756
+ if (topReferrer)
10757
+ console.log(` top referrer: ${truncateText(topReferrer.referer || "(direct)", 72)} (${topReferrer.clicks})`);
10758
+ if (topAgent)
10759
+ console.log(` top user agent: ${truncateText(topAgent.user_agent || "(unknown)", 72)} (${topAgent.clicks})`);
10760
+ printHint("Use `shortlinks stats <slug> --verbose` or `--json` for full stats.");
10761
+ return;
10762
+ }
10763
+ console.log(`domains=${stats.domains} links=${stats.links} clicks=${stats.clicks}`);
10764
+ }
10765
+ function printConfigSummary(data) {
10766
+ const config = data.config;
10767
+ console.log("shortlinks config");
10768
+ console.log(` path: ${data.path}`);
10769
+ console.log(` default domain: ${config.defaultDomain || "(unset)"}`);
10770
+ console.log(` public base URL: ${config.publicBaseUrl || "(unset)"}`);
10771
+ if (config.cloudflare) {
10772
+ console.log(` cloudflare worker: ${config.cloudflare.workerName || "(unset)"}`);
10773
+ console.log(` cloudflare origin: ${config.cloudflare.origin || "(unset)"}`);
10774
+ }
10775
+ printHint("Use `shortlinks config show --verbose` or `--json` for full config.");
10776
+ }
10777
+ function printCloudflarePlanSummary(plan) {
10778
+ console.log(`Cloudflare plan for ${plan.hostname}`);
10779
+ console.log(` CNAME: ${plan.hostname} -> ${plan.target} proxied=${yesNo(plan.proxied)}`);
10780
+ console.log(` worker: ${plan.workerName}`);
10781
+ console.log(` origin: ${truncateText(plan.origin)}`);
10782
+ console.log(` deploy: ${plan.wranglerCommand}`);
10783
+ printHint("Use `--verbose` or `--json` for the full DNS payload.");
10784
+ }
10785
+ function printCloudflareDnsSummary(result) {
10786
+ if (result && typeof result === "object" && "dnsRecord" in result) {
10787
+ printCloudflarePlanSummary(result);
10788
+ return;
10789
+ }
10790
+ const value = result;
10791
+ console.log(`Cloudflare DNS ${value.action || "updated"} ${value.id || ""}`.trim());
10792
+ printHint("Use `--json` for the full API result.");
10793
+ }
10794
+ function printLocalPlanSummary(plan) {
10795
+ console.log(`Local plan for ${plan.domain}`);
10796
+ console.log(` target: ${plan.targetHost}:${plan.port}`);
10797
+ console.log(` hosts: ${plan.hostsEntry}`);
10798
+ console.log(` cert: ${truncateText(plan.certPath)}`);
10799
+ console.log(` key: ${truncateText(plan.keyPath)}`);
10800
+ console.log(` machines: ${plan.machinesCommand}`);
10801
+ printHint("Use `--verbose` or `--json` for the full Caddy snippet.");
10802
+ }
10803
+ function printDoctorSummary(data) {
10804
+ const missingCommands = Object.entries(data.commands).filter(([, present]) => !present).map(([name]) => name);
10805
+ const presentEnv = Object.entries(data.environment).filter(([, present]) => present).map(([name]) => name);
10806
+ console.log(`${data.service} doctor`);
10807
+ console.log(` store: ${data.store}`);
10808
+ console.log(` db: ${data.db_exists ? "found" : "missing"} ${truncateText(data.db_path)}`);
10809
+ console.log(` stats: domains=${data.stats.domains} links=${data.stats.links} clicks=${data.stats.clicks}`);
10810
+ console.log(` commands: ${missingCommands.length ? `missing ${missingCommands.join(", ")}` : "ok"}`);
10811
+ console.log(` env: ${presentEnv.length ? presentEnv.join(", ") : "no optional env vars detected"}`);
10812
+ printHint("Use `shortlinks doctor --verbose` or `--json` for paths and full readiness data.");
10813
+ }
10814
+ function parseNumber(value) {
10815
+ if (value === undefined)
10816
+ return;
10817
+ const parsed = Number(value);
10818
+ if (!Number.isFinite(parsed))
10819
+ throw new Error(`Expected a number, got ${value}`);
10820
+ return parsed;
10821
+ }
10822
+ function collectValues(value, previous = []) {
10823
+ return [...previous, value];
10824
+ }
10825
+ function parseJsonObject2(value, fallback = {}) {
10826
+ if (!value)
10827
+ return fallback;
10828
+ const parsed = JSON.parse(value);
10829
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
10830
+ throw new Error("Expected a JSON object.");
10831
+ }
10832
+ return parsed;
10833
+ }
10834
+ function parseHeaders(values = []) {
10835
+ const headers = {};
10836
+ for (const value of values) {
10837
+ const index = value.indexOf("=");
10838
+ if (index <= 0)
10839
+ throw new Error(`Header must be name=value: ${value}`);
10840
+ headers[value.slice(0, index)] = value.slice(index + 1);
10841
+ }
10842
+ return headers;
10843
+ }
10844
+ function parseEventFilter(opts) {
10845
+ const filter = {};
10846
+ if (opts.type)
10847
+ filter.type = opts.type;
10848
+ if (opts.source)
10849
+ filter.source = opts.source;
10850
+ if (opts.subject)
10851
+ filter.subject = opts.subject;
10852
+ if (opts.severity)
10853
+ filter.severity = opts.severity;
10854
+ return Object.keys(filter).length ? [filter] : [];
10855
+ }
10856
+ function createEventsClient() {
10857
+ return new EventsClient;
10858
+ }
10859
+ function formatEventRow(event) {
10860
+ return `${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}${event.subject ? ` ${truncateText(event.subject, 48)}` : ""}`;
10861
+ }
10862
+ function formatChannelTarget(channel) {
10863
+ return channel.webhook?.url ?? channel.command?.command ?? channel.transport;
10864
+ }
10865
+ function registerCompactEventsCommands(program3) {
10866
+ const webhooks = program3.command("webhooks").description("Manage Hasna event webhook subscriptions");
10867
+ webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Output JSON").action(async (target, opts) => {
10868
+ try {
10869
+ const transport = opts.transport;
10870
+ const channel = {
10871
+ id: opts.id,
10872
+ name: opts.name,
10873
+ enabled: !opts.disabled,
10874
+ transport,
10875
+ filters: parseEventFilter(opts),
10876
+ retry: opts.retryAttempts || opts.retryBackoffMs ? { maxAttempts: opts.retryAttempts, backoffMs: opts.retryBackoffMs } : undefined,
10877
+ redact: opts.redact?.length ? { paths: opts.redact } : undefined
10878
+ };
10879
+ if (transport === "webhook") {
10880
+ channel.webhook = { url: target, secret: opts.secret, headers: parseHeaders(opts.header), timeoutMs: opts.timeoutMs };
10881
+ } else if (transport === "command") {
10882
+ channel.command = { command: target, args: opts.arg ?? [], timeoutMs: opts.timeoutMs };
10883
+ } else {
10884
+ throw new Error(`Transport ${transport} is reserved for future use and cannot be added yet.`);
10885
+ }
10886
+ const saved = sanitizeChannelForOutput(await createEventsClient().addChannel(channel));
10887
+ print(saved, opts, () => console.log(`Added ${saved.transport} channel ${saved.id}`));
10888
+ } catch (error) {
10889
+ handleError(error);
10890
+ }
10891
+ });
10892
+ webhooks.command("list").description("List configured subscriptions").option("--limit <n>", "Maximum rows").option("-j, --json", "Output JSON").action(async (opts) => {
10893
+ try {
10894
+ const channels = sanitizeChannelsForOutput(await createEventsClient().listChannels());
10895
+ const outputChannels = useJson(opts) && opts.limit === undefined ? channels : channels.slice(0, useJson(opts) ? parseLimit(opts.limit, channels.length) : humanLimit(opts));
10896
+ print(outputChannels, opts, () => {
10897
+ if (!channels.length) {
10898
+ console.log("No channels configured.");
10899
+ return;
10900
+ }
10901
+ for (const channel of outputChannels) {
10902
+ console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${truncateText(formatChannelTarget(channel), 80)}`);
10903
+ }
10904
+ console.log(source_default.dim(`Showing ${outputChannels.length} of ${channels.length} channel(s).`));
10905
+ if (channels.length > outputChannels.length)
10906
+ printHint(`Use --limit ${channels.length} or --json for more.`);
10907
+ });
10908
+ } catch (error) {
10909
+ handleError(error);
10910
+ }
10911
+ });
10912
+ webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Output JSON").action(async (id, opts) => {
10913
+ try {
10914
+ const removed = await createEventsClient().removeChannel(id);
10915
+ print({ removed }, opts, () => console.log(removed ? `Removed ${id}` : `Channel not found: ${id}`));
10916
+ } catch (error) {
10917
+ handleError(error);
10918
+ }
10919
+ });
10920
+ webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Output JSON").action(async (id, opts) => {
10921
+ try {
10922
+ const result = await createEventsClient().testChannel(id, {
10923
+ source: "shortlinks",
10924
+ type: opts.type,
10925
+ subject: opts.subject ?? id,
10926
+ message: opts.message,
10927
+ data: parseJsonObject2(opts.data, { test: true })
10928
+ });
10929
+ print(result, opts, () => console.log(`${result.status}: ${result.channelId}`));
10930
+ } catch (error) {
10931
+ handleError(error);
10932
+ }
10933
+ });
10934
+ const events = program3.command("events").description("Emit, list, and replay Hasna events");
10935
+ events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Output JSON").action(async (type, opts) => {
10936
+ try {
10937
+ const result = await createEventsClient().emit({
10938
+ source: opts.source ?? "shortlinks",
10939
+ type,
10940
+ subject: opts.subject,
10941
+ severity: opts.severity,
10942
+ message: opts.message,
10943
+ dedupeKey: opts.dedupeKey,
10944
+ data: parseJsonObject2(opts.data, {}),
10945
+ metadata: parseJsonObject2(opts.metadata, {})
10946
+ }, { deliver: opts.deliver, dedupe: opts.dedupe });
10947
+ print(result, opts, () => console.log(`${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`));
10948
+ } catch (error) {
10949
+ handleError(error);
10950
+ }
10951
+ });
10952
+ events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Maximum rows").option("-j, --json", "Output JSON").action(async (opts) => {
10953
+ try {
10954
+ let rows = await createEventsClient().listEvents();
10955
+ if (opts.source)
10956
+ rows = rows.filter((event) => event.source === opts.source);
10957
+ if (opts.type)
10958
+ rows = rows.filter((event) => event.type === opts.type);
10959
+ const limit = useJson(opts) ? jsonLimit(opts) : humanLimit(opts);
10960
+ const outputRows = limit === undefined ? rows : rows.slice(-limit);
10961
+ print(outputRows, opts, () => {
10962
+ if (!rows.length) {
10963
+ console.log("No events recorded.");
10964
+ return;
10965
+ }
10966
+ for (const event of outputRows)
10967
+ console.log(formatEventRow(event));
10968
+ console.log(source_default.dim(`Showing ${outputRows.length} of ${rows.length} event(s).`));
10969
+ if (rows.length > outputRows.length)
10970
+ printHint(`Use --limit ${rows.length} or --json for more.`);
10971
+ });
10972
+ } catch (error) {
10973
+ handleError(error);
10974
+ }
10975
+ });
10976
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Output JSON").action(async (opts) => {
10977
+ try {
10978
+ const result = await createEventsClient().replay({
10979
+ eventId: opts.id,
10980
+ source: opts.source,
10981
+ type: opts.type,
10982
+ dryRun: opts.dryRun
10983
+ });
10984
+ print(result, opts, () => console.log(`Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`));
10985
+ } catch (error) {
10986
+ handleError(error);
10987
+ }
10988
+ });
10989
+ }
10832
10990
  program2.name("shortlinks").description("Shortlink manager with custom domains, click tracking, and Cloudflare helpers \u2014 local SQLite or self-hosted cloud /v1 API storage").version(getPackageVersion()).option("--db <path>", "SQLite database path (local mode only)").option("-j, --json", "Output JSON for agents and scripts");
10833
10991
  program2.command("init").description("Initialize local shortlinks storage").option("--domain <hostname>", "Add a default shortlink domain").option("--public-base-url <url>", "Public URL base for generated links").option("-j, --json", "Output JSON").action(async (opts) => {
10834
10992
  try {
@@ -10856,7 +11014,7 @@ program2.command("init").description("Initialize local shortlinks storage").opti
10856
11014
  stats: await store.totalStats()
10857
11015
  };
10858
11016
  });
10859
- print2(result, opts, () => {
11017
+ print(result, opts, () => {
10860
11018
  console.log(source_default.green("shortlinks initialized"));
10861
11019
  console.log(` Data: ${result.data_dir}`);
10862
11020
  console.log(` DB: ${result.db_path}`);
@@ -10868,9 +11026,13 @@ program2.command("init").description("Initialize local shortlinks storage").opti
10868
11026
  }
10869
11027
  });
10870
11028
  var configCmd = program2.command("config").description("View and update local config");
10871
- configCmd.command("show").description("Show local config").option("-j, --json", "Output JSON").action((opts) => {
11029
+ configCmd.command("show").description("Show local config").option("--verbose", "Show full config object").option("-j, --json", "Output JSON").action((opts) => {
10872
11030
  const data = { path: getConfigPath(), config: loadConfig() };
10873
- print2(data, opts, () => console.log(JSON.stringify(data, null, 2)));
11031
+ print(data, opts, () => {
11032
+ if (printVerbose(data, opts))
11033
+ return;
11034
+ printConfigSummary(data);
11035
+ });
10874
11036
  });
10875
11037
  configCmd.command("set <key> <value>").description("Set config value: default-domain, public-base-url, cloudflare-account-id, cloudflare-worker-name, cloudflare-origin").option("-j, --json", "Output JSON").action((key, value, opts) => {
10876
11038
  try {
@@ -10894,7 +11056,7 @@ configCmd.command("set <key> <value>").description("Set config value: default-do
10894
11056
  default:
10895
11057
  throw new Error(`Unknown config key: ${key}`);
10896
11058
  }
10897
- print2({ path: getConfigPath(), config }, opts, () => console.log(source_default.green(`Set ${key}.`)));
11059
+ print({ path: getConfigPath(), config }, opts, () => console.log(source_default.green(`Set ${key}.`)));
10898
11060
  } catch (error) {
10899
11061
  handleError(error);
10900
11062
  }
@@ -10912,7 +11074,7 @@ domainCmd.command("add <hostname>").description("Add or update a custom domain")
10912
11074
  originUrl: opts.origin,
10913
11075
  notes: opts.notes
10914
11076
  }));
10915
- print2(domain, opts, () => {
11077
+ print(domain, opts, () => {
10916
11078
  console.log(source_default.green(`Domain ready: ${domain.hostname}`));
10917
11079
  if (domain.default_domain)
10918
11080
  console.log(source_default.dim("Default domain updated."));
@@ -10921,10 +11083,12 @@ domainCmd.command("add <hostname>").description("Add or update a custom domain")
10921
11083
  handleError(error);
10922
11084
  }
10923
11085
  });
10924
- domainCmd.command("list").description("List configured domains").option("-j, --json", "Output JSON").action(async (opts) => {
11086
+ domainCmd.command("list").description("List configured domains").option("--limit <n>", "Maximum rows").option("-j, --json", "Output JSON").action(async (opts) => {
10925
11087
  try {
10926
- const domains = await withRuntimeStore((store) => store.listDomains());
10927
- print2(domains, opts, () => {
11088
+ const allDomains = await withRuntimeStore((store) => store.listDomains());
11089
+ const outputDomains = useJson(opts) && opts.limit === undefined ? allDomains : allDomains.slice(0, useJson(opts) ? parseLimit(opts.limit, allDomains.length) : humanLimit(opts));
11090
+ print(outputDomains, opts, () => {
11091
+ const domains = outputDomains;
10928
11092
  if (domains.length === 0) {
10929
11093
  console.log(source_default.dim("No domains configured."));
10930
11094
  return;
@@ -10933,17 +11097,25 @@ domainCmd.command("list").description("List configured domains").option("-j, --j
10933
11097
  const marker = domain.default_domain ? source_default.green("*") : " ";
10934
11098
  console.log(`${marker} ${domain.hostname} ${source_default.dim(domain.provider)}`);
10935
11099
  }
11100
+ console.log(source_default.dim(`Showing ${domains.length} of ${allDomains.length} domain(s).`));
11101
+ if (allDomains.length > domains.length)
11102
+ printHint(`Use --limit ${allDomains.length} or --json for more.`);
11103
+ printHint("Use `shortlinks domain get <hostname>` for details.");
10936
11104
  });
10937
11105
  } catch (error) {
10938
11106
  handleError(error);
10939
11107
  }
10940
11108
  });
10941
- domainCmd.command("get <hostname>").description("Show a configured domain").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
11109
+ domainCmd.command("get <hostname>").alias("show").description("Show a configured domain").option("--verbose", "Show full domain object").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
10942
11110
  try {
10943
11111
  const domain = await withRuntimeStore((store) => store.getDomain(hostname2));
10944
11112
  if (!domain)
10945
11113
  throw new Error("Domain not found.");
10946
- print2(domain, opts, () => console.log(JSON.stringify(domain, null, 2)));
11114
+ print(domain, opts, () => {
11115
+ if (printVerbose(domain, opts))
11116
+ return;
11117
+ printDomainSummary(domain);
11118
+ });
10947
11119
  } catch (error) {
10948
11120
  handleError(error);
10949
11121
  }
@@ -10951,7 +11123,7 @@ domainCmd.command("get <hostname>").description("Show a configured domain").opti
10951
11123
  domainCmd.command("remove <hostname>").alias("delete").alias("rm").description("Delete a domain and all of its links and clicks").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
10952
11124
  try {
10953
11125
  const domain = await withRuntimeStore((store) => store.deleteDomain(hostname2));
10954
- print2({ deleted: true, hostname: domain.hostname }, opts, () => {
11126
+ print({ deleted: true, hostname: domain.hostname }, opts, () => {
10955
11127
  console.log(source_default.green(`Domain removed: ${domain.hostname}`));
10956
11128
  console.log(source_default.dim("Its links and clicks were deleted."));
10957
11129
  });
@@ -10959,7 +11131,7 @@ domainCmd.command("remove <hostname>").alias("delete").alias("rm").description("
10959
11131
  handleError(error);
10960
11132
  }
10961
11133
  });
10962
- domainCmd.command("setup <hostname>").description("Add a domain locally and optionally prepare Cloudflare DNS").option("--default", "Make this the default domain").option("--origin <url>", "Origin redirect server URL").option("--cloudflare", "Upsert Cloudflare CNAME record").option("--target <hostname>", "CNAME target for Cloudflare DNS").option("--zone-id <id>", "Cloudflare zone ID").option("--dry-run", "Show the Cloudflare plan without changing DNS").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
11134
+ domainCmd.command("setup <hostname>").description("Add a domain locally and optionally prepare Cloudflare DNS").option("--default", "Make this the default domain").option("--origin <url>", "Origin redirect server URL").option("--cloudflare", "Upsert Cloudflare CNAME record").option("--target <hostname>", "CNAME target for Cloudflare DNS").option("--zone-id <id>", "Cloudflare zone ID").option("--dry-run", "Show the Cloudflare plan without changing DNS").option("--verbose", "Show full setup result").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
10963
11135
  try {
10964
11136
  const result = await withRuntimeStore(async (store) => {
10965
11137
  const domain = await store.addDomain({
@@ -10976,33 +11148,31 @@ domainCmd.command("setup <hostname>").description("Add a domain locally and opti
10976
11148
  }) : null;
10977
11149
  return { domain, cloudflare };
10978
11150
  });
10979
- print2(result, opts, () => {
11151
+ print(result, opts, () => {
11152
+ if (printVerbose(result, opts))
11153
+ return;
10980
11154
  console.log(source_default.green(`Domain ready: ${result.domain.hostname}`));
10981
11155
  if (result.cloudflare)
10982
- console.log(JSON.stringify(result.cloudflare, null, 2));
11156
+ printCloudflareDnsSummary(result.cloudflare);
11157
+ if (!result.cloudflare)
11158
+ printHint("Use --cloudflare --dry-run to preview DNS changes.");
10983
11159
  });
10984
11160
  } catch (error) {
10985
11161
  handleError(error);
10986
11162
  }
10987
11163
  });
10988
- domainCmd.command("check <hostname>").description("Check domain availability through @hasna/domains").option("--dry-run", "Print the command without running it").option("-j, --json", "Output JSON").action((hostname2, opts) => {
11164
+ domainCmd.command("check <hostname>").description("Check domain availability through @hasna/domains").option("--dry-run", "Print the command without running it").option("--verbose", "Show full domains CLI output").option("-j, --json", "Output JSON").action((hostname2, opts) => {
10989
11165
  const result = runDomains("check", hostname2, { dryRun: opts.dryRun });
10990
- print2(result, opts, () => {
10991
- if (result.stdout.trim())
10992
- console.log(result.stdout.trim());
10993
- if (result.stderr.trim())
10994
- console.error(result.stderr.trim());
11166
+ print(result, opts, () => {
11167
+ printExternalCommandResult(result, opts, "domains check");
10995
11168
  if (result.status !== 0)
10996
11169
  process.exit(result.status || 1);
10997
11170
  });
10998
11171
  });
10999
- domainCmd.command("buy <hostname>").description("Buy a domain through @hasna/domains / Route 53").option("--dry-run", "Print the command without running it").option("-j, --json", "Output JSON").action((hostname2, opts) => {
11172
+ domainCmd.command("buy <hostname>").description("Buy a domain through @hasna/domains / Route 53").option("--dry-run", "Print the command without running it").option("--verbose", "Show full domains CLI output").option("-j, --json", "Output JSON").action((hostname2, opts) => {
11000
11173
  const result = runDomains("buy", hostname2, { dryRun: opts.dryRun });
11001
- print2(result, opts, () => {
11002
- if (result.stdout.trim())
11003
- console.log(result.stdout.trim());
11004
- if (result.stderr.trim())
11005
- console.error(result.stderr.trim());
11174
+ print(result, opts, () => {
11175
+ printExternalCommandResult(result, opts, "domains buy");
11006
11176
  if (result.status !== 0)
11007
11177
  process.exit(result.status || 1);
11008
11178
  });
@@ -11018,38 +11188,51 @@ async function createLinkAction(url, opts) {
11018
11188
  expiresAt: opts.expires,
11019
11189
  slugLength: opts.length ? Number(opts.length) : undefined
11020
11190
  }));
11021
- print2(link, opts, () => console.log(formatLink(link)));
11191
+ print(link, opts, () => console.log(formatLink(link)));
11022
11192
  } catch (error) {
11023
11193
  handleError(error);
11024
11194
  }
11025
11195
  }
11026
11196
  linkCmd.command("create <url>").description("Create a shortlink").option("--domain <hostname>", "Domain to use").option("--slug <slug>", "Custom slug").option("--title <title>", "Human title").option("--expires <date>", "Expiration date").option("--length <n>", "Generated slug length", "7").option("-j, --json", "Output JSON").action(createLinkAction);
11027
11197
  program2.command("create <url>").description("Create a shortlink").option("--domain <hostname>", "Domain to use").option("--slug <slug>", "Custom slug").option("--title <title>", "Human title").option("--expires <date>", "Expiration date").option("--length <n>", "Generated slug length", "7").option("-j, --json", "Output JSON").action(createLinkAction);
11028
- linkCmd.command("list").description("List shortlinks").option("--domain <hostname>", "Filter by domain").option("--active", "Only active links").option("--limit <n>", "Maximum rows", "100").option("-j, --json", "Output JSON").action(async (opts) => {
11198
+ linkCmd.command("list").description("List shortlinks").option("--domain <hostname>", "Filter by domain").option("--active", "Only active links").option("--limit <n>", "Maximum rows").option("-j, --json", "Output JSON").action(async (opts) => {
11029
11199
  try {
11200
+ const requestedLimit = useJson(opts) ? jsonLimit(opts) : humanLimit(opts) + 1;
11030
11201
  const links = await withRuntimeStore((store) => store.listLinks({
11031
11202
  domain: opts.domain,
11032
11203
  activeOnly: opts.active,
11033
- limit: Number(opts.limit)
11204
+ limit: requestedLimit
11034
11205
  }));
11035
- print2(links, opts, () => {
11206
+ print(links, opts, () => {
11036
11207
  if (links.length === 0) {
11037
11208
  console.log(source_default.dim("No links yet."));
11038
11209
  return;
11039
11210
  }
11040
- for (const link of links)
11041
- console.log(formatLink(link));
11211
+ const limit = humanLimit(opts);
11212
+ const displayed = links.slice(0, limit);
11213
+ for (const link of displayed) {
11214
+ const status = link.active ? "active" : "disabled";
11215
+ console.log(`${formatLink(link)} ${source_default.dim(status)}`);
11216
+ }
11217
+ console.log(source_default.dim(`Showing ${displayed.length}${links.length > displayed.length ? "+" : ""} link(s).`));
11218
+ if (links.length > displayed.length)
11219
+ printHint(`Use --limit ${limit * 2} or --json to see more rows.`);
11220
+ printHint("Use `shortlinks link get <slug>` for details.");
11042
11221
  });
11043
11222
  } catch (error) {
11044
11223
  handleError(error);
11045
11224
  }
11046
11225
  });
11047
- linkCmd.command("get <slug>").description("Show a shortlink").option("--domain <hostname>", "Domain to use").option("-j, --json", "Output JSON").action(async (slug, opts) => {
11226
+ linkCmd.command("get <slug>").alias("show").description("Show a shortlink").option("--domain <hostname>", "Domain to use").option("--verbose", "Show full shortlink object").option("-j, --json", "Output JSON").action(async (slug, opts) => {
11048
11227
  try {
11049
11228
  const link = await withRuntimeStore((store) => opts.domain ? store.getLink(opts.domain, slug) : store.getLink(slug));
11050
11229
  if (!link)
11051
11230
  throw new Error("Link not found.");
11052
- print2(link, opts, () => console.log(JSON.stringify(link, null, 2)));
11231
+ print(link, opts, () => {
11232
+ if (printVerbose(link, opts))
11233
+ return;
11234
+ printLinkSummary(link);
11235
+ });
11053
11236
  } catch (error) {
11054
11237
  handleError(error);
11055
11238
  }
@@ -11057,7 +11240,7 @@ linkCmd.command("get <slug>").description("Show a shortlink").option("--domain <
11057
11240
  linkCmd.command("disable <slug>").description("Disable a shortlink").option("--domain <hostname>", "Domain to use").option("-j, --json", "Output JSON").action(async (slug, opts) => {
11058
11241
  try {
11059
11242
  const link = await withRuntimeStore((store) => opts.domain ? store.setLinkActive(opts.domain, slug, false) : store.setLinkActive(slug, false));
11060
- print2(link, opts, () => console.log(source_default.green(`Disabled ${link.short_url}`)));
11243
+ print(link, opts, () => console.log(source_default.green(`Disabled ${link.short_url}`)));
11061
11244
  } catch (error) {
11062
11245
  handleError(error);
11063
11246
  }
@@ -11065,7 +11248,7 @@ linkCmd.command("disable <slug>").description("Disable a shortlink").option("--d
11065
11248
  linkCmd.command("enable <slug>").description("Enable a shortlink").option("--domain <hostname>", "Domain to use").option("-j, --json", "Output JSON").action(async (slug, opts) => {
11066
11249
  try {
11067
11250
  const link = await withRuntimeStore((store) => opts.domain ? store.setLinkActive(opts.domain, slug, true) : store.setLinkActive(slug, true));
11068
- print2(link, opts, () => console.log(source_default.green(`Enabled ${link.short_url}`)));
11251
+ print(link, opts, () => console.log(source_default.green(`Enabled ${link.short_url}`)));
11069
11252
  } catch (error) {
11070
11253
  handleError(error);
11071
11254
  }
@@ -11073,7 +11256,7 @@ linkCmd.command("enable <slug>").description("Enable a shortlink").option("--dom
11073
11256
  linkCmd.command("delete <slug>").description("Delete a shortlink").option("--domain <hostname>", "Domain to use").option("-j, --json", "Output JSON").action(async (slug, opts) => {
11074
11257
  try {
11075
11258
  const link = await withRuntimeStore((store) => opts.domain ? store.deleteLink(opts.domain, slug) : store.deleteLink(slug));
11076
- print2(link, opts, () => console.log(source_default.green(`Deleted ${link.short_url}`)));
11259
+ print(link, opts, () => console.log(source_default.green(`Deleted ${link.short_url}`)));
11077
11260
  } catch (error) {
11078
11261
  handleError(error);
11079
11262
  }
@@ -11083,19 +11266,23 @@ program2.command("resolve <slug>").description("Resolve a slug to its destinatio
11083
11266
  const link = await withRuntimeStore((store) => opts.domain ? store.getLink(opts.domain, slug) : store.getLink(slug));
11084
11267
  if (!link)
11085
11268
  throw new Error("Link not found.");
11086
- print2(link, opts, () => console.log(link.destination_url));
11269
+ print(link, opts, () => console.log(link.destination_url));
11087
11270
  } catch (error) {
11088
11271
  handleError(error);
11089
11272
  }
11090
11273
  });
11091
- program2.command("stats [slug]").description("Show overall stats or stats for a shortlink").option("--domain <hostname>", "Domain to use").option("-j, --json", "Output JSON").action(async (slug, opts) => {
11274
+ program2.command("stats [slug]").description("Show overall stats or stats for a shortlink").option("--domain <hostname>", "Domain to use").option("--verbose", "Show full stats object").option("-j, --json", "Output JSON").action(async (slug, opts) => {
11092
11275
  try {
11093
11276
  const result = await withRuntimeStore((store) => {
11094
11277
  if (slug)
11095
11278
  return opts.domain ? store.getStats(opts.domain, slug) : store.getStats(slug);
11096
11279
  return store.totalStats();
11097
11280
  });
11098
- print2(result, opts, () => console.log(JSON.stringify(result, null, 2)));
11281
+ print(result, opts, () => {
11282
+ if (printVerbose(result, opts))
11283
+ return;
11284
+ printStatsSummary(result);
11285
+ });
11099
11286
  } catch (error) {
11100
11287
  handleError(error);
11101
11288
  }
@@ -11115,7 +11302,7 @@ program2.command("serve").description("Run the on-box redirect server that recor
11115
11302
  }
11116
11303
  });
11117
11304
  var cfCmd = program2.command("cloudflare").description("Cloudflare DNS and Worker helpers");
11118
- cfCmd.command("plan <hostname>").description("Print the Cloudflare setup plan").requiredOption("--target <hostname>", "CNAME target").option("--origin <url>", "Origin redirect server URL", process.env.SHORTLINKS_ORIGIN || "https://shortlinks.example.com").option("--worker <name>", "Worker name", "shortlinks").option("--no-proxied", "Create unproxied DNS record").option("-j, --json", "Output JSON").action((hostname2, opts) => {
11305
+ cfCmd.command("plan <hostname>").description("Print the Cloudflare setup plan").requiredOption("--target <hostname>", "CNAME target").option("--origin <url>", "Origin redirect server URL", process.env.SHORTLINKS_ORIGIN || "https://shortlinks.example.com").option("--worker <name>", "Worker name", "shortlinks").option("--no-proxied", "Create unproxied DNS record").option("--verbose", "Show full Cloudflare setup plan").option("-j, --json", "Output JSON").action((hostname2, opts) => {
11119
11306
  try {
11120
11307
  const plan = createCloudflarePlan({
11121
11308
  hostname: hostname2,
@@ -11124,7 +11311,11 @@ cfCmd.command("plan <hostname>").description("Print the Cloudflare setup plan").
11124
11311
  workerName: opts.worker,
11125
11312
  proxied: opts.proxied
11126
11313
  });
11127
- print2(plan, opts, () => console.log(JSON.stringify(plan, null, 2)));
11314
+ print(plan, opts, () => {
11315
+ if (printVerbose(plan, opts))
11316
+ return;
11317
+ printCloudflarePlanSummary(plan);
11318
+ });
11128
11319
  } catch (error) {
11129
11320
  handleError(error);
11130
11321
  }
@@ -11132,7 +11323,7 @@ cfCmd.command("plan <hostname>").description("Print the Cloudflare setup plan").
11132
11323
  cfCmd.command("worker").description("Write Cloudflare Worker files").option("--out-dir <dir>", "Output directory", "cloudflare").option("--worker <name>", "Worker name", "shortlinks").option("--origin <url>", "Origin redirect server URL", process.env.SHORTLINKS_ORIGIN || "https://shortlinks.example.com").option("-j, --json", "Output JSON").action((opts) => {
11133
11324
  try {
11134
11325
  const result = writeWorkerFiles({ outDir: opts.outDir, workerName: opts.worker, origin: opts.origin });
11135
- print2(result, opts, () => {
11326
+ print(result, opts, () => {
11136
11327
  console.log(source_default.green(`Wrote ${result.workerPath}`));
11137
11328
  console.log(source_default.green(`Wrote ${result.wranglerPath}`));
11138
11329
  });
@@ -11140,7 +11331,7 @@ cfCmd.command("worker").description("Write Cloudflare Worker files").option("--o
11140
11331
  handleError(error);
11141
11332
  }
11142
11333
  });
11143
- cfCmd.command("dns <hostname>").description("Create or update the Cloudflare CNAME record").requiredOption("--target <hostname>", "CNAME target").option("--zone-id <id>", "Cloudflare zone ID").option("--dry-run", "Show plan without changing DNS").option("--no-proxied", "Create unproxied DNS record").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
11334
+ cfCmd.command("dns <hostname>").description("Create or update the Cloudflare CNAME record").requiredOption("--target <hostname>", "CNAME target").option("--zone-id <id>", "Cloudflare zone ID").option("--dry-run", "Show plan without changing DNS").option("--no-proxied", "Create unproxied DNS record").option("--verbose", "Show full Cloudflare API result or dry-run plan").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
11144
11335
  try {
11145
11336
  const result = await upsertCloudflareDnsRecord({
11146
11337
  hostname: hostname2,
@@ -11149,25 +11340,33 @@ cfCmd.command("dns <hostname>").description("Create or update the Cloudflare CNA
11149
11340
  dryRun: opts.dryRun,
11150
11341
  proxied: opts.proxied
11151
11342
  });
11152
- print2(result, opts, () => console.log(JSON.stringify(result, null, 2)));
11343
+ print(result, opts, () => {
11344
+ if (printVerbose(result, opts))
11345
+ return;
11346
+ printCloudflareDnsSummary(result);
11347
+ });
11153
11348
  } catch (error) {
11154
11349
  handleError(error);
11155
11350
  }
11156
11351
  });
11157
11352
  var localCmd = program2.command("local").description("Local domain setup helpers");
11158
- localCmd.command("plan <domain>").description("Render hosts and reverse-proxy setup for a local shortlink domain").option("--port <port>", "Local redirect server port", "8787").option("--target-host <host>", "Local target host", "127.0.0.1").option("-j, --json", "Output JSON").action((domain, opts) => {
11353
+ localCmd.command("plan <domain>").description("Render hosts and reverse-proxy setup for a local shortlink domain").option("--port <port>", "Local redirect server port", "8787").option("--target-host <host>", "Local target host", "127.0.0.1").option("--verbose", "Show full local setup plan").option("-j, --json", "Output JSON").action((domain, opts) => {
11159
11354
  try {
11160
11355
  const plan = createLocalSetupPlan({
11161
11356
  domain,
11162
11357
  port: Number(opts.port),
11163
11358
  targetHost: opts.targetHost
11164
11359
  });
11165
- print2(plan, opts, () => console.log(JSON.stringify(plan, null, 2)));
11360
+ print(plan, opts, () => {
11361
+ if (printVerbose(plan, opts))
11362
+ return;
11363
+ printLocalPlanSummary(plan);
11364
+ });
11166
11365
  } catch (error) {
11167
11366
  handleError(error);
11168
11367
  }
11169
11368
  });
11170
- localCmd.command("setup <domain>").description("Record local domain mapping with machines and print remaining sudo-only setup").option("--port <port>", "Local redirect server port", "8787").option("--target-host <host>", "Local target host", "127.0.0.1").option("--skip-machines", "Do not call machines dns add").option("-j, --json", "Output JSON").action((domain, opts) => {
11369
+ localCmd.command("setup <domain>").description("Record local domain mapping with machines and print remaining sudo-only setup").option("--port <port>", "Local redirect server port", "8787").option("--target-host <host>", "Local target host", "127.0.0.1").option("--skip-machines", "Do not call machines dns add").option("--verbose", "Show full local setup result").option("-j, --json", "Output JSON").action((domain, opts) => {
11171
11370
  try {
11172
11371
  const plan = createLocalSetupPlan({
11173
11372
  domain,
@@ -11180,17 +11379,21 @@ localCmd.command("setup <domain>").description("Record local domain mapping with
11180
11379
  targetHost: opts.targetHost
11181
11380
  });
11182
11381
  const result = { plan, machines };
11183
- print2(result, opts, () => {
11382
+ print(result, opts, () => {
11184
11383
  if (machines && machines.status !== 0) {
11185
11384
  console.error(source_default.yellow(machines.stderr.trim() || "machines dns add failed"));
11186
11385
  }
11187
- console.log(JSON.stringify(result, null, 2));
11386
+ if (printVerbose(result, opts))
11387
+ return;
11388
+ printLocalPlanSummary(plan);
11389
+ if (machines)
11390
+ console.log(` machines status: ${machines.status ?? "not started"}`);
11188
11391
  });
11189
11392
  } catch (error) {
11190
11393
  handleError(error);
11191
11394
  }
11192
11395
  });
11193
- program2.command("doctor").description("Check local shortlinks tooling and integration readiness").option("-j, --json", "Output JSON").action(async (opts) => {
11396
+ program2.command("doctor").description("Check local shortlinks tooling and integration readiness").option("--verbose", "Show full diagnostic object").option("-j, --json", "Output JSON").action(async (opts) => {
11194
11397
  try {
11195
11398
  const dbPath = getDatabasePath(program2.opts().db);
11196
11399
  const data = await withRuntimeStore(async (store) => ({
@@ -11217,10 +11420,14 @@ program2.command("doctor").description("Check local shortlinks tooling and integ
11217
11420
  shortlinks_origin_present: Boolean(process.env.SHORTLINKS_ORIGIN)
11218
11421
  }
11219
11422
  }));
11220
- print2(data, opts, () => console.log(JSON.stringify(data, null, 2)));
11423
+ print(data, opts, () => {
11424
+ if (printVerbose(data, opts))
11425
+ return;
11426
+ printDoctorSummary(data);
11427
+ });
11221
11428
  } catch (error) {
11222
11429
  handleError(error);
11223
11430
  }
11224
11431
  });
11225
- registerEventsCommands(program2, { source: "shortlinks" });
11432
+ registerCompactEventsCommands(program2);
11226
11433
  program2.parseAsync(process.argv).catch(handleError);