quotacap 0.0.25 → 0.0.27
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/dist/cli/advise.js +6 -1
- package/dist/cli/status.js +6 -1
- package/dist/cli/takeover.js +1 -1
- package/dist/cli/update.js +23 -5
- package/dist/runtime/updates.d.ts +20 -0
- package/dist/runtime/updates.js +121 -21
- package/dist/service/index.js +8 -2
- package/dist/service/macos.d.ts +1 -0
- package/dist/service/macos.js +10 -0
- package/dist/service/systemd.d.ts +1 -0
- package/dist/service/systemd.js +10 -0
- package/dist/version.js +1 -1
- package/dist/web/src/App.js +2 -2
- package/dist/web/src/components/PacingLegend.d.ts +3 -1
- package/dist/web/src/components/PacingLegend.js +3 -2
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-ClNhsr1-.js → index-hmABfkqB.js} +1 -1
- package/web/dist/index.html +1 -1
package/dist/cli/advise.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ensureConfig, getDbPath } from "../config.js";
|
|
|
4
4
|
import { createServiceClient } from "../runtime/client.js";
|
|
5
5
|
import { VERSION } from "../version.js";
|
|
6
6
|
import { isServiceManaged, runServiceCommand } from "../service/index.js";
|
|
7
|
-
import { detectChannel, printUpdateFooter, readUpdateCache, refreshUpdateCache, updateFooter, } from "../runtime/updates.js";
|
|
7
|
+
import { detectChannel, printUpdateFooter, printVersionLine, readUpdateCache, refreshUpdateCache, updateFooter, } from "../runtime/updates.js";
|
|
8
8
|
import { ClientError, OFFLINE_LABEL, emptySnapshot, offlineOnlyClient, resolveSnapshot } from "./snapshot-source.js";
|
|
9
9
|
import { checkSkew, execSkewWarning, olderCliWarning, takeoverManaged, unmanagedSkewWarning, } from "./takeover.js";
|
|
10
10
|
export function registerAdviseCommand(program, deps) {
|
|
@@ -67,6 +67,9 @@ export function registerAdviseCommand(program, deps) {
|
|
|
67
67
|
...takeoverOpts,
|
|
68
68
|
});
|
|
69
69
|
console.error(r.message);
|
|
70
|
+
// The takeover verified the daemon now reports the CLI version;
|
|
71
|
+
// keep the pre-check health from mislabeling the version line.
|
|
72
|
+
health.version = VERSION;
|
|
70
73
|
}
|
|
71
74
|
catch (e) {
|
|
72
75
|
console.error(e instanceof Error ? e.message : String(e));
|
|
@@ -108,6 +111,7 @@ export function registerAdviseCommand(program, deps) {
|
|
|
108
111
|
console.log(JSON.stringify(rec, null, 2));
|
|
109
112
|
else {
|
|
110
113
|
console.log(`${rec.use}: ${rec.reason}`);
|
|
114
|
+
printVersionLine(o, VERSION, health?.version);
|
|
111
115
|
printUpdateFooter(o, footer);
|
|
112
116
|
}
|
|
113
117
|
return;
|
|
@@ -123,6 +127,7 @@ export function registerAdviseCommand(program, deps) {
|
|
|
123
127
|
console.log(JSON.stringify(rec, null, 2));
|
|
124
128
|
else {
|
|
125
129
|
console.log(`${rec.use}: ${rec.reason}`);
|
|
130
|
+
printVersionLine(o, VERSION, health?.version);
|
|
126
131
|
printUpdateFooter(o, footer);
|
|
127
132
|
}
|
|
128
133
|
});
|
package/dist/cli/status.js
CHANGED
|
@@ -6,7 +6,7 @@ import { assertValidSortKey } from "../format/rows.js";
|
|
|
6
6
|
import { createServiceClient } from "../runtime/client.js";
|
|
7
7
|
import { VERSION } from "../version.js";
|
|
8
8
|
import { isServiceManaged, runServiceCommand } from "../service/index.js";
|
|
9
|
-
import { detectChannel, printUpdateFooter, readUpdateCache, refreshUpdateCache, updateFooter, } from "../runtime/updates.js";
|
|
9
|
+
import { detectChannel, printUpdateFooter, printVersionLine, readUpdateCache, refreshUpdateCache, updateFooter, } from "../runtime/updates.js";
|
|
10
10
|
import { ClientError, OFFLINE_LABEL, offlineOnlyClient, resolveSnapshot } from "./snapshot-source.js";
|
|
11
11
|
import { checkSkew, execSkewWarning, olderCliWarning, takeoverManaged, unmanagedSkewWarning, } from "./takeover.js";
|
|
12
12
|
const WIDE_COLUMNS = 100;
|
|
@@ -76,6 +76,9 @@ export function registerStatusCommand(program, deps) {
|
|
|
76
76
|
...takeoverOpts,
|
|
77
77
|
});
|
|
78
78
|
console.error(r.message);
|
|
79
|
+
// The takeover verified the daemon now reports the CLI version;
|
|
80
|
+
// keep the pre-check health from mislabeling the version line.
|
|
81
|
+
health.version = VERSION;
|
|
79
82
|
}
|
|
80
83
|
catch (e) {
|
|
81
84
|
console.error(e instanceof Error ? e.message : String(e));
|
|
@@ -122,6 +125,7 @@ export function registerStatusCommand(program, deps) {
|
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
124
127
|
console.log(e.message);
|
|
128
|
+
printVersionLine(o, VERSION, health?.version);
|
|
125
129
|
printUpdateFooter(o, footer);
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
@@ -153,6 +157,7 @@ export function registerStatusCommand(program, deps) {
|
|
|
153
157
|
if (issues)
|
|
154
158
|
console.log("\n" + issues);
|
|
155
159
|
}
|
|
160
|
+
printVersionLine(o, VERSION, health?.version);
|
|
156
161
|
printUpdateFooter(o, footer);
|
|
157
162
|
});
|
|
158
163
|
}
|
package/dist/cli/takeover.js
CHANGED
|
@@ -119,7 +119,7 @@ export async function takeoverManaged(opts) {
|
|
|
119
119
|
: "unknown";
|
|
120
120
|
const execService = opts.execService ?? ((args, o) => runServiceCommand(args, o ?? {}, {}));
|
|
121
121
|
const verb = opts.refreshRegistration ? "install" : "restart";
|
|
122
|
-
const code = await execService([verb], opts.refreshRegistration ? { version: cliVersion } : undefined);
|
|
122
|
+
const code = await execService([verb], opts.refreshRegistration ? { version: cliVersion, quiet: true } : undefined);
|
|
123
123
|
if (code !== 0) {
|
|
124
124
|
throw new TakeoverError(`daemon upgrade failed: service ${verb} exited ${code}`);
|
|
125
125
|
}
|
package/dist/cli/update.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ensureConfig, getDbPath } from "../config.js";
|
|
|
4
4
|
import { createServiceClient, ServiceError, ServiceUnavailable, } from "../runtime/client.js";
|
|
5
5
|
import { readClaim } from "../runtime/owner.js";
|
|
6
6
|
import { compareVersions } from "../runtime/versions.js";
|
|
7
|
-
import { detectChannel,
|
|
7
|
+
import { detectChannel, resolveLatestVersionDetailed, updateNpm, updateStandalone, writeUpdateCache, } from "../runtime/updates.js";
|
|
8
8
|
import { isServiceManaged, runServiceCommand } from "../service/index.js";
|
|
9
9
|
import { checkSkew, formatWedged, olderCliWarning, takeoverManaged, takeoverUnmanaged, WedgedError, } from "./takeover.js";
|
|
10
10
|
export function stoppedDaemonMessage(oldVersion, newVersion) {
|
|
@@ -137,6 +137,17 @@ export async function postUpdateTakeover(opts) {
|
|
|
137
137
|
function stripV(v) {
|
|
138
138
|
return v.startsWith("v") ? v.slice(1) : v;
|
|
139
139
|
}
|
|
140
|
+
// A rate-limited response names its reset (local HH:MM) and the pin escape
|
|
141
|
+
// hatch; every other failure keeps the generic network message.
|
|
142
|
+
function rateLimitError(resetsAtMs) {
|
|
143
|
+
const hint = "or run 'quotacap update --to <version>'";
|
|
144
|
+
if (resetsAtMs === null) {
|
|
145
|
+
return `GitHub's anonymous API rate limit is exhausted; retry later, ${hint}`;
|
|
146
|
+
}
|
|
147
|
+
const d = new Date(resetsAtMs);
|
|
148
|
+
const hhmm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
149
|
+
return `GitHub's anonymous API rate limit is exhausted (resets ${hhmm}); retry then, ${hint}`;
|
|
150
|
+
}
|
|
140
151
|
export function registerUpdateCommand(program, deps) {
|
|
141
152
|
const exit = deps?.exit ?? process.exit;
|
|
142
153
|
const fetchFn = deps?.fetchFn ?? fetch;
|
|
@@ -146,7 +157,7 @@ export function registerUpdateCommand(program, deps) {
|
|
|
146
157
|
.command("update")
|
|
147
158
|
.description("check for and apply updates")
|
|
148
159
|
.option("--check", "only check for updates, changing nothing")
|
|
149
|
-
.option("--
|
|
160
|
+
.option("--to <version>", "update to a specific version")
|
|
150
161
|
.option("--json", "machine-readable output")
|
|
151
162
|
.action(async (o) => {
|
|
152
163
|
const current = VERSION;
|
|
@@ -176,17 +187,24 @@ export function registerUpdateCommand(program, deps) {
|
|
|
176
187
|
exit(1);
|
|
177
188
|
return;
|
|
178
189
|
}
|
|
179
|
-
const pin = typeof o.
|
|
190
|
+
const pin = typeof o.to === "string" && o.to ? stripV(o.to) : null;
|
|
180
191
|
let latest = null;
|
|
192
|
+
let rateLimited = null;
|
|
181
193
|
try {
|
|
182
|
-
|
|
194
|
+
const resolved = await resolveLatestVersionDetailed({ fetchFn });
|
|
195
|
+
if (resolved.status === "ok")
|
|
196
|
+
latest = resolved.release;
|
|
197
|
+
else if (resolved.status === "rate-limited")
|
|
198
|
+
rateLimited = resolved;
|
|
183
199
|
}
|
|
184
200
|
catch {
|
|
185
201
|
latest = null;
|
|
186
202
|
}
|
|
187
203
|
const target = pin ?? latest?.version ?? null;
|
|
188
204
|
if (!target) {
|
|
189
|
-
const error =
|
|
205
|
+
const error = rateLimited
|
|
206
|
+
? rateLimitError(rateLimited.resetsAtMs)
|
|
207
|
+
: "could not resolve the latest release; check your network and retry";
|
|
190
208
|
if (asJson) {
|
|
191
209
|
emit({
|
|
192
210
|
channel,
|
|
@@ -13,7 +13,21 @@ export interface ReleaseInfo {
|
|
|
13
13
|
url: string;
|
|
14
14
|
}
|
|
15
15
|
export declare function releasesApiUrl(): string;
|
|
16
|
+
export declare function releasesPageUrl(): string;
|
|
16
17
|
export declare function releaseDownloadBase(version: string): string;
|
|
18
|
+
export type LatestVersionResolution = {
|
|
19
|
+
status: "ok";
|
|
20
|
+
release: ReleaseInfo;
|
|
21
|
+
} | {
|
|
22
|
+
status: "rate-limited";
|
|
23
|
+
resetsAtMs: number | null;
|
|
24
|
+
} | {
|
|
25
|
+
status: "unavailable";
|
|
26
|
+
};
|
|
27
|
+
export declare function resolveLatestVersionDetailed(opts?: {
|
|
28
|
+
fetchFn?: typeof fetch;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
}): Promise<LatestVersionResolution>;
|
|
17
31
|
export declare function resolveLatestVersion(opts?: {
|
|
18
32
|
fetchFn?: typeof fetch;
|
|
19
33
|
timeoutMs?: number;
|
|
@@ -23,11 +37,13 @@ export interface UpdateCache {
|
|
|
23
37
|
latest: string;
|
|
24
38
|
channel: string;
|
|
25
39
|
current: string;
|
|
40
|
+
lastFailureAt?: string;
|
|
26
41
|
}
|
|
27
42
|
export declare function updatesCachePath(): string;
|
|
28
43
|
export declare function readUpdateCache(p?: string): UpdateCache | null;
|
|
29
44
|
export declare function writeUpdateCache(cache: UpdateCache, p?: string): void;
|
|
30
45
|
export declare const UPDATE_CACHE_TTL_MS: number;
|
|
46
|
+
export declare const UPDATE_FAILURE_RETRY_MS: number;
|
|
31
47
|
export declare function updateCacheStale(cache: UpdateCache | null, nowMs?: number): boolean;
|
|
32
48
|
export declare function refreshUpdateCache(opts: {
|
|
33
49
|
channel: string;
|
|
@@ -41,6 +57,10 @@ export declare function updateFooter(current: string, latest: string | null): st
|
|
|
41
57
|
export declare function printUpdateFooter(o: {
|
|
42
58
|
json?: boolean;
|
|
43
59
|
}, footer: string | null): void;
|
|
60
|
+
export declare function printVersionLine(o: {
|
|
61
|
+
json?: boolean;
|
|
62
|
+
compact?: boolean;
|
|
63
|
+
}, cliVersion: string, daemonVersion: unknown): void;
|
|
44
64
|
export declare function platformAssetName(platform?: NodeJS.Platform, arch?: string): string;
|
|
45
65
|
export declare function verifyChecksum(data: Buffer, sumsText: string, name: string): boolean;
|
|
46
66
|
export declare function defaultExtractTar(tarball: string, destDir: string): void;
|
package/dist/runtime/updates.js
CHANGED
|
@@ -129,6 +129,33 @@ export function releasesApiUrl() {
|
|
|
129
129
|
return `${base}/api/releases/latest`;
|
|
130
130
|
return "https://api.github.com/repos/carlosboeing/quotacap/releases/latest";
|
|
131
131
|
}
|
|
132
|
+
// Default version endpoint: the releases page answers 302 to the latest tag
|
|
133
|
+
// and sits outside the anonymous API rate-limit bucket shared with every
|
|
134
|
+
// other API consumer on the machine.
|
|
135
|
+
export function releasesPageUrl() {
|
|
136
|
+
return "https://github.com/carlosboeing/quotacap/releases/latest";
|
|
137
|
+
}
|
|
138
|
+
// Header read that tolerates real Headers and the plain-object headers that
|
|
139
|
+
// hermetic fetch mocks use.
|
|
140
|
+
function responseHeader(res, name) {
|
|
141
|
+
const headers = res.headers;
|
|
142
|
+
if (!headers)
|
|
143
|
+
return null;
|
|
144
|
+
if (typeof headers.get === "function") {
|
|
145
|
+
return headers.get(name);
|
|
146
|
+
}
|
|
147
|
+
if (typeof headers === "object") {
|
|
148
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
149
|
+
if (k.toLowerCase() === name && typeof v === "string")
|
|
150
|
+
return v;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function tagFromReleaseUrl(location) {
|
|
156
|
+
const m = /\/releases\/tag\/([^/?#]+)/.exec(location);
|
|
157
|
+
return m ? m[1] : "";
|
|
158
|
+
}
|
|
132
159
|
export function releaseDownloadBase(version) {
|
|
133
160
|
const base = process.env.QUOTACAP_RELEASE_BASE_URL?.replace(/\/+$/, "");
|
|
134
161
|
if (base)
|
|
@@ -137,32 +164,66 @@ export function releaseDownloadBase(version) {
|
|
|
137
164
|
? "https://github.com/carlosboeing/quotacap/releases/latest/download"
|
|
138
165
|
: `https://github.com/carlosboeing/quotacap/releases/download/v${version}`;
|
|
139
166
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
167
|
+
function rateLimitOrUnavailable(res) {
|
|
168
|
+
if (responseHeader(res, "x-ratelimit-remaining") === "0") {
|
|
169
|
+
const reset = responseHeader(res, "x-ratelimit-reset");
|
|
170
|
+
const secs = reset !== null && reset.trim() !== "" ? Number(reset) : NaN;
|
|
171
|
+
return {
|
|
172
|
+
status: "rate-limited",
|
|
173
|
+
resetsAtMs: Number.isFinite(secs) ? secs * 1000 : null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { status: "unavailable" };
|
|
177
|
+
}
|
|
178
|
+
// Full resolution outcome: the release, a rate-limited response carrying its
|
|
179
|
+
// reset time, or an undifferentiated failure. Never throws.
|
|
180
|
+
export async function resolveLatestVersionDetailed(opts = {}) {
|
|
143
181
|
const fetchFn = opts.fetchFn ?? fetch;
|
|
144
182
|
const timeoutMs = opts.timeoutMs ?? 10000;
|
|
183
|
+
const base = process.env.QUOTACAP_RELEASE_BASE_URL?.replace(/\/+$/, "");
|
|
145
184
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
185
|
+
if (base) {
|
|
186
|
+
const res = await fetchFn(releasesApiUrl(), {
|
|
187
|
+
headers: { accept: "application/vnd.github+json" },
|
|
188
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
189
|
+
});
|
|
190
|
+
if (res.ok) {
|
|
191
|
+
const body = (await res.json());
|
|
192
|
+
const tag = typeof body?.tag_name === "string" ? body.tag_name : "";
|
|
193
|
+
const version = tag.startsWith("v") ? tag.slice(1) : tag;
|
|
194
|
+
if (parseVersion(version)) {
|
|
195
|
+
const url = typeof body?.html_url === "string" && body.html_url
|
|
196
|
+
? body.html_url
|
|
197
|
+
: "https://github.com/carlosboeing/quotacap/releases/latest";
|
|
198
|
+
return { status: "ok", release: { version, url } };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return rateLimitOrUnavailable(res);
|
|
202
|
+
}
|
|
203
|
+
const res = await fetchFn(releasesPageUrl(), {
|
|
204
|
+
redirect: "manual",
|
|
148
205
|
signal: AbortSignal.timeout(timeoutMs),
|
|
149
206
|
});
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
: "https://github.com/carlosboeing/quotacap/releases/latest";
|
|
160
|
-
return { version, url };
|
|
207
|
+
if (res.status >= 300 && res.status < 400) {
|
|
208
|
+
const location = responseHeader(res, "location") ?? "";
|
|
209
|
+
const tag = tagFromReleaseUrl(location);
|
|
210
|
+
const version = tag.startsWith("v") ? tag.slice(1) : tag;
|
|
211
|
+
if (tag && parseVersion(version)) {
|
|
212
|
+
return { status: "ok", release: { version, url: location } };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return rateLimitOrUnavailable(res);
|
|
161
216
|
}
|
|
162
217
|
catch {
|
|
163
|
-
return
|
|
218
|
+
return { status: "unavailable" };
|
|
164
219
|
}
|
|
165
220
|
}
|
|
221
|
+
// Latest published version, or null when the release cannot be resolved
|
|
222
|
+
// (network failure, non-redirect, bad JSON, unparseable tag). Never throws.
|
|
223
|
+
export async function resolveLatestVersion(opts = {}) {
|
|
224
|
+
const r = await resolveLatestVersionDetailed(opts);
|
|
225
|
+
return r.status === "ok" ? r.release : null;
|
|
226
|
+
}
|
|
166
227
|
export function updatesCachePath() {
|
|
167
228
|
return path.join(process.env.QUOTACAP_HOME ?? os.homedir(), ".quotacap", "updates.json");
|
|
168
229
|
}
|
|
@@ -180,6 +241,11 @@ export function readUpdateCache(p = updatesCachePath()) {
|
|
|
180
241
|
if (typeof parsed.channel !== "string" || typeof parsed.current !== "string") {
|
|
181
242
|
return null;
|
|
182
243
|
}
|
|
244
|
+
if (parsed.lastFailureAt !== undefined &&
|
|
245
|
+
(typeof parsed.lastFailureAt !== "string" ||
|
|
246
|
+
Number.isNaN(new Date(parsed.lastFailureAt).getTime()))) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
183
249
|
return parsed;
|
|
184
250
|
}
|
|
185
251
|
catch {
|
|
@@ -201,25 +267,48 @@ export function writeUpdateCache(cache, p = updatesCachePath()) {
|
|
|
201
267
|
catch { }
|
|
202
268
|
}
|
|
203
269
|
export const UPDATE_CACHE_TTL_MS = 24 * 3600 * 1000;
|
|
270
|
+
// Negative-cache floor: a failed refresh suppresses further network attempts
|
|
271
|
+
// for this long. Short against the daily TTL so a recovered endpoint is
|
|
272
|
+
// re-probed soon, long enough that status/advise/poll storms back off.
|
|
273
|
+
export const UPDATE_FAILURE_RETRY_MS = 30 * 60 * 1000;
|
|
204
274
|
export function updateCacheStale(cache, nowMs = Date.now()) {
|
|
205
275
|
if (!cache)
|
|
206
276
|
return true;
|
|
207
277
|
return nowMs - new Date(cache.checkedAt).getTime() >= UPDATE_CACHE_TTL_MS;
|
|
208
278
|
}
|
|
209
|
-
// Refresh the daily cache when older than 24 hours or absent.
|
|
210
|
-
//
|
|
279
|
+
// Refresh the daily cache when older than 24 hours or absent. A failure
|
|
280
|
+
// inside the retry floor performs no network call and keeps serving the
|
|
281
|
+
// previously cached latest; a fresh failure stamps lastFailureAt so the next
|
|
282
|
+
// attempt backs off. checkedAt is never stamped on failure, so the daily
|
|
283
|
+
// check is unaffected. The result is the cache to read from.
|
|
211
284
|
export async function refreshUpdateCache(opts) {
|
|
212
285
|
const cachePath = opts.cachePath ?? updatesCachePath();
|
|
213
286
|
const existing = readUpdateCache(cachePath);
|
|
214
287
|
const now = opts.now?.() ?? new Date();
|
|
215
288
|
if (!updateCacheStale(existing, now.getTime()))
|
|
216
289
|
return existing;
|
|
290
|
+
if (existing?.lastFailureAt &&
|
|
291
|
+
now.getTime() - new Date(existing.lastFailureAt).getTime() < UPDATE_FAILURE_RETRY_MS) {
|
|
292
|
+
return existing;
|
|
293
|
+
}
|
|
217
294
|
const latest = await resolveLatestVersion({
|
|
218
295
|
fetchFn: opts.fetchFn,
|
|
219
296
|
timeoutMs: opts.timeoutMs ?? 3000,
|
|
220
297
|
});
|
|
221
|
-
if (!latest)
|
|
222
|
-
|
|
298
|
+
if (!latest) {
|
|
299
|
+
if (!existing)
|
|
300
|
+
return existing;
|
|
301
|
+
// Re-read before stamping: a concurrent refresh may have succeeded while
|
|
302
|
+
// our request was in flight, and stamping our stale snapshot would
|
|
303
|
+
// clobber its newer write and suppress retries for the floor.
|
|
304
|
+
const current = readUpdateCache(cachePath);
|
|
305
|
+
if (current && JSON.stringify(current) !== JSON.stringify(existing)) {
|
|
306
|
+
return current;
|
|
307
|
+
}
|
|
308
|
+
const stamped = { ...existing, lastFailureAt: now.toISOString() };
|
|
309
|
+
writeUpdateCache(stamped, cachePath);
|
|
310
|
+
return stamped;
|
|
311
|
+
}
|
|
223
312
|
const fresh = {
|
|
224
313
|
checkedAt: now.toISOString(),
|
|
225
314
|
latest: latest.version,
|
|
@@ -244,6 +333,17 @@ export function printUpdateFooter(o, footer) {
|
|
|
244
333
|
return;
|
|
245
334
|
console.error(footer);
|
|
246
335
|
}
|
|
336
|
+
// Version identity line, printed next to the update footer under the same
|
|
337
|
+
// stderr/TTY rules, plus never under --compact (prompt renderers must not
|
|
338
|
+
// see per-render noise). The daemon part appears only when the reachable
|
|
339
|
+
// daemon reports a version that differs from the CLI.
|
|
340
|
+
export function printVersionLine(o, cliVersion, daemonVersion) {
|
|
341
|
+
if (o.json || o.compact || !process.stderr.isTTY)
|
|
342
|
+
return;
|
|
343
|
+
const daemon = typeof daemonVersion === "string" && daemonVersion ? daemonVersion : null;
|
|
344
|
+
const suffix = daemon && daemon !== cliVersion ? ` · daemon ${daemon}` : "";
|
|
345
|
+
console.error(`quotacap ${cliVersion}${suffix}`);
|
|
346
|
+
}
|
|
247
347
|
export function platformAssetName(platform = process.platform, arch = process.arch) {
|
|
248
348
|
const os = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : null;
|
|
249
349
|
const a = arch === "arm64" ? "arm64" : arch === "x64" ? "x64" : null;
|
package/dist/service/index.js
CHANGED
|
@@ -75,8 +75,14 @@ export async function runServiceCommand(args, opts = {}, deps = {}) {
|
|
|
75
75
|
case "install":
|
|
76
76
|
// Internal: the post-update refresh passes the target version so
|
|
77
77
|
// install compares and records it instead of the old in-process
|
|
78
|
-
// VERSION
|
|
79
|
-
|
|
78
|
+
// VERSION, plus quiet so a good update prints its own three lines
|
|
79
|
+
// instead of the install chatter. The `service install` CLI never
|
|
80
|
+
// sets opts.version or opts.quiet.
|
|
81
|
+
await backend.install({
|
|
82
|
+
...deps,
|
|
83
|
+
version: opts.version ?? deps.version,
|
|
84
|
+
quiet: opts.quiet ?? deps.quiet,
|
|
85
|
+
});
|
|
80
86
|
break;
|
|
81
87
|
case "uninstall":
|
|
82
88
|
await backend.uninstall(deps);
|
package/dist/service/macos.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ServiceDeps {
|
|
|
21
21
|
sleep?: (ms: number) => Promise<void>;
|
|
22
22
|
bootstrapTimeoutMs?: number;
|
|
23
23
|
version?: string;
|
|
24
|
+
quiet?: boolean;
|
|
24
25
|
}
|
|
25
26
|
export declare function foregroundGuidance(verb: string, platform: string): string;
|
|
26
27
|
export declare function defaultRunLaunchctl(args: string[]): string;
|
package/dist/service/macos.js
CHANGED
|
@@ -279,6 +279,8 @@ export async function install(deps = {}) {
|
|
|
279
279
|
(deps.print ?? console.log)(foregroundGuidance("install", platform));
|
|
280
280
|
return;
|
|
281
281
|
}
|
|
282
|
+
if (deps.quiet)
|
|
283
|
+
deps = { ...deps, print: () => { } };
|
|
282
284
|
const { home, uid, dataDir, print, run, lint, sleep, bootstrapTimeoutMs } = resolved(deps);
|
|
283
285
|
const version = deps.version ?? VERSION;
|
|
284
286
|
// Provision beside the data dir the service will use (identical to the
|
|
@@ -342,6 +344,14 @@ export async function install(deps = {}) {
|
|
|
342
344
|
installedAt: (deps.now?.() ?? new Date()).toISOString(),
|
|
343
345
|
}, dataDir);
|
|
344
346
|
bootoutQuiet(run, uid, print);
|
|
347
|
+
// bootout is asynchronous: without this the replacement races the dying
|
|
348
|
+
// process for the port, the same race restart() guards against.
|
|
349
|
+
// deps.port keeps tests off the real daemon's port.
|
|
350
|
+
const upgradePort = deps.port ?? (await readConfig()).port;
|
|
351
|
+
const waitReleased = deps.waitReleased ?? defaultWaitReleased;
|
|
352
|
+
if (!(await waitReleased(upgradePort))) {
|
|
353
|
+
print(`port ${upgradePort} is still in use after stopping; starting anyway`);
|
|
354
|
+
}
|
|
345
355
|
}
|
|
346
356
|
else {
|
|
347
357
|
writeServiceMetadata({
|
|
@@ -18,6 +18,7 @@ export interface SystemdDeps {
|
|
|
18
18
|
error?: (msg: string) => void;
|
|
19
19
|
now?: () => Date;
|
|
20
20
|
version?: string;
|
|
21
|
+
quiet?: boolean;
|
|
21
22
|
}
|
|
22
23
|
export declare function formatSystemctlError(args: string[], detail: string): Error;
|
|
23
24
|
export declare function defaultRunSystemctl(args: string[]): string;
|
package/dist/service/systemd.js
CHANGED
|
@@ -106,6 +106,8 @@ export async function install(deps = {}) {
|
|
|
106
106
|
(deps.print ?? console.log)(foregroundGuidance("install", platform));
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
|
+
if (deps.quiet)
|
|
110
|
+
deps = { ...deps, print: () => { } };
|
|
109
111
|
const { home, dataDir, print, run } = resolved(deps);
|
|
110
112
|
const version = deps.version ?? VERSION;
|
|
111
113
|
// Provision beside the data dir the service will use (identical to the
|
|
@@ -157,6 +159,14 @@ export async function install(deps = {}) {
|
|
|
157
159
|
installedAt: (deps.now?.() ?? new Date()).toISOString(),
|
|
158
160
|
}, dataDir);
|
|
159
161
|
stopQuiet(run, print);
|
|
162
|
+
// systemctl stop can return before the port is released; same race as
|
|
163
|
+
// launchd. deps.port mirrors start(): tests must never probe the real
|
|
164
|
+
// daemon's port.
|
|
165
|
+
const upgradePort = deps.port ?? (await readConfig()).port;
|
|
166
|
+
const waitReleased = deps.waitReleased ?? defaultWaitReleased;
|
|
167
|
+
if (!(await waitReleased(upgradePort))) {
|
|
168
|
+
print(`port ${upgradePort} is still in use after stopping; starting anyway`);
|
|
169
|
+
}
|
|
160
170
|
}
|
|
161
171
|
else {
|
|
162
172
|
writeServiceMetadata({
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// generated by scripts/build-embed.mjs — do not edit
|
|
2
|
-
export const VERSION = "0.0.
|
|
2
|
+
export const VERSION = "0.0.27";
|
package/dist/web/src/App.js
CHANGED
|
@@ -95,7 +95,7 @@ function App() {
|
|
|
95
95
|
: null;
|
|
96
96
|
const header = (_jsx(Header, { runtime: snapshot?.runtime ?? null, unreachable: error?.kind === "service-unavailable", refreshing: refreshing, onRefresh: () => void refresh(), onSettings: () => setSettingsOpen(true) }));
|
|
97
97
|
if (snapshot && setupMode) {
|
|
98
|
-
return (_jsxs(_Fragment, { children: [header, _jsx("main", { children: _jsx(Onboarding, { snapshot: snapshot, onFirstPoll: () => void refresh(), polling: refreshing, pollError: pollErrorText }) }), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot.providers, onRefresh: () => void refresh(), refreshing: refreshing, onClose: () => setSettingsOpen(false) }), _jsx(SiteFooter, {})] }));
|
|
98
|
+
return (_jsxs(_Fragment, { children: [header, _jsx("main", { children: _jsx(Onboarding, { snapshot: snapshot, onFirstPoll: () => void refresh(), polling: refreshing, pollError: pollErrorText }) }), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot.providers, onRefresh: () => void refresh(), refreshing: refreshing, onClose: () => setSettingsOpen(false) }), _jsx(SiteFooter, { version: snapshot.runtime.version })] }));
|
|
99
99
|
}
|
|
100
100
|
return (_jsxs(_Fragment, { children: [header, _jsxs("main", { children: [notice && (_jsx("div", { "data-testid": "refresh-notice", role: "status", children: notice })), loading && !snapshot && (_jsx("div", { "data-testid": "state-loading", "aria-busy": "true", "aria-label": "Loading quota state", children: [0, 1, 2].map((i) => (_jsx("div", { "aria-hidden": "true", className: "skel", style: {
|
|
101
101
|
height: 64,
|
|
@@ -109,7 +109,7 @@ function App() {
|
|
|
109
109
|
}, onViewAdvice: () => {
|
|
110
110
|
setSelectedProvider(null);
|
|
111
111
|
setAdviceOpen(true);
|
|
112
|
-
} }))] }), _jsx(ProviderDrawer, { provider: snapshot?.providers.find((p) => p.id === selectedProvider) ?? null, asOf: snapshot?.asOf ?? "", recommendation: snapshot?.recommendation ?? null, onClose: () => setSelectedProvider(null), onRenamed: () => void load() }), snapshot && (_jsx(AdviceDrawer, { open: adviceOpen, recommendation: snapshot.recommendation, providers: snapshot.providers, asOf: snapshot.asOf, onClose: () => setAdviceOpen(false) })), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot?.providers ?? [], onRefresh: () => void refresh(), refreshing: refreshing, onClose: () => setSettingsOpen(false) }), _jsx(SiteFooter, {})] }));
|
|
112
|
+
} }))] }), _jsx(ProviderDrawer, { provider: snapshot?.providers.find((p) => p.id === selectedProvider) ?? null, asOf: snapshot?.asOf ?? "", recommendation: snapshot?.recommendation ?? null, onClose: () => setSelectedProvider(null), onRenamed: () => void load() }), snapshot && (_jsx(AdviceDrawer, { open: adviceOpen, recommendation: snapshot.recommendation, providers: snapshot.providers, asOf: snapshot.asOf, onClose: () => setAdviceOpen(false) })), _jsx(SettingsDrawer, { open: settingsOpen, providers: snapshot?.providers ?? [], onRefresh: () => void refresh(), refreshing: refreshing, onClose: () => setSettingsOpen(false) }), _jsx(SiteFooter, { version: snapshot?.runtime.version })] }));
|
|
113
113
|
}
|
|
114
114
|
const el = typeof document === "undefined" ? null : document.getElementById("app");
|
|
115
115
|
if (el)
|
|
@@ -38,6 +38,7 @@ function servingHost() {
|
|
|
38
38
|
return "127.0.0.1";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export function SiteFooter() {
|
|
42
|
-
|
|
41
|
+
export function SiteFooter({ version }) {
|
|
42
|
+
const name = version ? `QuotaCap v${version}` : "QuotaCap";
|
|
43
|
+
return (_jsxs("footer", { className: "sitefoot", children: [_jsxs("div", { className: "sitefoot-start", children: [_jsxs("span", { children: [name, " \u00B7 local quota tracker \u00B7 bound to ", _jsx("code", { children: servingHost() })] }), _jsx(PacingLegend, {})] }), _jsx("span", { children: "MIT licence \u00B7 no telemetry \u00B7 history stays on this machine" })] }));
|
|
43
44
|
}
|