@hasna/domains 0.0.15 → 0.0.17

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 CHANGED
@@ -17,6 +17,7 @@ Domain portfolio and DNS management for AI agents — CLI + MCP server with SQLi
17
17
  - **Registrar sync** — Namecheap and GoDaddy integration
18
18
  - **Brand monitoring** — typosquat/threat detection via Brandsight API
19
19
  - **MCP server** — full Model Context Protocol support for AI agents
20
+ - **Interactive TUI** — browse your portfolio in the terminal with `domains interactive`
20
21
 
21
22
  ## Installation
22
23
 
@@ -29,7 +30,10 @@ Data is stored at `~/.hasna/domains/domains.db`. Override with `HASNA_DOMAINS_DI
29
30
  ## CLI Usage
30
31
 
31
32
  ```bash
32
- # Domain management
33
+ # Interactive portfolio browser (Ink TUI)
34
+ domains interactive
35
+ domains interactive --status active
36
+ ```
33
37
  domains add --name example.com --registrar Namecheap --expires-at 2025-01-01
34
38
  domains list
35
39
  domains list --status active --registrar Namecheap
@@ -74,7 +78,12 @@ domains sync --provider namecheap
74
78
  domains sync --provider godaddy
75
79
  domains sync --all
76
80
  domains renew example.com --provider namecheap
77
- domains check example.com
81
+
82
+ # Availability checks — pick the registrar with --provider
83
+ domains check example.com # default registrar (config default-registrar, else namecheap)
84
+ domains check --provider route53 example.com # AWS Route 53 availability
85
+ domains check --provider godaddy example.com
86
+ domains r53 check example.com # Route 53-specific subcommand (same backend)
78
87
 
79
88
  # Brand monitoring
80
89
  domains monitor mybrand
@@ -82,6 +91,13 @@ domains similar example.com
82
91
  domains threats example.com
83
92
  ```
84
93
 
94
+ > **Prefer the `domains` CLI over raw `aws` commands.** For Route 53 availability,
95
+ > registration status, and DNS, use `domains check --provider route53`,
96
+ > `domains r53 status`, and `domains dns …` instead of `aws route53domains …` /
97
+ > `aws route53 …`. The CLI applies the configured purchase profile, records the
98
+ > result in the local portfolio DB, and works the same across registrars — raw
99
+ > `aws` calls bypass all of that.
100
+
85
101
  ## MCP Server
86
102
 
87
103
  ```bash
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerInteractiveCommand(program: Command): void;
3
+ //# sourceMappingURL=interactive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/interactive.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKzC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgBjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqBzC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoM/D"}
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuBzC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuM/D"}
package/dist/cli/index.js CHANGED
@@ -37527,6 +37527,7 @@ Registrar / DNS Providers:`);
37527
37527
  }
37528
37528
 
37529
37529
  // src/cli/commands/providers.ts
37530
+ init_config();
37530
37531
  init_domains();
