@geolonia/geonicdb-cli 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ function migrateV1ToV2(data) {
25
25
  const knownKeys = [
26
26
  "url",
27
27
  "service",
28
+ "tenantId",
28
29
  "token",
29
30
  "refreshToken",
30
31
  "format",
@@ -186,6 +187,15 @@ function printInfo(message) {
186
187
  function printWarning(message) {
187
188
  console.error(chalk.yellow(message));
188
189
  }
190
+ function printApiKeyBox(key) {
191
+ const border = "\u2500".repeat(key.length + 4);
192
+ console.error("");
193
+ console.error(chalk.green(` \u250C${border}\u2510`));
194
+ console.error(chalk.green(` \u2502 ${chalk.bold(key)} \u2502`));
195
+ console.error(chalk.green(` \u2514${border}\u2518`));
196
+ console.error("");
197
+ console.error(chalk.yellow("\u26A0 \u3053\u306E API \u30AD\u30FC\u5024\u3092\u5B89\u5168\u306B\u4FDD\u5B58\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4E8C\u5EA6\u3068\u8868\u793A\u3055\u308C\u307E\u305B\u3093\u3002"));
198
+ }
189
199
  function printCount(count) {
190
200
  console.log(chalk.dim(`Count: ${count}`));
191
201
  }
@@ -583,7 +593,7 @@ function registerConfigCommand(program2) {
583
593
  command: "geonic config get url --profile staging"
584
594
  }
585
595
  ]);
586
- const list = config.command("list").description("List all config values").action((...args) => {
596
+ const list = config.command("list").description("List all config values for the active (or specified) profile").action((...args) => {
587
597
  const cmd = args[args.length - 1];
588
598
  const profile = cmd.optsWithGlobals().profile;
589
599
  const all = loadConfig(profile);
@@ -597,9 +607,13 @@ function registerConfigCommand(program2) {
597
607
  {
598
608
  description: "List all configuration values",
599
609
  command: "geonic config list"
610
+ },
611
+ {
612
+ description: "List config for a specific profile",
613
+ command: "geonic config list --profile staging"
600
614
  }
601
615
  ]);
602
- const del = config.command("delete").description("Delete a config value").argument("<key>", "Configuration key").action((...args) => {
616
+ const del = config.command("delete").description("Remove a config key from the active (or specified) profile").argument("<key>", "Configuration key").action((...args) => {
603
617
  const cmd = args[args.length - 1];
604
618
  const key = args[0];
605
619
  const profile = cmd.optsWithGlobals().profile;
@@ -608,8 +622,16 @@ function registerConfigCommand(program2) {
608
622
  });
609
623
  addExamples(del, [
610
624
  {
611
- description: "Delete a config value",
625
+ description: "Remove the saved server URL",
612
626
  command: "geonic config delete url"
627
+ },
628
+ {
629
+ description: "Clear the saved authentication token",
630
+ command: "geonic config delete token"
631
+ },
632
+ {
633
+ description: "Remove API key from a specific profile",
634
+ command: "geonic config delete apiKey --profile staging"
613
635
  }
614
636
  ]);
615
637
  }
@@ -664,6 +686,7 @@ var GdbClient = class _GdbClient {
664
686
  clientId;
665
687
  clientSecret;
666
688
  onTokenRefresh;
689
+ onBeforeRefresh;
667
690
  verbose;
668
691
  dryRun;
669
692
  refreshPromise;
@@ -676,6 +699,7 @@ var GdbClient = class _GdbClient {
676
699
  this.clientId = options.clientId;
677
700
  this.clientSecret = options.clientSecret;
678
701
  this.onTokenRefresh = options.onTokenRefresh;
702
+ this.onBeforeRefresh = options.onBeforeRefresh;
679
703
  this.verbose = options.verbose ?? false;
680
704
  this.dryRun = options.dryRun ?? false;
681
705
  }
@@ -804,6 +828,17 @@ var GdbClient = class _GdbClient {
804
828
  }
805
829
  }
806
830
  async doRefresh() {
831
+ if (this.onBeforeRefresh) {
832
+ const latest = this.onBeforeRefresh();
833
+ if (latest.token && latest.token !== this.token) {
834
+ this.token = latest.token;
835
+ if (latest.refreshToken) this.refreshToken = latest.refreshToken;
836
+ return true;
837
+ }
838
+ if (latest.refreshToken) {
839
+ this.refreshToken = latest.refreshToken;
840
+ }
841
+ }
807
842
  if (this.refreshToken) {
808
843
  try {
809
844
  const url = this.buildUrl("/auth/refresh");
@@ -985,6 +1020,10 @@ function createClient(cmd) {
985
1020
  if (refreshToken) cfg.refreshToken = refreshToken;
986
1021
  saveConfig(cfg, opts.profile);
987
1022
  },
1023
+ onBeforeRefresh: usingCliToken ? void 0 : () => {
1024
+ const cfg = loadConfig(opts.profile);
1025
+ return { token: cfg.token, refreshToken: cfg.refreshToken };
1026
+ },
988
1027
  verbose: opts.verbose,
989
1028
  dryRun: opts.dryRun
990
1029
  });
@@ -1132,7 +1171,8 @@ async function promptTenantSelection(tenants, currentTenantId) {
1132
1171
  const t = tenants[i];
1133
1172
  const current = t.tenantId === currentTenantId ? " \u2190 current" : "";
1134
1173
  const marker = t.tenantId === currentTenantId ? " *" : " ";
1135
- console.log(`${marker} ${i + 1}) ${t.tenantId} (${t.role})${current}`);
1174
+ const label = t.name ? `${t.name} (${t.tenantId})` : t.tenantId;
1175
+ console.log(`${marker} ${i + 1}) ${label} [${t.role}]${current}`);
1136
1176
  }
1137
1177
  for (; ; ) {
1138
1178
  const answer = await rl.question("\nSelect tenant number (Enter to keep current): ");
@@ -1319,6 +1359,10 @@ function addMeOAuthClientsSubcommand(me) {
1319
1359
  {
1320
1360
  description: "List your OAuth clients",
1321
1361
  command: "geonic me oauth-clients list"
1362
+ },
1363
+ {
1364
+ description: "List in table format for a quick overview",
1365
+ command: "geonic me oauth-clients list --format table"
1322
1366
  }
1323
1367
  ]);
1324
1368
  const create = oauthClients.command("create [json]").description("Create a new OAuth client").option("--name <name>", "Client name").option("--policy <policyId>", "Policy ID to attach").option("--save", "Save credentials to config for automatic re-authentication").action(
@@ -1439,7 +1483,7 @@ function addMeOAuthClientsSubcommand(me) {
1439
1483
  command: "geonic me oauth-clients update <client-id> --inactive"
1440
1484
  }
1441
1485
  ]);
1442
- const del = oauthClients.command("delete <id>").description("Delete an OAuth client").action(
1486
+ const del = oauthClients.command("delete <id>").description("Delete an OAuth client and revoke its credentials").action(
1443
1487
  withErrorHandler(async (id, _opts, cmd) => {
1444
1488
  const client = createClient(cmd);
1445
1489
  await client.rawRequest(
@@ -1451,11 +1495,15 @@ function addMeOAuthClientsSubcommand(me) {
1451
1495
  );
1452
1496
  addExamples(del, [
1453
1497
  {
1454
- description: "Delete an OAuth client",
1498
+ description: "Delete an OAuth client by ID",
1455
1499
  command: "geonic me oauth-clients delete <client-id>"
1500
+ },
1501
+ {
1502
+ description: "Revoke a compromised client",
1503
+ command: "geonic me oauth-clients delete abc123-def456"
1456
1504
  }
1457
1505
  ]);
1458
- const regenerateSecret = oauthClients.command("regenerate-secret <clientId>").description("Regenerate the client secret of an OAuth client").action(
1506
+ const regenerateSecret = oauthClients.command("regenerate-secret <clientId>").description("Regenerate the client secret \u2014 the old secret is immediately invalidated").action(
1459
1507
  withErrorHandler(async (clientId, _opts, cmd) => {
1460
1508
  const client = createClient(cmd);
1461
1509
  const format = getFormat(cmd);
@@ -1472,11 +1520,54 @@ function addMeOAuthClientsSubcommand(me) {
1472
1520
  {
1473
1521
  description: "Regenerate client secret",
1474
1522
  command: "geonic me oauth-clients regenerate-secret <client-id>"
1523
+ },
1524
+ {
1525
+ description: "Rotate secret after a security incident",
1526
+ command: "geonic me oauth-clients regenerate-secret abc123-def456"
1475
1527
  }
1476
1528
  ]);
1477
1529
  }
1478
1530
 
1479
1531
  // src/commands/me-api-keys.ts
1532
+ function cleanApiKeyData(data) {
1533
+ if (Array.isArray(data)) return data.map(cleanApiKeyData);
1534
+ if (typeof data !== "object" || data === null) return data;
1535
+ const obj = { ...data };
1536
+ if (obj.key === "******") delete obj.key;
1537
+ return obj;
1538
+ }
1539
+ function handleSaveKey(data, cmd) {
1540
+ const globalOpts = resolveOptions(cmd);
1541
+ const key = data.key;
1542
+ if (!key) {
1543
+ printError("Response missing key. API key was created, but it could not be saved.");
1544
+ process.exitCode = 1;
1545
+ return false;
1546
+ }
1547
+ try {
1548
+ const config = loadConfig(globalOpts.profile);
1549
+ config.apiKey = key;
1550
+ saveConfig(config, globalOpts.profile);
1551
+ console.error("API key saved to config. X-Api-Key header will be sent automatically.");
1552
+ return true;
1553
+ } catch (err) {
1554
+ printError(`Failed to save API key to config: ${err instanceof Error ? err.message : String(err)}`);
1555
+ printApiKeyBox(key);
1556
+ process.exitCode = 1;
1557
+ return false;
1558
+ }
1559
+ }
1560
+ function showKeyResult(data, save, cmd) {
1561
+ const key = data.key;
1562
+ if (!key) {
1563
+ printError("Response missing key. The new API key value was not returned.");
1564
+ process.exitCode = 1;
1565
+ return false;
1566
+ }
1567
+ if (save) return handleSaveKey(data, cmd);
1568
+ printApiKeyBox(key);
1569
+ return true;
1570
+ }
1480
1571
  function addMeApiKeysSubcommand(me) {
1481
1572
  const apiKeys = me.command("api-keys").description("Manage your API keys");
1482
1573
  const list = apiKeys.command("list").description("List your API keys").action(
@@ -1484,13 +1575,19 @@ function addMeApiKeysSubcommand(me) {
1484
1575
  const client = createClient(cmd);
1485
1576
  const format = getFormat(cmd);
1486
1577
  const response = await client.rawRequest("GET", "/me/api-keys");
1578
+ response.data = cleanApiKeyData(response.data);
1487
1579
  outputResponse(response, format);
1580
+ console.error("\u203B API \u30AD\u30FC\u5024\u306F\u4F5C\u6210\u6642 (create) \u307E\u305F\u306F\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u6642 (refresh) \u306B\u306E\u307F\u8868\u793A\u3055\u308C\u307E\u3059\u3002");
1488
1581
  })
1489
1582
  );
1490
1583
  addExamples(list, [
1491
1584
  {
1492
1585
  description: "List your API keys",
1493
1586
  command: "geonic me api-keys list"
1587
+ },
1588
+ {
1589
+ description: "List in table format for a quick overview",
1590
+ command: "geonic me api-keys list --format table"
1494
1591
  }
1495
1592
  ]);
1496
1593
  const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--policy <policyId>", "Policy ID to attach").option("--origins <origins>", "Allowed origins (comma-separated)").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--save", "Save the API key to config for automatic use").action(
@@ -1540,24 +1637,9 @@ function addMeApiKeysSubcommand(me) {
1540
1637
  const format = getFormat(cmd);
1541
1638
  const response = await client.rawRequest("POST", "/me/api-keys", { body });
1542
1639
  const data = response.data;
1543
- if (opts.save) {
1544
- const globalOpts = resolveOptions(cmd);
1545
- const key = data.key;
1546
- if (!key) {
1547
- printError("Response missing key. API key was created, but it could not be saved.");
1548
- outputResponse(response, format);
1549
- process.exitCode = 1;
1550
- return;
1551
- }
1552
- const config = loadConfig(globalOpts.profile);
1553
- config.apiKey = key;
1554
- saveConfig(config, globalOpts.profile);
1555
- console.error("API key saved to config. X-Api-Key header will be sent automatically.");
1556
- } else {
1557
- printWarning("Save the API key now \u2014 it will not be shown again. Use --save to store it automatically.");
1558
- }
1640
+ const ok = showKeyResult(data, !!opts.save, cmd);
1559
1641
  outputResponse(response, format);
1560
- console.error("API key created.");
1642
+ if (ok) console.error("API key created.");
1561
1643
  })
1562
1644
  );
1563
1645
  addNotes(create, [
@@ -1586,6 +1668,35 @@ function addMeApiKeysSubcommand(me) {
1586
1668
  command: "geonic me api-keys create --name my-app --dpop-required"
1587
1669
  }
1588
1670
  ]);
1671
+ const refresh = apiKeys.command("refresh <keyId>").description("Refresh (rotate) an API key \u2014 generates a new key value").option("--save", "Save the new API key to config for automatic use").action(
1672
+ withErrorHandler(async (keyId, _opts, cmd) => {
1673
+ const opts = cmd.opts();
1674
+ const client = createClient(cmd);
1675
+ const format = getFormat(cmd);
1676
+ const response = await client.rawRequest(
1677
+ "POST",
1678
+ `/me/api-keys/${encodeURIComponent(String(keyId))}/refresh`
1679
+ );
1680
+ const data = response.data;
1681
+ const ok = showKeyResult(data, !!opts.save, cmd);
1682
+ outputResponse(response, format);
1683
+ if (ok) console.error("API key refreshed.");
1684
+ })
1685
+ );
1686
+ addNotes(refresh, [
1687
+ "Refreshing generates a new key value while keeping keyId, name, and policy settings.",
1688
+ "The previous key value is immediately invalidated."
1689
+ ]);
1690
+ addExamples(refresh, [
1691
+ {
1692
+ description: "Refresh an API key",
1693
+ command: "geonic me api-keys refresh <key-id>"
1694
+ },
1695
+ {
1696
+ description: "Refresh and save new key to config",
1697
+ command: "geonic me api-keys refresh <key-id> --save"
1698
+ }
1699
+ ]);
1589
1700
  const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--policy-id <policyId>", "Policy ID to attach (use 'null' to unbind)").option("--origins <origins>", "Allowed origins (comma-separated)").option("--rate-limit <n>", "Rate limit (requests per minute)").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP requirement").option("--active", "Activate the API key").option("--inactive", "Deactivate the API key").action(
1590
1701
  withErrorHandler(async (keyId, json, _opts, cmd) => {
1591
1702
  const opts = cmd.opts();
@@ -1657,7 +1768,7 @@ function addMeApiKeysSubcommand(me) {
1657
1768
  command: `geonic me api-keys update <key-id> '{"name":"new-name","rateLimit":{"perMinute":60}}'`
1658
1769
  }
1659
1770
  ]);
1660
- const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
1771
+ const del = apiKeys.command("delete <keyId>").description("Delete an API key \u2014 immediately revokes access for any client using it").action(
1661
1772
  withErrorHandler(async (keyId, _opts, cmd) => {
1662
1773
  const client = createClient(cmd);
1663
1774
  await client.rawRequest(
@@ -1669,8 +1780,12 @@ function addMeApiKeysSubcommand(me) {
1669
1780
  );
1670
1781
  addExamples(del, [
1671
1782
  {
1672
- description: "Delete an API key",
1783
+ description: "Delete an API key by ID",
1673
1784
  command: "geonic me api-keys delete <key-id>"
1785
+ },
1786
+ {
1787
+ description: "Revoke a leaked or unused key",
1788
+ command: "geonic me api-keys delete abc123-def456"
1674
1789
  }
1675
1790
  ]);
1676
1791
  }
@@ -1690,9 +1805,13 @@ function addMePoliciesSubcommand(me) {
1690
1805
  {
1691
1806
  description: "List your personal policies",
1692
1807
  command: "geonic me policies list"
1808
+ },
1809
+ {
1810
+ description: "List in table format for a quick overview",
1811
+ command: "geonic me policies list --format table"
1693
1812
  }
1694
1813
  ]);
1695
- const get = policies.command("get <policyId>").description("Get a personal policy by ID").action(
1814
+ const get = policies.command("get <policyId>").description("Get a personal policy by ID to inspect its XACML rules and target resources").action(
1696
1815
  withErrorHandler(async (policyId, _opts, cmd) => {
1697
1816
  const client = createClient(cmd);
1698
1817
  const format = getFormat(cmd);
@@ -1707,9 +1826,13 @@ function addMePoliciesSubcommand(me) {
1707
1826
  {
1708
1827
  description: "Get a personal policy by ID",
1709
1828
  command: "geonic me policies get <policy-id>"
1829
+ },
1830
+ {
1831
+ description: "Inspect policy rules and permitted actions",
1832
+ command: "geonic me policies get my-readonly --format table"
1710
1833
  }
1711
1834
  ]);
1712
- const create = policies.command("create [json]").description(
1835
+ const create = policies.command("create [json]").summary("Create a personal XACML policy").description(
1713
1836
  `Create a personal XACML policy
1714
1837
 
1715
1838
  Constraints (enforced server-side):
@@ -1800,7 +1923,7 @@ Example \u2014 GET-only policy for /v2/**:
1800
1923
  command: "geonic me policies update <policy-id> @patch.json"
1801
1924
  }
1802
1925
  ]);
1803
- const del = policies.command("delete <policyId>").description("Delete a personal policy").action(
1926
+ const del = policies.command("delete <policyId>").description("Delete a personal policy \u2014 any API key or OAuth client bound to it loses access granted by this policy").action(
1804
1927
  withErrorHandler(async (policyId, _opts, cmd) => {
1805
1928
  const client = createClient(cmd);
1806
1929
  await client.rawRequest(
@@ -1812,8 +1935,12 @@ Example \u2014 GET-only policy for /v2/**:
1812
1935
  );
1813
1936
  addExamples(del, [
1814
1937
  {
1815
- description: "Delete a personal policy",
1938
+ description: "Delete a personal policy by ID",
1816
1939
  command: "geonic me policies delete <policy-id>"
1940
+ },
1941
+ {
1942
+ description: "Remove a policy (unbind from API keys/OAuth clients first)",
1943
+ command: "geonic me policies delete my-readonly"
1817
1944
  }
1818
1945
  ]);
1819
1946
  }
@@ -1871,8 +1998,10 @@ function createLoginCommand() {
1871
1998
  const password = await promptPassword();
1872
1999
  const client = createClient(cmd);
1873
2000
  const body = { email, password };
1874
- if (loginOpts.tenantId) {
1875
- body.tenantId = loginOpts.tenantId;
2001
+ const requestTenantId = loginOpts.tenantId;
2002
+ const serviceFlag = globalOpts.service;
2003
+ if (requestTenantId) {
2004
+ body.tenantId = requestTenantId;
1876
2005
  }
1877
2006
  const response = await client.rawRequest("POST", "/auth/login", {
1878
2007
  body,
@@ -1886,12 +2015,28 @@ function createLoginCommand() {
1886
2015
  process.exit(1);
1887
2016
  }
1888
2017
  const availableTenants = data.availableTenants;
1889
- const currentTenantId = data.tenantId;
1890
- if (availableTenants && availableTenants.length > 1 && !loginOpts.tenantId) {
1891
- const selectedTenantId = await promptTenantSelection(availableTenants, currentTenantId);
1892
- if (selectedTenantId && selectedTenantId !== currentTenantId) {
2018
+ let finalTenantId = data.tenantId;
2019
+ if (availableTenants && availableTenants.length > 1 && !requestTenantId) {
2020
+ let resolvedTenantId;
2021
+ if (serviceFlag) {
2022
+ const match = availableTenants.find(
2023
+ (t) => t.name === serviceFlag || t.tenantId === serviceFlag
2024
+ );
2025
+ if (match) {
2026
+ resolvedTenantId = match.tenantId;
2027
+ } else {
2028
+ printError(
2029
+ `Tenant "${serviceFlag}" not found. Available: ${availableTenants.map((t) => t.name ?? t.tenantId).join(", ")}`
2030
+ );
2031
+ process.exit(1);
2032
+ }
2033
+ }
2034
+ if (!resolvedTenantId) {
2035
+ resolvedTenantId = await promptTenantSelection(availableTenants, finalTenantId);
2036
+ }
2037
+ if (resolvedTenantId && resolvedTenantId !== finalTenantId) {
1893
2038
  const reloginResponse = await client.rawRequest("POST", "/auth/login", {
1894
- body: { email, password, tenantId: selectedTenantId },
2039
+ body: { email, password, tenantId: resolvedTenantId },
1895
2040
  skipTenantHeader: true
1896
2041
  });
1897
2042
  const reloginData = reloginResponse.data;
@@ -1902,6 +2047,7 @@ function createLoginCommand() {
1902
2047
  }
1903
2048
  token = newToken;
1904
2049
  refreshToken = reloginData.refreshToken;
2050
+ finalTenantId = resolvedTenantId;
1905
2051
  }
1906
2052
  }
1907
2053
  const config = loadConfig(globalOpts.profile);
@@ -1911,13 +2057,30 @@ function createLoginCommand() {
1911
2057
  } else {
1912
2058
  delete config.refreshToken;
1913
2059
  }
2060
+ if (finalTenantId) {
2061
+ config.service = finalTenantId;
2062
+ config.tenantId = finalTenantId;
2063
+ } else {
2064
+ delete config.service;
2065
+ delete config.tenantId;
2066
+ }
2067
+ if (availableTenants && availableTenants.length > 0) {
2068
+ config.availableTenants = availableTenants.map((t) => ({
2069
+ tenantId: t.tenantId,
2070
+ ...t.name ? { name: t.name } : {},
2071
+ role: t.role
2072
+ }));
2073
+ } else {
2074
+ delete config.availableTenants;
2075
+ }
1914
2076
  saveConfig(config, globalOpts.profile);
1915
- printSuccess("Login successful. Token saved to config.");
2077
+ const tenantLabel = finalTenantId ? ` (tenant: ${availableTenants?.find((t) => t.tenantId === finalTenantId)?.name ?? finalTenantId})` : "";
2078
+ printSuccess(`Login successful${tenantLabel}. Token saved to config.`);
1916
2079
  })
1917
2080
  );
1918
2081
  }
1919
2082
  function createLogoutCommand() {
1920
- return new Command("logout").description("Clear saved authentication token").action(
2083
+ return new Command("logout").description("Clear saved authentication token and notify the server").action(
1921
2084
  withErrorHandler(async (...args) => {
1922
2085
  const cmd = args[args.length - 1];
1923
2086
  const globalOpts = resolveOptions(cmd);
@@ -1990,7 +2153,7 @@ async function fetchNonce(baseUrl, apiKey) {
1990
2153
  return await response.json();
1991
2154
  }
1992
2155
  function createNonceCommand() {
1993
- return new Command("nonce").description("Get a nonce and PoW challenge for API key authentication").option("--api-key <key>", "API key to get nonce for").action(
2156
+ return new Command("nonce").description("Request a nonce and Proof-of-Work challenge (step 1 of API key to JWT exchange)").option("--api-key <key>", "API key to get nonce for").action(
1994
2157
  withErrorHandler(async (...args) => {
1995
2158
  const cmd = args[args.length - 1];
1996
2159
  const nonceOpts = cmd.opts();
@@ -2032,7 +2195,7 @@ function solvePoW(challenge, difficulty) {
2032
2195
  throw new Error(`PoW could not be solved within ${MAX_POW_ITERATIONS} iterations`);
2033
2196
  }
2034
2197
  function createTokenExchangeCommand() {
2035
- return new Command("token-exchange").description("Exchange API key for a session JWT via nonce + PoW").option("--api-key <key>", "API key to exchange").option("--save", "Save the obtained token to profile config").action(
2198
+ return new Command("token-exchange").description("Exchange an API key for a session JWT (fetches nonce, solves PoW, returns token)").option("--api-key <key>", "API key to exchange").option("--save", "Save the obtained token to profile config").action(
2036
2199
  withErrorHandler(async (...args) => {
2037
2200
  const cmd = args[args.length - 1];
2038
2201
  const exchangeOpts = cmd.opts();
@@ -2096,8 +2259,12 @@ function registerAuthCommands(program2) {
2096
2259
  command: "geonic auth login --client-credentials --client-id MY_ID --client-secret MY_SECRET"
2097
2260
  },
2098
2261
  {
2099
- description: "Login to a specific tenant",
2262
+ description: "Login to a specific tenant by ID",
2100
2263
  command: "geonic auth login --tenant-id my-tenant"
2264
+ },
2265
+ {
2266
+ description: "Login to a tenant by name",
2267
+ command: "geonic auth login -s demo_smartcity"
2101
2268
  }
2102
2269
  ]);
2103
2270
  auth.addCommand(login);
@@ -2106,6 +2273,10 @@ function registerAuthCommands(program2) {
2106
2273
  {
2107
2274
  description: "Clear saved authentication token",
2108
2275
  command: "geonic auth logout"
2276
+ },
2277
+ {
2278
+ description: "Logout from a specific profile",
2279
+ command: "geonic auth logout --profile staging"
2109
2280
  }
2110
2281
  ]);
2111
2282
  auth.addCommand(logout);
@@ -2114,14 +2285,26 @@ function registerAuthCommands(program2) {
2114
2285
  {
2115
2286
  description: "Get a nonce for API key authentication",
2116
2287
  command: "geonic auth nonce --api-key gdb_abcdef..."
2288
+ },
2289
+ {
2290
+ description: "Use a pre-configured API key",
2291
+ command: "geonic auth nonce"
2117
2292
  }
2118
2293
  ]);
2119
2294
  auth.addCommand(nonce);
2120
2295
  const tokenExchange = createTokenExchangeCommand();
2121
2296
  addExamples(tokenExchange, [
2122
2297
  {
2123
- description: "Exchange API key for a JWT and save it",
2298
+ description: "Exchange API key for a JWT and save it to the active profile",
2124
2299
  command: "geonic auth token-exchange --api-key gdb_abcdef... --save"
2300
+ },
2301
+ {
2302
+ description: "Exchange and print the JWT without saving",
2303
+ command: "geonic auth token-exchange --api-key gdb_abcdef..."
2304
+ },
2305
+ {
2306
+ description: "Use a pre-configured API key and save the resulting token",
2307
+ command: "geonic auth token-exchange --save"
2125
2308
  }
2126
2309
  ]);
2127
2310
  auth.addCommand(tokenExchange);
@@ -2158,8 +2341,8 @@ function registerAuthCommands(program2) {
2158
2341
 
2159
2342
  // src/commands/profile.ts
2160
2343
  function registerProfileCommands(program2) {
2161
- const profile = program2.command("profile").description("Manage connection profiles");
2162
- const list = profile.command("list").description("List all profiles").action(() => {
2344
+ const profile = program2.command("profile").description("Manage connection profiles (each profile stores its own URL, token, and tenant)");
2345
+ const list = profile.command("list").description("List all profiles (active profile marked with *)").action(() => {
2163
2346
  const profiles = listProfiles();
2164
2347
  for (const p of profiles) {
2165
2348
  const marker = p.active ? " *" : "";
@@ -2172,22 +2355,57 @@ function registerProfileCommands(program2) {
2172
2355
  command: "geonic profile list"
2173
2356
  }
2174
2357
  ]);
2175
- const use = profile.command("use <name>").description("Switch active profile").action((name) => {
2358
+ const use = profile.command("use <name>").description("Switch active profile (auto-refreshes expired tokens)").action(async (name) => {
2176
2359
  try {
2177
2360
  setCurrentProfile(name);
2178
- printSuccess(`Switched to profile "${name}".`);
2179
2361
  } catch (err) {
2180
2362
  printError(err.message);
2181
2363
  process.exit(1);
2182
2364
  }
2365
+ const config = loadConfig(name);
2366
+ const tenantLabel = config.tenantId ? ` (tenant: ${config.availableTenants?.find((t) => t.tenantId === config.tenantId)?.name ?? config.tenantId})` : "";
2367
+ if (config.token && config.refreshToken && config.url) {
2368
+ const status = getTokenStatus(config.token);
2369
+ if (status.isExpired || status.isExpiringSoon) {
2370
+ try {
2371
+ const baseUrl = validateUrl(config.url);
2372
+ const url = new URL("/auth/refresh", baseUrl).toString();
2373
+ const response = await fetch(url, {
2374
+ method: "POST",
2375
+ headers: { "Content-Type": "application/json" },
2376
+ body: JSON.stringify({ refreshToken: config.refreshToken })
2377
+ });
2378
+ if (response.ok) {
2379
+ const data = await response.json();
2380
+ const newToken = data.accessToken ?? data.token;
2381
+ const newRefreshToken = data.refreshToken;
2382
+ if (newToken) {
2383
+ config.token = newToken;
2384
+ if (newRefreshToken) config.refreshToken = newRefreshToken;
2385
+ saveConfig(config, name);
2386
+ printSuccess(`Switched to profile "${name}"${tenantLabel}. Token refreshed.`);
2387
+ return;
2388
+ }
2389
+ }
2390
+ printWarning("Token refresh failed. You may need to re-login.");
2391
+ } catch {
2392
+ printWarning("Token refresh failed. You may need to re-login.");
2393
+ }
2394
+ }
2395
+ }
2396
+ printSuccess(`Switched to profile "${name}"${tenantLabel}.`);
2183
2397
  });
2184
2398
  addExamples(use, [
2185
2399
  {
2186
2400
  description: "Switch to staging profile",
2187
2401
  command: "geonic profile use staging"
2402
+ },
2403
+ {
2404
+ description: "Switch to production profile",
2405
+ command: "geonic profile use production"
2188
2406
  }
2189
2407
  ]);
2190
- const profileCreate = profile.command("create <name>").description("Create a new profile").action((name) => {
2408
+ const profileCreate = profile.command("create <name>").description("Create a new named profile for a separate environment or tenant").action((name) => {
2191
2409
  try {
2192
2410
  createProfile(name);
2193
2411
  printSuccess(`Profile "${name}" created.`);
@@ -2200,9 +2418,13 @@ function registerProfileCommands(program2) {
2200
2418
  {
2201
2419
  description: "Create a new profile for staging",
2202
2420
  command: "geonic profile create staging"
2421
+ },
2422
+ {
2423
+ description: "Create a profile for a different tenant",
2424
+ command: "geonic profile create tenant-b"
2203
2425
  }
2204
2426
  ]);
2205
- const del = profile.command("delete <name>").description("Delete a profile").action((name) => {
2427
+ const del = profile.command("delete <name>").description("Delete a profile and its stored configuration").action((name) => {
2206
2428
  try {
2207
2429
  deleteProfile(name);
2208
2430
  printSuccess(`Profile "${name}" deleted.`);
@@ -2213,11 +2435,11 @@ function registerProfileCommands(program2) {
2213
2435
  });
2214
2436
  addExamples(del, [
2215
2437
  {
2216
- description: "Delete a profile",
2438
+ description: "Delete the staging profile",
2217
2439
  command: "geonic profile delete staging"
2218
2440
  }
2219
2441
  ]);
2220
- const show = profile.command("show [name]").description("Show profile settings").action((name) => {
2442
+ const show = profile.command("show [name]").description("Show profile settings (URL, tenant, token status, etc.)").action((name) => {
2221
2443
  const profileName = name ?? getCurrentProfile();
2222
2444
  const config = loadConfig(profileName);
2223
2445
  const entries = Object.entries(config).filter(([, v]) => v !== void 0);
@@ -2228,6 +2450,13 @@ function registerProfileCommands(program2) {
2228
2450
  for (const [key, value] of entries) {
2229
2451
  if ((key === "token" || key === "refreshToken" || key === "apiKey") && typeof value === "string") {
2230
2452
  console.log(`${key}: ***`);
2453
+ } else if (key === "availableTenants" && Array.isArray(value)) {
2454
+ console.log(`${key}:`);
2455
+ for (const t of value) {
2456
+ const label = t.name ? `${t.name} (${t.tenantId})` : t.tenantId;
2457
+ const current = t.tenantId === config.tenantId ? " \u2190 current" : "";
2458
+ console.log(` - ${label} [${t.role}]${current}`);
2459
+ }
2231
2460
  } else {
2232
2461
  console.log(`${key}: ${value}`);
2233
2462
  }
@@ -2247,7 +2476,7 @@ function registerProfileCommands(program2) {
2247
2476
 
2248
2477
  // src/commands/attrs.ts
2249
2478
  function addAttrsSubcommands(attrs) {
2250
- const list = attrs.command("list").description("List all attributes of an entity").argument("<entityId>", "Entity ID").action(
2479
+ const list = attrs.command("list").description("List all attributes of an entity, returning each attribute's type, value, and metadata").argument("<entityId>", "Entity ID").action(
2251
2480
  withErrorHandler(
2252
2481
  async (entityId, _opts, cmd) => {
2253
2482
  const client = createClient(cmd);
@@ -2263,9 +2492,17 @@ function addAttrsSubcommands(attrs) {
2263
2492
  {
2264
2493
  description: "List all attributes of an entity",
2265
2494
  command: "geonic entities attrs list urn:ngsi-ld:Sensor:001"
2495
+ },
2496
+ {
2497
+ description: "List attributes in table format",
2498
+ command: "geonic entities attrs list urn:ngsi-ld:Sensor:001 --format table"
2499
+ },
2500
+ {
2501
+ description: "List attributes with keyValues output",
2502
+ command: "geonic entities attrs list urn:ngsi-ld:Building:store01 --format json"
2266
2503
  }
2267
2504
  ]);
2268
- const get = attrs.command("get").description("Get a specific attribute of an entity").argument("<entityId>", "Entity ID").argument("<attrName>", "Attribute name").action(
2505
+ const get = attrs.command("get").description("Get the value and metadata of a single attribute on an entity").argument("<entityId>", "Entity ID").argument("<attrName>", "Attribute name").action(
2269
2506
  withErrorHandler(
2270
2507
  async (entityId, attrName, _opts, cmd) => {
2271
2508
  const client = createClient(cmd);
@@ -2279,11 +2516,19 @@ function addAttrsSubcommands(attrs) {
2279
2516
  );
2280
2517
  addExamples(get, [
2281
2518
  {
2282
- description: "Get a specific attribute",
2519
+ description: "Get the temperature attribute of a sensor",
2283
2520
  command: "geonic entities attrs get urn:ngsi-ld:Sensor:001 temperature"
2521
+ },
2522
+ {
2523
+ description: "Get a Relationship attribute to see what it links to",
2524
+ command: "geonic entities attrs get urn:ngsi-ld:Building:store01 owner"
2525
+ },
2526
+ {
2527
+ description: "Get an attribute in table format",
2528
+ command: "geonic entities attrs get urn:ngsi-ld:Sensor:001 location --format table"
2284
2529
  }
2285
2530
  ]);
2286
- const add = attrs.command("add").description(
2531
+ const add = attrs.command("add").summary("Add attributes to an entity").description(
2287
2532
  'Add attributes to an entity\n\nJSON payload example:\n {"humidity": {"type": "Property", "value": 60}}'
2288
2533
  ).argument("<entityId>", "Entity ID").argument("[json]", "JSON payload (inline, @file, - for stdin, or omit for interactive/pipe)").action(
2289
2534
  withErrorHandler(
@@ -2312,7 +2557,7 @@ function addAttrsSubcommands(attrs) {
2312
2557
  command: "cat attrs.json | geonic entities attrs add urn:ngsi-ld:Sensor:001"
2313
2558
  }
2314
2559
  ]);
2315
- const attrUpdate = attrs.command("update").description(
2560
+ const attrUpdate = attrs.command("update").summary("Update a specific attribute of an entity").description(
2316
2561
  'Update a specific attribute of an entity\n\nJSON payload example:\n {"type": "Property", "value": 25}'
2317
2562
  ).argument("<entityId>", "Entity ID").argument("<attrName>", "Attribute name").argument("[json]", "JSON payload (inline, @file, - for stdin, or omit for interactive/pipe)").action(
2318
2563
  withErrorHandler(
@@ -2337,7 +2582,7 @@ function addAttrsSubcommands(attrs) {
2337
2582
  command: "geonic entities attrs update urn:ngsi-ld:Sensor:001 temperature @attr.json"
2338
2583
  }
2339
2584
  ]);
2340
- const del = attrs.command("delete").description("Delete a specific attribute from an entity").argument("<entityId>", "Entity ID").argument("<attrName>", "Attribute name").action(
2585
+ const del = attrs.command("delete").description("Remove an attribute from an entity permanently").argument("<entityId>", "Entity ID").argument("<attrName>", "Attribute name").action(
2341
2586
  withErrorHandler(
2342
2587
  async (entityId, attrName, _opts, cmd) => {
2343
2588
  const client = createClient(cmd);
@@ -2350,8 +2595,12 @@ function addAttrsSubcommands(attrs) {
2350
2595
  );
2351
2596
  addExamples(del, [
2352
2597
  {
2353
- description: "Delete a specific attribute",
2598
+ description: "Remove the temperature attribute from a sensor",
2354
2599
  command: "geonic entities attrs delete urn:ngsi-ld:Sensor:001 temperature"
2600
+ },
2601
+ {
2602
+ description: "Remove a deprecated attribute from a building entity",
2603
+ command: "geonic entities attrs delete urn:ngsi-ld:Building:store01 legacyCode"
2355
2604
  }
2356
2605
  ]);
2357
2606
  }
@@ -2477,7 +2726,7 @@ function registerEntitiesCommand(program2) {
2477
2726
  command: "geonic entities get urn:ngsi-ld:Sensor:001 --key-values"
2478
2727
  }
2479
2728
  ]);
2480
- const create = entities.command("create").description(
2729
+ const create = entities.command("create").summary("Create a new entity").description(
2481
2730
  'Create a new entity\n\nJSON payload example:\n {\n "id": "urn:ngsi-ld:Sensor:001",\n "type": "Sensor",\n "temperature": {"type": "Property", "value": 25}\n }'
2482
2731
  ).argument("[json]", "JSON payload (inline, @file, - for stdin, or omit for interactive/pipe)").action(
2483
2732
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2509,7 +2758,7 @@ function registerEntitiesCommand(program2) {
2509
2758
  command: "geonic entities create"
2510
2759
  }
2511
2760
  ]);
2512
- const update = entities.command("update").description(
2761
+ const update = entities.command("update").summary("Update attributes of an entity (PATCH)").description(
2513
2762
  'Update attributes of an entity (PATCH)\n\nJSON payload: only specified attributes are modified.\n e.g. {"temperature": {"type": "Property", "value": 30}}'
2514
2763
  ).argument("<id>", "Entity ID").argument("[json]", "JSON payload (inline, @file, - for stdin, or omit for interactive/pipe)").action(
2515
2764
  withErrorHandler(
@@ -2538,7 +2787,7 @@ function registerEntitiesCommand(program2) {
2538
2787
  command: "cat attrs.json | geonic entities update urn:ngsi-ld:Sensor:001"
2539
2788
  }
2540
2789
  ]);
2541
- const replace = entities.command("replace").description(
2790
+ const replace = entities.command("replace").summary("Replace all attributes of an entity (PUT)").description(
2542
2791
  'Replace all attributes of an entity (PUT)\n\nJSON payload: all existing attributes are replaced.\n e.g. {"temperature": {"type": "Property", "value": 20}}'
2543
2792
  ).argument("<id>", "Entity ID").argument("[json]", "JSON payload (inline, @file, - for stdin, or omit for interactive/pipe)").action(
2544
2793
  withErrorHandler(
@@ -2585,7 +2834,7 @@ function registerEntitiesCommand(program2) {
2585
2834
  command: "cat entities.json | geonic entities upsert"
2586
2835
  }
2587
2836
  ]);
2588
- const del = entities.command("delete").description("Delete an entity by ID").argument("<id>", "Entity ID").action(
2837
+ const del = entities.command("delete").description("Permanently delete an entity and all its attributes").argument("<id>", "Entity ID").action(
2589
2838
  withErrorHandler(async (id, _opts, cmd) => {
2590
2839
  const client = createClient(cmd);
2591
2840
  await client.delete(`/entities/${encodeURIComponent(id)}`);
@@ -2596,6 +2845,10 @@ function registerEntitiesCommand(program2) {
2596
2845
  {
2597
2846
  description: "Delete an entity by ID",
2598
2847
  command: "geonic entities delete urn:ngsi-ld:Sensor:001"
2848
+ },
2849
+ {
2850
+ description: "Delete with explicit service tenant",
2851
+ command: "geonic entities delete urn:ngsi-ld:Sensor:001 --service my-tenant"
2599
2852
  }
2600
2853
  ]);
2601
2854
  registerAttrsSubcommand(entities);
@@ -2604,7 +2857,7 @@ function registerEntitiesCommand(program2) {
2604
2857
  // src/commands/batch.ts
2605
2858
  function registerBatchCommand(program2) {
2606
2859
  const batch = program2.command("entityOperations").alias("batch").description("Perform batch operations on entities");
2607
- const create = batch.command("create [json]").description(
2860
+ const create = batch.command("create [json]").summary("Batch create entities").description(
2608
2861
  'Batch create entities\n\nJSON payload: an array of NGSI-LD entities.\n e.g. [{"id": "urn:ngsi-ld:Sensor:001", "type": "Sensor"}, ...]'
2609
2862
  ).action(
2610
2863
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2629,7 +2882,7 @@ function registerBatchCommand(program2) {
2629
2882
  command: "cat entities.json | geonic batch create"
2630
2883
  }
2631
2884
  ]);
2632
- const upsert = batch.command("upsert [json]").description(
2885
+ const upsert = batch.command("upsert [json]").summary("Batch upsert entities").description(
2633
2886
  "Batch upsert entities\n\nJSON payload: an array of NGSI-LD entities.\nCreates entities that don't exist, updates those that do."
2634
2887
  ).action(
2635
2888
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2654,7 +2907,7 @@ function registerBatchCommand(program2) {
2654
2907
  command: "cat entities.json | geonic batch upsert"
2655
2908
  }
2656
2909
  ]);
2657
- const update = batch.command("update [json]").description(
2910
+ const update = batch.command("update [json]").summary("Batch update entity attributes").description(
2658
2911
  "Batch update entity attributes\n\nJSON payload: an array of NGSI-LD entities with attributes to update.\nEach entity must include id and type; only specified attributes are modified."
2659
2912
  ).action(
2660
2913
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2675,7 +2928,7 @@ function registerBatchCommand(program2) {
2675
2928
  command: "cat updates.json | geonic batch update"
2676
2929
  }
2677
2930
  ]);
2678
- const del = batch.command("delete [json]").description(
2931
+ const del = batch.command("delete [json]").summary("Batch delete entities by ID").description(
2679
2932
  'Batch delete entities by ID\n\nJSON payload: an array of entity ID strings.\n e.g. ["urn:ngsi-ld:Sensor:001","urn:ngsi-ld:Sensor:002"]'
2680
2933
  ).action(
2681
2934
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2700,7 +2953,7 @@ function registerBatchCommand(program2) {
2700
2953
  command: "cat entity-ids.json | geonic batch delete"
2701
2954
  }
2702
2955
  ]);
2703
- const query = batch.command("query [json]").description(
2956
+ const query = batch.command("query [json]").summary("Query entities by posting a query payload").description(
2704
2957
  'Query entities by posting a query payload\n\nJSON payload example:\n {\n "entities": [{"type": "Sensor"}],\n "attrs": ["temperature"],\n "q": "temperature>30"\n }'
2705
2958
  ).action(
2706
2959
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2725,7 +2978,7 @@ function registerBatchCommand(program2) {
2725
2978
  command: "cat query.json | geonic batch query"
2726
2979
  }
2727
2980
  ]);
2728
- const merge = batch.command("merge [json]").description(
2981
+ const merge = batch.command("merge [json]").summary("Batch merge-patch entities").description(
2729
2982
  "Batch merge-patch entities\n\nJSON payload: an array of NGSI-LD entities.\nEach entity must include id and type; attributes are merge-patched."
2730
2983
  ).action(
2731
2984
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2778,7 +3031,7 @@ function registerSubscriptionsCommand(program2) {
2778
3031
  command: "geonic subscriptions list --count"
2779
3032
  }
2780
3033
  ]);
2781
- const get = subscriptions.command("get <id>").description("Get a subscription by ID").action(
3034
+ const get = subscriptions.command("get <id>").description("Get a subscription by ID to inspect its notification config, watched attributes, and status").action(
2782
3035
  withErrorHandler(async (id, _opts, cmd) => {
2783
3036
  const client = createClient(cmd);
2784
3037
  const format = getFormat(cmd);
@@ -2790,11 +3043,15 @@ function registerSubscriptionsCommand(program2) {
2790
3043
  );
2791
3044
  addExamples(get, [
2792
3045
  {
2793
- description: "Get subscription by ID",
3046
+ description: "Get subscription details by ID",
2794
3047
  command: "geonic subscriptions get urn:ngsi-ld:Subscription:001"
3048
+ },
3049
+ {
3050
+ description: "Inspect notification endpoint and status in table format",
3051
+ command: "geonic subscriptions get urn:ngsi-ld:Subscription:001 --format table"
2795
3052
  }
2796
3053
  ]);
2797
- const create = subscriptions.command("create [json]").description(
3054
+ const create = subscriptions.command("create [json]").summary("Create a subscription").description(
2798
3055
  'Create a subscription\n\nJSON payload example:\n {\n "type": "Subscription",\n "entities": [{"type": "Sensor"}],\n "watchedAttributes": ["temperature"],\n "notification": {"endpoint": {"uri": "http://localhost:3000/notify"}}\n }'
2799
3056
  ).action(
2800
3057
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2824,7 +3081,7 @@ function registerSubscriptionsCommand(program2) {
2824
3081
  command: "geonic subscriptions create"
2825
3082
  }
2826
3083
  ]);
2827
- const update = subscriptions.command("update <id> [json]").description(
3084
+ const update = subscriptions.command("update <id> [json]").summary("Update a subscription").description(
2828
3085
  'Update a subscription\n\nJSON payload: only specified fields are updated.\n e.g. {"description": "Updated subscription"}'
2829
3086
  ).action(
2830
3087
  withErrorHandler(
@@ -2855,7 +3112,7 @@ function registerSubscriptionsCommand(program2) {
2855
3112
  command: "cat sub.json | geonic subscriptions update urn:ngsi-ld:Subscription:001"
2856
3113
  }
2857
3114
  ]);
2858
- const del = subscriptions.command("delete <id>").description("Delete a subscription").action(
3115
+ const del = subscriptions.command("delete <id>").description("Delete a subscription and stop its notifications").action(
2859
3116
  withErrorHandler(async (id, _opts, cmd) => {
2860
3117
  const client = createClient(cmd);
2861
3118
  await client.delete(
@@ -2866,8 +3123,12 @@ function registerSubscriptionsCommand(program2) {
2866
3123
  );
2867
3124
  addExamples(del, [
2868
3125
  {
2869
- description: "Delete a subscription",
3126
+ description: "Delete a subscription by ID",
2870
3127
  command: "geonic subscriptions delete urn:ngsi-ld:Subscription:001"
3128
+ },
3129
+ {
3130
+ description: "Stop notifications by removing the subscription (using alias)",
3131
+ command: "geonic sub delete urn:ngsi-ld:Subscription:001"
2871
3132
  }
2872
3133
  ]);
2873
3134
  }
@@ -2898,7 +3159,7 @@ function registerRegistrationsCommand(program2) {
2898
3159
  command: "geonic registrations list --limit 10"
2899
3160
  }
2900
3161
  ]);
2901
- const get = registrations.command("get <id>").description("Get a registration by ID").action(
3162
+ const get = registrations.command("get <id>").description("Get a registration by ID to inspect its federation endpoint and entity routing").action(
2902
3163
  withErrorHandler(async (id, _opts, cmd) => {
2903
3164
  const client = createClient(cmd);
2904
3165
  const format = getFormat(cmd);
@@ -2910,11 +3171,15 @@ function registerRegistrationsCommand(program2) {
2910
3171
  );
2911
3172
  addExamples(get, [
2912
3173
  {
2913
- description: "Get registration by ID",
3174
+ description: "Get registration details by ID",
2914
3175
  command: "geonic registrations get urn:ngsi-ld:ContextSourceRegistration:001"
3176
+ },
3177
+ {
3178
+ description: "Inspect federation config in table format",
3179
+ command: "geonic registrations get urn:ngsi-ld:ContextSourceRegistration:001 --format table"
2915
3180
  }
2916
3181
  ]);
2917
- const create = registrations.command("create [json]").description(
3182
+ const create = registrations.command("create [json]").summary("Create a registration").description(
2918
3183
  'Create a registration\n\nJSON payload example:\n {\n "type": "ContextSourceRegistration",\n "information": [{"entities": [{"type": "Room"}]}],\n "endpoint": "http://localhost:4000/source"\n }'
2919
3184
  ).action(
2920
3185
  withErrorHandler(async (json, _opts, cmd) => {
@@ -2969,7 +3234,7 @@ function registerRegistrationsCommand(program2) {
2969
3234
  command: "cat registration.json | geonic registrations update urn:ngsi-ld:ContextSourceRegistration:001"
2970
3235
  }
2971
3236
  ]);
2972
- const del = registrations.command("delete <id>").description("Delete a registration").action(
3237
+ const del = registrations.command("delete <id>").description("Delete a registration and remove its forwarding rule").action(
2973
3238
  withErrorHandler(async (id, _opts, cmd) => {
2974
3239
  const client = createClient(cmd);
2975
3240
  await client.delete(
@@ -2980,16 +3245,20 @@ function registerRegistrationsCommand(program2) {
2980
3245
  );
2981
3246
  addExamples(del, [
2982
3247
  {
2983
- description: "Delete a registration",
3248
+ description: "Delete a registration by ID",
2984
3249
  command: "geonic registrations delete urn:ngsi-ld:ContextSourceRegistration:001"
3250
+ },
3251
+ {
3252
+ description: "Remove forwarding rule (using alias)",
3253
+ command: "geonic reg delete urn:ngsi-ld:ContextSourceRegistration:001"
2985
3254
  }
2986
3255
  ]);
2987
3256
  }
2988
3257
 
2989
3258
  // src/commands/types.ts
2990
3259
  function registerTypesCommand(program2) {
2991
- const types = program2.command("types").description("Browse entity types");
2992
- const list = types.command("list").description("List available entity types").action(
3260
+ const types = program2.command("types").description("Discover what entity types exist in the broker and inspect their structure");
3261
+ const list = types.command("list").description("List all entity types currently stored in the broker").action(
2993
3262
  withErrorHandler(async (_opts, cmd) => {
2994
3263
  const client = createClient(cmd);
2995
3264
  const format = getFormat(cmd);
@@ -2999,11 +3268,15 @@ function registerTypesCommand(program2) {
2999
3268
  );
3000
3269
  addExamples(list, [
3001
3270
  {
3002
- description: "List all entity types",
3271
+ description: "List all entity types in the broker",
3003
3272
  command: "geonic types list"
3273
+ },
3274
+ {
3275
+ description: "List entity types in table format for a quick overview",
3276
+ command: "geonic types list --format table"
3004
3277
  }
3005
3278
  ]);
3006
- const get = types.command("get <typeName>").description("Get details for an entity type").action(
3279
+ const get = types.command("get <typeName>").description("Show attribute names and types for a given entity type").action(
3007
3280
  withErrorHandler(
3008
3281
  async (typeName, _opts, cmd) => {
3009
3282
  const client = createClient(cmd);
@@ -3017,8 +3290,16 @@ function registerTypesCommand(program2) {
3017
3290
  );
3018
3291
  addExamples(get, [
3019
3292
  {
3020
- description: "Get details for a specific type",
3293
+ description: "Inspect the Sensor type to see its attributes",
3021
3294
  command: "geonic types get Sensor"
3295
+ },
3296
+ {
3297
+ description: "Inspect a Building type in table format",
3298
+ command: "geonic types get Building --format table"
3299
+ },
3300
+ {
3301
+ description: "Inspect a fully-qualified NGSI-LD type",
3302
+ command: "geonic types get https://uri.fiware.org/ns/data-models#AirQualityObserved"
3022
3303
  }
3023
3304
  ]);
3024
3305
  }
@@ -3144,7 +3425,7 @@ function registerTemporalCommand(program2) {
3144
3425
  command: "geonic temporal entities get urn:ngsi-ld:Sensor:001 --last-n 10"
3145
3426
  }
3146
3427
  ]);
3147
- const create = entities.command("create [json]").description(
3428
+ const create = entities.command("create [json]").summary("Create a temporal entity").description(
3148
3429
  "Create a temporal entity\n\nJSON payload: an NGSI-LD entity with temporal attribute instances.\nEach attribute value is an array of {value, observedAt} objects."
3149
3430
  ).action(createCreateAction());
3150
3431
  addExamples(create, [
@@ -3161,11 +3442,15 @@ function registerTemporalCommand(program2) {
3161
3442
  command: "geonic temporal entities create"
3162
3443
  }
3163
3444
  ]);
3164
- const del = entities.command("delete <id>").description("Delete a temporal entity by ID").action(createDeleteAction());
3445
+ const del = entities.command("delete <id>").description("Delete a temporal entity and all its historical attribute data").action(createDeleteAction());
3165
3446
  addExamples(del, [
3166
3447
  {
3167
3448
  description: "Delete temporal data for an entity",
3168
3449
  command: "geonic temporal entities delete urn:ngsi-ld:Sensor:001"
3450
+ },
3451
+ {
3452
+ description: "Remove all historical records for a specific entity",
3453
+ command: "geonic temporal entities delete urn:ngsi-ld:WeatherStation:tokyo-01"
3169
3454
  }
3170
3455
  ]);
3171
3456
  const opsQuery = addQueryOptions(
@@ -3201,8 +3486,8 @@ function registerTemporalCommand(program2) {
3201
3486
 
3202
3487
  // src/commands/snapshots.ts
3203
3488
  function registerSnapshotsCommand(program2) {
3204
- const snapshots = program2.command("snapshots").description("Manage snapshots");
3205
- const list = snapshots.command("list").description("List snapshots").option("--limit <n>", "Maximum number of snapshots to return", parseInt).option("--offset <n>", "Skip first N snapshots", parseInt).action(
3489
+ const snapshots = program2.command("snapshots").description("Manage point-in-time snapshots of entity data for backup and cloning");
3490
+ const list = snapshots.command("list").description("List all available snapshots with their IDs, timestamps, and status").option("--limit <n>", "Maximum number of snapshots to return", parseInt).option("--offset <n>", "Skip first N snapshots", parseInt).action(
3206
3491
  withErrorHandler(async (_opts, cmd) => {
3207
3492
  const client = createClient(cmd);
3208
3493
  const format = getFormat(cmd);
@@ -3220,11 +3505,19 @@ function registerSnapshotsCommand(program2) {
3220
3505
  command: "geonic snapshots list"
3221
3506
  },
3222
3507
  {
3223
- description: "List with a limit",
3508
+ description: "List the 10 most recent snapshots",
3224
3509
  command: "geonic snapshots list --limit 10"
3510
+ },
3511
+ {
3512
+ description: "Paginate through snapshots",
3513
+ command: "geonic snapshots list --limit 5 --offset 10"
3514
+ },
3515
+ {
3516
+ description: "List snapshots in table format",
3517
+ command: "geonic snapshots list --format table"
3225
3518
  }
3226
3519
  ]);
3227
- const get = snapshots.command("get <id>").description("Get a snapshot by ID").action(
3520
+ const get = snapshots.command("get <id>").description("Retrieve details of a specific snapshot including its status and metadata").action(
3228
3521
  withErrorHandler(async (id, _opts, cmd) => {
3229
3522
  const client = createClient(cmd);
3230
3523
  const format = getFormat(cmd);
@@ -3236,11 +3529,15 @@ function registerSnapshotsCommand(program2) {
3236
3529
  );
3237
3530
  addExamples(get, [
3238
3531
  {
3239
- description: "Get a specific snapshot",
3240
- command: "geonic snapshots get <snapshot-id>"
3532
+ description: "Get details of a snapshot by its ID",
3533
+ command: "geonic snapshots get abc123"
3534
+ },
3535
+ {
3536
+ description: "Get snapshot details in table format",
3537
+ command: "geonic snapshots get abc123 --format table"
3241
3538
  }
3242
3539
  ]);
3243
- const create = snapshots.command("create").description("Create a new snapshot").action(
3540
+ const create = snapshots.command("create").description("Create a point-in-time snapshot of all current entity data").action(
3244
3541
  withErrorHandler(async (_opts, cmd) => {
3245
3542
  const client = createClient(cmd);
3246
3543
  await client.post("/snapshots");
@@ -3249,11 +3546,15 @@ function registerSnapshotsCommand(program2) {
3249
3546
  );
3250
3547
  addExamples(create, [
3251
3548
  {
3252
- description: "Create a new snapshot",
3549
+ description: "Create a snapshot of the current entity data",
3253
3550
  command: "geonic snapshots create"
3551
+ },
3552
+ {
3553
+ description: "Create a snapshot before performing a bulk update",
3554
+ command: "geonic snapshots create && geonic batch upsert @bulk-update.json"
3254
3555
  }
3255
3556
  ]);
3256
- const del = snapshots.command("delete <id>").description("Delete a snapshot by ID").action(
3557
+ const del = snapshots.command("delete <id>").description("Permanently delete a snapshot to free storage").action(
3257
3558
  withErrorHandler(async (id, _opts, cmd) => {
3258
3559
  const client = createClient(cmd);
3259
3560
  await client.delete(
@@ -3264,11 +3565,15 @@ function registerSnapshotsCommand(program2) {
3264
3565
  );
3265
3566
  addExamples(del, [
3266
3567
  {
3267
- description: "Delete a snapshot",
3268
- command: "geonic snapshots delete <snapshot-id>"
3568
+ description: "Delete a snapshot by its ID",
3569
+ command: "geonic snapshots delete abc123"
3570
+ },
3571
+ {
3572
+ description: "Delete an old snapshot to reclaim storage",
3573
+ command: "geonic snapshots delete old-backup-id"
3269
3574
  }
3270
3575
  ]);
3271
- const clone = snapshots.command("clone <id>").description("Clone a snapshot by ID").action(
3576
+ const clone = snapshots.command("clone <id>").description("Clone a snapshot to create a duplicate for testing or migration").action(
3272
3577
  withErrorHandler(async (id, _opts, cmd) => {
3273
3578
  const client = createClient(cmd);
3274
3579
  const format = getFormat(cmd);
@@ -3284,8 +3589,12 @@ function registerSnapshotsCommand(program2) {
3284
3589
  );
3285
3590
  addExamples(clone, [
3286
3591
  {
3287
- description: "Clone a snapshot",
3288
- command: "geonic snapshots clone <snapshot-id>"
3592
+ description: "Clone a snapshot for use in a test environment",
3593
+ command: "geonic snapshots clone abc123"
3594
+ },
3595
+ {
3596
+ description: "Clone a production snapshot to a staging profile",
3597
+ command: "geonic snapshots clone abc123 --profile staging"
3289
3598
  }
3290
3599
  ]);
3291
3600
  }
@@ -3293,7 +3602,7 @@ function registerSnapshotsCommand(program2) {
3293
3602
  // src/commands/admin/tenants.ts
3294
3603
  function registerTenantsCommand(parent) {
3295
3604
  const tenants = parent.command("tenants").description("Manage tenants");
3296
- const list = tenants.command("list").description("List all tenants").action(
3605
+ const list = tenants.command("list").description("List all tenants in the system, including their status and configuration").action(
3297
3606
  withErrorHandler(async (_opts, cmd) => {
3298
3607
  const client = createClient(cmd);
3299
3608
  const format = getFormat(cmd);
@@ -3305,9 +3614,13 @@ function registerTenantsCommand(parent) {
3305
3614
  {
3306
3615
  description: "List all tenants",
3307
3616
  command: "geonic admin tenants list"
3617
+ },
3618
+ {
3619
+ description: "List tenants in table format",
3620
+ command: "geonic admin tenants list --format table"
3308
3621
  }
3309
3622
  ]);
3310
- const get = tenants.command("get <id>").description("Get a tenant by ID").action(
3623
+ const get = tenants.command("get <id>").description("Get a tenant's details \u2014 name, description, status, and creation date").action(
3311
3624
  withErrorHandler(async (id, _opts, cmd) => {
3312
3625
  const client = createClient(cmd);
3313
3626
  const format = getFormat(cmd);
@@ -3320,11 +3633,15 @@ function registerTenantsCommand(parent) {
3320
3633
  );
3321
3634
  addExamples(get, [
3322
3635
  {
3323
- description: "Get a tenant by ID",
3636
+ description: "Get tenant details by ID",
3324
3637
  command: "geonic admin tenants get <tenant-id>"
3638
+ },
3639
+ {
3640
+ description: "Get tenant details in table format",
3641
+ command: "geonic admin tenants get <tenant-id> --format table"
3325
3642
  }
3326
3643
  ]);
3327
- const create = tenants.command("create [json]").description(
3644
+ const create = tenants.command("create [json]").summary("Create a new tenant").description(
3328
3645
  'Create a new tenant\n\nJSON payload example:\n {\n "name": "production",\n "description": "Production environment tenant"\n }'
3329
3646
  ).action(
3330
3647
  withErrorHandler(async (json, _opts, cmd) => {
@@ -3360,7 +3677,7 @@ function registerTenantsCommand(parent) {
3360
3677
  command: "geonic admin tenants create"
3361
3678
  }
3362
3679
  ]);
3363
- const update = tenants.command("update <id> [json]").description(
3680
+ const update = tenants.command("update <id> [json]").summary("Update a tenant").description(
3364
3681
  'Update a tenant\n\nJSON payload: only specified fields are updated.\n e.g. {"name": "new-name", "description": "Updated description"}'
3365
3682
  ).action(
3366
3683
  withErrorHandler(
@@ -3400,7 +3717,7 @@ function registerTenantsCommand(parent) {
3400
3717
  command: "geonic admin tenants update <tenant-id>"
3401
3718
  }
3402
3719
  ]);
3403
- const del = tenants.command("delete <id>").description("Delete a tenant").action(
3720
+ const del = tenants.command("delete <id>").description("Permanently delete a tenant and all its associated data. This action cannot be undone").action(
3404
3721
  withErrorHandler(async (id, _opts, cmd) => {
3405
3722
  const client = createClient(cmd);
3406
3723
  await client.rawRequest(
@@ -3412,11 +3729,15 @@ function registerTenantsCommand(parent) {
3412
3729
  );
3413
3730
  addExamples(del, [
3414
3731
  {
3415
- description: "Delete a tenant",
3732
+ description: "Delete a tenant by ID",
3416
3733
  command: "geonic admin tenants delete <tenant-id>"
3734
+ },
3735
+ {
3736
+ description: "Delete with verbose output to confirm",
3737
+ command: "geonic admin tenants delete <tenant-id> --verbose"
3417
3738
  }
3418
3739
  ]);
3419
- const activate = tenants.command("activate <id>").description("Activate a tenant").action(
3740
+ const activate = tenants.command("activate <id>").description("Activate a tenant, restoring API access for all its users").action(
3420
3741
  withErrorHandler(async (id, _opts, cmd) => {
3421
3742
  const client = createClient(cmd);
3422
3743
  await client.rawRequest(
@@ -3428,11 +3749,11 @@ function registerTenantsCommand(parent) {
3428
3749
  );
3429
3750
  addExamples(activate, [
3430
3751
  {
3431
- description: "Activate a tenant",
3752
+ description: "Activate a deactivated tenant",
3432
3753
  command: "geonic admin tenants activate <tenant-id>"
3433
3754
  }
3434
3755
  ]);
3435
- const deactivate = tenants.command("deactivate <id>").description("Deactivate a tenant").action(
3756
+ const deactivate = tenants.command("deactivate <id>").description("Deactivate a tenant, blocking API access for all its users until reactivated").action(
3436
3757
  withErrorHandler(async (id, _opts, cmd) => {
3437
3758
  const client = createClient(cmd);
3438
3759
  await client.rawRequest(
@@ -3444,7 +3765,7 @@ function registerTenantsCommand(parent) {
3444
3765
  );
3445
3766
  addExamples(deactivate, [
3446
3767
  {
3447
- description: "Deactivate a tenant",
3768
+ description: "Deactivate a tenant to temporarily suspend access",
3448
3769
  command: "geonic admin tenants deactivate <tenant-id>"
3449
3770
  }
3450
3771
  ]);
@@ -3453,7 +3774,7 @@ function registerTenantsCommand(parent) {
3453
3774
  // src/commands/admin/users.ts
3454
3775
  function registerUsersCommand(parent) {
3455
3776
  const users = parent.command("users").description("Manage users");
3456
- const list = users.command("list").description("List all users").action(
3777
+ const list = users.command("list").description("List all users across tenants, showing email, role, and status").action(
3457
3778
  withErrorHandler(async (_opts, cmd) => {
3458
3779
  const client = createClient(cmd);
3459
3780
  const format = getFormat(cmd);
@@ -3465,9 +3786,17 @@ function registerUsersCommand(parent) {
3465
3786
  {
3466
3787
  description: "List all users",
3467
3788
  command: "geonic admin users list"
3789
+ },
3790
+ {
3791
+ description: "List users in table format",
3792
+ command: "geonic admin users list --format table"
3793
+ },
3794
+ {
3795
+ description: "List users for a specific tenant",
3796
+ command: "geonic admin users list --service <tenant-id>"
3468
3797
  }
3469
3798
  ]);
3470
- const get = users.command("get <id>").description("Get a user by ID").action(
3799
+ const get = users.command("get <id>").description("Get a user's details \u2014 email, role, tenant, status, and login history").action(
3471
3800
  withErrorHandler(async (id, _opts, cmd) => {
3472
3801
  const client = createClient(cmd);
3473
3802
  const format = getFormat(cmd);
@@ -3480,11 +3809,15 @@ function registerUsersCommand(parent) {
3480
3809
  );
3481
3810
  addExamples(get, [
3482
3811
  {
3483
- description: "Get a user by ID",
3812
+ description: "Inspect a user's account details",
3484
3813
  command: "geonic admin users get <user-id>"
3814
+ },
3815
+ {
3816
+ description: "Get user details in table format",
3817
+ command: "geonic admin users get <user-id> --format table"
3485
3818
  }
3486
3819
  ]);
3487
- const create = users.command("create [json]").description(
3820
+ const create = users.command("create [json]").summary("Create a new user").description(
3488
3821
  'Create a new user\n\nJSON payload example:\n {\n "email": "user@example.com",\n "password": "SecurePassword123!",\n "role": "tenant_admin",\n "tenantId": "<tenant-id>"\n }\n\nRoles: super_admin, tenant_admin, user\ntenantId is required for tenant_admin and user roles.'
3489
3822
  ).action(
3490
3823
  withErrorHandler(async (json, _opts, cmd) => {
@@ -3516,7 +3849,7 @@ function registerUsersCommand(parent) {
3516
3849
  command: "cat user.json | geonic admin users create"
3517
3850
  }
3518
3851
  ]);
3519
- const update = users.command("update <id> [json]").description(
3852
+ const update = users.command("update <id> [json]").summary("Update a user").description(
3520
3853
  'Update a user\n\nJSON payload: only specified fields are updated.\n e.g. {"role": "admin"}'
3521
3854
  ).action(
3522
3855
  withErrorHandler(
@@ -3548,7 +3881,7 @@ function registerUsersCommand(parent) {
3548
3881
  command: "cat user.json | geonic admin users update <user-id>"
3549
3882
  }
3550
3883
  ]);
3551
- const del = users.command("delete <id>").description("Delete a user").action(
3884
+ const del = users.command("delete <id>").description("Permanently delete a user account. This revokes all access and cannot be undone").action(
3552
3885
  withErrorHandler(async (id, _opts, cmd) => {
3553
3886
  const client = createClient(cmd);
3554
3887
  await client.rawRequest(
@@ -3560,11 +3893,15 @@ function registerUsersCommand(parent) {
3560
3893
  );
3561
3894
  addExamples(del, [
3562
3895
  {
3563
- description: "Delete a user",
3896
+ description: "Delete a user by ID",
3564
3897
  command: "geonic admin users delete <user-id>"
3898
+ },
3899
+ {
3900
+ description: "Delete with verbose output",
3901
+ command: "geonic admin users delete <user-id> --verbose"
3565
3902
  }
3566
3903
  ]);
3567
- const activate = users.command("activate <id>").description("Activate a user").action(
3904
+ const activate = users.command("activate <id>").description("Activate a user account, allowing them to log in and access the API").action(
3568
3905
  withErrorHandler(async (id, _opts, cmd) => {
3569
3906
  const client = createClient(cmd);
3570
3907
  await client.rawRequest(
@@ -3576,11 +3913,11 @@ function registerUsersCommand(parent) {
3576
3913
  );
3577
3914
  addExamples(activate, [
3578
3915
  {
3579
- description: "Activate a user",
3916
+ description: "Activate a deactivated user",
3580
3917
  command: "geonic admin users activate <user-id>"
3581
3918
  }
3582
3919
  ]);
3583
- const deactivate = users.command("deactivate <id>").description("Deactivate a user").action(
3920
+ const deactivate = users.command("deactivate <id>").description("Deactivate a user account, preventing login until reactivated").action(
3584
3921
  withErrorHandler(async (id, _opts, cmd) => {
3585
3922
  const client = createClient(cmd);
3586
3923
  await client.rawRequest(
@@ -3592,11 +3929,11 @@ function registerUsersCommand(parent) {
3592
3929
  );
3593
3930
  addExamples(deactivate, [
3594
3931
  {
3595
- description: "Deactivate a user",
3932
+ description: "Deactivate a user to suspend their access",
3596
3933
  command: "geonic admin users deactivate <user-id>"
3597
3934
  }
3598
3935
  ]);
3599
- const unlock = users.command("unlock <id>").description("Unlock a user").action(
3936
+ const unlock = users.command("unlock <id>").description("Unlock a user account that was locked due to repeated failed login attempts").action(
3600
3937
  withErrorHandler(async (id, _opts, cmd) => {
3601
3938
  const client = createClient(cmd);
3602
3939
  await client.rawRequest(
@@ -3616,8 +3953,8 @@ function registerUsersCommand(parent) {
3616
3953
 
3617
3954
  // src/commands/admin/policies.ts
3618
3955
  function registerPoliciesCommand(parent) {
3619
- const policies = parent.command("policies").description("Manage policies");
3620
- const list = policies.command("list").description("List all policies").action(
3956
+ const policies = parent.command("policies").description("Manage XACML access control policies");
3957
+ const list = policies.command("list").description("List all access control policies, showing their status and priority").action(
3621
3958
  withErrorHandler(async (_opts, cmd) => {
3622
3959
  const client = createClient(cmd);
3623
3960
  const format = getFormat(cmd);
@@ -3629,9 +3966,13 @@ function registerPoliciesCommand(parent) {
3629
3966
  {
3630
3967
  description: "List all policies",
3631
3968
  command: "geonic admin policies list"
3969
+ },
3970
+ {
3971
+ description: "List policies in table format for an overview",
3972
+ command: "geonic admin policies list --format table"
3632
3973
  }
3633
3974
  ]);
3634
- const get = policies.command("get <id>").description("Get a policy by ID").action(
3975
+ const get = policies.command("get <id>").description("Get a policy's full details \u2014 target rules, effect, priority, and status").action(
3635
3976
  withErrorHandler(async (id, _opts, cmd) => {
3636
3977
  const client = createClient(cmd);
3637
3978
  const format = getFormat(cmd);
@@ -3644,11 +3985,11 @@ function registerPoliciesCommand(parent) {
3644
3985
  );
3645
3986
  addExamples(get, [
3646
3987
  {
3647
- description: "Get a policy by ID",
3988
+ description: "Inspect a policy's rules and target configuration",
3648
3989
  command: "geonic admin policies get <policy-id>"
3649
3990
  }
3650
3991
  ]);
3651
- const create = policies.command("create [json]").description(
3992
+ const create = policies.command("create [json]").summary("Create a new policy").description(
3652
3993
  'Create a new policy\n\nJSON payload examples:\n\n Allow all entities:\n {\n "description": "Allow all entities",\n "rules": [{"ruleId": "allow-all", "effect": "Permit"}]\n }\n\n Allow GET access to a specific entity type:\n {\n "description": "Allow GET access to Landmark entities",\n "target": {\n "resources": [{"attributeId": "entityType", "matchValue": "Landmark"}],\n "actions": [{"attributeId": "method", "matchValue": "GET"}]\n },\n "rules": [{"ruleId": "permit-get", "effect": "Permit"}]\n }\n\nTarget fields:\n subjects \u2014 attributeId: role, userId, email, tenantId\n resources \u2014 attributeId: path, entityType, entityId, entityOwner, tenantService, servicePath\n actions \u2014 attributeId: method (GET, POST, PATCH, DELETE)\n\nEach element: {attributeId, matchValue, matchFunction?}\n matchFunction: "string-equal" (default) | "string-regexp" | "glob"\n\nPriority: smaller value = higher precedence (e.g. priority 10 overrides user default at 100).\n tenant_admin: minimum priority 10. user self-service (/me/policies): fixed at 100.\n\nDefault role policies (priority 100):\n user \u2192 /v2/** and /ngsi-ld/** all methods Permit; other data APIs GET only\n api_key \u2192 all Deny, anonymous \u2192 all Deny'
3653
3994
  ).action(
3654
3995
  withErrorHandler(async (json, _opts, cmd) => {
@@ -3688,7 +4029,7 @@ function registerPoliciesCommand(parent) {
3688
4029
  command: "cat policy.json | geonic admin policies create"
3689
4030
  }
3690
4031
  ]);
3691
- const update = policies.command("update <id> [json]").description(
4032
+ const update = policies.command("update <id> [json]").summary("Update a policy").description(
3692
4033
  'Update a policy\n\nJSON payload: only specified fields are updated.\n e.g. {"description": "Updated policy"}'
3693
4034
  ).action(
3694
4035
  withErrorHandler(
@@ -3720,7 +4061,7 @@ function registerPoliciesCommand(parent) {
3720
4061
  command: "cat policy.json | geonic admin policies update <policy-id>"
3721
4062
  }
3722
4063
  ]);
3723
- const del = policies.command("delete <id>").description("Delete a policy").action(
4064
+ const del = policies.command("delete <id>").description("Delete a policy. Users or API keys referencing this policy will lose the access it granted").action(
3724
4065
  withErrorHandler(async (id, _opts, cmd) => {
3725
4066
  const client = createClient(cmd);
3726
4067
  await client.rawRequest(
@@ -3732,11 +4073,11 @@ function registerPoliciesCommand(parent) {
3732
4073
  );
3733
4074
  addExamples(del, [
3734
4075
  {
3735
- description: "Delete a policy",
4076
+ description: "Delete a policy by ID",
3736
4077
  command: "geonic admin policies delete <policy-id>"
3737
4078
  }
3738
4079
  ]);
3739
- const activate = policies.command("activate <id>").description("Activate a policy").action(
4080
+ const activate = policies.command("activate <id>").description("Activate a policy so its access control rules are enforced").action(
3740
4081
  withErrorHandler(async (id, _opts, cmd) => {
3741
4082
  const client = createClient(cmd);
3742
4083
  await client.rawRequest(
@@ -3748,11 +4089,11 @@ function registerPoliciesCommand(parent) {
3748
4089
  );
3749
4090
  addExamples(activate, [
3750
4091
  {
3751
- description: "Activate a policy",
4092
+ description: "Enable a policy to start enforcing its rules",
3752
4093
  command: "geonic admin policies activate <policy-id>"
3753
4094
  }
3754
4095
  ]);
3755
- const deactivate = policies.command("deactivate <id>").description("Deactivate a policy").action(
4096
+ const deactivate = policies.command("deactivate <id>").description("Deactivate a policy, suspending its rules without deleting it").action(
3756
4097
  withErrorHandler(async (id, _opts, cmd) => {
3757
4098
  const client = createClient(cmd);
3758
4099
  await client.rawRequest(
@@ -3764,7 +4105,7 @@ function registerPoliciesCommand(parent) {
3764
4105
  );
3765
4106
  addExamples(deactivate, [
3766
4107
  {
3767
- description: "Deactivate a policy",
4108
+ description: "Temporarily disable a policy without deleting it",
3768
4109
  command: "geonic admin policies deactivate <policy-id>"
3769
4110
  }
3770
4111
  ]);
@@ -3773,7 +4114,7 @@ function registerPoliciesCommand(parent) {
3773
4114
  // src/commands/admin/oauth-clients.ts
3774
4115
  function registerOAuthClientsCommand(parent) {
3775
4116
  const oauthClients = parent.command("oauth-clients").description("Manage OAuth clients");
3776
- const list = oauthClients.command("list").description("List all OAuth clients").action(
4117
+ const list = oauthClients.command("list").description("List all registered OAuth clients and their configurations").action(
3777
4118
  withErrorHandler(async (_opts, cmd) => {
3778
4119
  const client = createClient(cmd);
3779
4120
  const format = getFormat(cmd);
@@ -3785,9 +4126,13 @@ function registerOAuthClientsCommand(parent) {
3785
4126
  {
3786
4127
  description: "List all OAuth clients",
3787
4128
  command: "geonic admin oauth-clients list"
4129
+ },
4130
+ {
4131
+ description: "List OAuth clients in table format",
4132
+ command: "geonic admin oauth-clients list --format table"
3788
4133
  }
3789
4134
  ]);
3790
- const get = oauthClients.command("get <id>").description("Get an OAuth client by ID").action(
4135
+ const get = oauthClients.command("get <id>").description("Get an OAuth client's details \u2014 name, client ID, policy, and redirect URIs").action(
3791
4136
  withErrorHandler(async (id, _opts, cmd) => {
3792
4137
  const client = createClient(cmd);
3793
4138
  const format = getFormat(cmd);
@@ -3800,11 +4145,11 @@ function registerOAuthClientsCommand(parent) {
3800
4145
  );
3801
4146
  addExamples(get, [
3802
4147
  {
3803
- description: "Get an OAuth client by ID",
4148
+ description: "Inspect an OAuth client's configuration",
3804
4149
  command: "geonic admin oauth-clients get <client-id>"
3805
4150
  }
3806
4151
  ]);
3807
- const create = oauthClients.command("create [json]").description(
4152
+ const create = oauthClients.command("create [json]").summary("Create a new OAuth client").description(
3808
4153
  'Create a new OAuth client\n\nJSON payload example:\n {\n "name": "my-app",\n "policyId": "<policy-id>"\n }'
3809
4154
  ).action(
3810
4155
  withErrorHandler(async (json, _opts, cmd) => {
@@ -3832,7 +4177,7 @@ function registerOAuthClientsCommand(parent) {
3832
4177
  command: "cat client.json | geonic admin oauth-clients create"
3833
4178
  }
3834
4179
  ]);
3835
- const update = oauthClients.command("update <id> [json]").description(
4180
+ const update = oauthClients.command("update <id> [json]").summary("Update an OAuth client").description(
3836
4181
  'Update an OAuth client\n\nJSON payload: only specified fields are updated.\n e.g. {"description": "Updated client"}'
3837
4182
  ).action(
3838
4183
  withErrorHandler(
@@ -3864,7 +4209,7 @@ function registerOAuthClientsCommand(parent) {
3864
4209
  command: "cat client.json | geonic admin oauth-clients update <client-id>"
3865
4210
  }
3866
4211
  ]);
3867
- const del = oauthClients.command("delete <id>").description("Delete an OAuth client").action(
4212
+ const del = oauthClients.command("delete <id>").description("Delete an OAuth client. Existing tokens issued by this client will be invalidated").action(
3868
4213
  withErrorHandler(async (id, _opts, cmd) => {
3869
4214
  const client = createClient(cmd);
3870
4215
  await client.rawRequest(
@@ -3876,14 +4221,14 @@ function registerOAuthClientsCommand(parent) {
3876
4221
  );
3877
4222
  addExamples(del, [
3878
4223
  {
3879
- description: "Delete an OAuth client",
4224
+ description: "Delete an OAuth client by ID",
3880
4225
  command: "geonic admin oauth-clients delete <client-id>"
3881
4226
  }
3882
4227
  ]);
3883
4228
  }
3884
4229
  function registerCaddeCommand(parent) {
3885
- const cadde = parent.command("cadde").description("Manage CADDE configuration");
3886
- const caddeGet = cadde.command("get").description("Get CADDE configuration").action(
4230
+ const cadde = parent.command("cadde").description("Manage CADDE (data exchange) configuration for cross-platform data sharing");
4231
+ const caddeGet = cadde.command("get").description("Get the current CADDE data exchange configuration (provider, endpoint, etc.)").action(
3887
4232
  withErrorHandler(async (_opts, cmd) => {
3888
4233
  const client = createClient(cmd);
3889
4234
  const format = getFormat(cmd);
@@ -3893,11 +4238,15 @@ function registerCaddeCommand(parent) {
3893
4238
  );
3894
4239
  addExamples(caddeGet, [
3895
4240
  {
3896
- description: "Get CADDE configuration",
4241
+ description: "View current CADDE configuration",
3897
4242
  command: "geonic admin cadde get"
4243
+ },
4244
+ {
4245
+ description: "View CADDE configuration in table format",
4246
+ command: "geonic admin cadde get --format table"
3898
4247
  }
3899
4248
  ]);
3900
- const caddeSet = cadde.command("set [json]").description(
4249
+ const caddeSet = cadde.command("set [json]").summary("Set CADDE configuration").description(
3901
4250
  'Set CADDE configuration\n\nJSON payload example:\n {\n "provider": "my-provider",\n "endpoint": "http://localhost:6000"\n }'
3902
4251
  ).action(
3903
4252
  withErrorHandler(async (json, _opts, cmd) => {
@@ -3925,7 +4274,7 @@ function registerCaddeCommand(parent) {
3925
4274
  command: "cat cadde-config.json | geonic admin cadde set"
3926
4275
  }
3927
4276
  ]);
3928
- const caddeDelete = cadde.command("delete").description("Delete CADDE configuration").action(
4277
+ const caddeDelete = cadde.command("delete").description("Remove the CADDE data exchange configuration, disabling cross-platform data sharing").action(
3929
4278
  withErrorHandler(async (_opts, cmd) => {
3930
4279
  const client = createClient(cmd);
3931
4280
  await client.rawRequest("DELETE", "/admin/cadde");
@@ -3934,13 +4283,20 @@ function registerCaddeCommand(parent) {
3934
4283
  );
3935
4284
  addExamples(caddeDelete, [
3936
4285
  {
3937
- description: "Delete CADDE configuration",
4286
+ description: "Remove CADDE configuration",
3938
4287
  command: "geonic admin cadde delete"
3939
4288
  }
3940
4289
  ]);
3941
4290
  }
3942
4291
 
3943
4292
  // src/commands/admin/api-keys.ts
4293
+ function cleanApiKeyData2(data) {
4294
+ if (Array.isArray(data)) return data.map(cleanApiKeyData2);
4295
+ if (typeof data !== "object" || data === null) return data;
4296
+ const obj = { ...data };
4297
+ if (obj.key === "******") delete obj.key;
4298
+ return obj;
4299
+ }
3944
4300
  function validateOrigins(body, opts) {
3945
4301
  if (opts.origins !== void 0) {
3946
4302
  const origins = String(opts.origins).split(",").map((s) => s.trim()).filter(Boolean);
@@ -3979,9 +4335,41 @@ function buildBodyFromFlags(opts) {
3979
4335
  if (opts.tenantId) payload.tenantId = opts.tenantId;
3980
4336
  return payload;
3981
4337
  }
4338
+ function handleSaveKey2(data, cmd) {
4339
+ const globalOpts = resolveOptions(cmd);
4340
+ const key = data.key;
4341
+ if (!key) {
4342
+ printError("Response missing key. API key was created, but it could not be saved.");
4343
+ process.exitCode = 1;
4344
+ return false;
4345
+ }
4346
+ try {
4347
+ const config = loadConfig(globalOpts.profile);
4348
+ config.apiKey = key;
4349
+ saveConfig(config, globalOpts.profile);
4350
+ console.error("API key saved to config. X-Api-Key header will be sent automatically.");
4351
+ return true;
4352
+ } catch (err) {
4353
+ printError(`Failed to save API key to config: ${err instanceof Error ? err.message : String(err)}`);
4354
+ printApiKeyBox(key);
4355
+ process.exitCode = 1;
4356
+ return false;
4357
+ }
4358
+ }
4359
+ function showKeyResult2(data, save, cmd) {
4360
+ const key = data.key;
4361
+ if (!key) {
4362
+ printError("Response missing key. The new API key value was not returned.");
4363
+ process.exitCode = 1;
4364
+ return false;
4365
+ }
4366
+ if (save) return handleSaveKey2(data, cmd);
4367
+ printApiKeyBox(key);
4368
+ return true;
4369
+ }
3982
4370
  function registerApiKeysCommand(parent) {
3983
4371
  const apiKeys = parent.command("api-keys").description("Manage API keys");
3984
- const list = apiKeys.command("list").description("List all API keys").option("--tenant-id <id>", "Filter by tenant ID").action(
4372
+ const list = apiKeys.command("list").description("List all API keys, showing name, tenant, policy, and status (key values are masked)").option("--tenant-id <id>", "Filter by tenant ID").action(
3985
4373
  withErrorHandler(async (_opts, cmd) => {
3986
4374
  const opts = cmd.opts();
3987
4375
  const client = createClient(cmd);
@@ -3991,7 +4379,9 @@ function registerApiKeysCommand(parent) {
3991
4379
  const response = await client.rawRequest("GET", "/admin/api-keys", {
3992
4380
  params
3993
4381
  });
4382
+ response.data = cleanApiKeyData2(response.data);
3994
4383
  outputResponse(response, format);
4384
+ console.error("\u203B API \u30AD\u30FC\u5024\u306F\u4F5C\u6210\u6642 (create) \u307E\u305F\u306F\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5\u6642 (refresh) \u306B\u306E\u307F\u8868\u793A\u3055\u308C\u307E\u3059\u3002");
3995
4385
  })
3996
4386
  );
3997
4387
  addExamples(list, [
@@ -3999,12 +4389,16 @@ function registerApiKeysCommand(parent) {
3999
4389
  description: "List all API keys",
4000
4390
  command: "geonic admin api-keys list"
4001
4391
  },
4392
+ {
4393
+ description: "List API keys in table format",
4394
+ command: "geonic admin api-keys list --format table"
4395
+ },
4002
4396
  {
4003
4397
  description: "List API keys for a specific tenant",
4004
4398
  command: "geonic admin api-keys list --tenant-id <tenant-id>"
4005
4399
  }
4006
4400
  ]);
4007
- const get = apiKeys.command("get <keyId>").description("Get an API key by ID").action(
4401
+ const get = apiKeys.command("get <keyId>").description("Get an API key's metadata \u2014 name, policy, allowed origins, and rate limit (key value is masked)").action(
4008
4402
  withErrorHandler(async (keyId, _opts, cmd) => {
4009
4403
  const client = createClient(cmd);
4010
4404
  const format = getFormat(cmd);
@@ -4012,12 +4406,13 @@ function registerApiKeysCommand(parent) {
4012
4406
  "GET",
4013
4407
  `/admin/api-keys/${encodeURIComponent(String(keyId))}`
4014
4408
  );
4409
+ response.data = cleanApiKeyData2(response.data);
4015
4410
  outputResponse(response, format);
4016
4411
  })
4017
4412
  );
4018
4413
  addExamples(get, [
4019
4414
  {
4020
- description: "Get an API key by ID",
4415
+ description: "Inspect an API key's configuration",
4021
4416
  command: "geonic admin api-keys get <key-id>"
4022
4417
  }
4023
4418
  ]);
@@ -4040,24 +4435,9 @@ function registerApiKeysCommand(parent) {
4040
4435
  body
4041
4436
  });
4042
4437
  const data = response.data;
4043
- if (opts.save) {
4044
- const globalOpts = resolveOptions(cmd);
4045
- const key = data.key;
4046
- if (!key) {
4047
- printError("Response missing key. API key was created, but it could not be saved.");
4048
- outputResponse(response, format);
4049
- process.exitCode = 1;
4050
- return;
4051
- }
4052
- const config = loadConfig(globalOpts.profile);
4053
- config.apiKey = key;
4054
- saveConfig(config, globalOpts.profile);
4055
- console.error("API key saved to config. X-Api-Key header will be sent automatically.");
4056
- } else {
4057
- printWarning("Save the API key now \u2014 it will not be shown again. Use --save to store it automatically.");
4058
- }
4438
+ const ok = showKeyResult2(data, !!opts.save, cmd);
4059
4439
  outputResponse(response, format);
4060
- console.error("API key created.");
4440
+ if (ok) console.error("API key created.");
4061
4441
  })
4062
4442
  );
4063
4443
  addNotes(create, [
@@ -4078,6 +4458,35 @@ function registerApiKeysCommand(parent) {
4078
4458
  command: "geonic admin api-keys create @key.json --save"
4079
4459
  }
4080
4460
  ]);
4461
+ const refresh = apiKeys.command("refresh <keyId>").description("Refresh (rotate) an API key \u2014 generates a new key value").option("--save", "Save the new API key to profile config").action(
4462
+ withErrorHandler(async (keyId, _opts, cmd) => {
4463
+ const opts = cmd.opts();
4464
+ const client = createClient(cmd);
4465
+ const format = getFormat(cmd);
4466
+ const response = await client.rawRequest(
4467
+ "POST",
4468
+ `/admin/api-keys/${encodeURIComponent(String(keyId))}/refresh`
4469
+ );
4470
+ const data = response.data;
4471
+ const ok = showKeyResult2(data, !!opts.save, cmd);
4472
+ outputResponse(response, format);
4473
+ if (ok) console.error("API key refreshed.");
4474
+ })
4475
+ );
4476
+ addNotes(refresh, [
4477
+ "Refreshing generates a new key value while keeping keyId, name, and policy settings.",
4478
+ "The previous key value is immediately invalidated."
4479
+ ]);
4480
+ addExamples(refresh, [
4481
+ {
4482
+ description: "Refresh an API key",
4483
+ command: "geonic admin api-keys refresh <key-id>"
4484
+ },
4485
+ {
4486
+ description: "Refresh and save new key to config",
4487
+ command: "geonic admin api-keys refresh <key-id> --save"
4488
+ }
4489
+ ]);
4081
4490
  const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--policy <policyId>", "Policy ID to attach").option("--origins <origins>", "Comma-separated origins").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP token binding").action(
4082
4491
  withErrorHandler(
4083
4492
  async (keyId, json, _opts, cmd) => {
@@ -4130,7 +4539,7 @@ function registerApiKeysCommand(parent) {
4130
4539
  command: "geonic admin api-keys update <key-id> @key.json"
4131
4540
  }
4132
4541
  ]);
4133
- const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
4542
+ const del = apiKeys.command("delete <keyId>").description("Delete an API key. Any requests using this key will be immediately rejected").action(
4134
4543
  withErrorHandler(async (keyId, _opts, cmd) => {
4135
4544
  const client = createClient(cmd);
4136
4545
  await client.rawRequest(
@@ -4142,7 +4551,7 @@ function registerApiKeysCommand(parent) {
4142
4551
  );
4143
4552
  addExamples(del, [
4144
4553
  {
4145
- description: "Delete an API key",
4554
+ description: "Delete an API key by ID",
4146
4555
  command: "geonic admin api-keys delete <key-id>"
4147
4556
  }
4148
4557
  ]);
@@ -4161,8 +4570,8 @@ function registerAdminCommand(program2) {
4161
4570
 
4162
4571
  // src/commands/rules.ts
4163
4572
  function registerRulesCommand(program2) {
4164
- const rules = program2.command("rules").description("Manage rule engine");
4165
- const list = rules.command("list").description("List all rules").action(
4573
+ const rules = program2.command("rules").description("Manage ReactiveCore rules that trigger actions based on entity changes");
4574
+ const list = rules.command("list").description("List all configured rules and their current status").action(
4166
4575
  withErrorHandler(async (_opts, cmd) => {
4167
4576
  const client = createClient(cmd);
4168
4577
  const format = getFormat(cmd);
@@ -4172,11 +4581,15 @@ function registerRulesCommand(program2) {
4172
4581
  );
4173
4582
  addExamples(list, [
4174
4583
  {
4175
- description: "List all rules",
4584
+ description: "List all rules as JSON",
4176
4585
  command: "geonic rules list"
4586
+ },
4587
+ {
4588
+ description: "List rules in table format to review status at a glance",
4589
+ command: "geonic rules list --format table"
4177
4590
  }
4178
4591
  ]);
4179
- const get = rules.command("get <id>").description("Get a rule by ID").action(
4592
+ const get = rules.command("get <id>").description("Get a rule's full definition including conditions, actions, and status").action(
4180
4593
  withErrorHandler(async (id, _opts, cmd) => {
4181
4594
  const client = createClient(cmd);
4182
4595
  const format = getFormat(cmd);
@@ -4189,11 +4602,15 @@ function registerRulesCommand(program2) {
4189
4602
  );
4190
4603
  addExamples(get, [
4191
4604
  {
4192
- description: "Get a specific rule",
4605
+ description: "Inspect a rule's conditions and actions",
4193
4606
  command: "geonic rules get <rule-id>"
4607
+ },
4608
+ {
4609
+ description: "Get a rule and check if it is active",
4610
+ command: "geonic rules get urn:ngsi-ld:Rule:high-temp-alert"
4194
4611
  }
4195
4612
  ]);
4196
- const create = rules.command("create [json]").description(
4613
+ const create = rules.command("create [json]").summary("Create a new rule").description(
4197
4614
  'Create a new rule\n\nJSON payload example:\n {\n "name": "high-temp-alert",\n "description": "Alert on high temperature",\n "conditions": [{"type": "celExpression", "expression": "entity.temperature > 30"}],\n "actions": [{"type": "webhook", "url": "http://localhost:5000/alert", "method": "POST"}]\n }'
4198
4615
  ).action(
4199
4616
  withErrorHandler(async (json, _opts, cmd) => {
@@ -4219,7 +4636,7 @@ function registerRulesCommand(program2) {
4219
4636
  command: "cat rule.json | geonic rules create"
4220
4637
  }
4221
4638
  ]);
4222
- const update = rules.command("update <id> [json]").description(
4639
+ const update = rules.command("update <id> [json]").summary("Update a rule").description(
4223
4640
  'Update a rule\n\nJSON payload: only specified fields are updated.\n e.g. {"description": "Updated rule"}'
4224
4641
  ).action(
4225
4642
  withErrorHandler(
@@ -4251,7 +4668,7 @@ function registerRulesCommand(program2) {
4251
4668
  command: "cat rule.json | geonic rules update <rule-id>"
4252
4669
  }
4253
4670
  ]);
4254
- const del = rules.command("delete <id>").description("Delete a rule").action(
4671
+ const del = rules.command("delete <id>").description("Permanently delete a rule and stop its processing").action(
4255
4672
  withErrorHandler(async (id, _opts, cmd) => {
4256
4673
  const client = createClient(cmd);
4257
4674
  await client.rawRequest(
@@ -4263,11 +4680,15 @@ function registerRulesCommand(program2) {
4263
4680
  );
4264
4681
  addExamples(del, [
4265
4682
  {
4266
- description: "Delete a rule",
4683
+ description: "Delete a rule by ID",
4267
4684
  command: "geonic rules delete <rule-id>"
4685
+ },
4686
+ {
4687
+ description: "Remove an obsolete alert rule",
4688
+ command: "geonic rules delete urn:ngsi-ld:Rule:old-alert"
4268
4689
  }
4269
4690
  ]);
4270
- const activate = rules.command("activate <id>").description("Activate a rule").action(
4691
+ const activate = rules.command("activate <id>").description("Enable a rule so it begins evaluating conditions and firing actions").action(
4271
4692
  withErrorHandler(async (id, _opts, cmd) => {
4272
4693
  const client = createClient(cmd);
4273
4694
  await client.rawRequest(
@@ -4279,11 +4700,15 @@ function registerRulesCommand(program2) {
4279
4700
  );
4280
4701
  addExamples(activate, [
4281
4702
  {
4282
- description: "Activate a rule",
4703
+ description: "Start processing a rule",
4283
4704
  command: "geonic rules activate <rule-id>"
4705
+ },
4706
+ {
4707
+ description: "Re-enable a previously deactivated rule",
4708
+ command: "geonic rules activate urn:ngsi-ld:Rule:high-temp-alert"
4284
4709
  }
4285
4710
  ]);
4286
- const deactivate = rules.command("deactivate <id>").description("Deactivate a rule").action(
4711
+ const deactivate = rules.command("deactivate <id>").description("Disable a rule without deleting it, pausing condition evaluation and actions").action(
4287
4712
  withErrorHandler(async (id, _opts, cmd) => {
4288
4713
  const client = createClient(cmd);
4289
4714
  await client.rawRequest(
@@ -4295,16 +4720,20 @@ function registerRulesCommand(program2) {
4295
4720
  );
4296
4721
  addExamples(deactivate, [
4297
4722
  {
4298
- description: "Deactivate a rule",
4723
+ description: "Temporarily pause a rule during maintenance",
4299
4724
  command: "geonic rules deactivate <rule-id>"
4725
+ },
4726
+ {
4727
+ description: "Disable a noisy alert rule without removing it",
4728
+ command: "geonic rules deactivate urn:ngsi-ld:Rule:high-temp-alert"
4300
4729
  }
4301
4730
  ]);
4302
4731
  }
4303
4732
 
4304
4733
  // src/commands/models.ts
4305
4734
  function registerModelsCommand(program2) {
4306
- const models = program2.command("custom-data-models").alias("models").description("Manage custom data models");
4307
- const list = models.command("list").description("List all models").action(
4735
+ const models = program2.command("custom-data-models").alias("models").description("Manage custom data models that define entity type schemas and property constraints");
4736
+ const list = models.command("list").description("List all registered data models for the current tenant").action(
4308
4737
  withErrorHandler(async (_opts, cmd) => {
4309
4738
  const client = createClient(cmd);
4310
4739
  const format = getFormat(cmd);
@@ -4314,11 +4743,15 @@ function registerModelsCommand(program2) {
4314
4743
  );
4315
4744
  addExamples(list, [
4316
4745
  {
4317
- description: "List all models",
4746
+ description: "List all data models as JSON",
4318
4747
  command: "geonic models list"
4748
+ },
4749
+ {
4750
+ description: "Browse available data models in table format",
4751
+ command: "geonic models list --format table"
4319
4752
  }
4320
4753
  ]);
4321
- const get = models.command("get <id>").description("Get a model by ID").action(
4754
+ const get = models.command("get <id>").description("Get a data model's full schema including property definitions and constraints").action(
4322
4755
  withErrorHandler(async (id, _opts, cmd) => {
4323
4756
  const client = createClient(cmd);
4324
4757
  const format = getFormat(cmd);
@@ -4331,11 +4764,15 @@ function registerModelsCommand(program2) {
4331
4764
  );
4332
4765
  addExamples(get, [
4333
4766
  {
4334
- description: "Get a specific model",
4767
+ description: "Inspect a model's property definitions",
4335
4768
  command: "geonic models get <model-id>"
4769
+ },
4770
+ {
4771
+ description: "View the schema for a Sensor data model",
4772
+ command: "geonic models get urn:ngsi-ld:DataModel:Sensor"
4336
4773
  }
4337
4774
  ]);
4338
- const create = models.command("create [json]").description(
4775
+ const create = models.command("create [json]").summary("Create a new model").description(
4339
4776
  'Create a new model\n\nJSON payload example:\n {\n "type": "Sensor",\n "domain": "iot",\n "description": "IoT Sensor",\n "propertyDetails": {\n "temperature": {"ngsiType": "Property", "valueType": "Number", "example": 25}\n }\n }'
4340
4777
  ).action(
4341
4778
  withErrorHandler(async (json, _opts, cmd) => {
@@ -4361,7 +4798,7 @@ function registerModelsCommand(program2) {
4361
4798
  command: "cat model.json | geonic models create"
4362
4799
  }
4363
4800
  ]);
4364
- const update = models.command("update <id> [json]").description(
4801
+ const update = models.command("update <id> [json]").summary("Update a model").description(
4365
4802
  'Update a model\n\nJSON payload: only specified fields are updated.\n e.g. {"description": "Updated model"}'
4366
4803
  ).action(
4367
4804
  withErrorHandler(
@@ -4393,7 +4830,7 @@ function registerModelsCommand(program2) {
4393
4830
  command: "cat model.json | geonic models update <model-id>"
4394
4831
  }
4395
4832
  ]);
4396
- const del = models.command("delete <id>").description("Delete a model").action(
4833
+ const del = models.command("delete <id>").description("Delete a data model definition (does not affect existing entities)").action(
4397
4834
  withErrorHandler(async (id, _opts, cmd) => {
4398
4835
  const client = createClient(cmd);
4399
4836
  await client.rawRequest(
@@ -4405,16 +4842,20 @@ function registerModelsCommand(program2) {
4405
4842
  );
4406
4843
  addExamples(del, [
4407
4844
  {
4408
- description: "Delete a model",
4845
+ description: "Delete a data model by ID",
4409
4846
  command: "geonic models delete <model-id>"
4847
+ },
4848
+ {
4849
+ description: "Remove a deprecated model definition",
4850
+ command: "geonic models delete urn:ngsi-ld:DataModel:LegacySensor"
4410
4851
  }
4411
4852
  ]);
4412
4853
  }
4413
4854
 
4414
4855
  // src/commands/catalog.ts
4415
4856
  function registerCatalogCommand(program2) {
4416
- const catalog = program2.command("catalog").description("Browse DCAT-AP catalog");
4417
- const get = catalog.command("get").description("Get the catalog").action(
4857
+ const catalog = program2.command("catalog").description("Browse the DCAT-AP data catalog for discovering and previewing datasets");
4858
+ const get = catalog.command("get").description("Get the DCAT-AP catalog metadata including title, publisher, and dataset count").action(
4418
4859
  withErrorHandler(async (_opts, cmd) => {
4419
4860
  const client = createClient(cmd);
4420
4861
  const format = getFormat(cmd);
@@ -4424,12 +4865,16 @@ function registerCatalogCommand(program2) {
4424
4865
  );
4425
4866
  addExamples(get, [
4426
4867
  {
4427
- description: "Get the DCAT-AP catalog",
4868
+ description: "View catalog metadata (title, publisher, datasets summary)",
4428
4869
  command: "geonic catalog get"
4870
+ },
4871
+ {
4872
+ description: "Get catalog metadata in table format",
4873
+ command: "geonic catalog get --format table"
4429
4874
  }
4430
4875
  ]);
4431
- const datasets = catalog.command("datasets").description("Manage catalog datasets");
4432
- const datasetsList = datasets.command("list").description("List all datasets").action(
4876
+ const datasets = catalog.command("datasets").description("List, inspect, and preview datasets published in the catalog");
4877
+ const datasetsList = datasets.command("list").description("List all datasets published in the catalog").action(
4433
4878
  withErrorHandler(async (_opts, cmd) => {
4434
4879
  const client = createClient(cmd);
4435
4880
  const format = getFormat(cmd);
@@ -4439,11 +4884,15 @@ function registerCatalogCommand(program2) {
4439
4884
  );
4440
4885
  addExamples(datasetsList, [
4441
4886
  {
4442
- description: "List all catalog datasets",
4887
+ description: "List all catalog datasets as JSON",
4443
4888
  command: "geonic catalog datasets list"
4889
+ },
4890
+ {
4891
+ description: "Browse datasets in table format",
4892
+ command: "geonic catalog datasets list --format table"
4444
4893
  }
4445
4894
  ]);
4446
- const datasetsGet = datasets.command("get <id>").description("Get a dataset by ID").action(
4895
+ const datasetsGet = datasets.command("get <id>").description("Get a dataset's metadata including description, distributions, and license").action(
4447
4896
  withErrorHandler(async (id, _opts, cmd) => {
4448
4897
  const client = createClient(cmd);
4449
4898
  const format = getFormat(cmd);
@@ -4456,11 +4905,15 @@ function registerCatalogCommand(program2) {
4456
4905
  );
4457
4906
  addExamples(datasetsGet, [
4458
4907
  {
4459
- description: "Get a specific dataset",
4908
+ description: "Inspect a dataset's metadata and distributions",
4460
4909
  command: "geonic catalog datasets get <dataset-id>"
4910
+ },
4911
+ {
4912
+ description: "View dataset details including license and publisher",
4913
+ command: "geonic catalog datasets get urn:ngsi-ld:Dataset:weather-stations"
4461
4914
  }
4462
4915
  ]);
4463
- const datasetsSample = datasets.command("sample <id>").description("Get sample data for a dataset").action(
4916
+ const datasetsSample = datasets.command("sample <id>").description("Preview sample entities from a dataset to understand its structure and content").action(
4464
4917
  withErrorHandler(async (id, _opts, cmd) => {
4465
4918
  const client = createClient(cmd);
4466
4919
  const format = getFormat(cmd);
@@ -4473,8 +4926,12 @@ function registerCatalogCommand(program2) {
4473
4926
  );
4474
4927
  addExamples(datasetsSample, [
4475
4928
  {
4476
- description: "Get sample data for a dataset",
4929
+ description: "Preview sample entities from a dataset",
4477
4930
  command: "geonic catalog datasets sample <dataset-id>"
4931
+ },
4932
+ {
4933
+ description: "Preview data in table format to quickly assess content",
4934
+ command: "geonic catalog datasets sample <dataset-id> --format table"
4478
4935
  }
4479
4936
  ]);
4480
4937
  }
@@ -4482,7 +4939,7 @@ function registerCatalogCommand(program2) {
4482
4939
  // src/commands/health.ts
4483
4940
  import { createRequire } from "module";
4484
4941
  function registerHealthCommand(program2) {
4485
- const health = program2.command("health").description("Check the health status of the server").action(
4942
+ const health = program2.command("health").description("Check Context Broker connectivity and health status").action(
4486
4943
  withErrorHandler(async (_opts, cmd) => {
4487
4944
  const client = createClient(cmd);
4488
4945
  const format = getFormat(cmd);
@@ -4494,11 +4951,19 @@ function registerHealthCommand(program2) {
4494
4951
  {
4495
4952
  description: "Check server health",
4496
4953
  command: "geonic health"
4954
+ },
4955
+ {
4956
+ description: "Check health with table output",
4957
+ command: "geonic health --format table"
4958
+ },
4959
+ {
4960
+ description: "Check health of a specific server",
4961
+ command: "geonic health --url https://api.example.com"
4497
4962
  }
4498
4963
  ]);
4499
4964
  }
4500
4965
  function registerVersionCommand(program2) {
4501
- const version = program2.command("version").description("Display CLI and server version information").action(
4966
+ const version = program2.command("version").description("Display both CLI and server version information").action(
4502
4967
  withErrorHandler(async (_opts, cmd) => {
4503
4968
  const require2 = createRequire(import.meta.url);
4504
4969
  const pkg = require2("../package.json");
@@ -4515,11 +4980,16 @@ function registerVersionCommand(program2) {
4515
4980
  {
4516
4981
  description: "Show CLI and server version",
4517
4982
  command: "geonic version"
4983
+ },
4984
+ {
4985
+ description: "Show version as JSON",
4986
+ command: "geonic version --format json"
4518
4987
  }
4519
4988
  ]);
4520
4989
  }
4521
4990
 
4522
4991
  // src/commands/cli.ts
4992
+ import { execSync } from "child_process";
4523
4993
  import { createRequire as createRequire2 } from "module";
4524
4994
  function findOption(cmd, program2, flag) {
4525
4995
  return cmd.options.find((o) => o.long === flag || o.short === flag) || program2.options.find((o) => o.long === flag || o.short === flag);
@@ -4685,15 +5155,43 @@ function registerCliCommand(program2) {
4685
5155
  command: `echo 'eval "$(geonic cli completions zsh)"' >> ~/.zshrc`
4686
5156
  }
4687
5157
  ]);
4688
- const version = cli.command("version").description("Display the CLI version").action(() => {
5158
+ const version = cli.command("version").description("Display the currently installed CLI version").action(() => {
4689
5159
  const require2 = createRequire2(import.meta.url);
4690
5160
  const pkg = require2("../package.json");
4691
5161
  console.log(pkg.version);
4692
5162
  });
4693
5163
  addExamples(version, [
4694
5164
  {
4695
- description: "Show CLI version",
5165
+ description: "Show the installed version",
4696
5166
  command: "geonic cli version"
5167
+ },
5168
+ {
5169
+ description: "Use in scripts to check the installed version",
5170
+ command: 'echo "geonic $(geonic cli version)"'
5171
+ }
5172
+ ]);
5173
+ const update = cli.command("update").description("Update the CLI to the latest version via npm (requires global install)").action(
5174
+ withErrorHandler(async (...args) => {
5175
+ const cmd = args[args.length - 1];
5176
+ const opts = resolveOptions(cmd);
5177
+ const updateCommand = "npm update -g @geolonia/geonicdb-cli";
5178
+ if (opts.dryRun) {
5179
+ printInfo(`[dry-run] ${updateCommand}`);
5180
+ return;
5181
+ }
5182
+ printInfo("Updating @geolonia/geonicdb-cli...");
5183
+ execSync(updateCommand, { stdio: "inherit" });
5184
+ printSuccess("Update complete.");
5185
+ })
5186
+ );
5187
+ addExamples(update, [
5188
+ {
5189
+ description: "Update CLI to the latest version",
5190
+ command: "geonic cli update"
5191
+ },
5192
+ {
5193
+ description: "Preview the update command without executing it",
5194
+ command: "geonic cli update --dry-run"
4697
5195
  }
4698
5196
  ]);
4699
5197
  }