peakurl 0.0.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +76 -68
  2. package/bin/peakurl.js +347 -3
  3. package/package.json +18 -4
package/README.md CHANGED
@@ -1,124 +1,132 @@
1
1
  # PeakURL CLI
2
2
 
3
- `peakurl` is the official command-line interface for PeakURL.
3
+ The official command-line interface for PeakURL.
4
+
5
+ Use `peakurl` to create short links, inspect existing links, and manage your PeakURL account from the terminal.
4
6
 
5
7
  ## Install
6
8
 
9
+ Node.js 20 or later is required.
10
+
7
11
  ```bash
8
12
  npm install -g peakurl
9
13
  ```
10
14
 
11
- The npm package name is unscoped and the executable is also `peakurl`.
12
-
13
- ## Authentication
15
+ ## Quick Start
14
16
 
15
- The CLI uses a PeakURL API key and validates it with `GET /api/v1/users/me` before storing it.
16
- PeakURL API keys are opaque 48-character hex bearer tokens, for example `0123456789abcdef0123456789abcdef0123456789abcdef`.
17
- The CLI accepts either the site root URL such as `https://peakurl.org` or the dashboard-exposed API base URL such as `https://peakurl.org/api/v1`.
17
+ Sign in with your PeakURL API key:
18
18
 
19
19
  ```bash
20
- peakurl login --base-url https://peakurl.org --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
20
+ peakurl login \
21
+ --base-url https://peakurl.org \
22
+ --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
21
23
  ```
22
24
 
23
- You can also provide credentials through environment variables, which is useful in CI:
25
+ Create and review a short link:
24
26
 
25
27
  ```bash
26
- export PEAKURL_BASE_URL=https://peakurl.org
27
- export PEAKURL_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef
28
+ peakurl create \
29
+ https://example.com/articles/launch \
30
+ --alias launch \
31
+ --title "Launch Post"
32
+
33
+ peakurl list
34
+ peakurl whoami
28
35
  ```
29
36
 
30
- Stored credentials live in the OS-standard per-user config directory for `peakurl`.
37
+ ## Authentication
31
38
 
32
- ## Commands
39
+ The CLI uses PeakURL bearer API keys and validates them against `GET /api/v1/users/me` before saving credentials.
33
40
 
34
- ### `peakurl login`
41
+ - `--base-url` accepts either the site root, such as `https://peakurl.org`, or the API base URL, such as `https://peakurl.org/api/v1`
42
+ - API keys are opaque 48-character hex tokens
43
+ - Credentials are stored in the standard per-user config location for `peakurl`
44
+
45
+ For CI or automation, you can also authenticate with environment variables:
35
46
 
36
47
  ```bash
37
- peakurl login --base-url https://peakurl.org --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
48
+ export PEAKURL_BASE_URL=https://peakurl.org
49
+ export PEAKURL_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef
38
50
  ```
39
51
 
40
- ### `peakurl whoami`
52
+ ## Commands
41
53
 
42
- ```bash
43
- peakurl whoami
44
- peakurl whoami --json
45
- ```
54
+ | Command | Description |
55
+ | ------------------------------ | ---------------------------------------------------------- |
56
+ | `peakurl login` | Validate and save your PeakURL credentials. |
57
+ | `peakurl whoami` | Show the current authenticated account. |
58
+ | `peakurl create <url>` | Create a new short link. |
59
+ | `peakurl list` | List links in your account. |
60
+ | `peakurl get <id-or-alias>` | Fetch a single link by ID or alias. |
61
+ | `peakurl delete <id-or-alias>` | Delete a link by ID or alias. |
62
+ | `peakurl update` | Show the latest available CLI version and install command. |
46
63
 
47
- ### `peakurl create <url>`
64
+ ## Examples
65
+
66
+ Create a short link:
48
67
 
49
68
  ```bash
50
- peakurl create https://example.com/articles/launch --alias launch --title "Launch Post"
51
- peakurl create https://example.com --json
69
+ peakurl create \
70
+ https://example.com \
71
+ --alias example \
72
+ --title "Example"
52
73
  ```
53
74
 
54
- ### `peakurl list`
75
+ List links as JSON:
55
76
 
56
77
  ```bash
57
- peakurl list
58
- peakurl list --search launch --limit 10 --json
78
+ peakurl list \
79
+ --limit 10 \
80
+ --json
59
81
  ```
60
82
 
61
- ### `peakurl get <id-or-alias>`
83
+ Inspect a link:
62
84
 
