@rigkit/cli 0.2.9 → 0.2.11

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.
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { discoverProjectConfigs, resolveConfigPaths } from "./project.ts";
6
6
 
7
7
  describe("CLI project resolution", () => {
8
- test("resolves -chdir to that directory's rig.config.ts", () => {
8
+ test("resolves --chdir to that directory's rig.config.ts", () => {
9
9
  const cwd = mkdtempSync(join(tmpdir(), "rigkit-cli-"));
10
10
  mkdirSync(join(cwd, "example"));
11
11
  writeFileSync(join(cwd, "example", "rig.config.ts"), "export default {}\n");
@@ -15,7 +15,7 @@ describe("CLI project resolution", () => {
15
15
  expect(paths.configPath).toBe(join(cwd, "example", "rig.config.ts"));
16
16
  });
17
17
 
18
- test("resolves -config project root from the config dirname", () => {
18
+ test("resolves --config project root from the config dirname", () => {
19
19
  const cwd = mkdtempSync(join(tmpdir(), "rigkit-cli-"));
20
20
  const paths = resolveConfigPaths({ cwd, config: "machines/platform.ts" });
21
21
 
@@ -40,7 +40,7 @@ describe("CLI project resolution", () => {
40
40
  writeFileSync(join(cwd, "web.rig.config.ts"), "export default {}\n");
41
41
 
42
42
  expect(() => resolveConfigPaths({ cwd })).toThrow(
43
- /Found named Rigkit configs[\s\S]*api\.rig\.config\.ts[\s\S]*web\.rig\.config\.ts[\s\S]*rig -chdir=\. -config=api\.rig\.config\.ts <command>/,
43
+ /Found named Rigkit configs[\s\S]*api\.rig\.config\.ts[\s\S]*web\.rig\.config\.ts[\s\S]*rig --chdir=\. --config=api\.rig\.config\.ts <command>/,
44
44
  );
45
45
  });
46
46
 
package/src/project.ts CHANGED
@@ -159,7 +159,7 @@ function appendConfigFilesHint(
159
159
  options: { commandCwd: string; hint?: ConfigFilesHint },
160
160
  ): string {
161
161
  const hint = options.hint;
162
- if (!hint) return `${message} Run "rig init" or pass -config=<file>.`;
162
+ if (!hint) return `${message} Run "rig init" or pass --config=<file>.`;
163
163
 
164
164
  const configFile = hint.files[0]!;
165
165
  const configPath = displayPath(options.commandCwd, join(hint.dir, configFile));
@@ -171,8 +171,8 @@ function appendConfigFilesHint(
171
171
  ...hint.files.map((file) => `- ${file}`),
172
172
  "",
173
173
  "Choose one explicitly:",
174
- ` rig -config=${configPath} <command>`,
175
- ` rig -chdir=${projectDir} -config=${configFile} <command>`,
174
+ ` rig --config=${configPath} <command>`,
175
+ ` rig --chdir=${projectDir} --config=${configFile} <command>`,
176
176
  ].join("\n");
177
177
  }
178
178
 
@@ -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.9";
1
+ export const RIGKIT_CLI_VERSION = "0.2.11";