37531
37532
  function registerProviderCommands(program2) {
37532
37533
  program2.command("providers").description("Show which registrar providers are configured").option("--json", "Output as JSON", false).action((opts) => {
@@ -37677,17 +37678,14 @@ function registerProviderCommands(program2) {
37677
37678
  process.exit(1);
37678
37679
  }
37679
37680
  });
37680
- program2.command("check").description("Check domain availability").argument("<name>", "Domain name (e.g. example.com)").option("--json", "Output as JSON", false).action(async (name, opts) => {
37681
+ program2.command("check").description("Check domain availability via a registrar provider (route53, namecheap, godaddy)").argument("<name>", "Domain name (e.g. example.com)").option("--provider <name>", "Registrar provider \u2014 defaults to config default-registrar, else namecheap").option("--json", "Output as JSON", false).action(async (name, opts) => {
37681
37682
  try {
37682
- const result = await checkAvailability(name);
37683
+ const providerName = opts.provider ?? loadConfig3().default_registrar ?? "namecheap";
37684
+ const result = providerName === "namecheap" ? await checkAvailability(name) : await getRegistrarProvider(providerName).checkAvailability(name);
37683
37685
  if (opts.json) {
37684
- console.log(JSON.stringify(result, null, 2));
37686
+ console.log(JSON.stringify({ provider: providerName, ...result }, null, 2));
37685
37687
  } else {
37686
- if (result.available) {
37687
- console.log(`${result.domain} is AVAILABLE`);
37688
- } else {
37689
- console.log(`${result.domain} is NOT available`);
37690
- }
37688
+ console.log(`${result.domain} is ${result.available ? "AVAILABLE" : "NOT available"} (via ${providerName})`);
37691
37689
  }
37692
37690
  } catch (error) {
37693
37691
  console.error(`Availability check failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -39944,6 +39942,649 @@ Domain: ${name}`);
39944
39942
  });
39945
39943
  }
39946
39944
 
39945
+ // src/cli/commands/interactive.tsx
39946
+ import chalk from "chalk";
39947
+ import { render } from "ink";
39948
+
39949
+ // src/cli/tui/App.tsx
39950
+ init_domains();
39951
+ init_dns_records();
39952
+ import { useCallback, useEffect, useMemo, useState as useState2 } from "react";
39953
+ import { Box as Box5, Text as Text5, useApp, useInput as useInput2 } from "ink";
39954
+
39955
+ // src/cli/tui/Header.tsx
39956
+ import { Box, Text } from "ink";
39957
+
39958
+ // src/cli/tui/format.ts
39959
+ var TABLE_COLS = {
39960
+ name: 28,
39961
+ status: 14,
39962
+ expires: 12,
39963
+ registrar: 16
39964
+ };
39965
+ function pad(value, width) {
39966
+ if (value.length > width)
39967
+ return value.slice(0, width - 1) + "\u2026";
39968
+ return value.padEnd(width);
39969
+ }
39970
+ function formatDate(iso) {
39971
+ if (!iso)
39972
+ return "\u2014";
39973
+ return iso.slice(0, 10);
39974
+ }
39975
+ function daysUntil(iso) {
39976
+ if (!iso)
39977
+ return "\u2014";
39978
+ const ms = new Date(iso).getTime() - Date.now();
39979
+ const days = Math.ceil(ms / (1000 * 60 * 60 * 24));
39980
+ if (days < 0)
39981
+ return "expired";
39982
+ if (days === 0)
39983
+ return "today";
39984
+ return `${days}d`;
39985
+ }
39986
+ var STATUS_COLORS = {
39987
+ discovered: "gray",
39988
+ researching: "blue",
39989
+ offered: "yellow",
39990
+ negotiating: "yellow",
39991
+ purchased: "green",
39992
+ active: "green",
39993
+ not_available: "red",
39994
+ premium_only: "magenta",
39995
+ declined: "red",
39996
+ expired: "red",
39997
+ transferring: "cyan",
39998
+ redemption: "red"
39999
+ };
40000
+ var DOMAIN_FILTERS = ["all", "active", "expiring", "premium"];
40001
+ function filterLabel(filter) {
40002
+ switch (filter) {
40003
+ case "all":
40004
+ return "All";
40005
+ case "active":
40006
+ return "Active";
40007
+ case "expiring":
40008
+ return "Expiring (30d)";
40009
+ case "premium":
40010
+ return "Premium";
40011
+ }
40012
+ }
40013
+
40014
+ // src/cli/tui/Header.tsx
40015
+ import { jsxDEV } from "react/jsx-dev-runtime";
40016
+ function Header({ count, filter, view }) {
40017
+ const viewLabel = view === "list" ? "Portfolio" : view === "detail" ? "Detail" : "Search";
40018
+ return /* @__PURE__ */ jsxDEV(Box, {
40019
+ flexDirection: "column",
40020
+ marginBottom: 1,
40021
+ children: [
40022
+ /* @__PURE__ */ jsxDEV(Box, {
40023
+ children: [
40024
+ /* @__PURE__ */ jsxDEV(Text, {
40025
+ bold: true,
40026
+ color: "cyan",
40027
+ children: "domains"
40028
+ }, undefined, false, undefined, this),
40029
+ /* @__PURE__ */ jsxDEV(Text, {
40030
+ dimColor: true,
40031
+ children: " \u2014 interactive portfolio browser"
40032
+ }, undefined, false, undefined, this)
40033
+ ]
40034
+ }, undefined, true, undefined, this),
40035
+ /* @__PURE__ */ jsxDEV(Box, {
40036
+ children: /* @__PURE__ */ jsxDEV(Text, {
40037
+ dimColor: true,
40038
+ children: [
40039
+ viewLabel,
40040
+ " \xB7 ",
40041
+ filterLabel(filter),
40042
+ " \xB7 ",
40043
+ count,
40044
+ " domain",
40045
+ count === 1 ? "" : "s"
40046
+ ]
40047
+ }, undefined, true, undefined, this)
40048
+ }, undefined, false, undefined, this)
40049
+ ]
40050
+ }, undefined, true, undefined, this);
40051
+ }
40052
+
40053
+ // src/cli/tui/DomainTable.tsx
40054
+ import { Box as Box2, Text as Text2 } from "ink";
40055
+ import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
40056
+ var VISIBLE_ROWS = 16;
40057
+ function DomainTable({ domains, selectedIndex, compact = false }) {
40058
+ if (domains.length === 0) {
40059
+ return /* @__PURE__ */ jsxDEV2(Box2, {
40060
+ flexDirection: "column",
40061
+ paddingLeft: compact ? 0 : 1,
40062
+ children: /* @__PURE__ */ jsxDEV2(Text2, {
40063
+ dimColor: true,
40064
+ children: "No domains match this filter. Press [f] to change filter or [r] to refresh."
40065
+ }, undefined, false, undefined, this)
40066
+ }, undefined, false, undefined, this);
40067
+ }
40068
+ const start = Math.max(0, Math.min(selectedIndex - Math.floor(VISIBLE_ROWS / 2), Math.max(0, domains.length - VISIBLE_ROWS)));
40069
+ const visible = domains.slice(start, start + VISIBLE_ROWS);
40070
+ return /* @__PURE__ */ jsxDEV2(Box2, {
40071
+ flexDirection: "column",
40072
+ width: "100%",
40073
+ paddingRight: 0,
40074
+ children: [
40075
+ /* @__PURE__ */ jsxDEV2(Box2, {
40076
+ children: /* @__PURE__ */ jsxDEV2(Text2, {
40077
+ bold: true,
40078
+ dimColor: true,
40079
+ children: [
40080
+ pad("NAME", TABLE_COLS.name),
40081
+ pad("STATUS", TABLE_COLS.status),
40082
+ pad("EXPIRES", TABLE_COLS.expires),
40083
+ pad("REGISTRAR", TABLE_COLS.registrar),
40084
+ !compact ? " TTL" : ""
40085
+ ]
40086
+ }, undefined, true, undefined, this)
40087
+ }, undefined, false, undefined, this),
40088
+ /* @__PURE__ */ jsxDEV2(Text2, {
40089
+ dimColor: true,
40090
+ children: "\u2500".repeat(TABLE_COLS.name + TABLE_COLS.status + TABLE_COLS.expires + TABLE_COLS.registrar + (compact ? 0 : 6))
40091
+ }, undefined, false, undefined, this),
40092
+ start > 0 && /* @__PURE__ */ jsxDEV2(Text2, {
40093
+ dimColor: true,
40094
+ children: [
40095
+ " \u2191 ",
40096
+ start,
40097
+ " more above"
40098
+ ]
40099
+ }, undefined, true, undefined, this),
40100
+ visible.map((domain, offset) => {
40101
+ const index = start + offset;
40102
+ const selected = index === selectedIndex;
40103
+ const statusColor = STATUS_COLORS[domain.status] ?? "white";
40104
+ return /* @__PURE__ */ jsxDEV2(Box2, {
40105
+ children: [
40106
+ /* @__PURE__ */ jsxDEV2(Text2, {
40107
+ color: selected ? "cyan" : undefined,
40108
+ bold: selected,
40109
+ children: selected ? "\u276F " : " "
40110
+ }, undefined, false, undefined, this),
40111
+ /* @__PURE__ */ jsxDEV2(Text2, {
40112
+ bold: selected,
40113
+ children: pad(domain.name, TABLE_COLS.name)
40114
+ }, undefined, false, undefined, this),
40115
+ /* @__PURE__ */ jsxDEV2(Text2, {
40116
+ color: statusColor,
40117
+ children: pad(domain.status, TABLE_COLS.status)
40118
+ }, undefined, false, undefined, this),
40119
+ /* @__PURE__ */ jsxDEV2(Text2, {
40120
+ children: pad(formatDate(domain.expires_at), TABLE_COLS.expires)
40121
+ }, undefined, false, undefined, this),
40122
+ /* @__PURE__ */ jsxDEV2(Text2, {
40123
+ dimColor: true,
40124
+ children: pad(domain.registrar ?? "\u2014", TABLE_COLS.registrar)
40125
+ }, undefined, false, undefined, this),
40126
+ !compact && /* @__PURE__ */ jsxDEV2(Text2, {
40127
+ dimColor: true,
40128
+ children: [
40129
+ " ",
40130
+ daysUntil(domain.expires_at)
40131
+ ]
40132
+ }, undefined, true, undefined, this)
40133
+ ]
40134
+ }, domain.id, true, undefined, this);
40135
+ }),
40136
+ start + visible.length < domains.length && /* @__PURE__ */ jsxDEV2(Text2, {
40137
+ dimColor: true,
40138
+ children: [
40139
+ " \u2193 ",
40140
+ domains.length - start - visible.length,
40141
+ " more below"
40142
+ ]
40143
+ }, undefined, true, undefined, this)
40144
+ ]
40145
+ }, undefined, true, undefined, this);
40146
+ }
40147
+
40148
+ // src/cli/tui/DomainDetail.tsx
40149
+ import { Box as Box3, Text as Text3 } from "ink";
40150
+ import { jsxDEV as jsxDEV3, Fragment } from "react/jsx-dev-runtime";
40151
+ function DomainDetail({ details, dnsRecords = [], compact = false }) {
40152
+ const { domain, offers, emails } = details;
40153
+ const statusColor = STATUS_COLORS[domain.status] ?? "white";
40154
+ return /* @__PURE__ */ jsxDEV3(Box3, {
40155
+ flexDirection: "column",
40156
+ width: compact ? "45%" : "100%",
40157
+ paddingLeft: compact ? 1 : 0,
40158
+ children: [
40159
+ !compact && /* @__PURE__ */ jsxDEV3(Box3, {
40160
+ marginBottom: 1,
40161
+ children: /* @__PURE__ */ jsxDEV3(Text3, {
40162
+ dimColor: true,
40163
+ children: "[esc] back \xB7 [r] refresh \xB7 q quit"
40164
+ }, undefined, false, undefined, this)
40165
+ }, undefined, false, undefined, this),
40166
+ /* @__PURE__ */ jsxDEV3(Box3, {
40167
+ flexDirection: "column",
40168
+ marginBottom: 1,
40169
+ children: [
40170
+ /* @__PURE__ */ jsxDEV3(Text3, {
40171
+ bold: true,
40172
+ color: "cyan",
40173
+ children: domain.name
40174
+ }, undefined, false, undefined, this),
40175
+ /* @__PURE__ */ jsxDEV3(Text3, {
40176
+ color: statusColor,
40177
+ children: [
40178
+ "[",
40179
+ domain.status,
40180
+ "]"
40181
+ ]
40182
+ }, undefined, true, undefined, this)
40183
+ ]
40184
+ }, undefined, true, undefined, this),
40185
+ /* @__PURE__ */ jsxDEV3(Field, {
40186
+ label: "Registrar",
40187
+ value: domain.registrar
40188
+ }, undefined, false, undefined, this),
40189
+ /* @__PURE__ */ jsxDEV3(Field, {
40190
+ label: "Registered",
40191
+ value: formatDate(domain.registered_at)
40192
+ }, undefined, false, undefined, this),
40193
+ /* @__PURE__ */ jsxDEV3(Field, {
40194
+ label: "Expires",
40195
+ value: formatDate(domain.expires_at)
40196
+ }, undefined, false, undefined, this),
40197
+ /* @__PURE__ */ jsxDEV3(Field, {
40198
+ label: "Auto-renew",
40199
+ value: domain.auto_renew ? "yes" : "no"
40200
+ }, undefined, false, undefined, this),
40201
+ domain.is_premium && /* @__PURE__ */ jsxDEV3(Fragment, {
40202
+ children: [
40203
+ /* @__PURE__ */ jsxDEV3(Field, {
40204
+ label: "Premium",
40205
+ value: "yes"
40206
+ }, undefined, false, undefined, this),
40207
+ /* @__PURE__ */ jsxDEV3(Field, {
40208
+ label: "Premium ask",
40209
+ value: domain.premium_price?.toString() ?? "\u2014"
40210
+ }, undefined, false, undefined, this)
40211
+ ]
40212
+ }, undefined, true, undefined, this),
40213
+ domain.purchase_price !== null && /* @__PURE__ */ jsxDEV3(Field, {
40214
+ label: "Purchase price",
40215
+ value: String(domain.purchase_price)
40216
+ }, undefined, false, undefined, this),
40217
+ /* @__PURE__ */ jsxDEV3(Field, {
40218
+ label: "SSL issuer",
40219
+ value: domain.ssl_issuer
40220
+ }, undefined, false, undefined, this),
40221
+ /* @__PURE__ */ jsxDEV3(Field, {
40222
+ label: "SSL expires",
40223
+ value: formatDate(domain.ssl_expires_at)
40224
+ }, undefined, false, undefined, this),
40225
+ domain.nameservers.length > 0 && /* @__PURE__ */ jsxDEV3(Box3, {
40226
+ flexDirection: "column",
40227
+ marginTop: 1,
40228
+ children: [
40229
+ /* @__PURE__ */ jsxDEV3(Text3, {
40230
+ bold: true,
40231
+ dimColor: true,
40232
+ children: "Nameservers"
40233
+ }, undefined, false, undefined, this),
40234
+ domain.nameservers.slice(0, compact ? 3 : 8).map((ns) => /* @__PURE__ */ jsxDEV3(Text3, {
40235
+ dimColor: true,
40236
+ children: [
40237
+ " ",
40238
+ ns
40239
+ ]
40240
+ }, ns, true, undefined, this))
40241
+ ]
40242
+ }, undefined, true, undefined, this),
40243
+ !compact && dnsRecords.length > 0 && /* @__PURE__ */ jsxDEV3(Box3, {
40244
+ flexDirection: "column",
40245
+ marginTop: 1,
40246
+ children: [
40247
+ /* @__PURE__ */ jsxDEV3(Text3, {
40248
+ bold: true,
40249
+ children: [
40250
+ "DNS records (",
40251
+ dnsRecords.length,
40252
+ ")"
40253
+ ]
40254
+ }, undefined, true, undefined, this),
40255
+ dnsRecords.slice(0, 8).map((record) => /* @__PURE__ */ jsxDEV3(Text3, {
40256
+ dimColor: true,
40257
+ children: [
40258
+ " ",
40259
+ record.type,
40260
+ " ",
40261
+ record.name,
40262
+ " \u2192 ",
40263
+ record.value
40264
+ ]
40265
+ }, record.id, true, undefined, this)),
40266
+ dnsRecords.length > 8 && /* @__PURE__ */ jsxDEV3(Text3, {
40267
+ dimColor: true,
40268
+ children: [
40269
+ " \u2026 ",
40270
+ dnsRecords.length - 8,
40271
+ " more"
40272
+ ]
40273
+ }, undefined, true, undefined, this)
40274
+ ]
40275
+ }, undefined, true, undefined, this),
40276
+ (offers.length > 0 || emails.length > 0) && /* @__PURE__ */ jsxDEV3(Box3, {
40277
+ marginTop: 1,
40278
+ children: /* @__PURE__ */ jsxDEV3(Text3, {
40279
+ dimColor: true,
40280
+ children: [
40281
+ "Offers: ",
40282
+ offers.length,
40283
+ " \xB7 Linked emails: ",
40284
+ emails.length,
40285
+ dnsRecords.length > 0 ? ` \xB7 DNS: ${dnsRecords.length}` : ""
40286
+ ]
40287
+ }, undefined, true, undefined, this)
40288
+ }, undefined, false, undefined, this),
40289
+ domain.notes && !compact && /* @__PURE__ */ jsxDEV3(Box3, {
40290
+ flexDirection: "column",
40291
+ marginTop: 1,
40292
+ children: [
40293
+ /* @__PURE__ */ jsxDEV3(Text3, {
40294
+ bold: true,
40295
+ dimColor: true,
40296
+ children: "Notes"
40297
+ }, undefined, false, undefined, this),
40298
+ /* @__PURE__ */ jsxDEV3(Text3, {
40299
+ wrap: "wrap",
40300
+ children: domain.notes
40301
+ }, undefined, false, undefined, this)
40302
+ ]
40303
+ }, undefined, true, undefined, this),
40304
+ !compact && /* @__PURE__ */ jsxDEV3(Box3, {
40305
+ marginTop: 1,
40306
+ children: /* @__PURE__ */ jsxDEV3(Text3, {
40307
+ dimColor: true,
40308
+ children: [
40309
+ "ID: ",
40310
+ domain.id
40311
+ ]
40312
+ }, undefined, true, undefined, this)
40313
+ }, undefined, false, undefined, this)
40314
+ ]
40315
+ }, undefined, true, undefined, this);
40316
+ }
40317
+ function Field({ label, value }) {
40318
+ if (!value || value === "\u2014") {
40319
+ return /* @__PURE__ */ jsxDEV3(Box3, {
40320
+ children: [
40321
+ /* @__PURE__ */ jsxDEV3(Text3, {
40322
+ dimColor: true,
40323
+ children: label.padEnd(14)
40324
+ }, undefined, false, undefined, this),
40325
+ /* @__PURE__ */ jsxDEV3(Text3, {
40326
+ dimColor: true,
40327
+ children: "\u2014"
40328
+ }, undefined, false, undefined, this)
40329
+ ]
40330
+ }, undefined, true, undefined, this);
40331
+ }
40332
+ return /* @__PURE__ */ jsxDEV3(Box3, {
40333
+ children: [
40334
+ /* @__PURE__ */ jsxDEV3(Text3, {
40335
+ dimColor: true,
40336
+ children: label.padEnd(14)
40337
+ }, undefined, false, undefined, this),
40338
+ /* @__PURE__ */ jsxDEV3(Text3, {
40339
+ children: value
40340
+ }, undefined, false, undefined, this)
40341
+ ]
40342
+ }, undefined, true, undefined, this);
40343
+ }
40344
+
40345
+ // src/cli/tui/SearchView.tsx
40346
+ import { useState } from "react";
40347
+ import { Box as Box4, Text as Text4, useInput } from "ink";
40348
+ import TextInput from "ink-text-input";
40349
+ import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime";
40350
+ function SearchView({
40351
+ results,
40352
+ selectedIndex,
40353
+ onSearch,
40354
+ onSelect,
40355
+ onBack
40356
+ }) {
40357
+ const [query, setQuery] = useState("");
40358
+ useInput((input, key) => {
40359
+ if (key.escape)
40360
+ onBack();
40361
+ if (key.return && results[selectedIndex]) {
40362
+ onSelect(results[selectedIndex]);
40363
+ }
40364
+ });
40365
+ return /* @__PURE__ */ jsxDEV4(Box4, {
40366
+ flexDirection: "column",
40367
+ children: [
40368
+ /* @__PURE__ */ jsxDEV4(Box4, {
40369
+ marginBottom: 1,
40370
+ children: /* @__PURE__ */ jsxDEV4(Text4, {
40371
+ bold: true,
40372
+ children: "Search domains"
40373
+ }, undefined, false, undefined, this)
40374
+ }, undefined, false, undefined, this),
40375
+ /* @__PURE__ */ jsxDEV4(Box4, {
40376
+ marginBottom: 1,
40377
+ children: [
40378
+ /* @__PURE__ */ jsxDEV4(Text4, {
40379
+ dimColor: true,
40380
+ children: "Query: "
40381
+ }, undefined, false, undefined, this),
40382
+ /* @__PURE__ */ jsxDEV4(TextInput, {
40383
+ value: query,
40384
+ onChange: (value) => {
40385
+ setQuery(value);
40386
+ onSearch(value);
40387
+ },
40388
+ placeholder: "name, registrar, notes\u2026"
40389
+ }, undefined, false, undefined, this)
40390
+ ]
40391
+ }, undefined, true, undefined, this),
40392
+ /* @__PURE__ */ jsxDEV4(Text4, {
40393
+ dimColor: true,
40394
+ children: "[enter] open \xB7 [esc] back"
40395
+ }, undefined, false, undefined, this),
40396
+ /* @__PURE__ */ jsxDEV4(Box4, {
40397
+ marginTop: 1,
40398
+ children: /* @__PURE__ */ jsxDEV4(DomainTable, {
40399
+ domains: results,
40400
+ selectedIndex
40401
+ }, undefined, false, undefined, this)
40402
+ }, undefined, false, undefined, this)
40403
+ ]
40404
+ }, undefined, true, undefined, this);
40405
+ }
40406
+
40407
+ // src/cli/tui/App.tsx
40408
+ import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
40409
+ function loadDomainsForFilter(activeFilter) {
40410
+ switch (activeFilter) {
40411
+ case "active":
40412
+ return listDomains({ status: "active" });
40413
+ case "premium":
40414
+ return listDomains({ is_premium: true });
40415
+ case "expiring":
40416
+ return listExpiring(30);
40417
+ default:
40418
+ return listDomains();
40419
+ }
40420
+ }
40421
+ function App({ initialStatus }) {
40422
+ const { exit } = useApp();
40423
+ const [view, setView] = useState2("list");
40424
+ const initialFilter = initialStatus === "active" ? "active" : initialStatus === "premium" ? "premium" : "all";
40425
+ const [filter, setFilter] = useState2(initialFilter);
40426
+ const [selectedIndex, setSelectedIndex] = useState2(0);
40427
+ const [details, setDetails] = useState2(null);
40428
+ const [searchResults, setSearchResults] = useState2([]);
40429
+ const [searchIndex, setSearchIndex] = useState2(0);
40430
+ const [domains, setDomains] = useState2(() => loadDomainsForFilter(initialFilter));
40431
+ const refresh = useCallback(() => {
40432
+ const next = loadDomainsForFilter(filter);
40433
+ setDomains(next);
40434
+ setSelectedIndex((current) => Math.min(current, Math.max(0, next.length - 1)));
40435
+ return next;
40436
+ }, [filter]);
40437
+ useEffect(() => {
40438
+ refresh();
40439
+ }, [refresh]);
40440
+ const selectedDomain = domains[selectedIndex] ?? null;
40441
+ const previewDetails = useMemo(() => {
40442
+ if (!selectedDomain)
40443
+ return null;
40444
+ return getDomainDetails(selectedDomain.id);
40445
+ }, [selectedDomain]);
40446
+ const previewDns = useMemo(() => {
40447
+ if (!selectedDomain)
40448
+ return [];
40449
+ return listDnsRecords(selectedDomain.id);
40450
+ }, [selectedDomain]);
40451
+ useInput2((input, key) => {
40452
+ if (input === "q" && view !== "search") {
40453
+ exit();
40454
+ return;
40455
+ }
40456
+ if (view === "list") {
40457
+ if (key.upArrow || input === "k") {
40458
+ setSelectedIndex((index) => Math.max(0, index - 1));
40459
+ } else if (key.downArrow || input === "j") {
40460
+ setSelectedIndex((index) => Math.min(domains.length - 1, index + 1));
40461
+ } else if (key.return && selectedDomain) {
40462
+ const full = getDomainDetails(selectedDomain.id);
40463
+ if (full) {
40464
+ setDetails(full);
40465
+ setView("detail");
40466
+ }
40467
+ } else if (input === "/") {
40468
+ setSearchResults([]);
40469
+ setSearchIndex(0);
40470
+ setView("search");
40471
+ } else if (input === "f") {
40472
+ setFilter((current) => {
40473
+ const index = DOMAIN_FILTERS.indexOf(current);
40474
+ return DOMAIN_FILTERS[(index + 1) % DOMAIN_FILTERS.length];
40475
+ });
40476
+ } else if (input === "r") {
40477
+ refresh();
40478
+ }
40479
+ return;
40480
+ }
40481
+ if (view === "search") {
40482
+ if (key.upArrow || input === "k") {
40483
+ setSearchIndex((index) => Math.max(0, index - 1));
40484
+ } else if (key.downArrow || input === "j") {
40485
+ setSearchIndex((index) => Math.min(searchResults.length - 1, index + 1));
40486
+ }
40487
+ return;
40488
+ }
40489
+ if (view === "detail") {
40490
+ if (key.escape) {
40491
+ setView("list");
40492
+ refresh();
40493
+ } else if (input === "r" && details) {
40494
+ setDetails(getDomainDetails(details.domain.id));
40495
+ }
40496
+ }
40497
+ });
40498
+ const handleSearch = useCallback((query) => {
40499
+ const trimmed = query.trim();
40500
+ if (trimmed.length < 2) {
40501
+ setSearchResults([]);
40502
+ setSearchIndex(0);
40503
+ return;
40504
+ }
40505
+ const results = searchDomains(trimmed);
40506
+ setSearchResults(results);
40507
+ setSearchIndex(0);
40508
+ }, []);
40509
+ const handleSearchSelect = useCallback((domain) => {
40510
+ const full = getDomainDetails(domain.id);
40511
+ if (full) {
40512
+ setDetails(full);
40513
+ setView("detail");
40514
+ }
40515
+ }, []);
40516
+ const detailDns = details ? listDnsRecords(details.domain.id) : [];
40517
+ return /* @__PURE__ */ jsxDEV5(Box5, {
40518
+ flexDirection: "column",
40519
+ padding: 1,
40520
+ children: [
40521
+ /* @__PURE__ */ jsxDEV5(Header, {
40522
+ count: view === "search" ? searchResults.length : domains.length,
40523
+ filter,
40524
+ view
40525
+ }, undefined, false, undefined, this),
40526
+ view === "list" && /* @__PURE__ */ jsxDEV5(Box5, {
40527
+ flexDirection: "column",
40528
+ children: [
40529
+ /* @__PURE__ */ jsxDEV5(DomainTable, {
40530
+ domains,
40531
+ selectedIndex
40532
+ }, undefined, false, undefined, this),
40533
+ previewDetails && /* @__PURE__ */ jsxDEV5(Box5, {
40534
+ marginTop: 1,
40535
+ flexDirection: "column",
40536
+ children: [
40537
+ /* @__PURE__ */ jsxDEV5(Text5, {
40538
+ bold: true,
40539
+ dimColor: true,
40540
+ children: "Selected"
40541
+ }, undefined, false, undefined, this),
40542
+ /* @__PURE__ */ jsxDEV5(DomainDetail, {
40543
+ details: previewDetails,
40544
+ dnsRecords: previewDns,
40545
+ compact: true
40546
+ }, undefined, false, undefined, this)
40547
+ ]
40548
+ }, undefined, true, undefined, this),
40549
+ /* @__PURE__ */ jsxDEV5(Box5, {
40550
+ marginTop: 1,
40551
+ children: /* @__PURE__ */ jsxDEV5(Text5, {
40552
+ dimColor: true,
40553
+ children: "\u2191\u2193 j/k navigate \xB7 enter detail \xB7 / search \xB7 f filter \xB7 r refresh \xB7 q quit"
40554
+ }, undefined, false, undefined, this)
40555
+ }, undefined, false, undefined, this)
40556
+ ]
40557
+ }, undefined, true, undefined, this),
40558
+ view === "detail" && details && /* @__PURE__ */ jsxDEV5(DomainDetail, {
40559
+ details,
40560
+ dnsRecords: detailDns
40561
+ }, undefined, false, undefined, this),
40562
+ view === "search" && /* @__PURE__ */ jsxDEV5(SearchView, {
40563
+ results: searchResults,
40564
+ selectedIndex: searchIndex,
40565
+ onSearch: handleSearch,
40566
+ onSelect: handleSearchSelect,
40567
+ onBack: () => setView("list")
40568
+ }, undefined, false, undefined, this)
40569
+ ]
40570
+ }, undefined, true, undefined, this);
40571
+ }
40572
+
40573
+ // src/cli/commands/interactive.tsx
40574
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
40575
+ function registerInteractiveCommand(program2) {
40576
+ program2.command("interactive").description("Launch interactive domain portfolio browser (Ink TUI)").option("--status <status>", "Initial filter status (e.g. active, premium)").action((opts) => {
40577
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
40578
+ console.error(chalk.red("Interactive mode requires a TTY terminal."));
40579
+ console.error(chalk.dim("Use `domains domain list` or `domains domain get <name>` for non-interactive use."));
40580
+ process.exit(1);
40581
+ }
40582
+ render(/* @__PURE__ */ jsxDEV6(App, {
40583
+ initialStatus: opts.status
40584
+ }, undefined, false, undefined, this));
40585
+ });
40586
+ }
40587
+
39947
40588
  // src/cli/index.ts
39948
40589
  init_version();
39949
40590
  var program2 = new Command;
@@ -39968,4 +40609,5 @@ registerOutreachCommand(program2);
39968
40609
  registerSedoCommand(program2);
39969
40610
  registerWalletCommand(program2);
39970
40611
  registerProvisionCommand(program2);
40612
+ registerInteractiveCommand(program2);
39971
40613
  program2.parse(process.argv);
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ export interface AppProps {
3
+ initialStatus?: string;
4
+ }
5
+ export declare function App({ initialStatus }: AppProps): React.JSX.Element;
6
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../src/cli/tui/App.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAC;AAmBzE,MAAM,WAAW,QAAQ;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAeD,wBAAgB,GAAG,CAAC,EAAE,aAAa,EAAE,EAAE,QAAQ,qBAqJ9C"}
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import type { DomainDetails } from "../../db/domains.js";
3
+ import type { DnsRecord } from "../../db/dns-records.js";
4
+ interface DomainDetailProps {
5
+ details: DomainDetails;
6
+ dnsRecords?: DnsRecord[];
7
+ compact?: boolean;
8
+ }
9
+ export declare function DomainDetail({ details, dnsRecords, compact }: DomainDetailProps): React.JSX.Element;
10
+ export {};
11
+ //# sourceMappingURL=DomainDetail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DomainDetail.d.ts","sourceRoot":"","sources":["../../../src/cli/tui/DomainDetail.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGzD,UAAU,iBAAiB;IACzB,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,UAAe,EAAE,OAAe,EAAE,EAAE,iBAAiB,qBAqF5F"}
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import type { Domain } from "../../db/domains.js";
3
+ interface DomainTableProps {
4
+ domains: Domain[];
5
+ selectedIndex: number;
6
+ compact?: boolean;
7
+ }
8
+ export declare function DomainTable({ domains, selectedIndex, compact }: DomainTableProps): React.JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=DomainTable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DomainTable.d.ts","sourceRoot":"","sources":["../../../src/cli/tui/DomainTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAWlD,UAAU,gBAAgB;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,OAAe,EAAE,EAAE,gBAAgB,qBA2DxF"}
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import type { DomainFilter } from "./format.js";
3
+ interface HeaderProps {
4
+ count: number;
5
+ filter: DomainFilter;
6
+ view: "list" | "detail" | "search";
7
+ }
8
+ export declare function Header({ count, filter, view }: HeaderProps): React.JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=Header.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Header.d.ts","sourceRoot":"","sources":["../../../src/cli/tui/Header.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACpC;AAED,wBAAgB,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,WAAW,qBAmB1D"}
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import type { Domain } from "../../db/domains.js";
3
+ interface SearchViewProps {
4
+ results: Domain[];
5
+ selectedIndex: number;
6
+ onSearch: (query: string) => void;
7
+ onSelect: (domain: Domain) => void;
8
+ onBack: () => void;
9
+ }
10
+ export declare function SearchView({ results, selectedIndex, onSearch, onSelect, onBack, }: SearchViewProps): React.JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=SearchView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchView.d.ts","sourceRoot":"","sources":["../../../src/cli/tui/SearchView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAGlD,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,MAAM,GACP,EAAE,eAAe,qBAgCjB"}
@@ -0,0 +1,17 @@
1
+ import type { Domain, DomainStatus } from "../../db/domains.js";
2
+ export declare const TABLE_COLS: {
3
+ readonly name: 28;
4
+ readonly status: 14;
5
+ readonly expires: 12;
6
+ readonly registrar: 16;
7
+ };
8
+ export declare function pad(value: string, width: number): string;
9
+ export declare function formatDate(iso: string | null | undefined): string;
10
+ export declare function daysUntil(iso: string | null | undefined): string;
11
+ export declare const STATUS_COLORS: Record<DomainStatus, string>;
12
+ export declare function domainRowLabel(domain: Domain): string;
13
+ export declare function stripAnsi(value: string | undefined): string;
14
+ export type DomainFilter = "all" | "active" | "expiring" | "premium";
15
+ export declare const DOMAIN_FILTERS: DomainFilter[];
16
+ export declare function filterLabel(filter: DomainFilter): string;
17
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/cli/tui/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEhE,eAAO,MAAM,UAAU;;;;;CAKb,CAAC;AAEX,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGjE;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAOhE;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAatD,CAAC;AAEF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE3D;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAErE,eAAO,MAAM,cAAc,EAAE,YAAY,EAA6C,CAAC;AAEvF,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAWxD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/domains",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Domain portfolio and DNS management for AI agents — CLI + MCP server with SQLite",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,7 +21,7 @@
21
21
  "README.md"
22
22
  ],
23
23
  "scripts": {
24
- "build": "bun build src/cli/index.ts --outdir dist/cli --target bun --external @modelcontextprotocol/sdk --external @hasna/cloud && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @hasna/cloud && bun build src/index.ts --outdir dist --target bun --external @hasna/cloud && tsc --emitDeclarationOnly --outDir dist",
24
+ "build": "bun build src/cli/index.ts --outdir dist/cli --target bun --external ink --external react --external chalk --external ink-text-input --external @modelcontextprotocol/sdk --external @hasna/cloud && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @hasna/cloud && bun build src/index.ts --outdir dist --target bun --external @hasna/cloud && tsc --emitDeclarationOnly --outDir dist",
25
25
  "typecheck": "tsc --noEmit",
26
26
  "test": "bun test",
27
27
  "dev:cli": "bun run src/cli/index.ts",
@@ -65,11 +65,17 @@
65
65
  "@hasna/contacts": "^0.6.16",
66
66
  "@hasna/wallets": "^0.1.7",
67
67
  "@modelcontextprotocol/sdk": "^1.12.1",
68
+ "chalk": "^5.4.1",
68
69
  "commander": "^13.1.0",
70
+ "ink": "^5.2.0",
71
+ "ink-text-input": "^6.0.0",
72
+ "react": "^18.3.1",
69
73
  "zod": "^3.24.2"
70
74
  },
71
75
  "devDependencies": {
72
76
  "@types/bun": "^1.2.4",
77
+ "@types/react": "^18.3.18",
78
+ "ink-testing-library": "^4.0.0",
73
79
  "typescript": "^5.7.3"
74
80
  }
75
81
  }