63
85
  ```bash
64
- peakurl get launch
65
- peakurl get 681f3b63e7e44c0a1e83aa11 --json
86
+ peakurl get example
66
87
  ```
67
88
 
68
- ### `peakurl delete <id-or-alias>`
89
+ Delete a link:
69
90
 
70
91
  ```bash
71
- peakurl delete launch
72
- peakurl delete 681f3b63e7e44c0a1e83aa11 --quiet
92
+ peakurl delete example
73
93
  ```
74
94
 
75
- When you pass an alias or short code to `delete`, the CLI resolves it to the
76
- stable PeakURL row ID first because the current backend delete route operates on
77
- IDs.
78
-
79
- ## Flags
95
+ When `delete` receives an alias or short code, the CLI resolves it to the underlying PeakURL row ID before deleting it.
80
96
 
81
- Common flags:
97
+ Check the latest available CLI version:
82
98
 
83
- - `--json` prints machine-readable output.
84
- - `--quiet` prints minimal output for scripts.
99
+ ```bash
100
+ peakurl update --check
101
+ ```
85
102
 
86
- Create flags:
103
+ Show the recommended install command:
87
104
 
88
- - `--alias`
89
- - `--title`
90
- - `--password`
91
- - `--status`
92
- - `--expires-at`
93
- - `--utm-source`
94
- - `--utm-medium`
95
- - `--utm-campaign`
96
- - `--utm-term`
97
- - `--utm-content`
105
+ ```bash
106
+ peakurl update
107
+ ```
98
108
 
99
- List flags:
109
+ Install the latest version manually:
100
110
 
101
- - `--page`
102
- - `--limit`
103
- - `--search`
104
- - `--sort-by`
105
- - `--sort-order`
111
+ ```bash
112
+ npm install -g peakurl@latest
113
+ ```
106
114
 
107
- ## Development
115
+ Disable update notices in the current shell:
108
116
 
109
117
  ```bash
110
- npm install
111
- npm run build
112
- npm run typecheck
113
- npm test
118
+ export PEAKURL_DISABLE_UPDATE_CHECK=1
114
119
  ```
115
120
 
116
- Production builds emit the published executable at `bin/peakurl.js`.
121
+ ## Output
117
122
 
118
- ## Release
123
+ - Human-readable output is the default
124
+ - `--json` prints machine-readable JSON where supported
125
+ - `--quiet` minimizes output for scripts
119
126
 
120
- GitHub Actions publishes the CLI to the public npm registry when a GitHub
121
- Release is published.
127
+ ## Links
122
128
 
123
- The workflow expects a repository secret named `NPM_TOKEN` and publishes to
124
- `https://registry.npmjs.org/` with provenance enabled.
129
+ - Website: <https://peakurl.org/>
130
+ - API docs: <https://peakurl.org/docs/api>
131
+ - npm package: <https://www.npmjs.com/package/peakurl>
132
+ - Issues: <https://github.com/PeakURL/PeakURL-CLI/issues>
package/bin/peakurl.js CHANGED
@@ -234,9 +234,18 @@ import { chmod, mkdir, readFile, writeFile } from "fs/promises";
234
234
  import { dirname, join } from "path";
235
235
  import envPaths from "env-paths";
236
236
  var CONFIG_FILENAME = "config.json";
237
+ var STATE_FILENAME = "state.json";
237
238
  function defaultConfigPath() {
238
239
  return join(envPaths("peakurl", { suffix: "" }).config, CONFIG_FILENAME);
239
240
  }
