quotacap 0.0.39 → 0.0.41
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 +1 -1
- package/dist/adapters/agy.js +2 -2
- package/dist/adapters/claude.js +1 -1
- package/dist/adapters/codex.js +29 -20
- package/dist/adapters/grok.js +1 -1
- package/dist/adapters/kimi.d.ts +41 -0
- package/dist/adapters/kimi.js +453 -21
- package/dist/adapters/manual.js +2 -2
- package/dist/adapters/muse.js +3 -3
- package/dist/adapters/opencode-go.js +30 -2
- package/dist/adapters/pty.d.ts +9 -0
- package/dist/adapters/pty.js +16 -0
- package/dist/adapters/types.d.ts +17 -2
- package/dist/advisory/engine.d.ts +4 -2
- package/dist/advisory/engine.js +55 -14
- package/dist/advisory/snapshot.js +11 -3
- package/dist/advisory/types.d.ts +11 -3
- package/dist/cli/advise.js +1 -0
- package/dist/cli/clients.d.ts +4 -0
- package/dist/cli/models.js +5 -1
- package/dist/cli/progress.d.ts +32 -0
- package/dist/cli/progress.js +70 -0
- package/dist/cli/runtime.d.ts +5 -0
- package/dist/cli/runtime.js +18 -8
- package/dist/cli/status.js +1 -0
- package/dist/cli/takeover.d.ts +3 -0
- package/dist/cli/takeover.js +7 -1
- package/dist/cli/update.d.ts +4 -0
- package/dist/cli/update.js +8 -1
- package/dist/diagnostics/failure.js +46 -11
- package/dist/format/rows.d.ts +6 -1
- package/dist/format/rows.js +27 -4
- package/dist/format/terminal.js +5 -4
- package/dist/http/server.js +3 -3
- package/dist/runtime/poll.js +1 -1
- package/dist/service/index.js +3 -1
- package/dist/service/macos.d.ts +2 -0
- package/dist/service/macos.js +4 -1
- package/dist/service/systemd.d.ts +2 -0
- package/dist/service/systemd.js +5 -3
- package/dist/store/db.js +12 -1
- package/dist/store/quotas.js +33 -8
- package/dist/version.js +1 -1
- package/dist/web/src/components/AdviceDrawer.js +10 -5
- package/dist/web/src/components/FaultBanner.d.ts +10 -5
- package/dist/web/src/components/FaultBanner.js +19 -9
- package/dist/web/src/components/PaceBar.d.ts +9 -5
- package/dist/web/src/components/PaceBar.js +32 -10
- package/dist/web/src/components/ProviderCard.js +4 -2
- package/dist/web/src/components/ProviderDrawer.d.ts +7 -1
- package/dist/web/src/components/ProviderDrawer.js +46 -9
- package/dist/web/src/components/ProviderRow.js +4 -4
- package/dist/web/src/components/Recommendation.d.ts +5 -2
- package/dist/web/src/components/Recommendation.js +8 -4
- package/dist/web/src/components/ResetRail.js +3 -2
- package/dist/web/src/components/SubscriptionList.d.ts +2 -1
- package/dist/web/src/components/SubscriptionList.js +8 -5
- package/dist/web/src/components/WindowsRow.d.ts +30 -0
- package/dist/web/src/components/WindowsRow.js +68 -0
- package/dist/web/src/pages/Onboarding.js +2 -1
- package/dist/web/src/state.d.ts +12 -0
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/index-BUK8xUHm.js +12 -0
- package/web/dist/assets/{index-NtT9XTrm.css → index-Dk6l5h27.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CD8jaN87.js +0 -12
package/dist/cli/runtime.js
CHANGED
|
@@ -8,6 +8,7 @@ import { createServiceClient, ServiceError, ServiceUnavailable, } from "../runti
|
|
|
8
8
|
import { isServiceInstalled, isServiceManaged, runServiceCommand, serviceSupported, } from "../service/index.js";
|
|
9
9
|
import { detectChannel, refreshUpdateCache } from "../runtime/updates.js";
|
|
10
10
|
import { checkSkew, execSkewWarning, formatWedged, olderCliWarning, takeoverManaged, takeoverUnmanaged, upgradedMessage, waitForHealthy, WedgedError, } from "./takeover.js";
|
|
11
|
+
import { phase } from "./progress.js";
|
|
11
12
|
// Shared `web` launcher handler: the `web` command and the bare `quotacap`
|
|
12
13
|
// invocation both run this. Decision order: healthy daemons open (with the
|
|
13
14
|
// skew matrix), registered-but-down services start, and only the last resort
|
|
@@ -28,7 +29,8 @@ export async function launchWeb(o, deps = {}) {
|
|
|
28
29
|
const isManaged = deps.isManaged ?? isServiceManaged;
|
|
29
30
|
const isInstalled = deps.isInstalled ?? isServiceInstalled;
|
|
30
31
|
const supported = deps.serviceSupported ?? serviceSupported;
|
|
31
|
-
const
|
|
32
|
+
const phaseFn = deps.phase ?? phase;
|
|
33
|
+
const takeoverOpts = { phase: phaseFn, ...deps.takeoverOpts };
|
|
32
34
|
const checkUpdates = deps.checkUpdates ??
|
|
33
35
|
(() => refreshUpdateCache({ channel: detectChannel(), current: VERSION }));
|
|
34
36
|
const sleep = takeoverOpts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
@@ -56,7 +58,7 @@ export async function launchWeb(o, deps = {}) {
|
|
|
56
58
|
const skew = checkSkew(health, VERSION, process.execPath);
|
|
57
59
|
if (skew === "match") {
|
|
58
60
|
if (health.polling === "in-progress" && !health.lastCompletedPollAt) {
|
|
59
|
-
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000) });
|
|
61
|
+
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000), phase: phaseFn });
|
|
60
62
|
}
|
|
61
63
|
await openDashboard(`http://127.0.0.1:${port}`, openBrowser);
|
|
62
64
|
return;
|
|
@@ -64,7 +66,7 @@ export async function launchWeb(o, deps = {}) {
|
|
|
64
66
|
if (skew === "exec-only") {
|
|
65
67
|
console.error(execSkewWarning(String(health.exec)));
|
|
66
68
|
if (health.polling === "in-progress" && !health.lastCompletedPollAt) {
|
|
67
|
-
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000) });
|
|
69
|
+
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000), phase: phaseFn });
|
|
68
70
|
}
|
|
69
71
|
await openDashboard(`http://127.0.0.1:${port}`, openBrowser);
|
|
70
72
|
return;
|
|
@@ -72,7 +74,7 @@ export async function launchWeb(o, deps = {}) {
|
|
|
72
74
|
if (skew === "cli-older") {
|
|
73
75
|
console.error(olderCliWarning(String(health.version ?? "unknown")));
|
|
74
76
|
if (health.polling === "in-progress" && !health.lastCompletedPollAt) {
|
|
75
|
-
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000) });
|
|
77
|
+
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000), phase: phaseFn });
|
|
76
78
|
}
|
|
77
79
|
await openDashboard(`http://127.0.0.1:${port}`, openBrowser);
|
|
78
80
|
return;
|
|
@@ -95,7 +97,7 @@ export async function launchWeb(o, deps = {}) {
|
|
|
95
97
|
exit(1);
|
|
96
98
|
return;
|
|
97
99
|
}
|
|
98
|
-
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000) });
|
|
100
|
+
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000), phase: phaseFn });
|
|
99
101
|
await openDashboard(`http://127.0.0.1:${port}`, openBrowser);
|
|
100
102
|
return;
|
|
101
103
|
}
|
|
@@ -116,7 +118,7 @@ export async function launchWeb(o, deps = {}) {
|
|
|
116
118
|
}
|
|
117
119
|
// Unmanaged successor: foreground-start the new daemon and open it.
|
|
118
120
|
const started = await start(o.port ? { port } : undefined);
|
|
119
|
-
await waitForInitialPoll(started.port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000) });
|
|
121
|
+
await waitForInitialPoll(started.port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000), phase: phaseFn });
|
|
120
122
|
await openDashboard(`http://127.0.0.1:${started.port}`, openBrowser);
|
|
121
123
|
return;
|
|
122
124
|
}
|
|
@@ -146,16 +148,18 @@ export async function launchWeb(o, deps = {}) {
|
|
|
146
148
|
detail = e?.message ?? String(e);
|
|
147
149
|
}
|
|
148
150
|
if (!detail) {
|
|
151
|
+
phaseFn("waiting for daemon…");
|
|
149
152
|
const healthy = await waitForHealthy(port, VERSION, {
|
|
150
153
|
createClient,
|
|
151
154
|
sleep,
|
|
152
155
|
timeoutMs: readyTimeoutMs,
|
|
156
|
+
phase: phaseFn,
|
|
153
157
|
});
|
|
154
158
|
if (!healthy)
|
|
155
159
|
detail = `not ready within ${Math.round(readyTimeoutMs / 1000)}s`;
|
|
156
160
|
}
|
|
157
161
|
if (!detail) {
|
|
158
|
-
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000) });
|
|
162
|
+
await waitForInitialPoll(port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000), phase: phaseFn });
|
|
159
163
|
await openDashboard(`http://127.0.0.1:${port}`, openBrowser);
|
|
160
164
|
return;
|
|
161
165
|
}
|
|
@@ -167,7 +171,7 @@ export async function launchWeb(o, deps = {}) {
|
|
|
167
171
|
if (!foregroundOnly && !attemptedServiceStart && supported()) {
|
|
168
172
|
console.error("for a background service that survives terminal closes, run 'quotacap service install'");
|
|
169
173
|
}
|
|
170
|
-
await waitForInitialPoll(started.port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000) });
|
|
174
|
+
await waitForInitialPoll(started.port, { createClient, sleep, timeoutMs: Math.min(readyTimeoutMs, 5000), phase: phaseFn });
|
|
171
175
|
await openDashboard(`http://127.0.0.1:${started.port}`, openBrowser);
|
|
172
176
|
}
|
|
173
177
|
export async function waitForInitialPoll(port, opts = {}) {
|
|
@@ -176,6 +180,7 @@ export async function waitForInitialPoll(port, opts = {}) {
|
|
|
176
180
|
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
177
181
|
const deadline = Date.now() + timeoutMs;
|
|
178
182
|
const client = createClient({ port, timeoutMs: 1000 });
|
|
183
|
+
let emitted = false;
|
|
179
184
|
while (Date.now() < deadline) {
|
|
180
185
|
await sleep(250);
|
|
181
186
|
try {
|
|
@@ -183,6 +188,10 @@ export async function waitForInitialPoll(port, opts = {}) {
|
|
|
183
188
|
if (!h?.ok || h?.polling !== "in-progress" || h?.lastCompletedPollAt) {
|
|
184
189
|
return;
|
|
185
190
|
}
|
|
191
|
+
if (!emitted) {
|
|
192
|
+
(opts.phase ?? phase)("waiting for first readings…");
|
|
193
|
+
emitted = true;
|
|
194
|
+
}
|
|
186
195
|
}
|
|
187
196
|
catch {
|
|
188
197
|
return;
|
|
@@ -254,6 +263,7 @@ export function registerRuntimeCommands(program, deps) {
|
|
|
254
263
|
serviceSupported: supported,
|
|
255
264
|
takeoverOpts,
|
|
256
265
|
checkUpdates,
|
|
266
|
+
phase: deps?.phase,
|
|
257
267
|
}));
|
|
258
268
|
const service = program
|
|
259
269
|
.command("service")
|
package/dist/cli/status.js
CHANGED
package/dist/cli/takeover.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ServiceClient } from "../runtime/client.js";
|
|
2
2
|
import { type ClaimInfo } from "../runtime/owner.js";
|
|
3
3
|
import { isServiceManaged } from "../service/index.js";
|
|
4
|
+
import { type PhaseFn } from "./progress.js";
|
|
4
5
|
export type SkewKind = "match" | "exec-only" | "cli-newer" | "cli-older";
|
|
5
6
|
export interface HealthView {
|
|
6
7
|
ok?: unknown;
|
|
@@ -37,6 +38,8 @@ export interface WaitOpts {
|
|
|
37
38
|
createClient?: CreateTakeoverClient;
|
|
38
39
|
sleep?: SleepFn;
|
|
39
40
|
timeoutMs?: number;
|
|
41
|
+
phase?: PhaseFn;
|
|
42
|
+
json?: boolean;
|
|
40
43
|
}
|
|
41
44
|
export declare function waitForHealthy(port: number, expectedVersion: string, opts?: WaitOpts): Promise<any | null>;
|
|
42
45
|
export declare function waitForRelease(port: number, dataDir?: string, opts?: WaitOpts): Promise<boolean>;
|
package/dist/cli/takeover.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createServiceClient, ServiceError, ServiceUnavailable, } from "../runti
|
|
|
9
9
|
import { readToken } from "../runtime/token.js";
|
|
10
10
|
import { readClaim } from "../runtime/owner.js";
|
|
11
11
|
import { runServiceCommand, isServiceManaged } from "../service/index.js";
|
|
12
|
+
import { phase } from "./progress.js";
|
|
12
13
|
export { parseVersion, compareVersions } from "../runtime/versions.js";
|
|
13
14
|
import { compareVersions, parseVersion } from "../runtime/versions.js";
|
|
14
15
|
export function checkSkew(health, cliVersion = VERSION, cliExec = process.execPath) {
|
|
@@ -69,6 +70,7 @@ export function formatWedged(opts) {
|
|
|
69
70
|
`stop the process listening on port ${port} in its own terminal and retry`);
|
|
70
71
|
}
|
|
71
72
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
73
|
+
const silentPhase = () => { };
|
|
72
74
|
// Poll /health until it reports the expected version (or the deadline).
|
|
73
75
|
export async function waitForHealthy(port, expectedVersion, opts = {}) {
|
|
74
76
|
const createClient = opts.createClient ?? createServiceClient;
|
|
@@ -119,7 +121,10 @@ export async function takeoverManaged(opts) {
|
|
|
119
121
|
: "unknown";
|
|
120
122
|
const execService = opts.execService ?? ((args, o) => runServiceCommand(args, o ?? {}, {}));
|
|
121
123
|
const verb = opts.refreshRegistration ? "install" : "restart";
|
|
122
|
-
|
|
124
|
+
(opts.phase ?? phase)("waiting for daemon…", { json: opts.json });
|
|
125
|
+
// The line above already reports this wait, so the nested restart stays
|
|
126
|
+
// silent instead of repeating it past the caller's --json suppression.
|
|
127
|
+
const code = await execService([verb], opts.refreshRegistration ? { version: cliVersion, quiet: true } : { phase: silentPhase });
|
|
123
128
|
if (code !== 0) {
|
|
124
129
|
throw new TakeoverError(`daemon upgrade failed: service ${verb} exited ${code}`);
|
|
125
130
|
}
|
|
@@ -144,6 +149,7 @@ export async function takeoverUnmanaged(opts) {
|
|
|
144
149
|
if (!token) {
|
|
145
150
|
throw new WedgedError(formatWedged({ claim: readClaim(dataDir), port, managed: false }));
|
|
146
151
|
}
|
|
152
|
+
(opts.phase ?? phase)("waiting for daemon to stop…", { json: opts.json });
|
|
147
153
|
const createClient = opts.createClient ?? createServiceClient;
|
|
148
154
|
const client = createClient({ port, token, timeoutMs: 5000 });
|
|
149
155
|
try {
|
package/dist/cli/update.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|
|
2
2
|
import { type ServiceClient } from "../runtime/client.js";
|
|
3
3
|
import { type ChannelEnv } from "../runtime/updates.js";
|
|
4
4
|
import { type SleepFn } from "./takeover.js";
|
|
5
|
+
import { type PhaseFn } from "./progress.js";
|
|
5
6
|
export interface UpdateJson {
|
|
6
7
|
channel: string;
|
|
7
8
|
current: string;
|
|
@@ -28,9 +29,12 @@ export interface UpdateCommandDeps {
|
|
|
28
29
|
sleep?: SleepFn;
|
|
29
30
|
timeoutMs?: number;
|
|
30
31
|
readToken?: () => string | undefined;
|
|
32
|
+
phase?: PhaseFn;
|
|
33
|
+
json?: boolean;
|
|
31
34
|
};
|
|
32
35
|
spawnNpm?: (args: string[]) => Promise<number>;
|
|
33
36
|
extractTar?: (tarball: string, destDir: string) => void;
|
|
37
|
+
phase?: PhaseFn;
|
|
34
38
|
}
|
|
35
39
|
export declare function stoppedDaemonMessage(oldVersion: string, newVersion: string): string;
|
|
36
40
|
export interface PostUpdateTakeoverOpts {
|
package/dist/cli/update.js
CHANGED
|
@@ -7,6 +7,7 @@ import { compareVersions } from "../runtime/versions.js";
|
|
|
7
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
|
+
import { phase } from "./progress.js";
|
|
10
11
|
export function stoppedDaemonMessage(oldVersion, newVersion) {
|
|
11
12
|
return (`Stopped old daemon (${oldVersion}); no background service is registered. ` +
|
|
12
13
|
`Run 'quotacap web' to start ${newVersion}, or 'quotacap service install' to run it in the background.`);
|
|
@@ -190,6 +191,10 @@ export function registerUpdateCommand(program, deps) {
|
|
|
190
191
|
const pin = typeof o.to === "string" && o.to ? stripV(o.to) : null;
|
|
191
192
|
let latest = null;
|
|
192
193
|
let rateLimited = null;
|
|
194
|
+
const phaseFn = deps?.phase ?? phase;
|
|
195
|
+
if (!o.check && !pin) {
|
|
196
|
+
phaseFn("resolving latest version…", { json: asJson });
|
|
197
|
+
}
|
|
193
198
|
try {
|
|
194
199
|
const resolved = await resolveLatestVersionDetailed({ fetchFn });
|
|
195
200
|
if (resolved.status === "ok")
|
|
@@ -298,9 +303,10 @@ export function registerUpdateCommand(program, deps) {
|
|
|
298
303
|
createClient: deps?.createClient,
|
|
299
304
|
isManaged: deps?.isManaged,
|
|
300
305
|
execService: deps?.execService,
|
|
301
|
-
takeoverOpts: deps?.takeoverOpts,
|
|
306
|
+
takeoverOpts: { phase: phaseFn, json: asJson, ...deps?.takeoverOpts },
|
|
302
307
|
};
|
|
303
308
|
if (channel === "standalone") {
|
|
309
|
+
phaseFn(`downloading ${target}…`, { json: asJson });
|
|
304
310
|
const swap = await updateStandalone({
|
|
305
311
|
target,
|
|
306
312
|
execPath,
|
|
@@ -356,6 +362,7 @@ export function registerUpdateCommand(program, deps) {
|
|
|
356
362
|
}
|
|
357
363
|
// npm global.
|
|
358
364
|
const npmSpec = pin ?? "latest";
|
|
365
|
+
phaseFn(`updating npm package…`, { json: asJson });
|
|
359
366
|
const npm = await updateNpm({
|
|
360
367
|
target: npmSpec,
|
|
361
368
|
execPath,
|
|
@@ -40,14 +40,25 @@ function stripAnsiAndControls(s) {
|
|
|
40
40
|
.replace(/[\r\n\t]+/g, " ");
|
|
41
41
|
}
|
|
42
42
|
const UNRECOGNIZED_OMITTED = "Unrecognized diagnostic text omitted";
|
|
43
|
+
// Vendor account-confirmation wording. The sentence is safe to recognize.
|
|
44
|
+
// The browser link that follows it can carry a sign-in token, so the link is never stored.
|
|
45
|
+
// "account is not eligible" is the Antigravity sentence; a bare "not eligible" is not.
|
|
46
|
+
// The hyphen covers "Sign-in with". "signed in with" does not match.
|
|
47
|
+
const ACCOUNT_CONFIRMATION = /\b(?:account is not eligible|verify your account|eligibility check failed|device code|sign(?:ing)?[ -]in with)\b/i;
|
|
43
48
|
function matchPrecedence(evidence, errorName, errorCode, text) {
|
|
44
|
-
// Order 1: timeout — Structured AbortError or an explicit QuotaCap abort/timeout checkpoint
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
evidence?.checkpoint === "
|
|
50
|
-
|
|
49
|
+
// Order 1: timeout — Structured AbortError or an explicit QuotaCap abort/timeout checkpoint.
|
|
50
|
+
// A ready or completion timeout whose transcript is an account-confirmation
|
|
51
|
+
// screen falls through, so the sign-in sentence can win later. An explicit
|
|
52
|
+
// abort stays a timeout even if that screen is in the transcript.
|
|
53
|
+
const signInTimeout = ACCOUNT_CONFIRMATION.test(text) &&
|
|
54
|
+
(evidence?.checkpoint === "ready timeout" || evidence?.checkpoint === "completion timeout");
|
|
55
|
+
const isAbort = !signInTimeout &&
|
|
56
|
+
(errorName === "AbortError" ||
|
|
57
|
+
errorCode === "ABORT_ERR" ||
|
|
58
|
+
evidence?.checkpoint === "abort" ||
|
|
59
|
+
evidence?.checkpoint === "ready timeout" ||
|
|
60
|
+
evidence?.checkpoint === "completion timeout" ||
|
|
61
|
+
/\bpty aborted\b/i.test(text));
|
|
51
62
|
if (isAbort) {
|
|
52
63
|
return { code: "timeout", phrase: "timed out" };
|
|
53
64
|
}
|
|
@@ -232,6 +243,15 @@ function matchPrecedence(evidence, errorName, errorCode, text) {
|
|
|
232
243
|
if (/\bbad rolling pct\b/i.test(text)) {
|
|
233
244
|
return { code: "parse_error", phrase: "bad rolling pct" };
|
|
234
245
|
}
|
|
246
|
+
if (/\bbad monthly pct\b/i.test(text)) {
|
|
247
|
+
return { code: "parse_error", phrase: "bad monthly pct" };
|
|
248
|
+
}
|
|
249
|
+
if (/\bbad monthly reset\b/i.test(text)) {
|
|
250
|
+
return { code: "parse_error", phrase: "bad monthly reset" };
|
|
251
|
+
}
|
|
252
|
+
if (/\bunknown monthly status\b/i.test(text)) {
|
|
253
|
+
return { code: "parse_error", phrase: "unknown monthly status" };
|
|
254
|
+
}
|
|
235
255
|
if (/\busage status not ok\b/i.test(text)) {
|
|
236
256
|
return { code: "parse_error", phrase: "usage status not ok" };
|
|
237
257
|
}
|
|
@@ -243,6 +263,13 @@ function matchPrecedence(evidence, errorName, errorCode, text) {
|
|
|
243
263
|
if (/\blimits refresh requested\b/i.test(text) || /\brefresh requested\b/i.test(text)) {
|
|
244
264
|
return { code: "service_unavailable", phrase: "limits refresh requested" };
|
|
245
265
|
}
|
|
266
|
+
// Account confirmation is after network, parse, and service-unavailable.
|
|
267
|
+
// A sign-in menu line must not hide a harder failure. It is before the
|
|
268
|
+
// text timeout match so a completion timeout sitting on that screen still
|
|
269
|
+
// classifies as auth.
|
|
270
|
+
if (ACCOUNT_CONFIRMATION.test(text)) {
|
|
271
|
+
return { code: "auth", phrase: "account confirmation required" };
|
|
272
|
+
}
|
|
246
273
|
// Order 9: timeout — Remaining explicit timeout or timed out errors
|
|
247
274
|
if (/\btimed out\b/i.test(text) || /\btimeout\b/i.test(text)) {
|
|
248
275
|
return { code: "timeout", phrase: "timed out" };
|
|
@@ -347,14 +374,16 @@ export function diagnosticError(reason, evidence) {
|
|
|
347
374
|
if (parsed.durationMs !== undefined)
|
|
348
375
|
durationMs = parsed.durationMs;
|
|
349
376
|
}
|
|
350
|
-
//
|
|
377
|
+
// Keep error lines and account-confirmation lines. Other PTY screen text
|
|
378
|
+
// stays out so a spinner or model list cannot change the classification.
|
|
351
379
|
let safeStdoutText = "";
|
|
352
380
|
if (evidence?.stdout) {
|
|
353
381
|
if (evidence.source === "pty") {
|
|
354
382
|
const lines = evidence.stdout.split(/[\r\n]+/);
|
|
355
383
|
const matchedLines = lines.filter((l) => /\b(error|failed|failure|fatal|unauthorized|unauthorised|denied|forbidden|panic|exception)\b/i.test(l) ||
|
|
356
384
|
/\btrust\b/i.test(l) ||
|
|
357
|
-
/\b(stdin is not a terminal|device not configured|not a tty|inappropriate ioctl|node-pty)\b/i.test(l)
|
|
385
|
+
/\b(stdin is not a terminal|device not configured|not a tty|inappropriate ioctl|node-pty)\b/i.test(l) ||
|
|
386
|
+
ACCOUNT_CONFIRMATION.test(l));
|
|
358
387
|
safeStdoutText = matchedLines.join(" ");
|
|
359
388
|
}
|
|
360
389
|
else {
|
|
@@ -419,7 +448,7 @@ function mapCategory(code) {
|
|
|
419
448
|
return "unknown";
|
|
420
449
|
}
|
|
421
450
|
}
|
|
422
|
-
function getSummaryAndAction(code, providerName, provider) {
|
|
451
|
+
function getSummaryAndAction(code, providerName, provider, errorDetail) {
|
|
423
452
|
switch (code) {
|
|
424
453
|
case "terminal_error":
|
|
425
454
|
return {
|
|
@@ -427,6 +456,12 @@ function getSummaryAndAction(code, providerName, provider) {
|
|
|
427
456
|
action: `Open ${providerName} directly in your terminal to check whether it starts. If it works there, inspect the QuotaCap service log and report the adapter failure. Refresh in the dashboard retries through the same service.`,
|
|
428
457
|
};
|
|
429
458
|
case "auth":
|
|
459
|
+
if (/\baccount confirmation required\b/i.test(errorDetail)) {
|
|
460
|
+
return {
|
|
461
|
+
summary: `${providerName} needs you to confirm the subscription account`,
|
|
462
|
+
action: `Open ${providerName}, complete the browser sign-in it shows, then select Refresh in the QuotaCap dashboard.`,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
430
465
|
return {
|
|
431
466
|
summary: `${providerName} login required`,
|
|
432
467
|
action: `Open ${providerName} and follow its sign-in instructions. Then select Refresh in the QuotaCap dashboard.`,
|
|
@@ -486,7 +521,7 @@ export function classifyFailure(provider, reason) {
|
|
|
486
521
|
}
|
|
487
522
|
const category = mapCategory(diag.diagnosticCode);
|
|
488
523
|
const pName = formatProviderName(provider);
|
|
489
|
-
const { summary, action } = getSummaryAndAction(diag.diagnosticCode, pName, provider);
|
|
524
|
+
const { summary, action } = getSummaryAndAction(diag.diagnosticCode, pName, provider, diag.errorDetail);
|
|
490
525
|
return {
|
|
491
526
|
diagnosticCode: diag.diagnosticCode,
|
|
492
527
|
errorDetail: diag.errorDetail,
|
package/dist/format/rows.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderSnapshot } from "../advisory/types.js";
|
|
2
|
-
export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Watch" | "Measuring" | "Not reporting";
|
|
2
|
+
export type StateWord = "On track" | "Behind pace" | "Ahead of pace" | "Cap risk" | "Watch" | "Measuring" | "Not reporting" | "Monthly exhausted";
|
|
3
3
|
export type RailCell = "used" | "tick" | "unused";
|
|
4
4
|
export type SortKey = "recommended" | "reset-asc" | "reset-desc" | "used-desc" | "used-asc";
|
|
5
5
|
export type CompactSuffix = "" | "!" | "~" | "?";
|
|
@@ -16,7 +16,12 @@ export interface Row {
|
|
|
16
16
|
}
|
|
17
17
|
export declare const RAIL_CELLS = 20;
|
|
18
18
|
export declare const SORT_KEYS: SortKey[];
|
|
19
|
+
export declare function weeklyPctOf(q: {
|
|
20
|
+
weeklyPct?: number;
|
|
21
|
+
usedPct?: number;
|
|
22
|
+
} | null | undefined): number | undefined;
|
|
19
23
|
export declare function fmtDuration(ms: number): string;
|
|
24
|
+
export declare function weekdayOf(iso: string | undefined): string;
|
|
20
25
|
export declare function countdownText(ps: ProviderSnapshot, now: Date): string;
|
|
21
26
|
export declare function elapsedPct(ps: ProviderSnapshot, now: Date): number | null;
|
|
22
27
|
export declare function usedPctOf(ps: ProviderSnapshot): number | null;
|
package/dist/format/rows.js
CHANGED
|
@@ -9,6 +9,11 @@ function msOf(iso) {
|
|
|
9
9
|
const ms = new Date(iso).getTime();
|
|
10
10
|
return Number.isNaN(ms) ? null : ms;
|
|
11
11
|
}
|
|
12
|
+
// The CLI talks HTTP to a daemon that may predate the rename, which sent only
|
|
13
|
+
// `usedPct`. Read both for one release; drop the fallback with the aliases.
|
|
14
|
+
export function weeklyPctOf(q) {
|
|
15
|
+
return q?.weeklyPct ?? q?.usedPct;
|
|
16
|
+
}
|
|
12
17
|
// Compact duration: 7d / 12h / 45m. Countdown convention floors; sub-minute
|
|
13
18
|
// spans read 1m so a live window never prints 0m.
|
|
14
19
|
export function fmtDuration(ms) {
|
|
@@ -18,6 +23,12 @@ export function fmtDuration(ms) {
|
|
|
18
23
|
return `${Math.floor(ms / HOUR_MS)}h`;
|
|
19
24
|
return `${Math.max(1, Math.floor(ms / MIN_MS))}m`;
|
|
20
25
|
}
|
|
26
|
+
// Short weekday of an ISO instant in the locked English copy, e.g. "Tue".
|
|
27
|
+
// Pinned to en-US: the sentence it feeds ("unused until Tue") is English, so
|
|
28
|
+
// a non-English machine must not print a localized weekday inside it.
|
|
29
|
+
export function weekdayOf(iso) {
|
|
30
|
+
return new Date(iso ?? "").toLocaleDateString("en-US", { weekday: "short" });
|
|
31
|
+
}
|
|
21
32
|
export function countdownText(ps, now) {
|
|
22
33
|
const resetsMs = msOf(ps.quota?.resetsAt);
|
|
23
34
|
let text;
|
|
@@ -40,14 +51,14 @@ export function elapsedPct(ps, now) {
|
|
|
40
51
|
return Math.round(Math.min(100, Math.max(0, pct)));
|
|
41
52
|
}
|
|
42
53
|
export function usedPctOf(ps) {
|
|
43
|
-
const u = ps.quota
|
|
54
|
+
const u = weeklyPctOf(ps.quota);
|
|
44
55
|
return typeof u === "number" && Number.isFinite(u) ? Math.round(u) : null;
|
|
45
56
|
}
|
|
46
57
|
export function railCells(ps, now) {
|
|
47
58
|
const cells = new Array(RAIL_CELLS).fill("unused");
|
|
48
59
|
if (ps.exclusionReason !== null)
|
|
49
60
|
return cells;
|
|
50
|
-
const u = ps.quota
|
|
61
|
+
const u = weeklyPctOf(ps.quota);
|
|
51
62
|
const used = typeof u === "number" && Number.isFinite(u)
|
|
52
63
|
? Math.min(RAIL_CELLS, Math.max(0, Math.round((u / 100) * RAIL_CELLS)))
|
|
53
64
|
: 0;
|
|
@@ -70,6 +81,10 @@ export function stateWord(ps) {
|
|
|
70
81
|
const adv = ps.advisory;
|
|
71
82
|
if (!adv)
|
|
72
83
|
return "Not reporting";
|
|
84
|
+
// A blocked month outranks every weekly verdict: weekly leftover cannot be
|
|
85
|
+
// spent until the month resets.
|
|
86
|
+
if (adv.bindingWindow === "monthly" && adv.bindingRemaining === 0)
|
|
87
|
+
return "Monthly exhausted";
|
|
73
88
|
if (adv.status === "at risk")
|
|
74
89
|
return "Cap risk";
|
|
75
90
|
if (adv.status === "watch")
|
|
@@ -124,7 +139,13 @@ export function forecastText(ps, now) {
|
|
|
124
139
|
if (!adv)
|
|
125
140
|
return "no readings yet";
|
|
126
141
|
let text;
|
|
127
|
-
if (adv.
|
|
142
|
+
if (adv.bindingWindow === "monthly" && adv.bindingRemaining <= 0) {
|
|
143
|
+
text = `unused until ${weekdayOf(ps.quota?.monthlyResetsAt)}`;
|
|
144
|
+
}
|
|
145
|
+
else if (adv.bindingWindow === "monthly") {
|
|
146
|
+
text = `${Math.round(adv.bindingRemaining)}% of month left`;
|
|
147
|
+
}
|
|
148
|
+
else if (adv.remaining <= 0) {
|
|
128
149
|
text = "Exhausted";
|
|
129
150
|
}
|
|
130
151
|
else if (adv.paceSource === "unknown" || adv.burnRate === null) {
|
|
@@ -179,7 +200,7 @@ function wasteOf(p) {
|
|
|
179
200
|
return typeof w === "number" && Number.isFinite(w) ? w : null;
|
|
180
201
|
}
|
|
181
202
|
function finiteUsed(p) {
|
|
182
|
-
const u = p.quota
|
|
203
|
+
const u = weeklyPctOf(p.quota);
|
|
183
204
|
return typeof u === "number" && Number.isFinite(u) ? u : null;
|
|
184
205
|
}
|
|
185
206
|
export function sortProviders(providers, key, use) {
|
|
@@ -192,6 +213,8 @@ export function sortProviders(providers, key, use) {
|
|
|
192
213
|
return 0;
|
|
193
214
|
if (p.exclusionReason !== null)
|
|
194
215
|
return 2;
|
|
216
|
+
if (p.advisory?.bindingWindow === "monthly" && p.advisory.bindingRemaining === 0)
|
|
217
|
+
return 2;
|
|
195
218
|
return 1;
|
|
196
219
|
};
|
|
197
220
|
indexed.sort((a, b) => {
|
package/dist/format/terminal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildRow, compactSuffix, sortProviders, } from "./rows.js";
|
|
1
|
+
import { buildRow, compactSuffix, sortProviders, weeklyPctOf, } from "./rows.js";
|
|
2
2
|
const GAP = " ";
|
|
3
3
|
const MIN_PROV_WIDTH = 12;
|
|
4
4
|
const MAX_PROV_WIDTH = 22;
|
|
@@ -32,6 +32,7 @@ const STATE_COLORS = {
|
|
|
32
32
|
"Cap risk": "\x1b[31m",
|
|
33
33
|
"Measuring": "\x1b[90m",
|
|
34
34
|
"Not reporting": "\x1b[90m",
|
|
35
|
+
"Monthly exhausted": "\x1b[31m",
|
|
35
36
|
};
|
|
36
37
|
// provWidth covers the 2-char prefix plus name, measured before ANSI.
|
|
37
38
|
export function provWidthFor(ids) {
|
|
@@ -92,7 +93,7 @@ export function renderWide(snapshot, opts) {
|
|
|
92
93
|
"PACE (%/DAY)".padEnd(paceWidth),
|
|
93
94
|
"USED VS TIME".padEnd(20),
|
|
94
95
|
"RESETS".padEnd(9),
|
|
95
|
-
"STATE".padEnd(
|
|
96
|
+
"STATE".padEnd(19),
|
|
96
97
|
"FORECAST",
|
|
97
98
|
].join(GAP);
|
|
98
99
|
const lines = rows.map(({ p, row }) => {
|
|
@@ -103,7 +104,7 @@ export function renderWide(snapshot, opts) {
|
|
|
103
104
|
const pace = (opts.ascii ? asciiText(row.pace) : row.pace).padEnd(paceWidth);
|
|
104
105
|
const rail = renderRail(row.railCells, g, row.state, color);
|
|
105
106
|
const countdown = (opts.ascii ? asciiText(row.countdown) : row.countdown).padEnd(9);
|
|
106
|
-
const state = row.state.padEnd(
|
|
107
|
+
const state = row.state.padEnd(19);
|
|
107
108
|
const forecast = opts.ascii ? asciiText(row.forecast) : row.forecast;
|
|
108
109
|
const main = [
|
|
109
110
|
color ? colorProviderField(name, recommended, row.state) : name,
|
|
@@ -158,7 +159,7 @@ export function renderCompact(snapshot, _now) {
|
|
|
158
159
|
const entries = sorted
|
|
159
160
|
.filter((p) => p.quota !== null)
|
|
160
161
|
.map((p) => {
|
|
161
|
-
const u = p.quota
|
|
162
|
+
const u = weeklyPctOf(p.quota);
|
|
162
163
|
const used = typeof u === "number" && Number.isFinite(u) ? String(Math.round(u)) : "?";
|
|
163
164
|
return `[${p.id}:${used}%${compactSuffix(p)}]`;
|
|
164
165
|
});
|
package/dist/http/server.js
CHANGED
|
@@ -220,7 +220,7 @@ export function buildApp(ctx) {
|
|
|
220
220
|
for (const [id, catalog] of catalogs) {
|
|
221
221
|
const identity = providerIdentity(id, ctx.providerNames);
|
|
222
222
|
const quota = quotaMap.get(id);
|
|
223
|
-
const leftoverPct = quota?.
|
|
223
|
+
const leftoverPct = quota?.weeklyPct != null ? Math.round((100 - quota.weeklyPct) * 10) / 10 : null;
|
|
224
224
|
out.push({
|
|
225
225
|
id,
|
|
226
226
|
displayName: identity.displayName,
|
|
@@ -452,8 +452,8 @@ export function buildApp(ctx) {
|
|
|
452
452
|
upsertQuota(ctx.db, {
|
|
453
453
|
provider: parsed.provider,
|
|
454
454
|
plan: parsed.plan,
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
weeklyPct: parsed.weeklyPct,
|
|
456
|
+
fiveHourPct: parsed.fiveHourPct,
|
|
457
457
|
resetsAt: parsed.resetsAt,
|
|
458
458
|
periodStart: parsed.periodStart,
|
|
459
459
|
source: "manual",
|
package/dist/runtime/poll.js
CHANGED
|
@@ -138,7 +138,7 @@ export function createCoordinator(opts) {
|
|
|
138
138
|
success: true,
|
|
139
139
|
failureCategory: null,
|
|
140
140
|
});
|
|
141
|
-
const usedPct = row.value?.
|
|
141
|
+
const usedPct = row.value?.weeklyPct;
|
|
142
142
|
const pctSuffix = typeof usedPct === "number" && Number.isFinite(usedPct)
|
|
143
143
|
? ` (${usedPct}% used)`
|
|
144
144
|
: "";
|
package/dist/service/index.js
CHANGED
|
@@ -94,7 +94,9 @@ export async function runServiceCommand(args, opts = {}, deps = {}) {
|
|
|
94
94
|
await backend.stop(deps);
|
|
95
95
|
break;
|
|
96
96
|
case "restart":
|
|
97
|
-
|
|
97
|
+
// Internal: a takeover passes its own phase reporter so the nested
|
|
98
|
+
// restart honours the calling command's progress suppression.
|
|
99
|
+
await backend.restart({ ...deps, phase: opts.phase ?? deps.phase });
|
|
98
100
|
break;
|
|
99
101
|
case "status":
|
|
100
102
|
await backend.status(deps, { json: !!opts.json });
|
package/dist/service/macos.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PhaseFn } from "../cli/progress.js";
|
|
1
2
|
export declare const SERVICE_LABEL = "quotacap";
|
|
2
3
|
export declare const PROVIDER_BINS: string[];
|
|
3
4
|
export declare function serviceSupported(platform?: string): boolean;
|
|
@@ -22,6 +23,7 @@ export interface ServiceDeps {
|
|
|
22
23
|
bootstrapTimeoutMs?: number;
|
|
23
24
|
version?: string;
|
|
24
25
|
quiet?: boolean;
|
|
26
|
+
phase?: PhaseFn;
|
|
25
27
|
}
|
|
26
28
|
export declare function foregroundGuidance(verb: string, platform: string): string;
|
|
27
29
|
export declare function defaultRunLaunchctl(args: string[]): string;
|
package/dist/service/macos.js
CHANGED
|
@@ -9,6 +9,7 @@ import { VERSION } from "../version.js";
|
|
|
9
9
|
import { getDbPath, readServiceMetadata, writeServiceMetadata, } from "../config.js";
|
|
10
10
|
import { ensureConfig, readConfig } from "../config.js";
|
|
11
11
|
import { createServiceClient } from "../runtime/client.js";
|
|
12
|
+
import { phase, phaseIfSlow } from "../cli/progress.js";
|
|
12
13
|
export const SERVICE_LABEL = "quotacap";
|
|
13
14
|
export const PROVIDER_BINS = ["claude", "codex", "kimi", "grok", "agy", "muse"];
|
|
14
15
|
export function serviceSupported(platform = process.platform) {
|
|
@@ -428,7 +429,8 @@ export async function start(deps = {}) {
|
|
|
428
429
|
run(["enable", `gui/${uid}/${SERVICE_LABEL}`]);
|
|
429
430
|
const waitReady = deps.waitReady ?? defaultWaitReady;
|
|
430
431
|
const cfg = await readConfig();
|
|
431
|
-
|
|
432
|
+
// An already-loaded, ready job answers at once; report only a real wait.
|
|
433
|
+
const ready = await phaseIfSlow(waitReady(cfg.port), () => (deps.phase ?? phase)("waiting for daemon…", { quiet: deps.quiet }));
|
|
432
434
|
if (!ready) {
|
|
433
435
|
throw new Error(`service did not become ready on port ${cfg.port}; check ${path.join(dataDir, "logs", "service.log")}`);
|
|
434
436
|
}
|
|
@@ -445,6 +447,7 @@ export async function restart(deps = {}) {
|
|
|
445
447
|
// process for the port and start() fails its readiness check.
|
|
446
448
|
// deps.port keeps tests off the real daemon's port.
|
|
447
449
|
const port = deps.port ?? (await readConfig()).port;
|
|
450
|
+
(deps.phase ?? phase)("waiting for daemon to stop…", { quiet: deps.quiet });
|
|
448
451
|
const waitReleased = deps.waitReleased ?? defaultWaitReleased;
|
|
449
452
|
if (!(await waitReleased(port))) {
|
|
450
453
|
(deps.print ?? console.log)(`port ${port} is still in use after stopping; starting anyway`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PhaseFn } from "../cli/progress.js";
|
|
1
2
|
import { defaultWhich, type ServiceStatus } from "./macos.js";
|
|
2
3
|
export declare const SERVICE_UNIT = "quotacap.service";
|
|
3
4
|
export declare function serviceSupported(platform?: string): boolean;
|
|
@@ -19,6 +20,7 @@ export interface SystemdDeps {
|
|
|
19
20
|
now?: () => Date;
|
|
20
21
|
version?: string;
|
|
21
22
|
quiet?: boolean;
|
|
23
|
+
phase?: PhaseFn;
|
|
22
24
|
}
|
|
23
25
|
export declare function formatSystemctlError(args: string[], detail: string): Error;
|
|
24
26
|
export declare function defaultRunSystemctl(args: string[]): string;
|
package/dist/service/systemd.js
CHANGED
|
@@ -8,9 +8,9 @@ import fs from "node:fs";
|
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { VERSION } from "../version.js";
|
|
11
|
-
import { getDbPath, readServiceMetadata, writeServiceMetadata, } from "../config.js";
|
|
12
|
-
import { ensureConfig, readConfig } from "../config.js";
|
|
11
|
+
import { getDbPath, readServiceMetadata, writeServiceMetadata, ensureConfig, readConfig, } from "../config.js";
|
|
13
12
|
import { createServiceClient } from "../runtime/client.js";
|
|
13
|
+
import { phase, phaseIfSlow } from "../cli/progress.js";
|
|
14
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) {
|
|
@@ -241,7 +241,8 @@ export async function start(deps = {}) {
|
|
|
241
241
|
}
|
|
242
242
|
const { dataDir, print, run } = resolved(deps);
|
|
243
243
|
run(["enable", "--now", SERVICE_UNIT]);
|
|
244
|
-
|
|
244
|
+
// An already-active, ready unit answers at once; report only a real wait.
|
|
245
|
+
const port = await phaseIfSlow(waitForReady(deps, dataDir), () => (deps.phase ?? phase)("waiting for daemon…", { quiet: deps.quiet }));
|
|
245
246
|
print(`service started and ready on port ${port}`);
|
|
246
247
|
}
|
|
247
248
|
export async function restart(deps = {}) {
|
|
@@ -254,6 +255,7 @@ export async function restart(deps = {}) {
|
|
|
254
255
|
// systemctl stop can return before the port is released; same race as launchd.
|
|
255
256
|
// deps.port mirrors start(): tests must never probe the real daemon's port.
|
|
256
257
|
const port = deps.port ?? (await readConfig()).port;
|
|
258
|
+
(deps.phase ?? phase)("waiting for daemon to stop…", { quiet: deps.quiet });
|
|
257
259
|
const waitReleased = deps.waitReleased ?? defaultWaitReleased;
|
|
258
260
|
if (!(await waitReleased(port))) {
|
|
259
261
|
(deps.print ?? console.log)(`port ${port} is still in use after stopping; starting anyway`);
|