quotacap 0.0.40 → 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/dist/adapters/codex.js +27 -18
- package/dist/adapters/kimi.d.ts +41 -0
- package/dist/adapters/kimi.js +451 -19
- package/dist/adapters/muse.js +1 -1
- package/dist/adapters/pty.d.ts +9 -0
- package/dist/adapters/pty.js +16 -0
- package/dist/cli/advise.js +1 -0
- package/dist/cli/clients.d.ts +4 -0
- package/dist/cli/models.js +4 -0
- 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 +37 -11
- 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/version.js +1 -1
- package/dist/web/src/components/FaultBanner.d.ts +10 -5
- package/dist/web/src/components/FaultBanner.js +19 -9
- package/dist/web/src/pages/Onboarding.js +2 -1
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +1 -1
- package/web/dist/assets/{index-CsOiHZyD.js → index-BUK8xUHm.js} +1 -1
- package/web/dist/index.html +1 -1
package/dist/cli/advise.js
CHANGED
package/dist/cli/clients.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ServiceClient } from "../runtime/client.js";
|
|
|
3
3
|
import type { CatalogFetcher } from "../catalog/index.js";
|
|
4
4
|
import type { SleepFn } from "./takeover.js";
|
|
5
5
|
import type { UpdateCache } from "../runtime/updates.js";
|
|
6
|
+
import type { PhaseFn } from "./progress.js";
|
|
6
7
|
export interface CreateClientOptions {
|
|
7
8
|
port: number;
|
|
8
9
|
token?: string;
|
|
@@ -24,8 +25,11 @@ export interface ClientCommandDeps {
|
|
|
24
25
|
sleep?: SleepFn;
|
|
25
26
|
timeoutMs?: number;
|
|
26
27
|
readToken?: () => string | undefined;
|
|
28
|
+
phase?: PhaseFn;
|
|
29
|
+
json?: boolean;
|
|
27
30
|
};
|
|
28
31
|
checkUpdates?: () => Promise<UpdateCache | null>;
|
|
29
32
|
catalogFetchers?: Record<string, CatalogFetcher>;
|
|
33
|
+
phase?: PhaseFn;
|
|
30
34
|
}
|
|
31
35
|
export declare function registerClientCommands(program: Command, deps?: ClientCommandDeps): void;
|
package/dist/cli/models.js
CHANGED
|
@@ -6,6 +6,7 @@ import { providerIdentity } from "../advisory/provider-names.js";
|
|
|
6
6
|
import { readToken } from "../runtime/token.js";
|
|
7
7
|
import { createServiceClient } from "../runtime/client.js";
|
|
8
8
|
import { OFFLINE_LABEL } from "./snapshot-source.js";
|
|
9
|
+
import { phase } from "./progress.js";
|
|
9
10
|
function projectModels(catalogs, quotaMap, providerNames) {
|
|
10
11
|
const out = [];
|
|
11
12
|
for (const [id, catalog] of catalogs) {
|
|
@@ -70,6 +71,9 @@ export function registerModelsCommand(program, deps = {}) {
|
|
|
70
71
|
});
|
|
71
72
|
let body;
|
|
72
73
|
try {
|
|
74
|
+
if (o.refresh) {
|
|
75
|
+
(deps.phase ?? phase)("refreshing model catalogs…", { json: !!o.json });
|
|
76
|
+
}
|
|
73
77
|
const url = o.refresh
|
|
74
78
|
? (o.provider ? `/api/models/refresh?provider=${encodeURIComponent(o.provider)}` : "/api/models/refresh")
|
|
75
79
|
: (o.provider ? `/api/models?provider=${encodeURIComponent(o.provider)}` : "/api/models");
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface ProgressOptions {
|
|
2
|
+
/** Suppress output when machine-readable JSON is requested. */
|
|
3
|
+
json?: boolean;
|
|
4
|
+
/** Explicitly silence progress (e.g. quiet mode). */
|
|
5
|
+
quiet?: boolean;
|
|
6
|
+
/** Whether the output stream is interactive. Defaults to process.stderr.isTTY. */
|
|
7
|
+
isTTY?: boolean;
|
|
8
|
+
/** Custom writer for stderr (defaults to console.error). */
|
|
9
|
+
stderr?: (message: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export type PhaseFn = (message: string, opts?: ProgressOptions) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Format a progress phase string ensuring consistent trailing ellipsis without duplication.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatPhase(message: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Determine whether progress messages should be printed given the options.
|
|
18
|
+
*/
|
|
19
|
+
export declare function shouldEmitProgress(opts?: ProgressOptions): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Emit a progress phase message strictly to stderr if interactive and non-json.
|
|
22
|
+
*/
|
|
23
|
+
export declare function phase(message: string, opts?: ProgressOptions): void;
|
|
24
|
+
/**
|
|
25
|
+
* Await work, calling emit only if it is still pending after graceMs, so an
|
|
26
|
+
* already-settled fast path stays silent.
|
|
27
|
+
*/
|
|
28
|
+
export declare function phaseIfSlow<T>(work: Promise<T>, emit: () => void, graceMs?: number): Promise<T>;
|
|
29
|
+
/**
|
|
30
|
+
* Create a phase reporter with pre-bound default options.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createPhase(baseOpts?: ProgressOptions): (message: string, overrideOpts?: ProgressOptions) => void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Shared progress helper for multi-second CLI operations.
|
|
2
|
+
// Strictly writes to stderr with a consistent lowercase phrasing ending in '…'.
|
|
3
|
+
// Automatically suppressed under --json, quiet mode, and non-interactive (non-TTY) runs.
|
|
4
|
+
/**
|
|
5
|
+
* Format a progress phase string ensuring consistent trailing ellipsis without duplication.
|
|
6
|
+
*/
|
|
7
|
+
export function formatPhase(message) {
|
|
8
|
+
const trimmed = message.trim();
|
|
9
|
+
if (!trimmed)
|
|
10
|
+
return "";
|
|
11
|
+
if (trimmed.endsWith("...")) {
|
|
12
|
+
return `${trimmed.slice(0, -3)}…`;
|
|
13
|
+
}
|
|
14
|
+
if (trimmed.endsWith("…")) {
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
return `${trimmed}…`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Determine whether progress messages should be printed given the options.
|
|
21
|
+
*/
|
|
22
|
+
export function shouldEmitProgress(opts = {}) {
|
|
23
|
+
if (opts.json || opts.quiet)
|
|
24
|
+
return false;
|
|
25
|
+
if (opts.isTTY !== undefined)
|
|
26
|
+
return Boolean(opts.isTTY);
|
|
27
|
+
return Boolean(process.stderr.isTTY);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Emit a progress phase message strictly to stderr if interactive and non-json.
|
|
31
|
+
*/
|
|
32
|
+
export function phase(message, opts = {}) {
|
|
33
|
+
const formatted = formatPhase(message);
|
|
34
|
+
if (!formatted)
|
|
35
|
+
return;
|
|
36
|
+
if (!shouldEmitProgress(opts))
|
|
37
|
+
return;
|
|
38
|
+
const write = opts.stderr ?? console.error;
|
|
39
|
+
write(formatted);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Await work, calling emit only if it is still pending after graceMs, so an
|
|
43
|
+
* already-settled fast path stays silent.
|
|
44
|
+
*/
|
|
45
|
+
export async function phaseIfSlow(work, emit, graceMs = 250) {
|
|
46
|
+
let timer;
|
|
47
|
+
const slow = new Promise((r) => {
|
|
48
|
+
timer = setTimeout(() => r("slow"), graceMs);
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
const first = await Promise.race([
|
|
52
|
+
work.then(() => "done", () => "done"),
|
|
53
|
+
slow,
|
|
54
|
+
]);
|
|
55
|
+
if (first === "slow")
|
|
56
|
+
emit();
|
|
57
|
+
return await work;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a phase reporter with pre-bound default options.
|
|
65
|
+
*/
|
|
66
|
+
export function createPhase(baseOpts = {}) {
|
|
67
|
+
return (message, overrideOpts = {}) => {
|
|
68
|
+
phase(message, { ...baseOpts, ...overrideOpts });
|
|
69
|
+
};
|
|
70
|
+
}
|
package/dist/cli/runtime.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|
|
2
2
|
import { startService, type StartServiceOptions } from "../runtime/service.js";
|
|
3
3
|
import { type ServiceClient } from "../runtime/client.js";
|
|
4
4
|
import { type SleepFn } from "./takeover.js";
|
|
5
|
+
import { type PhaseFn } from "./progress.js";
|
|
5
6
|
export type StartServiceFn = (opts?: StartServiceOptions) => ReturnType<typeof startService>;
|
|
6
7
|
export type OpenBrowserFn = (url: string) => Promise<void> | void;
|
|
7
8
|
export type ExecServiceFn = (args: string[], opts?: Record<string, any>) => Promise<number>;
|
|
@@ -20,10 +21,13 @@ export interface RuntimeCommandDeps {
|
|
|
20
21
|
sleep?: SleepFn;
|
|
21
22
|
timeoutMs?: number;
|
|
22
23
|
readToken?: () => string | undefined;
|
|
24
|
+
phase?: PhaseFn;
|
|
25
|
+
json?: boolean;
|
|
23
26
|
};
|
|
24
27
|
checkUpdates?: () => Promise<unknown>;
|
|
25
28
|
isInstalled?: () => Promise<boolean>;
|
|
26
29
|
serviceSupported?: () => boolean;
|
|
30
|
+
phase?: PhaseFn;
|
|
27
31
|
}
|
|
28
32
|
export interface WebLaunchOptions {
|
|
29
33
|
port?: string;
|
|
@@ -38,5 +42,6 @@ export declare function waitForInitialPoll(port: number, opts?: {
|
|
|
38
42
|
}) => any;
|
|
39
43
|
sleep?: SleepFn;
|
|
40
44
|
timeoutMs?: number;
|
|
45
|
+
phase?: PhaseFn;
|
|
41
46
|
}): Promise<void>;
|
|
42
47
|
export declare function registerRuntimeCommands(program: Command, deps?: RuntimeCommandDeps): void;
|
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
|
}
|
|
@@ -252,6 +263,13 @@ function matchPrecedence(evidence, errorName, errorCode, text) {
|
|
|
252
263
|
if (/\blimits refresh requested\b/i.test(text) || /\brefresh requested\b/i.test(text)) {
|
|
253
264
|
return { code: "service_unavailable", phrase: "limits refresh requested" };
|
|
254
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
|
+
}
|
|
255
273
|
// Order 9: timeout — Remaining explicit timeout or timed out errors
|
|
256
274
|
if (/\btimed out\b/i.test(text) || /\btimeout\b/i.test(text)) {
|
|
257
275
|
return { code: "timeout", phrase: "timed out" };
|
|
@@ -356,14 +374,16 @@ export function diagnosticError(reason, evidence) {
|
|
|
356
374
|
if (parsed.durationMs !== undefined)
|
|
357
375
|
durationMs = parsed.durationMs;
|
|
358
376
|
}
|
|
359
|
-
//
|
|
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.
|
|
360
379
|
let safeStdoutText = "";
|
|
361
380
|
if (evidence?.stdout) {
|
|
362
381
|
if (evidence.source === "pty") {
|
|
363
382
|
const lines = evidence.stdout.split(/[\r\n]+/);
|
|
364
383
|
const matchedLines = lines.filter((l) => /\b(error|failed|failure|fatal|unauthorized|unauthorised|denied|forbidden|panic|exception)\b/i.test(l) ||
|
|
365
384
|
/\btrust\b/i.test(l) ||
|
|
366
|
-
/\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));
|
|
367
387
|
safeStdoutText = matchedLines.join(" ");
|
|
368
388
|
}
|
|
369
389
|
else {
|
|
@@ -428,7 +448,7 @@ function mapCategory(code) {
|
|
|
428
448
|
return "unknown";
|
|
429
449
|
}
|
|
430
450
|
}
|
|
431
|
-
function getSummaryAndAction(code, providerName, provider) {
|
|
451
|
+
function getSummaryAndAction(code, providerName, provider, errorDetail) {
|
|
432
452
|
switch (code) {
|
|
433
453
|
case "terminal_error":
|
|
434
454
|
return {
|
|
@@ -436,6 +456,12 @@ function getSummaryAndAction(code, providerName, provider) {
|
|
|
436
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.`,
|
|
437
457
|
};
|
|
438
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
|
+
}
|
|
439
465
|
return {
|
|
440
466
|
summary: `${providerName} login required`,
|
|
441
467
|
action: `Open ${providerName} and follow its sign-in instructions. Then select Refresh in the QuotaCap dashboard.`,
|
|
@@ -495,7 +521,7 @@ export function classifyFailure(provider, reason) {
|
|
|
495
521
|
}
|
|
496
522
|
const category = mapCategory(diag.diagnosticCode);
|
|
497
523
|
const pName = formatProviderName(provider);
|
|
498
|
-
const { summary, action } = getSummaryAndAction(diag.diagnosticCode, pName, provider);
|
|
524
|
+
const { summary, action } = getSummaryAndAction(diag.diagnosticCode, pName, provider, diag.errorDetail);
|
|
499
525
|
return {
|
|
500
526
|
diagnosticCode: diag.diagnosticCode,
|
|
501
527
|
errorDetail: diag.errorDetail,
|
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`);
|
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.41";
|
|
@@ -3,12 +3,17 @@ import type { ProviderView } from "../state.js";
|
|
|
3
3
|
export interface Repair {
|
|
4
4
|
text: string;
|
|
5
5
|
}
|
|
6
|
+
/** Dashboard wording for a server auth action. The button on this page is Retry. */
|
|
7
|
+
export declare function authInstruction(name: string, attempt: {
|
|
8
|
+
summary?: string | null;
|
|
9
|
+
action?: string | null;
|
|
10
|
+
} | null | undefined): string;
|
|
6
11
|
/**
|
|
7
|
-
* Repair routing per excluded provider. Auth failures
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* Repair routing per excluded provider. Auth failures use the server's
|
|
13
|
+
* summary and action, with Retry in place of the dashboard Refresh control.
|
|
14
|
+
* OpenCode Go names its own login command. Every other exclusion points at
|
|
15
|
+
* a re-poll. Service start/install is covered by the service-unavailable
|
|
16
|
+
* panel when the daemon itself is down.
|
|
12
17
|
*/
|
|
13
18
|
export declare function repairFor(provider: ProviderView): Repair;
|
|
14
19
|
export declare function FaultBanner({ providers, onRepoll, repolling, polling, }: {
|