241
+ function defaultStatePath() {
242
+ return join(envPaths("peakurl", { suffix: "" }).config, STATE_FILENAME);
243
+ }
244
+ async function ensureParentDirectory(filePath) {
245
+ const directory = dirname(filePath);
246
+ await mkdir(directory, { recursive: true, mode: 448 });
247
+ return directory;
248
+ }
240
249
  var ConfigStore = class {
241
250
  filePath;
242
251
  /**
@@ -292,8 +301,7 @@ var ConfigStore = class {
292
301
  * @param config Normalized credential set to write.
293
302
  */
294
303
  async save(config) {
295
- const directory = dirname(this.filePath);
296
- await mkdir(directory, { recursive: true, mode: 448 });
304
+ const directory = await ensureParentDirectory(this.filePath);
297
305
  await writeFile(this.filePath, `${JSON.stringify(config, null, 2)}
298
306
  `, {
299
307
  mode: 384
@@ -305,6 +313,46 @@ var ConfigStore = class {
305
313
  }
306
314
  }
307
315
  };
316
+ var StateStore = class {
317
+ filePath;
318
+ /**
319
+ * Creates a state store bound to one on-disk file.
320
+ *
321
+ * @param filePath Optional override used by tests or advanced callers.
322
+ */
323
+ constructor(filePath = defaultStatePath()) {
324
+ this.filePath = filePath;
325
+ }
326
+ /**
327
+ * Loads cached state from disk.
328
+ *
329
+ * Missing or invalid files are treated as empty state because the CLI can
330
+ * always recompute update metadata on the next successful network check.
331
+ *
332
+ * @returns Parsed state object or an empty object.
333
+ */
334
+ async load() {
335
+ try {
336
+ const content = await readFile(this.filePath, "utf8");
337
+ const parsed = JSON.parse(content);
338
+ return parsed && typeof parsed === "object" ? parsed : {};
339
+ } catch {
340
+ return {};
341
+ }
342
+ }
343
+ /**
344
+ * Persists cached state to disk.
345
+ *
346
+ * @param state State payload to save.
347
+ */
348
+ async save(state) {
349
+ await ensureParentDirectory(this.filePath);
350
+ await writeFile(this.filePath, `${JSON.stringify(state, null, 2)}
351
+ `, {
352
+ mode: 384
353
+ });
354
+ }
355
+ };
308
356
 
309
357
  // src/lib/auth.ts
310
358
  function resolveLoginConfig(input, env) {
@@ -476,6 +524,47 @@ function writeStderr(message = "") {
476
524
  process.stderr.write(`${message}
477
525
  `);
478
526
  }
527
+ function writeNoticeBox(title, lines, target = "stderr") {
528
+ const contentLines = lines.length > 0 ? lines : [""];
529
+ const width = Math.max(
530
+ title.length,
531
+ ...contentLines.map((line) => line.length)
532
+ );
533
+ const stream = target === "stdout" ? process.stdout : process.stderr;
534
+ const useTuiBox = stream.isTTY;
535
+ const border = useTuiBox ? {
536
+ topLeft: "\u250C",
537
+ topRight: "\u2510",
538
+ bottomLeft: "\u2514",
539
+ bottomRight: "\u2518",
540
+ horizontal: "\u2500",
541
+ vertical: "\u2502",
542
+ separatorLeft: "\u251C",
543
+ separatorRight: "\u2524"
544
+ } : {
545
+ topLeft: "+",
546
+ topRight: "+",
547
+ bottomLeft: "+",
548
+ bottomRight: "+",
549
+ horizontal: "-",
550
+ vertical: "|",
551
+ separatorLeft: "+",
552
+ separatorRight: "+"
553
+ };
554
+ const topBorder = `${border.topLeft}${border.horizontal.repeat(width + 2)}${border.topRight}`;
555
+ const separator = `${border.separatorLeft}${border.horizontal.repeat(width + 2)}${border.separatorRight}`;
556
+ const bottomBorder = `${border.bottomLeft}${border.horizontal.repeat(width + 2)}${border.bottomRight}`;
557
+ const writeLine = target === "stdout" ? writeStdout : writeStderr;
558
+ writeLine(topBorder);
559
+ writeLine(`${border.vertical} ${title.padEnd(width)} ${border.vertical}`);
560
+ writeLine(separator);
561
+ for (const line of contentLines) {
562
+ writeLine(
563
+ `${border.vertical} ${line.padEnd(width)} ${border.vertical}`
564
+ );
565
+ }
566
+ writeLine(bottomBorder);
567
+ }
479
568
  function writeJson(value) {
480
569
  writeStdout(JSON.stringify(value, null, 2));
481
570
  }
@@ -633,6 +722,244 @@ async function loginCommand(options) {
633
722
  writeStdout(formatUserDetails(response.data));
634
723
  }
635
724
 
725
+ // src/lib/update.ts
726
+ var PACKAGE_NAME = "peakurl";
727
+ var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
728
+ var CACHE_TTL_MS = 1e3 * 60 * 60 * 12;
729
+ var NOTICE_TTL_MS = 1e3 * 60 * 60 * 24;
730
+ function readTimestamp(value) {
731
+ if (!value) {
732
+ return null;
733
+ }
734
+ const parsed = Date.parse(value);
735
+ return Number.isNaN(parsed) ? null : parsed;
736
+ }
737
+ function getRegistryUrl(env) {
738
+ return env.PEAKURL_NPM_REGISTRY_URL?.trim() || DEFAULT_REGISTRY_URL;
739
+ }
740
+ function normalizeRegistryUrl(registryUrl) {
741
+ return registryUrl.replace(/\/+$/, "");
742
+ }
743
+ function parseSemver(version) {
744
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
745
+ version
746
+ );
747
+ if (!match) {
748
+ return null;
749
+ }
750
+ return {
751
+ major: Number.parseInt(match[1], 10),
752
+ minor: Number.parseInt(match[2], 10),
753
+ patch: Number.parseInt(match[3], 10),
754
+ prerelease: match[4] ? match[4].split(".") : []
755
+ };
756
+ }
757
+ function comparePrerelease(left, right) {
758
+ if (left.length === 0 && right.length === 0) {
759
+ return 0;
760
+ }
761
+ if (left.length === 0) {
762
+ return 1;
763
+ }
764
+ if (right.length === 0) {
765
+ return -1;
766
+ }
767
+ const length = Math.max(left.length, right.length);
768
+ for (let index = 0; index < length; index += 1) {
769
+ const leftPart = left[index];
770
+ const rightPart = right[index];
771
+ if (leftPart === void 0) {
772
+ return -1;
773
+ }
774
+ if (rightPart === void 0) {
775
+ return 1;
776
+ }
777
+ const leftNumber = /^\d+$/.test(leftPart) ? Number.parseInt(leftPart, 10) : null;
778
+ const rightNumber = /^\d+$/.test(rightPart) ? Number.parseInt(rightPart, 10) : null;
779
+ if (leftNumber !== null && rightNumber !== null) {
780
+ if (leftNumber !== rightNumber) {
781
+ return leftNumber > rightNumber ? 1 : -1;
782
+ }
783
+ continue;
784
+ }
785
+ if (leftNumber !== null) {
786
+ return -1;
787
+ }
788
+ if (rightNumber !== null) {
789
+ return 1;
790
+ }
791
+ if (leftPart !== rightPart) {
792
+ return leftPart > rightPart ? 1 : -1;
793
+ }
794
+ }
795
+ return 0;
796
+ }
797
+ function compareSemver(left, right) {
798
+ const parsedLeft = parseSemver(left);
799
+ const parsedRight = parseSemver(right);
800
+ if (!parsedLeft || !parsedRight) {
801
+ return left.localeCompare(right, void 0, { numeric: true });
802
+ }
803
+ if (parsedLeft.major !== parsedRight.major) {
804
+ return parsedLeft.major > parsedRight.major ? 1 : -1;
805
+ }
806
+ if (parsedLeft.minor !== parsedRight.minor) {
807
+ return parsedLeft.minor > parsedRight.minor ? 1 : -1;
808
+ }
809
+ if (parsedLeft.patch !== parsedRight.patch) {
810
+ return parsedLeft.patch > parsedRight.patch ? 1 : -1;
811
+ }
812
+ return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease);
813
+ }
814
+ function getInstallCommand() {
815
+ return `npm install -g ${PACKAGE_NAME}@latest`;
816
+ }
817
+ async function fetchLatestVersion(env) {
818
+ const registryUrl = normalizeRegistryUrl(getRegistryUrl(env));
819
+ const url = `${registryUrl}/${PACKAGE_NAME}/latest`;
820
+ const controller = new AbortController();
821
+ const timeout = setTimeout(() => controller.abort(), 2e3);
822
+ try {
823
+ const response = await fetch(url, {
824
+ headers: {
825
+ accept: "application/json"
826
+ },
827
+ signal: controller.signal
828
+ });
829
+ if (!response.ok) {
830
+ return null;
831
+ }
832
+ const payload = await response.json();
833
+ return typeof payload.version === "string" ? payload.version : null;
834
+ } catch {
835
+ return null;
836
+ } finally {
837
+ clearTimeout(timeout);
838
+ }
839
+ }
840
+ async function loadLatestVersionFromState(store) {
841
+ const state = await store.load();
842
+ return state.update ?? {};
843
+ }
844
+ async function saveUpdateState(store, update) {
845
+ const state = await store.load();
846
+ await store.save({
847
+ ...state,
848
+ update
849
+ });
850
+ }
851
+ async function resolveLatestVersion(env, options) {
852
+ const store = options?.store ?? new StateStore();
853
+ const updateState = await loadLatestVersionFromState(store);
854
+ const lastCheckedAt = readTimestamp(updateState.lastCheckedAt);
855
+ const now = Date.now();
856
+ if (!options?.forceRefresh && updateState.latestVersion && lastCheckedAt !== null && now - lastCheckedAt < CACHE_TTL_MS) {
857
+ return updateState.latestVersion;
858
+ }
859
+ const latestVersion = await fetchLatestVersion(env);
860
+ if (!latestVersion) {
861
+ return updateState.latestVersion ?? null;
862
+ }
863
+ await saveUpdateState(store, {
864
+ ...updateState,
865
+ latestVersion,
866
+ lastCheckedAt: new Date(now).toISOString()
867
+ });
868
+ return latestVersion;
869
+ }
870
+ async function getUpdateStatus(currentVersion, env, options) {
871
+ const resolvedLatestVersion = await resolveLatestVersion(env, {
872
+ forceRefresh: options?.forceRefresh,
873
+ store: options?.store
874
+ });
875
+ if (!resolvedLatestVersion && options?.forceRefresh) {
876
+ throw new CliError(
877
+ `Could not reach the npm registry to check the latest ${PACKAGE_NAME} version.`
878
+ );
879
+ }
880
+ const latestVersion = resolvedLatestVersion ?? currentVersion;
881
+ return {
882
+ currentVersion,
883
+ latestVersion,
884
+ isOutdated: compareSemver(currentVersion, latestVersion) < 0,
885
+ installCommand: getInstallCommand()
886
+ };
887
+ }
888
+ function writeUpdateNotice(status) {
889
+ writeNoticeBox("Update Available", [
890
+ `${PACKAGE_NAME} ${status.currentVersion} -> ${status.latestVersion}`,
891
+ `Run: ${status.installCommand}`
892
+ ]);
893
+ }
894
+ async function maybeShowUpdateNotice(options) {
895
+ if (options.env.PEAKURL_DISABLE_UPDATE_CHECK === "1" || options.commandName === "update" || options.options?.json || options.options?.quiet) {
896
+ return;
897
+ }
898
+ if (options.env.PEAKURL_FORCE_UPDATE_NOTICE !== "1" && !process.stderr.isTTY) {
899
+ return;
900
+ }
901
+ const store = new StateStore();
902
+ const updateState = await loadLatestVersionFromState(store);
903
+ const status = await getUpdateStatus(options.currentVersion, options.env, {
904
+ store
905
+ });
906
+ if (!status.isOutdated) {
907
+ return;
908
+ }
909
+ const lastNotifiedAt = readTimestamp(updateState.lastNotifiedAt);
910
+ const alreadyNotifiedForVersion = updateState.lastNotifiedVersion === status.latestVersion;
911
+ if (alreadyNotifiedForVersion && lastNotifiedAt !== null && Date.now() - lastNotifiedAt < NOTICE_TTL_MS) {
912
+ return;
913
+ }
914
+ await saveUpdateState(store, {
915
+ ...updateState,
916
+ lastNotifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
917
+ lastNotifiedVersion: status.latestVersion
918
+ });
919
+ writeUpdateNotice(status);
920
+ }
921
+
922
+ // src/commands/update.ts
923
+ async function updateCommand(options, currentVersion) {
924
+ const status = await getUpdateStatus(currentVersion, process.env, {
925
+ forceRefresh: true
926
+ });
927
+ const payload = {
928
+ success: true,
929
+ message: status.isOutdated ? `A newer PeakURL CLI version is available (${status.latestVersion}).` : `PeakURL CLI ${status.currentVersion} is up to date.`,
930
+ data: {
931
+ currentVersion: status.currentVersion,
932
+ latestVersion: status.latestVersion,
933
+ isOutdated: status.isOutdated,
934
+ installCommand: status.installCommand,
935
+ checkOnly: Boolean(options.check)
936
+ },
937
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
938
+ };
939
+ if (options.json) {
940
+ writeJson(payload);
941
+ return;
942
+ }
943
+ if (!status.isOutdated) {
944
+ if (!options.quiet) {
945
+ writeStdout(`PeakURL CLI ${status.currentVersion} is up to date.`);
946
+ }
947
+ return;
948
+ }
949
+ if (options.quiet) {
950
+ writeStdout(status.installCommand);
951
+ return;
952
+ }
953
+ writeNoticeBox(
954
+ "Update Available",
955
+ [
956
+ `peakurl ${status.currentVersion} -> ${status.latestVersion}`,
957
+ `Run: ${status.installCommand}`
958
+ ],
959
+ "stdout"
960
+ );
961
+ }
962
+
636
963
  // src/commands/whoami.ts
637
964
  async function whoamiCommand(options) {
638
965
  const config = await resolveStoredConfig(process.env);
@@ -669,7 +996,8 @@ async function readVersion() {
669
996
  }
670
997
  async function main() {
671
998
  const program = new Command();
672
- program.name("peakurl").description("PeakURL command-line interface").version(await readVersion()).showHelpAfterError().showSuggestionAfterError().addHelpText(
999
+ const version = await readVersion();
1000
+ program.name("peakurl").description("PeakURL command-line interface").version(version).showHelpAfterError().showSuggestionAfterError().addHelpText(
673
1001
  "after",
674
1002
  `
675
1003
  Examples:
@@ -677,9 +1005,19 @@ Examples:
677
1005
  peakurl whoami --json
678
1006
  peakurl create https://example.com --alias example
679
1007
  peakurl list --limit 10
1008
+ peakurl update --check
680
1009
  peakurl get example
681
1010
  peakurl delete example --quiet`
682
1011
  ).exitOverride();
1012
+ program.hook("preAction", async (_command, actionCommand) => {
1013
+ const options = actionCommand.optsWithGlobals();
1014
+ await maybeShowUpdateNotice({
1015
+ currentVersion: version,
1016
+ commandName: actionCommand.name(),
1017
+ options,
1018
+ env: process.env
1019
+ });
1020
+ });
683
1021
  program.command("login").description(
684
1022
  "Save PeakURL credentials after verifying them with GET /users/me."
685
1023
  ).option(
@@ -694,6 +1032,12 @@ Examples:
694
1032
  program.command("list").description("List PeakURL short links.").option("--page <number>", "Page number", parsePositiveInteger("page")).option("--limit <number>", "Page size", parsePositiveInteger("limit")).option("--search <query>", "Search term").option("--sort-by <field>", "Sort field").option("--sort-order <order>", "Sort order, for example asc or desc").option("--json", "Print machine-readable output").option("--quiet", "Print a minimal per-link value").action(listCommand);
695
1033
  program.command("get").description("Fetch a single PeakURL short link by id or alias.").argument("<id-or-alias>", "Link identifier or alias").option("--json", "Print machine-readable output").option("--quiet", "Print only the short URL").action(getCommand);
696
1034
  program.command("delete").description("Delete a PeakURL short link by id or alias.").argument("<id-or-alias>", "Link identifier or alias").option("--json", "Print machine-readable output").option("--quiet", "Suppress success output").action(deleteCommand);
1035
+ program.command("update").description(
1036
+ "Check for a newer CLI version and print the npm command to install it."
1037
+ ).option(
1038
+ "--check",
1039
+ "Alias for checking update status without changing anything"
1040
+ ).option("--json", "Print machine-readable output").option("--quiet", "Print minimal output").action((options) => updateCommand(options, version));
697
1041
  try {
698
1042
  await program.parseAsync(process.argv);
699
1043
  } catch (error) {
package/package.json CHANGED
@@ -1,11 +1,25 @@
1
1
  {
2
2
  "name": "peakurl",
3
- "version": "0.0.3",
4
- "description": "PeakURL command-line interface",
3
+ "version": "0.1.1",
4
+ "description": "Official CLI for creating, listing, and managing PeakURL short links from the terminal",
5
+ "homepage": "https://peakurl.org",
6
+ "bugs": {
7
+ "url": "https://github.com/PeakURL/PeakURL-CLI/issues"
8
+ },
5
9
  "repository": {
6
10
  "type": "git",
7
- "url": "https://github.com/PeakURL/PeakURL-CLI.git"
11
+ "url": "git+https://github.com/PeakURL/PeakURL-CLI.git"
8
12
  },
13
+ "keywords": [
14
+ "peakurl",
15
+ "url shortener",
16
+ "short links",
17
+ "link shortening",
18
+ "cli",
19
+ "command line",
20
+ "terminal",
21
+ "productivity"
22
+ ],
9
23
  "type": "module",
10
24
  "bin": {
11
25
  "peakurl": "bin/peakurl.js"
@@ -21,7 +35,7 @@
21
35
  "dev": "tsx src/index.ts",
22
36
  "format": "prettier --write .",
23
37
  "format:check": "prettier --check .",
24
- "test": "tsx --test test/*.test.ts",
38
+ "test": "npm run build && tsx --test test/*.test.ts",
25
39
  "typecheck": "tsc --noEmit",
26
40
  "prepare": "npm run build"
27
41
  },