@geolonia/geonicdb-cli 0.1.0 → 0.3.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/README.md CHANGED
@@ -77,6 +77,7 @@ geonic entities list --help
77
77
  | `-f, --format <fmt>` | Output format: `json`, `table`, `geojson` |
78
78
  | `--no-color` | Disable color output |
79
79
  | `-v, --verbose` | Verbose output |
80
+ | `--dry-run` | Print the equivalent `curl` command without executing |
80
81
 
81
82
  Options are resolved in this order (first wins):
82
83
 
@@ -388,6 +389,31 @@ Specify the output format with `--format` or `geonic config set format <fmt>`.
388
389
 
389
390
  Use `--key-values` on `entities list` and `entities get` to request simplified key-value format from the API.
390
391
 
392
+ ## Dry Run
393
+
394
+ Use `--dry-run` on any command to print the equivalent `curl` command instead of executing the request. The output can be copied and run directly in a terminal.
395
+
396
+ ```bash
397
+ $ geonic entities list --type Sensor --dry-run
398
+ curl \
399
+ -H 'Content-Type: application/ld+json' \
400
+ -H 'Accept: application/ld+json' \
401
+ -H 'Authorization: Bearer <token>' \
402
+ 'http://localhost:3000/ngsi-ld/v1/entities?type=Sensor'
403
+ ```
404
+
405
+ Works with all operations including POST with body:
406
+
407
+ ```bash
408
+ $ geonic entities create '{"id":"Room1","type":"Room"}' --dry-run
409
+ curl \
410
+ -X POST \
411
+ -H 'Content-Type: application/ld+json' \
412
+ -H 'Accept: application/ld+json' \
413
+ -d '{"id":"Room1","type":"Room"}' \
414
+ 'http://localhost:3000/ngsi-ld/v1/entities'
415
+ ```
416
+
391
417
  ## Configuration
392
418
 
393
419
  The CLI stores configuration in `~/.config/geonic/config.json`.
