@rigkit/cli 0.2.8 → 0.2.10

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.
@@ -0,0 +1,224 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { defaultRigkitHome } from "@rigkit/runtime-client";
4
+ import * as ui from "./ui.ts";
5
+
6
+ type NoticeStream = {
7
+ isTTY?: boolean;
8
+ write(chunk: string): unknown;
9
+ };
10
+
11
+ type LatestRelease = {
12
+ version: string;
13
+ tag?: string;
14
+ installerUrl?: string;
15
+ releaseUrl?: string;
16
+ };
17
+
18
+ type UpdateCache = {
19
+ checkedAt: string;
20
+ updateUrl: string;
21
+ latest?: LatestRelease;
22
+ };
23
+
24
+ type UpdateCheckOptions = {
25
+ commandName?: string;
26
+ json: boolean;
27
+ currentVersion: string;
28
+ stream?: NoticeStream;
29
+ };
30
+
31
+ const DEFAULT_UPDATE_URL = "https://www.rigkit.dev/latest.json";
32
+ const DEFAULT_INSTALL_URL = "https://www.rigkit.dev/install";
33
+ const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
34
+ const DEFAULT_TIMEOUT_MS = 900;
35
+
36
+ export async function maybePrintUpdateNotice(options: UpdateCheckOptions): Promise<void> {
37
+ const stream = options.stream ?? process.stderr;
38
+ if (!shouldCheckForUpdates(options, stream)) return;
39
+
40
+ const updateUrl = process.env.RIGKIT_UPDATE_URL?.trim() || DEFAULT_UPDATE_URL;
41
+ const latest = await resolveLatestRelease(updateUrl);
42
+ if (!latest || !isNewerVersion(latest.version, options.currentVersion)) return;
43
+
44
+ stream.write(renderUpdateNotice({
45
+ currentVersion: options.currentVersion,
46
+ latest,
47
+ }));
48
+ }
49
+
50
+ function shouldCheckForUpdates(options: UpdateCheckOptions, stream: NoticeStream): boolean {
51
+ if (options.json) return false;
52
+ if (options.commandName === "completion") return false;
53
+
54
+ const mode = normalizeUpdateCheckMode(process.env.RIGKIT_UPDATE_CHECK);
55
+ if (mode === "off") return false;
56
+ if (mode === "force") return true;
57
+ if (process.env.CI) return false;
58
+ return Boolean(stream.isTTY);
59
+ }
60
+
61
+ function normalizeUpdateCheckMode(value: string | undefined): "auto" | "force" | "off" {
62
+ switch (value?.trim().toLowerCase()) {
63
+ case "0":
64
+ case "false":
65
+ case "no":
66
+ case "off":
67
+ return "off";
68
+ case "1":
69
+ case "true":
70
+ case "yes":
71
+ case "on":
72
+ case "force":
73
+ case "always":
74
+ return "force";
75
+ default:
76
+ return "auto";
77
+ }
78
+ }
79
+
80
+ async function resolveLatestRelease(updateUrl: string): Promise<LatestRelease | undefined> {
81
+ const cached = readUpdateCache(updateUrl);
82
+ if (cached && isFreshCache(cached)) return cached.latest;
83
+
84
+ const latest = await fetchLatestRelease(updateUrl);
85
+ if (latest) {
86
+ writeUpdateCache({ checkedAt: new Date().toISOString(), updateUrl, latest });
87
+ return latest;
88
+ }
89
+
90
+ return cached?.latest;
91
+ }
92
+
93
+ function readUpdateCache(updateUrl: string): UpdateCache | undefined {
94
+ const path = updateCachePath();
95
+ if (!existsSync(path)) return undefined;
96
+
97
+ try {
98
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as UpdateCache;
99
+ if (parsed.updateUrl !== updateUrl || !parsed.latest?.version) return undefined;
100
+ return parsed;
101
+ } catch {
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ function isFreshCache(cache: UpdateCache): boolean {
107
+ const checkedAt = Date.parse(cache.checkedAt);
108
+ if (!Number.isFinite(checkedAt)) return false;
109
+ return Date.now() - checkedAt < CHECK_INTERVAL_MS;
110
+ }
111
+
112
+ async function fetchLatestRelease(updateUrl: string): Promise<LatestRelease | undefined> {
113
+ const timeoutMs = updateTimeoutMs();
114
+ const controller = new AbortController();
115
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
116
+
117
+ try {
118
+ const response = await fetch(updateUrl, {
119
+ headers: { "Accept": "application/json" },
120
+ signal: controller.signal,
121
+ });
122
+ if (!response.ok) return undefined;
123
+
124
+ const body = await response.json() as Partial<LatestRelease>;
125
+ if (typeof body.version !== "string" || body.version.trim() === "") return undefined;
126
+ return {
127
+ version: body.version.trim(),
128
+ tag: typeof body.tag === "string" ? body.tag : undefined,
129
+ installerUrl: typeof body.installerUrl === "string" ? body.installerUrl : undefined,
130
+ releaseUrl: typeof body.releaseUrl === "string" ? body.releaseUrl : undefined,
131
+ };
132
+ } catch {
133
+ return undefined;
134
+ } finally {
135
+ clearTimeout(timeout);
136
+ }
137
+ }
138
+
139
+ function writeUpdateCache(cache: UpdateCache): void {
140
+ const path = updateCachePath();
141
+ try {
142
+ mkdirSync(dirname(path), { recursive: true });
143
+ writeFileSync(path, `${JSON.stringify(cache, null, 2)}\n`);
144
+ } catch {
145
+ // Update checks are advisory. Cache failures should never affect a command.
146
+ }
147
+ }
148
+
149
+ function updateCachePath(): string {
150
+ return join(defaultRigkitHome(), "update-check.json");
151
+ }
152
+
153
+ function updateTimeoutMs(): number {
154
+ const parsed = Number(process.env.RIGKIT_UPDATE_TIMEOUT_MS);
155
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
156
+ }
157
+
158
+ function renderUpdateNotice(input: { currentVersion: string; latest: LatestRelease }): string {
159
+ const installUrl = input.latest.installerUrl || DEFAULT_INSTALL_URL;
160
+ return [
161
+ "",
162
+ `${ui.warn("!")} ${ui.bold(`rig ${input.latest.version} is available`)} ${ui.dim(`(current ${input.currentVersion})`)}`,
163
+ ui.hint(`update with: curl -fsSL ${installUrl} | sh`),
164
+ ].join("\n") + "\n";
165
+ }
166
+
167
+ function isNewerVersion(candidate: string, current: string): boolean {
168
+ const parsedCandidate = parseSemver(candidate);
169
+ const parsedCurrent = parseSemver(current);
170
+ if (!parsedCandidate || !parsedCurrent) return false;
171
+ return compareSemver(parsedCandidate, parsedCurrent) > 0;
172
+ }
173
+
174
+ type Semver = {
175
+ major: number;
176
+ minor: number;
177
+ patch: number;
178
+ prerelease: string[];
179
+ };
180
+
181
+ function parseSemver(value: string): Semver | undefined {
182
+ const match = value.trim().replace(/^v/, "").match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
183
+ if (!match) return undefined;
184
+ return {
185
+ major: Number(match[1]),
186
+ minor: Number(match[2]),
187
+ patch: Number(match[3]),
188
+ prerelease: match[4] ? match[4].split(".") : [],
189
+ };
190
+ }
191
+
192
+ function compareSemver(left: Semver, right: Semver): number {
193
+ for (const key of ["major", "minor", "patch"] as const) {
194
+ const delta = left[key] - right[key];
195
+ if (delta !== 0) return delta;
196
+ }
197
+
198
+ if (left.prerelease.length === 0 && right.prerelease.length > 0) return 1;
199
+ if (left.prerelease.length > 0 && right.prerelease.length === 0) return -1;
200
+
201
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
202
+ for (let index = 0; index < length; index += 1) {
203
+ const leftPart = left.prerelease[index];
204
+ const rightPart = right.prerelease[index];
205
+ if (leftPart === undefined && rightPart === undefined) return 0;
206
+ if (leftPart === undefined) return -1;
207
+ if (rightPart === undefined) return 1;
208
+
209
+ const leftNumeric = /^\d+$/.test(leftPart);
210
+ const rightNumeric = /^\d+$/.test(rightPart);
211
+ if (leftNumeric && rightNumeric) {
212
+ const delta = Number(leftPart) - Number(rightPart);
213
+ if (delta !== 0) return delta;
214
+ continue;
215
+ }
216
+ if (leftNumeric) return -1;
217
+ if (rightNumeric) return 1;
218
+
219
+ const delta = leftPart.localeCompare(rightPart);
220
+ if (delta !== 0) return delta;
221
+ }
222
+
223
+ return 0;
224
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const RIGKIT_CLI_VERSION = "0.2.8";
1
+ export const RIGKIT_CLI_VERSION = "0.2.10";