peakurl 0.1.0 → 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 +72 -28
  2. package/bin/peakurl.js +347 -3
  3. package/package.json +17 -4
package/README.md CHANGED
@@ -1,12 +1,12 @@
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
4
 
5
- It gives you a fast way to create, inspect, list, and remove short links from the terminal while keeping default output readable for humans and optional `--json` output friendly for scripts.
5
+ Use `peakurl` to create short links, inspect existing links, and manage your PeakURL account from the terminal.
6
6
 
7
7
  ## Install
8
8
 
9
- Requirements: Node.js 20 or later.
9
+ Node.js 20 or later is required.
10
10
 
11
11
  ```bash
12
12
  npm install -g peakurl
@@ -14,22 +14,35 @@ npm install -g peakurl
14
14
 
15
15
  ## Quick Start
16
16
 
17
+ Sign in with your PeakURL API key:
18
+
17
19
  ```bash
18
- peakurl login --base-url https://peakurl.org --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
19
- peakurl create https://example.com/articles/launch --alias launch --title "Launch Post"
20
+ peakurl login \
21
+ --base-url https://peakurl.org \
22
+ --api-key 0123456789abcdef0123456789abcdef0123456789abcdef
23
+ ```
24
+
25
+ Create and review a short link:
26
+
27
+ ```bash
28
+ peakurl create \
29
+ https://example.com/articles/launch \
30
+ --alias launch \
31
+ --title "Launch Post"
32
+
20
33
  peakurl list
21
34
  peakurl whoami
22
35
  ```
23
36
 
24
37
  ## Authentication
25
38
 
26
- The CLI uses PeakURL bearer API keys and validates them with `GET /api/v1/users/me` before storing them.
39
+ The CLI uses PeakURL bearer API keys and validates them against `GET /api/v1/users/me` before saving credentials.
27
40
 
28
- - `--base-url` accepts either the site root such as `https://peakurl.org` or the API base URL such as `https://peakurl.org/api/v1`
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`
29
42
  - API keys are opaque 48-character hex tokens
30
- - credentials are stored in the standard per-user config location for `peakurl`
43
+ - Credentials are stored in the standard per-user config location for `peakurl`
31
44
 
32
- For CI and automation, you can also use environment variables:
45
+ For CI or automation, you can also authenticate with environment variables:
33
46
 
34
47
  ```bash
35
48
  export PEAKURL_BASE_URL=https://peakurl.org
@@ -38,32 +51,33 @@ export PEAKURL_API_KEY=0123456789abcdef0123456789abcdef0123456789abcdef
38
51
 
39
52
  ## Commands
40
53
 
41
- | Command | Description |
42
- | ------------------------------ | ------------------------------------------- |
43
- | `peakurl login` | Validate and save your PeakURL credentials. |
44
- | `peakurl whoami` | Show the current authenticated account. |
45
- | `peakurl create <url>` | Create a new short link. |
46
- | `peakurl list` | List links in your account. |
47
- | `peakurl get <id-or-alias>` | Fetch a single link by ID or alias. |
48
- | `peakurl delete <id-or-alias>` | Delete a link by ID or alias. |
49
-
50
- ## Common Flags
51
-
52
- - `--json` prints machine-readable JSON output
53
- - `--quiet` minimizes output for scripts
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. |
54
63
 
55
64
  ## Examples
56
65
 
57
66
  Create a short link:
58
67
 
59
68
  ```bash
60
- peakurl create https://example.com --alias example --title "Example"
69
+ peakurl create \
70
+ https://example.com \
71
+ --alias example \
72
+ --title "Example"
61
73
  ```
62
74
 
63
75
  List links as JSON:
64
76
 
65
77
  ```bash
66
- peakurl list --limit 10 --json
78
+ peakurl list \
79
+ --limit 10 \
80
+ --json
67
81
  ```
68
82
 
69
83
  Inspect a link:
@@ -80,9 +94,39 @@ peakurl delete example
80
94
 
81
95
  When `delete` receives an alias or short code, the CLI resolves it to the underlying PeakURL row ID before deleting it.
82
96
 
97
+ Check the latest available CLI version:
98
+
99
+ ```bash
100
+ peakurl update --check
101
+ ```
102
+
103
+ Show the recommended install command:
104
+
105
+ ```bash
106
+ peakurl update
107
+ ```
108
+
109
+ Install the latest version manually:
110
+
111
+ ```bash
112
+ npm install -g peakurl@latest
113
+ ```
114
+
115
+ Disable update notices in the current shell:
116
+
117
+ ```bash
118
+ export PEAKURL_DISABLE_UPDATE_CHECK=1
119
+ ```
120
+
121
+ ## Output
122
+
123
+ - Human-readable output is the default
124
+ - `--json` prints machine-readable JSON where supported
125
+ - `--quiet` minimizes output for scripts
126
+
83
127
  ## Links
84
128
 
85
- - Website: https://peakurl.org/
86
- - API docs: https://peakurl.org/docs/api
87
- - npm package: https://www.npmjs.com/package/peakurl
88
- - Issues: https://github.com/PeakURL/PeakURL-CLI/issues
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,12 +1,25 @@
1
1
  {
2
2
  "name": "peakurl",
3
- "version": "0.1.0",
4
- "description": "PeakURL command-line interface",
5
- "homepage": "https://peakurl.org/",
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
+ },
6
9
  "repository": {
7
10
  "type": "git",
8
11
  "url": "git+https://github.com/PeakURL/PeakURL-CLI.git"
9
12
  },
13
+ "keywords": [
14
+ "peakurl",
15
+ "url shortener",
16
+ "short links",
17
+ "link shortening",
18
+ "cli",
19
+ "command line",
20
+ "terminal",
21
+ "productivity"
22
+ ],
10
23
  "type": "module",
11
24
  "bin": {
12
25
  "peakurl": "bin/peakurl.js"
@@ -22,7 +35,7 @@
22
35
  "dev": "tsx src/index.ts",
23
36
  "format": "prettier --write .",
24
37
  "format:check": "prettier --check .",
25
- "test": "tsx --test test/*.test.ts",
38
+ "test": "npm run build && tsx --test test/*.test.ts",
26
39
  "typecheck": "tsc --noEmit",
27
40
  "prepare": "npm run build"
28
41
  },