@profullstack/nichedb 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +212 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/nichedb",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "CLI and MCP bridge for NicheDB: browse collections, manage sources and feeds, search items, from any deployment",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -15,7 +15,7 @@ import { homedir, tmpdir } from 'node:os';
15
15
  import { join } from 'node:path';
16
16
  import { createInterface } from 'node:readline';
17
17
 
18
- export const VERSION = '0.31.0';
18
+ export const VERSION = '0.33.0';
19
19
  const GEO_FLAGS = ['lat', 'long', 'radius', 'unit', 'bbox', 'sort', 'offset'];
20
20
  const DEFAULT_API = process.env.NICHEDB_API ?? 'https://nichedb.dev';
21
21
  const CONFIG_DIR = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'nichedb');
@@ -182,6 +182,47 @@ export const COMMANDS = [
182
182
  summary: 'Print the RSS URL (or the feed itself with --fetch).',
183
183
  options: ['--fetch'],
184
184
  },
185
+ {
186
+ name: 'population',
187
+ usage: 'population [<country> [<state> [<city>]]] | zip <zip> | find <name>',
188
+ summary:
189
+ 'Population from the world to the ZIP code: an area, its measures and its largest children.',
190
+ options: ['--level country|state|city|zip', '--limit', '--offset', '--json'],
191
+ },
192
+ {
193
+ name: 'tlds',
194
+ usage: 'tlds [<query>] [--registrar <slug>] [--type <t>] [--max-renew <n>] [--trap] [--sort …]',
195
+ summary:
196
+ "Top-level domains with each one's cheapest register, renew and transfer price, renewal first.",
197
+ options: [
198
+ '--registrar <porkbun|dynadot|cloudflare|ovh>',
199
+ '--type <generic|country-code|…>',
200
+ '--max-renew <n>',
201
+ '--max-register <n>',
202
+ '--trap (renews at 2x the first year or more)',
203
+ '--idn / --no-idn',
204
+ '--status <delegated|removed|all>',
205
+ '--sort <renew|register|transfer|ratio|tld|registrars|first_seen>',
206
+ '--order <asc|desc>',
207
+ '--limit',
208
+ '--csv',
209
+ '--json',
210
+ ],
211
+ },
212
+ {
213
+ name: 'tld',
214
+ usage: 'tld <tld> | tld changes',
215
+ summary:
216
+ 'One top-level domain: registry, RDAP server and every registrar price; or what changed in the root.',
217
+ options: ['--json'],
218
+ },
219
+ {
220
+ name: 'check',
221
+ usage: 'check <name> [--tlds com,dev,io]',
222
+ summary:
223
+ 'Is a domain name registered? Asks the registry over RDAP; a bare word is tried under each ending.',
224
+ options: ['--tlds <list>', '--json'],
225
+ },
185
226
  {
186
227
  name: 'profiles',
187
228
  usage: 'profiles [<query>] [--since <iso>] [--mine]',
@@ -740,6 +781,176 @@ export async function run(
740
781
  out(await res.text());
741
782
  return 0;
742
783
  }
784
+ case 'population': {
785
+ const qs = new URLSearchParams();
786
+ if (flags.level) qs.set('level', flags.level);
787
+ if (flags.limit) qs.set('limit', flags.limit);
788
+ if (flags.offset) qs.set('offset', flags.offset);
789
+ const people = (n) =>
790
+ n === null || n === undefined ? '—' : Math.round(n).toLocaleString('en-US');
791
+ const [sub, ...args] = rest;
792
+
793
+ if (sub === 'find') {
794
+ const name = args.join(' ');
795
+ if (!name) throw new Error('population find <name>');
796
+ const { areas } = await client.get(
797
+ `/api/v1/population/search?q=${encodeURIComponent(name)}`,
798
+ );
799
+ if (json) {
800
+ out(JSON.stringify(areas, null, 2));
801
+ return 0;
802
+ }
803
+ for (const a of areas)
804
+ out(`${pad(a.key, 34)} ${pad(a.level, 8)} ${pad(people(a.population), 14)} ${a.title}`);
805
+ if (areas.length === 0) out('(no place by that name)');
806
+ return 0;
807
+ }
808
+
809
+ let path = '/api/v1/population';
810
+ if (sub === 'zip') {
811
+ if (!args[0]) throw new Error('population zip <zip>');
812
+ path = `/api/v1/population/zip/${encodeURIComponent(String(args[0]).trim())}`;
813
+ } else if (sub) {
814
+ path = `/api/v1/population/${encodeURIComponent(rest.join('-').toLowerCase())}`;
815
+ }
816
+ const body = await client.get(`${path}${qs.size ? `?${qs}` : ''}`);
817
+ if (json) {
818
+ out(JSON.stringify(body, null, 2));
819
+ return 0;
820
+ }
821
+ if (body.area) {
822
+ const a = body.area;
823
+ out(`${a.title} (${a.key}, ${a.level})`);
824
+ out(
825
+ ` ${pad('population', 24)} ${people(a.population)}${a.year ? ` (${a.survey ?? a.year})` : ''}`,
826
+ );
827
+ for (const [k, v] of Object.entries(a.measures ?? {})) {
828
+ if (typeof v === 'number') out(` ${pad(k, 24)} ${v.toLocaleString('en-US')}`);
829
+ }
830
+ out(` ${a.page}`);
831
+ }
832
+ const kids = body.children?.areas ?? body.countries ?? [];
833
+ const total = body.children?.total ?? body.total ?? kids.length;
834
+ if (kids.length) {
835
+ out(
836
+ `\n${body.children?.level ?? 'country'}: ${kids.length} of ${Number(total).toLocaleString('en-US')}`,
837
+ );
838
+ for (const k of kids) out(` ${pad(k.key, 34)} ${pad(people(k.population), 14)} ${k.name}`);
839
+ }
840
+ return 0;
841
+ }
842
+ case 'tlds': {
843
+ const qs = new URLSearchParams();
844
+ const term = rest.join(' ');
845
+ if (term) qs.set('q', term);
846
+ for (const [flag, param] of [
847
+ ['registrar', 'registrar'],
848
+ ['type', 'type'],
849
+ ['maxRenew', 'max_renew'],
850
+ ['maxRegister', 'max_register'],
851
+ ['status', 'status'],
852
+ ['sort', 'sort'],
853
+ ['order', 'order'],
854
+ ['limit', 'limit'],
855
+ ['offset', 'offset'],
856
+ ['manager', 'manager'],
857
+ ['band', 'band'],
858
+ ])
859
+ if (flags[flag] !== undefined && flags[flag] !== true) qs.set(param, flags[flag]);
860
+ if (flags.trap) qs.set('trap', '1');
861
+ if (flags.idn) qs.set('idn', '1');
862
+ if (flags.noIdn) qs.set('idn', '0');
863
+ if (flags.csv) {
864
+ qs.set('format', 'csv');
865
+ const res = await fetchImpl(`${api}/api/v1/tlds?${qs}`);
866
+ out((await res.text()).trimEnd());
867
+ return 0;
868
+ }
869
+ const r = await client.get(`/api/v1/tlds?${qs}`);
870
+ if (json) {
871
+ out(JSON.stringify(r, null, 2));
872
+ return 0;
873
+ }
874
+ const m = (p) =>
875
+ p ? `${p.amount.toFixed(2)} ${p.currency === 'USD' ? '' : p.currency}`.trim() : '-';
876
+ out(
877
+ `${pad('TLD', 22)} ${pad('REGISTER', 10)} ${pad('RENEW', 10)} ${pad('x', 5)} ${pad('TRANSFER', 10)} ${pad('CHEAPEST RENEWAL AT', 22)} REGISTRY`,
878
+ );
879
+ for (const t of r.tlds) {
880
+ out(
881
+ `${pad(`.${t.unicode ?? t.tld}`, 22)} ${pad(m(t.best?.register), 10)} ${pad(m(t.best?.renew), 10)} ${pad(t.renewal_ratio ? `${t.renewal_ratio}x` : '', 5)} ${pad(m(t.best?.transfer), 10)} ${pad(t.best?.renew?.registrar_name ?? '', 22)} ${t.manager ?? ''}`,
882
+ );
883
+ }
884
+ out(`(${r.tlds.length} of ${r.total}${r.compare_currency ? '; cheapest in USD' : ''})`);
885
+ return 0;
886
+ }
887
+ case 'tld': {
888
+ const [arg] = rest;
889
+ if (!arg) throw new Error('tld <tld> | tld changes');
890
+ if (arg === 'changes') {
891
+ const r = await client.get('/api/v1/tlds/changes');
892
+ if (json) {
893
+ out(JSON.stringify(r.changes, null, 2));
894
+ return 0;
895
+ }
896
+ for (const c of r.changes)
897
+ out(
898
+ `${when(c.at)} ${pad(c.change, 8)} .${pad(c.unicode ?? c.tld, 24)} ${c.list_version ?? ''}`,
899
+ );
900
+ if (!r.changes.length) out('(no changes since tracking began)');
901
+ return 0;
902
+ }
903
+ const t = await client.get(`/api/v1/tlds/${encodeURIComponent(arg.replace(/^\./, ''))}`);
904
+ if (json) {
905
+ out(JSON.stringify(t, null, 2));
906
+ return 0;
907
+ }
908
+ out(`.${t.unicode ?? t.tld}${t.unicode ? ` (${t.tld})` : ''} ${t.status} ${t.type ?? ''}`);
909
+ out(`registry ${t.manager ?? '-'}`);
910
+ out(`rdap ${t.rdap ?? 'none published'}`);
911
+ out('');
912
+ out(
913
+ `${pad('REGISTRAR', 24)} ${pad('CUR', 4)} ${pad('REGISTER', 10)} ${pad('RENEW', 10)} ${pad('TRANSFER', 10)} RESTORE`,
914
+ );
915
+ const f = (v) => (v === null || v === undefined ? '-' : Number(v).toFixed(2));
916
+ for (const p of t.prices)
917
+ out(
918
+ `${pad(p.registrar_name, 24)} ${pad(p.currency, 4)} ${pad(f(p.register), 10)} ${pad(f(p.renew), 10)} ${pad(f(p.transfer), 10)} ${f(p.restore)}`,
919
+ );
920
+ if (!t.prices.length) out('(no registrar read here sells it)');
921
+ out(`\n${t.page}`);
922
+ return 0;
923
+ }
924
+ case 'check': {
925
+ const name = rest.join(',');
926
+ if (!name) throw new Error('check <name> [--tlds com,dev,io]');
927
+ const qs = new URLSearchParams({ name });
928
+ if (flags.tlds && flags.tlds !== true) qs.set('tlds', flags.tlds);
929
+ const r = await client.get(`/api/v1/tlds/check?${qs}`);
930
+ if (json) {
931
+ out(JSON.stringify(r, null, 2));
932
+ return 0;
933
+ }
934
+ const word = {
935
+ registered: 'taken',
936
+ not_registered: 'not registered',
937
+ unknown: 'unknown',
938
+ invalid: 'invalid',
939
+ };
940
+ for (const x of r.results) {
941
+ const detail =
942
+ x.status === 'registered'
943
+ ? `${x.registrar ?? ''}${x.expires ? `, expires ${String(x.expires).slice(0, 10)}` : ''}`
944
+ : x.status === 'not_registered'
945
+ ? x.cheapest?.register
946
+ ? `from ${x.cheapest.register.amount.toFixed(2)} at ${x.cheapest.register.registrar_name}, renews ${x.cheapest.renew?.amount?.toFixed(2) ?? '?'}`
947
+ : ''
948
+ : (x.reason ?? '');
949
+ out(`${pad(x.name, 32)} ${pad(word[x.status] ?? x.status, 15)} ${detail}`);
950
+ }
951
+ if (r.truncated) out('(only the first names were checked)');
952
+ return 0;
953
+ }
743
954
  case 'profiles': {
744
955
  const term = rest.join(' ');
745
956
  const qs = new URLSearchParams();