quotacap 0.0.24 → 0.0.26

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.
@@ -66,6 +66,28 @@ export async function defaultWaitReady(port, timeoutMs = 15000) {
66
66
  }
67
67
  return false;
68
68
  }
69
+ /**
70
+ * `launchctl bootout` and `systemctl --user stop` both return before the job
71
+ * has actually gone, so a restart that starts immediately races the dying
72
+ * process for the port: the replacement fails to bind and the restart reports
73
+ * "did not become ready". Wait until nothing answers on the port.
74
+ *
75
+ * Resolves true as soon as the port is free, false if it never frees.
76
+ */
77
+ export async function defaultWaitReleased(port, timeoutMs = 10000) {
78
+ const deadline = Date.now() + timeoutMs;
79
+ const client = createServiceClient({ port, timeoutMs: 500 });
80
+ while (Date.now() < deadline) {
81
+ try {
82
+ await client.get("/health");
83
+ }
84
+ catch {
85
+ return true;
86
+ }
87
+ await new Promise((r) => setTimeout(r, 250));
88
+ }
89
+ return false;
90
+ }
69
91
  const TRANSIENT_PATTERNS = ["/_npx/", "/.npm/_npx/", "/.worktrees/"];
70
92
  export function resolveServiceExec(deps) {
71
93
  const execPath = deps?.execPath ?? process.execPath;
@@ -258,6 +280,7 @@ export async function install(deps = {}) {
258
280
  return;
259
281
  }
260
282
  const { home, uid, dataDir, print, run, lint, sleep, bootstrapTimeoutMs } = resolved(deps);
283
+ const version = deps.version ?? VERSION;
261
284
  // Provision beside the data dir the service will use (identical to the
262
285
  // QUOTACAP_HOME path in production; hermetic under injected test dirs).
263
286
  await ensureConfig(path.join(dataDir, "config.json"));
@@ -300,7 +323,7 @@ export async function install(deps = {}) {
300
323
  const samePlist = existing === plist;
301
324
  const samePaths = !!meta &&
302
325
  PROVIDER_BINS.every((bin) => (meta.providerPaths?.[bin] ?? null) === providerPaths[bin]);
303
- if (samePlist && samePaths && meta?.version === VERSION) {
326
+ if (samePlist && samePaths && meta?.version === version) {
304
327
  await bootstrapWithRetry(run, uid, plistFile, {
305
328
  expectReplace: false,
306
329
  sleep,
@@ -312,7 +335,7 @@ export async function install(deps = {}) {
312
335
  }
313
336
  // Upgrade: refresh metadata, stop the old job, replace, load.
314
337
  writeServiceMetadata({
315
- version: VERSION,
338
+ version,
316
339
  exec: argv[0],
317
340
  ...(entry ? { entry } : {}),
318
341
  providerPaths,
@@ -322,7 +345,7 @@ export async function install(deps = {}) {
322
345
  }
323
346
  else {
324
347
  writeServiceMetadata({
325
- version: VERSION,
348
+ version,
326
349
  exec: argv[0],
327
350
  ...(entry ? { entry } : {}),
328
351
  providerPaths,
@@ -405,6 +428,14 @@ export async function restart(deps = {}) {
405
428
  return;
406
429
  }
407
430
  await stop(deps);
431
+ // bootout is asynchronous: without this the replacement races the dying
432
+ // process for the port and start() fails its readiness check.
433
+ // deps.port keeps tests off the real daemon's port.
434
+ const port = deps.port ?? (await readConfig()).port;
435
+ const waitReleased = deps.waitReleased ?? defaultWaitReleased;
436
+ if (!(await waitReleased(port))) {
437
+ (deps.print ?? console.log)(`port ${port} is still in use after stopping; starting anyway`);
438
+ }
408
439
  await start(deps);
409
440
  }
410
441
  function parsePrintOutput(out) {
@@ -13,9 +13,11 @@ export interface SystemdDeps {
13
13
  runSystemctl?: (args: string[]) => string;
14
14
  which?: (bin: string) => string | null;
15
15
  waitReady?: (port: number) => Promise<boolean>;
16
+ waitReleased?: (port: number) => Promise<boolean>;
16
17
  print?: (msg: string) => void;
17
18
  error?: (msg: string) => void;
18
19
  now?: () => Date;
20
+ version?: string;
19
21
  }
20
22
  export declare function formatSystemctlError(args: string[], detail: string): Error;
21
23
  export declare function defaultRunSystemctl(args: string[]): string;
@@ -11,7 +11,7 @@ import { VERSION } from "../version.js";
11
11
  import { getDbPath, readServiceMetadata, writeServiceMetadata, } from "../config.js";
12
12
  import { ensureConfig, readConfig } from "../config.js";
13
13
  import { createServiceClient } from "../runtime/client.js";
14
- import { SERVICE_LABEL, PROVIDER_BINS, buildChildPath, defaultWaitReady, defaultWhich, formatMissingProviders, formatStatus, foregroundGuidance, resolveProviderPaths, resolveServiceExec, } from "./macos.js";
14
+ import { SERVICE_LABEL, PROVIDER_BINS, buildChildPath, defaultWaitReady, defaultWaitReleased, defaultWhich, formatMissingProviders, formatStatus, foregroundGuidance, resolveProviderPaths, resolveServiceExec, } from "./macos.js";
15
15
  export const SERVICE_UNIT = `${SERVICE_LABEL}.service`;
16
16
  export function serviceSupported(platform = process.platform) {
17
17
  return platform === "linux";
@@ -107,6 +107,7 @@ export async function install(deps = {}) {
107
107
  return;
108
108
  }
109
109
  const { home, dataDir, print, run } = resolved(deps);
110
+ const version = deps.version ?? VERSION;
110
111
  // Provision beside the data dir the service will use (identical to the
111
112
  // QUOTACAP_HOME path in production; hermetic under injected test dirs).
112
113
  await ensureConfig(path.join(dataDir, "config.json"));
@@ -141,7 +142,7 @@ export async function install(deps = {}) {
141
142
  const sameUnit = existing === unit;
142
143
  const samePaths = !!meta &&
143
144
  PROVIDER_BINS.every((bin) => (meta.providerPaths?.[bin] ?? null) === providerPaths[bin]);
144
- if (sameUnit && samePaths && meta?.version === VERSION) {
145
+ if (sameUnit && samePaths && meta?.version === version) {
145
146
  run(["enable", "--now", SERVICE_UNIT]);
146
147
  const port = await waitForReady(deps, dataDir);
147
148
  print(`service already installed and up to date; ready on port ${port}`);
@@ -149,7 +150,7 @@ export async function install(deps = {}) {
149
150
  }
150
151
  // Upgrade: refresh metadata, stop the old job, replace, load.
151
152
  writeServiceMetadata({
152
- version: VERSION,
153
+ version,
153
154
  exec: argv[0],
154
155
  ...(entry ? { entry } : {}),
155
156
  providerPaths,
@@ -159,7 +160,7 @@ export async function install(deps = {}) {
159
160
  }
160
161
  else {
161
162
  writeServiceMetadata({
162
- version: VERSION,
163
+ version,
163
164
  exec: argv[0],
164
165
  ...(entry ? { entry } : {}),
165
166
  providerPaths,
@@ -240,6 +241,13 @@ export async function restart(deps = {}) {
240
241
  return;
241
242
  }
242
243
  await stop(deps);
244
+ // systemctl stop can return before the port is released; same race as launchd.
245
+ // deps.port mirrors start(): tests must never probe the real daemon's port.
246
+ const port = deps.port ?? (await readConfig()).port;
247
+ const waitReleased = deps.waitReleased ?? defaultWaitReleased;
248
+ if (!(await waitReleased(port))) {
249
+ (deps.print ?? console.log)(`port ${port} is still in use after stopping; starting anyway`);
250
+ }
243
251
  await start(deps);
244
252
  }
245
253
  function parseShowOutput(out) {
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.24";
2
+ export const VERSION = "0.0.26";
@@ -175,7 +175,7 @@ export function ResetRail({ providers, asOf, onSelectProvider, }) {
175
175
  return (_jsxs("button", { "data-testid": "pin", type: "button", className: `pin ${pin.band} ${tierClass} ${alignClass}`, style: {
176
176
  left: `${pin.positionPct}%`,
177
177
  "--dot-color": paceColor,
178
- }, "aria-label": `${name}: ${pace}, resets ${when}`, onClick: () => onSelectProvider(pin.id), children: [pin.band === "above" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-when", children: when }), _jsxs("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: [name.split(" ")[0], pin.estimated ? " (est.)" : ""] }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: "pin-dot" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-dot" }), _jsx("span", { className: "pin-stem" }), _jsxs("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: [name.split(" ")[0], pin.estimated ? " (est.)" : ""] }), _jsx("span", { className: "pin-when", children: when })] })), _jsxs("span", { className: "pin-tooltip", role: "tooltip", children: [_jsx("b", { children: name }), _jsx("span", { className: "pt-sep", children: "\u00B7" }), provider?.quota ? `${provider.quota.usedPct}% used` : "no readings", _jsx("span", { className: "pt-sep", children: "\u00B7" }), left ? `${left} left` : when, pin.estimated ? " (est.)" : ""] })] }, pin.id));
178
+ }, "aria-label": `${name}: ${pace}, resets ${when}`, onClick: () => onSelectProvider(pin.id), children: [pin.band === "above" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: "pin-dot" })] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "pin-dot" }), _jsx("span", { className: "pin-stem" }), _jsx("span", { className: `pin-label ${paceCls}`, style: pinBorder, children: name }), _jsx("span", { className: "pin-when", children: pin.estimated ? `${when} (est.)` : when })] })), _jsxs("span", { className: "pin-tooltip", role: "tooltip", children: [_jsx("b", { children: name }), _jsx("span", { className: "pt-sep", children: "\u00B7" }), provider?.quota ? `${provider.quota.usedPct}% used` : "no readings", _jsx("span", { className: "pt-sep", children: "\u00B7" }), left ? `${left} left` : when, pin.estimated ? " (est.)" : ""] })] }, pin.id));
179
179
  }), placement.clusters.map((cluster, index) => {
180
180
  const alignClass = cluster.positionPct < 12 ? "pin-start" : cluster.positionPct > 88 ? "pin-end" : "";
181
181
  const tierClass = cluster.tier > 0 ? `tier${cluster.tier + 1}` : "tier1";