package/dist/index.js CHANGED
@@ -585,6 +585,12 @@ function registerConfigCommand(program2) {
585
585
  import { Command } from "commander";
586
586
 
587
587
  // src/client.ts
588
+ var DryRunSignal = class extends Error {
589
+ constructor() {
590
+ super("dry-run");
591
+ this.name = "DryRunSignal";
592
+ }
593
+ };
588
594
  var GdbClient = class _GdbClient {
589
595
  baseUrl;
590
596
  service;
@@ -593,6 +599,7 @@ var GdbClient = class _GdbClient {
593
599
  apiKey;
594
600
  onTokenRefresh;
595
601
  verbose;
602
+ dryRun;
596
603
  refreshPromise;
597
604
  constructor(options) {
598
605
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
@@ -602,6 +609,7 @@ var GdbClient = class _GdbClient {
602
609
  this.apiKey = options.apiKey;
603
610
  this.onTokenRefresh = options.onTokenRefresh;
604
611
  this.verbose = options.verbose ?? false;
612
+ this.dryRun = options.dryRun ?? false;
605
613
  }
606
614
  buildHeaders(extra) {
607
615
  const headers = {};
@@ -682,6 +690,28 @@ var GdbClient = class _GdbClient {
682
690
  });
683
691
  process.stderr.write("\n");
684
692
  }
693
+ static shellQuote(value) {
694
+ return `'${value.split("'").join(`'"'"'`)}'`;
695
+ }
696
+ static buildCurlCommand(method, url, headers, body) {
697
+ const parts = ["curl"];
698
+ if (method !== "GET") {
699
+ parts.push(`-X ${method}`);
700
+ }
701
+ for (const [key, value] of Object.entries(headers)) {
702
+ parts.push(`-H ${_GdbClient.shellQuote(`${key}: ${value}`)}`);
703
+ }
704
+ if (body) {
705
+ parts.push(`-d ${_GdbClient.shellQuote(body)}`);
706
+ }
707
+ parts.push(_GdbClient.shellQuote(url));
708
+ return parts.join(" \\\n ");
709
+ }
710
+ handleDryRun(method, url, headers, body) {
711
+ if (!this.dryRun) return;
712
+ console.log(_GdbClient.buildCurlCommand(method, url, headers, body));
713
+ throw new DryRunSignal();
714
+ }
685
715
  canRefresh() {
686
716
  return !!this.refreshToken && !this.apiKey;
687
717
  }
@@ -721,6 +751,7 @@ var GdbClient = class _GdbClient {
721
751
  const headers = this.buildHeaders(options?.headers);
722
752
  const body = options?.body ? JSON.stringify(options.body) : void 0;
723
753
  this.logRequest(method, url, headers, body);
754
+ this.handleDryRun(method, url, headers, body);
724
755
  const response = await fetch(url, { method, headers, body });
725
756
  this.logResponse(response);
726
757
  const countHeader = response.headers.get("NGSILD-Results-Count");
@@ -745,6 +776,7 @@ var GdbClient = class _GdbClient {
745
776
  const headers = this.buildHeaders(options?.headers);
746
777
  const body = options?.body ? JSON.stringify(options.body) : void 0;
747
778
  this.logRequest(method, url, headers, body);
779
+ this.handleDryRun(method, url, headers, body);
748
780
  const response = await fetch(url, { method, headers, body });
749
781
  this.logResponse(response);
750
782
  let data;
@@ -826,7 +858,8 @@ function resolveOptions(cmd) {
826
858
  color: opts.color,
827
859
  verbose: opts.verbose,
828
860
  profile: opts.profile,
829
- apiKey: opts.apiKey ?? process.env.GDB_API_KEY ?? config.apiKey
861
+ apiKey: opts.apiKey ?? process.env.GDB_API_KEY ?? config.apiKey,
862
+ dryRun: opts.dryRun
830
863
  };
831
864
  }
832
865
  function createClient(cmd) {
@@ -851,7 +884,8 @@ function createClient(cmd) {
851
884
  if (refreshToken) cfg.refreshToken = refreshToken;
852
885
  saveConfig(cfg, opts.profile);
853
886
  },
854
- verbose: opts.verbose
887
+ verbose: opts.verbose,
888
+ dryRun: opts.dryRun
855
889
  });
856
890
  }
857
891
  function getFormat(cmd) {
@@ -871,6 +905,9 @@ function withErrorHandler(fn) {
871
905
  try {
872
906
  await fn(...args);
873
907
  } catch (err) {
908
+ if (err instanceof DryRunSignal) {
909
+ return;
910
+ }
874
911
  if (err instanceof GdbClientError && err.status === 401) {
875
912
  printError("Authentication failed. Please run `geonic login` to re-authenticate.");
876
913
  } else if (err instanceof Error) {
@@ -1052,7 +1089,6 @@ async function clientCredentialsGrant(options) {
1052
1089
  }
1053
1090
 
1054
1091
  // src/commands/auth.ts
1055
- import chalk3 from "chalk";
1056
1092
  function createLoginCommand() {
1057
1093
  return new Command("login").description("Authenticate and save token").option("--client-credentials", "Use OAuth 2.0 Client Credentials flow").option("--client-id <id>", "OAuth client ID").option("--client-secret <secret>", "OAuth client secret").option("--scope <scopes>", "OAuth scopes (space-separated)").option("--tenant-id <id>", "Tenant ID for scoped authentication").action(
1058
1094
  withErrorHandler(async (...args) => {
@@ -1163,7 +1199,7 @@ function createMeAction() {
1163
1199
  const status = getTokenStatus(latestConfig.token);
1164
1200
  if (status.expiresAt) {
1165
1201
  if (status.isExpired) {
1166
- console.log(chalk3.red(`Token expires: ${status.expiresAt.toISOString()} (expired)`));
1202
+ printError(`Token expires: ${status.expiresAt.toISOString()} (expired)`);
1167
1203
  } else if (status.isExpiringSoon) {
1168
1204
  printWarning(
1169
1205
  `Token expires: ${status.expiresAt.toISOString()} (${formatDuration(status.remainingMs)} remaining)`
@@ -1537,15 +1573,13 @@ function registerEntitiesCommand(program2) {
1537
1573
  if (opts.attrs) params.attrs = String(opts.attrs);
1538
1574
  if (opts.georel) params.georel = String(opts.georel);
1539
1575
  if (opts.geometry) params.geometry = String(opts.geometry);
1540
- if (opts.coords) params.coords = String(opts.coords);
1576
+ if (opts.coords) params.coordinates = String(opts.coords);
1541
1577
  if (opts.spatialId) params.spatialId = String(opts.spatialId);
1542
1578
  if (opts.limit !== void 0) params.limit = String(opts.limit);
1543
1579
  if (opts.offset !== void 0) params.offset = String(opts.offset);
1544
1580
  if (opts.orderBy) params.orderBy = String(opts.orderBy);
1545
- if (opts.count) params.options = "count";
1546
- if (opts.keyValues) {
1547
- params.options = params.options ? `${params.options},keyValues` : "keyValues";
1548
- }
1581
+ if (opts.count) params.count = "true";
1582
+ if (opts.keyValues) params.options = "keyValues";
1549
1583
  const response = await client.get("/entities", params);
1550
1584
  outputResponse(response, format, !!opts.count);
1551
1585
  })
@@ -1861,7 +1895,7 @@ function registerSubscriptionsCommand(program2) {
1861
1895
  const params = {};
1862
1896
  if (cmdOpts.limit !== void 0) params["limit"] = String(cmdOpts.limit);
1863
1897
  if (cmdOpts.offset !== void 0) params["offset"] = String(cmdOpts.offset);
1864
- if (cmdOpts.count) params["options"] = "count";
1898
+ if (cmdOpts.count) params["count"] = "true";
1865
1899
  const response = await client.get("/subscriptions", params);
1866
1900
  outputResponse(response, format, !!cmdOpts.count);
1867
1901
  })
@@ -1965,8 +1999,8 @@ function registerRegistrationsCommand(program2) {
1965
1999
  const params = {};
1966
2000
  if (cmdOpts.limit !== void 0) params["limit"] = String(cmdOpts.limit);
1967
2001
  if (cmdOpts.offset !== void 0) params["offset"] = String(cmdOpts.offset);
1968
- if (cmdOpts.count) params["options"] = "count";
1969
- const response = await client.get("/registrations", params);
2002
+ if (cmdOpts.count) params["count"] = "true";
2003
+ const response = await client.get("/csourceRegistrations", params);
1970
2004
  outputResponse(response, format, !!cmdOpts.count);
1971
2005
  })
1972
2006
  );
@@ -1985,7 +2019,7 @@ function registerRegistrationsCommand(program2) {
1985
2019
  const client = createClient(cmd);
1986
2020
  const format = getFormat(cmd);
1987
2021
  const response = await client.get(
1988
- `/registrations/${encodeURIComponent(String(id))}`
2022
+ `/csourceRegistrations/${encodeURIComponent(String(id))}`
1989
2023
  );
1990
2024
  outputResponse(response, format);
1991
2025
  })
@@ -2001,7 +2035,7 @@ function registerRegistrationsCommand(program2) {
2001
2035
  const client = createClient(cmd);
2002
2036
  const format = getFormat(cmd);
2003
2037
  const data = await parseJsonInput(json);
2004
- const response = await client.post("/registrations", data);
2038
+ const response = await client.post("/csourceRegistrations", data);
2005
2039
  outputResponse(response, format);
2006
2040
  printSuccess("Registration created.");
2007
2041
  })
@@ -2019,7 +2053,7 @@ function registerRegistrationsCommand(program2) {
2019
2053
  const format = getFormat(cmd);
2020
2054
  const data = await parseJsonInput(json);
2021
2055
  const response = await client.patch(
2022
- `/registrations/${encodeURIComponent(String(id))}`,
2056
+ `/csourceRegistrations/${encodeURIComponent(String(id))}`,
2023
2057
  data
2024
2058
  );
2025
2059
  outputResponse(response, format);
@@ -2037,7 +2071,7 @@ function registerRegistrationsCommand(program2) {
2037
2071
  withErrorHandler(async (id, _opts, cmd) => {
2038
2072
  const client = createClient(cmd);
2039
2073
  await client.delete(
2040
- `/registrations/${encodeURIComponent(String(id))}`
2074
+ `/csourceRegistrations/${encodeURIComponent(String(id))}`
2041
2075
  );
2042
2076
  printSuccess("Registration deleted.");
2043
2077
  })
@@ -2102,14 +2136,14 @@ function createListAction() {
2102
2136
  if (cmdOpts.query) params["q"] = cmdOpts.query;
2103
2137
  if (cmdOpts.georel) params["georel"] = cmdOpts.georel;
2104
2138
  if (cmdOpts.geometry) params["geometry"] = cmdOpts.geometry;
2105
- if (cmdOpts.coords) params["coords"] = cmdOpts.coords;
2139
+ if (cmdOpts.coords) params["coordinates"] = cmdOpts.coords;
2106
2140
  if (cmdOpts.timeRel) params["timerel"] = cmdOpts.timeRel;
2107
2141
  if (cmdOpts.timeAt) params["timeAt"] = cmdOpts.timeAt;
2108
2142
  if (cmdOpts.endTimeAt) params["endTimeAt"] = cmdOpts.endTimeAt;
2109
2143
  if (cmdOpts.lastN !== void 0) params["lastN"] = String(cmdOpts.lastN);
2110
2144
  if (cmdOpts.limit !== void 0) params["limit"] = String(cmdOpts.limit);
2111
2145
  if (cmdOpts.offset !== void 0) params["offset"] = String(cmdOpts.offset);
2112
- if (cmdOpts.count) params["options"] = "count";
2146
+ if (cmdOpts.count) params["count"] = "true";
2113
2147
  const response = await client.get("/temporal/entities", params);
2114
2148
  outputResponse(response, format, cmdOpts.count);
2115
2149
  });
@@ -3238,8 +3272,8 @@ function generateCompletions(program2, line, point) {
3238
3272
  i++;
3239
3273
  }
3240
3274
  if (currentCmd.name() === "help" && currentCmd.parent === program2) {
3241
- const helpIdx = walkTokens.findIndex((t) => t === "help");
3242
- const rawArgs = helpIdx >= 0 ? walkTokens.slice(helpIdx + 1) : [];
3275
+ const helpIdx = walkTokens.indexOf("help");
3276
+ const rawArgs = walkTokens.slice(helpIdx + 1);
3243
3277
  const helpArgs = [];
3244
3278
  for (let j = 0; j < rawArgs.length; j++) {
3245
3279
  const t = rawArgs[j];
@@ -3382,7 +3416,7 @@ function createProgram() {
3382
3416
  const require2 = createRequire3(import.meta.url);
3383
3417
  const pkg = require2("../package.json");
3384
3418
  const program2 = new Command2();
3385
- program2.name("geonic").description(pkg.description).option("-u, --url <url>", "Base URL of the GeonicDB server").option("-s, --service <name>", "NGSILD-Tenant header").option("--token <token>", "Authentication token").option("-p, --profile <name>", "Use a named profile").option("--api-key <key>", "API key for authentication").option("-f, --format <fmt>", "Output format: json, table, geojson").option("--no-color", "Disable color output").option("-v, --verbose", "Verbose output");
3419
+ program2.name("geonic").description(pkg.description).option("-u, --url <url>", "Base URL of the GeonicDB server").option("-s, --service <name>", "NGSILD-Tenant header").option("--token <token>", "Authentication token").option("-p, --profile <name>", "Use a named profile").option("--api-key <key>", "API key for authentication").option("-f, --format <fmt>", "Output format: json, table, geojson").option("--no-color", "Disable color output").option("-v, --verbose", "Verbose output").option("--dry-run", "Print the equivalent curl command without executing");
3386
3420
  registerHelpCommand(program2);
3387
3421
  registerConfigCommand(program2);
3388
3422
  registerAuthCommands(program2);
@@ -3407,7 +3441,141 @@ function createProgram() {
3407
3441
  return program2;
3408
3442
  }
3409
3443
 
3444
+ // src/update-notifier.ts
3445
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3446
+ import { join as join2 } from "path";
3447
+ import chalk3 from "chalk";
3448
+ var PACKAGE_NAME = "@geolonia/geonicdb-cli";
3449
+ var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
3450
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3451
+ var FETCH_TIMEOUT_MS = 5e3;
3452
+ var CI_ENV_VARS = [
3453
+ "CI",
3454
+ "CONTINUOUS_INTEGRATION",
3455
+ "BUILD_NUMBER",
3456
+ "GITHUB_ACTIONS",
3457
+ "GITLAB_CI",
3458
+ "CIRCLECI",
3459
+ "JENKINS_URL",
3460
+ "HUDSON_URL",
3461
+ "TRAVIS"
3462
+ ];
3463
+ function getCacheFile() {
3464
+ return join2(getConfigDir(), "update-check.json");
3465
+ }
3466
+ function isCheckDisabled() {
3467
+ if (process.env.NO_UPDATE_NOTIFIER) return true;
3468
+ if (!process.stdout.isTTY) return true;
3469
+ for (const envVar of CI_ENV_VARS) {
3470
+ if (process.env[envVar]) return true;
3471
+ }
3472
+ return false;
3473
+ }
3474
+ function loadCache() {
3475
+ try {
3476
+ const raw = readFileSync3(getCacheFile(), "utf-8");
3477
+ return JSON.parse(raw);
3478
+ } catch {
3479
+ return null;
3480
+ }
3481
+ }
3482
+ function saveCache(cache) {
3483
+ try {
3484
+ ensureConfigDir();
3485
+ writeFileSync2(getCacheFile(), JSON.stringify(cache), "utf-8");
3486
+ } catch {
3487
+ }
3488
+ }
3489
+ function shouldCheck(cache) {
3490
+ if (!cache) return true;
3491
+ return Date.now() - cache.lastCheck >= CHECK_INTERVAL_MS;
3492
+ }
3493
+ function compareSemver(current, latest) {
3494
+ const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
3495
+ const [cMajor, cMinor, cPatch] = parse(current);
3496
+ const [lMajor, lMinor, lPatch] = parse(latest);
3497
+ if (lMajor !== cMajor) return lMajor > cMajor;
3498
+ if (lMinor !== cMinor) return lMinor > cMinor;
3499
+ return lPatch > cPatch;
3500
+ }
3501
+ async function fetchLatestVersion() {
3502
+ try {
3503
+ const res = await fetch(REGISTRY_URL, {
3504
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
3505
+ });
3506
+ if (!res.ok) return null;
3507
+ const data = await res.json();
3508
+ return data.version ?? null;
3509
+ } catch {
3510
+ return null;
3511
+ }
3512
+ }
3513
+ function getCurrentVersion() {
3514
+ return process.env.npm_package_version ?? "0.0.0";
3515
+ }
3516
+ function formatUpdateBox(current, latest) {
3517
+ const message = `Update available: ${current} \u2192 ${latest}`;
3518
+ const install = `Run ${chalk3.cyan(`npm i -g ${PACKAGE_NAME}`)} to update`;
3519
+ const lines = [message, install];
3520
+ const maxLen = Math.max(
3521
+ ...lines.map((l) => stripAnsi(l).length)
3522
+ );
3523
+ const pad = (line) => {
3524
+ const visible = stripAnsi(line).length;
3525
+ return line + " ".repeat(maxLen - visible);
3526
+ };
3527
+ const empty = " ".repeat(maxLen);
3528
+ const top = `\u256D${"\u2500".repeat(maxLen + 4)}\u256E`;
3529
+ const bottom = `\u2570${"\u2500".repeat(maxLen + 4)}\u256F`;
3530
+ const boxLines = [
3531
+ top,
3532
+ `\u2502 ${empty} \u2502`,
3533
+ ...lines.map((l) => `\u2502 ${pad(l)} \u2502`),
3534
+ `\u2502 ${empty} \u2502`,
3535
+ bottom
3536
+ ];
3537
+ return chalk3.yellow(boxLines.join("\n"));
3538
+ }
3539
+ function stripAnsi(str) {
3540
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
3541
+ }
3542
+ async function startUpdateCheck() {
3543
+ if (isCheckDisabled()) return null;
3544
+ let currentVersion;
3545
+ try {
3546
+ const pkgPath = new URL("../package.json", import.meta.url);
3547
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
3548
+ currentVersion = pkg.version;
3549
+ } catch {
3550
+ currentVersion = getCurrentVersion();
3551
+ }
3552
+ const cache = loadCache();
3553
+ if (!shouldCheck(cache)) {
3554
+ if (cache?.latestVersion && compareSemver(currentVersion, cache.latestVersion)) {
3555
+ return { currentVersion, latestVersion: cache.latestVersion };
3556
+ }
3557
+ return null;
3558
+ }
3559
+ const latestVersion = await fetchLatestVersion();
3560
+ saveCache({
3561
+ lastCheck: Date.now(),
3562
+ latestVersion: latestVersion ?? cache?.latestVersion
3563
+ });
3564
+ if (latestVersion && compareSemver(currentVersion, latestVersion)) {
3565
+ return { currentVersion, latestVersion };
3566
+ }
3567
+ return null;
3568
+ }
3569
+ function printUpdateNotification(result) {
3570
+ if (!result) return;
3571
+ const box = formatUpdateBox(result.currentVersion, result.latestVersion);
3572
+ process.stderr.write("\n" + box + "\n");
3573
+ }
3574
+
3410
3575
  // src/index.ts
3576
+ var updateCheckPromise = startUpdateCheck();
3411
3577
  var program = createProgram();
3412
- program.parse();
3578
+ await program.parseAsync();
3579
+ var updateResult = await updateCheckPromise;
3580
+ printUpdateNotification(updateResult);
3413
3581
  //# sourceMappingURL=index.js.map