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.
- package/README.md +72 -28
- package/bin/peakurl.js +347 -3
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# PeakURL CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official command-line interface for PeakURL.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
43
|
+
- Credentials are stored in the standard per-user config location for `peakurl`
|
|
31
44
|
|
|
32
|
-
For CI
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "PeakURL
|
|
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
|
},
|