quotacap 0.0.22 → 0.0.23
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 +4 -6
- package/dist/adapters/grok.js +2 -2
- package/dist/adapters/pty.d.ts +18 -0
- package/dist/adapters/pty.js +124 -32
- package/dist/advisory/snapshot.d.ts +6 -0
- package/dist/advisory/types.d.ts +7 -0
- package/dist/cli/advise.js +72 -6
- package/dist/cli/clients.d.ts +10 -0
- package/dist/cli/index.js +13 -8
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +29 -0
- package/dist/cli/runtime.d.ts +21 -0
- package/dist/cli/runtime.js +169 -42
- package/dist/cli/snapshot-source.d.ts +1 -0
- package/dist/cli/snapshot-source.js +9 -0
- package/dist/cli/status.js +70 -4
- package/dist/cli/takeover.d.ts +64 -0
- package/dist/cli/takeover.js +175 -0
- package/dist/cli/update.d.ts +61 -0
- package/dist/cli/update.js +392 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +28 -0
- package/dist/http/server.d.ts +8 -0
- package/dist/http/server.js +40 -0
- package/dist/runtime/poll.d.ts +2 -0
- package/dist/runtime/poll.js +7 -0
- package/dist/runtime/service.js +19 -2
- package/dist/runtime/updates.d.ts +74 -0
- package/dist/runtime/updates.js +445 -0
- package/dist/runtime/versions.d.ts +2 -0
- package/dist/runtime/versions.js +26 -0
- package/dist/service/index.d.ts +7 -2
- package/dist/service/index.js +67 -11
- package/dist/service/macos.d.ts +4 -0
- package/dist/service/macos.js +61 -10
- package/dist/service/systemd.d.ts +40 -0
- package/dist/service/systemd.js +387 -0
- package/dist/src/adapters/grok.js +2 -2
- package/dist/src/adapters/pty.d.ts +18 -0
- package/dist/src/adapters/pty.js +124 -32
- package/dist/src/advisory/snapshot.d.ts +6 -0
- package/dist/src/advisory/types.d.ts +7 -0
- package/dist/src/cli/advise.js +72 -6
- package/dist/src/cli/clients.d.ts +10 -0
- package/dist/src/cli/index.js +13 -8
- package/dist/src/cli/init.d.ts +2 -0
- package/dist/src/cli/init.js +29 -0
- package/dist/src/cli/runtime.d.ts +21 -0
- package/dist/src/cli/runtime.js +169 -42
- package/dist/src/cli/snapshot-source.d.ts +1 -0
- package/dist/src/cli/snapshot-source.js +9 -0
- package/dist/src/cli/status.js +70 -4
- package/dist/src/cli/takeover.d.ts +64 -0
- package/dist/src/cli/takeover.js +175 -0
- package/dist/src/cli/update.d.ts +61 -0
- package/dist/src/cli/update.js +392 -0
- package/dist/src/config.d.ts +7 -0
- package/dist/src/config.js +28 -0
- package/dist/src/http/server.d.ts +8 -0
- package/dist/src/http/server.js +40 -0
- package/dist/src/runtime/poll.d.ts +2 -0
- package/dist/src/runtime/poll.js +7 -0
- package/dist/src/runtime/service.js +19 -2
- package/dist/src/runtime/updates.d.ts +74 -0
- package/dist/src/runtime/updates.js +445 -0
- package/dist/src/runtime/versions.d.ts +2 -0
- package/dist/src/runtime/versions.js +26 -0
- package/dist/src/service/index.d.ts +7 -2
- package/dist/src/service/index.js +67 -11
- package/dist/src/service/macos.d.ts +4 -0
- package/dist/src/service/macos.js +61 -10
- package/dist/src/service/systemd.d.ts +40 -0
- package/dist/src/service/systemd.js +387 -0
- package/dist/src/version.js +1 -1
- package/dist/src/webAssets.js +1 -1
- package/dist/src/webHtml.js +1 -1
- package/dist/tests/adapters/grok.test.js +10 -0
- package/dist/tests/adapters/pty-perms.test.d.ts +1 -0
- package/dist/tests/adapters/pty-perms.test.js +78 -0
- package/dist/tests/cli/advise.test.js +73 -0
- package/dist/tests/cli/cli.test.js +1 -1
- package/dist/tests/cli/init.test.d.ts +1 -0
- package/dist/tests/cli/init.test.js +55 -0
- package/dist/tests/cli/launcher.test.d.ts +1 -0
- package/dist/tests/cli/launcher.test.js +271 -0
- package/dist/tests/cli/status.test.js +113 -0
- package/dist/tests/cli/takeover.test.d.ts +1 -0
- package/dist/tests/cli/takeover.test.js +325 -0
- package/dist/tests/cli/update.test.d.ts +1 -0
- package/dist/tests/cli/update.test.js +382 -0
- package/dist/tests/runtime/cli.test.js +206 -6
- package/dist/tests/runtime/http.test.js +79 -0
- package/dist/tests/runtime/poll.test.js +28 -0
- package/dist/tests/runtime/updates.test.d.ts +1 -0
- package/dist/tests/runtime/updates.test.js +509 -0
- package/dist/tests/service/macos.test.js +103 -2
- package/dist/tests/service/systemd.test.d.ts +1 -0
- package/dist/tests/service/systemd.test.js +330 -0
- package/dist/tests/smoke/dashboard.e2e.d.ts +1 -0
- package/dist/tests/smoke/dashboard.e2e.js +142 -0
- package/dist/tests/smoke/playwright.config.d.ts +2 -0
- package/dist/tests/smoke/playwright.config.js +11 -0
- package/dist/tests/web/dashboard.e2e.js +1 -1
- package/dist/tests/web/header.test.js +26 -0
- package/dist/version.js +1 -1
- package/dist/web/src/components/Header.js +5 -1
- package/dist/web/src/state.d.ts +7 -0
- package/dist/webAssets.js +1 -1
- package/dist/webHtml.js +1 -1
- package/package.json +2 -3
- package/web/dist/assets/{index-CyUkWdwJ.css → index-BEBqgvq8.css} +1 -1
- package/web/dist/assets/{index-PN4RpMKG.js → index-DZfttQgo.js} +1 -1
- package/web/dist/index.html +2 -2
- package/scripts/fix-pty-perms.mjs +0 -21
package/README.md
CHANGED
|
@@ -39,21 +39,19 @@ npm install -g quotacap
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
The binary is a self-contained executable from GitHub Releases (shipped as `quotacap-<os>-<arch>.tar.gz` with a `pty` sidecar for Kimi/Codex/Grok); `install.sh` handles the tarball and sidecar transparently.
|
|
42
|
+
One command takes you from zero to a running tracker: the installer verifies checksums, provisions config, registers the background login service, and opens the dashboard. Re-running it upgrades in place with no flags. Pass `--no-service` (foreground only) or `--no-open` (SSH sessions) to skip steps.
|
|
42
43
|
The npm package runs the same CLI on Node. To run without a global install, replace `quotacap` with `npx quotacap` in any command, for example `npx quotacap web`.
|
|
43
44
|
|
|
44
45
|
## Quick start
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
|
-
#
|
|
48
|
-
quotacap init # writes ~/.quotacap/config.json
|
|
49
|
-
quotacap web # dashboard at http://localhost:8787
|
|
50
|
-
|
|
51
|
-
# Terminal 2: after the dashboard table fills
|
|
48
|
+
quotacap # opens the dashboard, starting the service if needed
|
|
52
49
|
quotacap status
|
|
53
50
|
quotacap advise
|
|
51
|
+
quotacap update # upgrade to the latest release
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
`
|
|
54
|
+
No setup ceremony: config is provisioned on first use, and bare `quotacap` is the dashboard launcher. `quotacap web --foreground` and `quotacap daemon` keep the old hold-the-terminal mode for SSH, containers, and supervisors.
|
|
57
55
|
|
|
58
56
|
## MCP
|
|
59
57
|
|
package/dist/adapters/grok.js
CHANGED
|
@@ -10,7 +10,7 @@ const ABBR_MONTHS = {
|
|
|
10
10
|
jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
|
|
11
11
|
};
|
|
12
12
|
function parseGrokReset(raw, now) {
|
|
13
|
-
const m = raw.trim().match(/^([A-Za-z]+)\s+(\d{1,2}),\s
|
|
13
|
+
const m = raw.trim().match(/^([A-Za-z]+)\s+(\d{1,2}),\s*(\d{1,2}):(\d{2})$/);
|
|
14
14
|
if (!m)
|
|
15
15
|
return null;
|
|
16
16
|
const monStr = m[1].toLowerCase();
|
|
@@ -73,7 +73,7 @@ export function parseGrokTui(text, now = new Date()) {
|
|
|
73
73
|
if (Number.isFinite(v) && v >= 0)
|
|
74
74
|
creditsUsd = v;
|
|
75
75
|
}
|
|
76
|
-
const mReset = cleaned.match(/Resets:\s*([A-Za-z]+\s+\d+,\s
|
|
76
|
+
const mReset = cleaned.match(/Resets:\s*([A-Za-z]+\s+\d+,\s*\d+:\d+)/i);
|
|
77
77
|
let resetsAt = null;
|
|
78
78
|
let estimated = false;
|
|
79
79
|
if (mReset) {
|
package/dist/adapters/pty.d.ts
CHANGED
|
@@ -20,4 +20,22 @@ export interface PtyRunOptions {
|
|
|
20
20
|
/** Diagnostic label for the tracked child (adapter id). */
|
|
21
21
|
label?: string;
|
|
22
22
|
}
|
|
23
|
+
export interface SpawnHelperRepair {
|
|
24
|
+
helper: string;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Lazy replacement for the removed postinstall: best-effort `chmod 0o755`
|
|
30
|
+
* over every prebuilds entry `spawn-helper` under the given node-pty roots.
|
|
31
|
+
* Missing roots, missing prebuild dirs, and missing helpers are skipped
|
|
32
|
+
* silently; only attempted repairs are recorded.
|
|
33
|
+
*/
|
|
34
|
+
export declare function repairSpawnHelpers(roots: string[], chmod?: (p: string, mode: number) => void): SpawnHelperRepair[];
|
|
35
|
+
/**
|
|
36
|
+
* Human diagnostic for failed repairs, or null when nothing failed.
|
|
37
|
+
* Load-bearing for root-owned trees (e.g. `sudo` global installs) where the
|
|
38
|
+
* invoking user cannot repair: without it the failure would go opaque again.
|
|
39
|
+
*/
|
|
40
|
+
export declare function describeSpawnHelperRepairs(outcomes: SpawnHelperRepair[]): string | null;
|
|
23
41
|
export declare function runPty(opts: PtyRunOptions): Promise<string>;
|
package/dist/adapters/pty.js
CHANGED
|
@@ -18,17 +18,71 @@ function regexTest(re, s) {
|
|
|
18
18
|
const fresh = new RegExp(re.source, re.flags.replace("g", ""));
|
|
19
19
|
return fresh.test(s);
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Lazy replacement for the removed postinstall: best-effort `chmod 0o755`
|
|
23
|
+
* over every prebuilds entry `spawn-helper` under the given node-pty roots.
|
|
24
|
+
* Missing roots, missing prebuild dirs, and missing helpers are skipped
|
|
25
|
+
* silently; only attempted repairs are recorded.
|
|
26
|
+
*/
|
|
27
|
+
export function repairSpawnHelpers(roots, chmod = fs.chmodSync) {
|
|
28
|
+
const outcomes = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const root of roots) {
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = fs.readdirSync(path.join(root, "prebuilds"));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const helper = path.join(root, "prebuilds", entry, "spawn-helper");
|
|
40
|
+
if (seen.has(helper))
|
|
41
|
+
continue;
|
|
42
|
+
seen.add(helper);
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.existsSync(helper))
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
chmod(helper, 0o755);
|
|
52
|
+
outcomes.push({ helper, ok: true });
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
outcomes.push({ helper, ok: false, error: String(e?.message ?? e) });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return outcomes;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Human diagnostic for failed repairs, or null when nothing failed.
|
|
63
|
+
* Load-bearing for root-owned trees (e.g. `sudo` global installs) where the
|
|
64
|
+
* invoking user cannot repair: without it the failure would go opaque again.
|
|
65
|
+
*/
|
|
66
|
+
export function describeSpawnHelperRepairs(outcomes) {
|
|
67
|
+
const failed = outcomes.filter((o) => !o.ok);
|
|
68
|
+
if (failed.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const lines = failed.map((o) => `could not make ${o.helper} executable (${o.error ?? "unknown error"})`);
|
|
71
|
+
return (`${lines.join("; ")} — the install tree is likely root-owned (e.g. installed with sudo); ` +
|
|
72
|
+
`reinstall without sudo or fix the install's ownership so the invoking user can write it`);
|
|
73
|
+
}
|
|
74
|
+
function nodePtyPackageRoot(resolvedEntry) {
|
|
75
|
+
const dir = path.dirname(resolvedEntry);
|
|
76
|
+
// node-pty's main entry is lib/index.js; the package root is two levels up.
|
|
77
|
+
if (path.basename(dir) === "lib" && /^index\.[cm]?js$/.test(path.basename(resolvedEntry))) {
|
|
78
|
+
return path.dirname(dir);
|
|
79
|
+
}
|
|
80
|
+
return dir;
|
|
81
|
+
}
|
|
21
82
|
let ptyMod = null;
|
|
22
83
|
function getPty() {
|
|
23
84
|
if (ptyMod)
|
|
24
85
|
return ptyMod;
|
|
25
|
-
try {
|
|
26
|
-
const require = createRequire(import.meta.url);
|
|
27
|
-
ptyMod = require("node-pty");
|
|
28
|
-
if (ptyMod?.spawn)
|
|
29
|
-
return ptyMod;
|
|
30
|
-
}
|
|
31
|
-
catch { }
|
|
32
86
|
// Compiled binary (bun --compile) has no node_modules; try sidecar locations
|
|
33
87
|
const execDir = path.dirname(process.execPath);
|
|
34
88
|
const platformArch = `${process.platform}-${process.arch}`;
|
|
@@ -40,6 +94,29 @@ function getPty() {
|
|
|
40
94
|
path.join(os.homedir(), ".local", "share", "quotacap", "pty", platformArch, "node-pty"),
|
|
41
95
|
path.join(os.homedir(), ".quotacap", "pty", "node-pty"),
|
|
42
96
|
];
|
|
97
|
+
// Best-effort +x before requiring: the resolvable npm root (when present)
|
|
98
|
+
// plus every existing sidecar root. Successful repairs stay silent.
|
|
99
|
+
const repairRoots = [];
|
|
100
|
+
try {
|
|
101
|
+
const require0 = createRequire(import.meta.url);
|
|
102
|
+
repairRoots.push(nodePtyPackageRoot(require0.resolve("node-pty")));
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
for (const c of candidates) {
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync(c))
|
|
108
|
+
repairRoots.push(c);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
112
|
+
const repairs = repairSpawnHelpers(repairRoots);
|
|
113
|
+
try {
|
|
114
|
+
const require = createRequire(import.meta.url);
|
|
115
|
+
ptyMod = require("node-pty");
|
|
116
|
+
if (ptyMod?.spawn)
|
|
117
|
+
return ptyMod;
|
|
118
|
+
}
|
|
119
|
+
catch { }
|
|
43
120
|
for (const c of candidates) {
|
|
44
121
|
try {
|
|
45
122
|
if (!fs.existsSync(c))
|
|
@@ -65,7 +142,9 @@ function getPty() {
|
|
|
65
142
|
}
|
|
66
143
|
catch { }
|
|
67
144
|
}
|
|
68
|
-
|
|
145
|
+
const repairNote = describeSpawnHelperRepairs(repairs);
|
|
146
|
+
throw new Error("pty: node-pty not available — install with build tools (Xcode on macOS, build-essential + python3 on Linux) or use exec-based adapters only; for compiled binaries, ensure the pty sidecar is installed alongside the binary (see install.sh)" +
|
|
147
|
+
(repairNote ? `; ${repairNote}` : ""));
|
|
69
148
|
}
|
|
70
149
|
function isBunRuntime() {
|
|
71
150
|
return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.spawn === "function";
|
|
@@ -97,6 +176,11 @@ async function runPtyBun(opts) {
|
|
|
97
176
|
let transcript = "";
|
|
98
177
|
let exited = false;
|
|
99
178
|
let exitCode;
|
|
179
|
+
// Genuine PTY via the Bun.spawn `terminal` option (verified on Bun 1.3.11):
|
|
180
|
+
// the child's stdin, stdout, and stderr are all the PTY slave, input goes
|
|
181
|
+
// through proc.terminal.write, and merged output arrives in `data`.
|
|
182
|
+
// proc.stdin/stdout/stderr are null in this mode.
|
|
183
|
+
const decoder = new TextDecoder();
|
|
100
184
|
const proc = BunGlobal.spawn([opts.file, ...(opts.args ?? [])], {
|
|
101
185
|
cwd: opts.cwd ?? process.cwd(),
|
|
102
186
|
env: {
|
|
@@ -104,25 +188,26 @@ async function runPtyBun(opts) {
|
|
|
104
188
|
...(opts.env ?? {}),
|
|
105
189
|
TERM: "xterm-256color",
|
|
106
190
|
},
|
|
107
|
-
|
|
108
|
-
|
|
191
|
+
terminal: {
|
|
192
|
+
cols,
|
|
193
|
+
rows,
|
|
194
|
+
data(_term, chunk) {
|
|
195
|
+
try {
|
|
196
|
+
transcript += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
},
|
|
200
|
+
},
|
|
109
201
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
let reading = true;
|
|
114
|
-
const readLoop = (async () => {
|
|
202
|
+
if (!proc.terminal) {
|
|
203
|
+
// Old Bun runtimes silently ignore the unknown `terminal` option, so an
|
|
204
|
+
// absent proc.terminal is the fail-loud signal — never run half-attached.
|
|
115
205
|
try {
|
|
116
|
-
|
|
117
|
-
const { value, done } = await reader.read();
|
|
118
|
-
if (done)
|
|
119
|
-
break;
|
|
120
|
-
if (value)
|
|
121
|
-
transcript += decoder.decode(value);
|
|
122
|
-
}
|
|
206
|
+
proc.kill();
|
|
123
207
|
}
|
|
124
208
|
catch { }
|
|
125
|
-
|
|
209
|
+
throw new Error("pty: Bun runtime lacks terminal (PTY) support (Bun.spawn terminal option unavailable) — rebuild the standalone binary with a newer Bun version");
|
|
210
|
+
}
|
|
126
211
|
// Poll for exit
|
|
127
212
|
const exitPoll = (async () => {
|
|
128
213
|
try {
|
|
@@ -132,6 +217,7 @@ async function runPtyBun(opts) {
|
|
|
132
217
|
}
|
|
133
218
|
catch { }
|
|
134
219
|
})();
|
|
220
|
+
void exitPoll;
|
|
135
221
|
const checkCap = () => {
|
|
136
222
|
if (Buffer.byteLength(transcript, "utf8") > maxBytes) {
|
|
137
223
|
throw new Error(`pty transcript exceeds ${maxBytes} bytes`);
|
|
@@ -169,6 +255,10 @@ async function runPtyBun(opts) {
|
|
|
169
255
|
proc.kill();
|
|
170
256
|
}
|
|
171
257
|
catch { }
|
|
258
|
+
try {
|
|
259
|
+
proc.terminal?.close();
|
|
260
|
+
}
|
|
261
|
+
catch { }
|
|
172
262
|
}, opts.label ?? opts.file);
|
|
173
263
|
try {
|
|
174
264
|
if (opts.readyRegex) {
|
|
@@ -211,9 +301,7 @@ async function runPtyBun(opts) {
|
|
|
211
301
|
if (exited)
|
|
212
302
|
throw new Error(`pty exited before input (code ${exitCode})`);
|
|
213
303
|
try {
|
|
214
|
-
proc.
|
|
215
|
-
if (typeof proc.stdin.flush === "function")
|
|
216
|
-
proc.stdin.flush();
|
|
304
|
+
proc.terminal.write(opts.input);
|
|
217
305
|
}
|
|
218
306
|
catch (e) {
|
|
219
307
|
throw new Error(`pty write failed: ${e.message}`);
|
|
@@ -257,12 +345,17 @@ async function runPtyBun(opts) {
|
|
|
257
345
|
}
|
|
258
346
|
}
|
|
259
347
|
await kill();
|
|
260
|
-
|
|
348
|
+
// Drain late `data` callbacks, then close the terminal and flush the
|
|
349
|
+
// streamed decoder before the final checks.
|
|
350
|
+
await delay(200);
|
|
261
351
|
try {
|
|
262
|
-
|
|
352
|
+
proc.terminal.close();
|
|
353
|
+
}
|
|
354
|
+
catch { }
|
|
355
|
+
try {
|
|
356
|
+
transcript += decoder.decode();
|
|
263
357
|
}
|
|
264
358
|
catch { }
|
|
265
|
-
await Promise.race([readLoop, delay(200)]);
|
|
266
359
|
checkCap();
|
|
267
360
|
checkAborted();
|
|
268
361
|
const finalClean = stripAnsi(transcript);
|
|
@@ -272,12 +365,11 @@ async function runPtyBun(opts) {
|
|
|
272
365
|
return transcript;
|
|
273
366
|
}
|
|
274
367
|
catch (e) {
|
|
275
|
-
|
|
368
|
+
await kill();
|
|
276
369
|
try {
|
|
277
|
-
|
|
370
|
+
proc.terminal.close();
|
|
278
371
|
}
|
|
279
372
|
catch { }
|
|
280
|
-
await kill();
|
|
281
373
|
unregisterChild();
|
|
282
374
|
teardownAbort();
|
|
283
375
|
throw e;
|
|
@@ -9,6 +9,12 @@ export interface SnapshotOptions {
|
|
|
9
9
|
polling: "idle" | "in-progress" | "cooldown";
|
|
10
10
|
lastCompletedPollAt: string | null;
|
|
11
11
|
version: string;
|
|
12
|
+
update?: {
|
|
13
|
+
current: string;
|
|
14
|
+
latest: string | null;
|
|
15
|
+
upToDate: boolean;
|
|
16
|
+
checkedAt: string | null;
|
|
17
|
+
};
|
|
12
18
|
};
|
|
13
19
|
}
|
|
14
20
|
export declare function buildSnapshot(db: any, opts: SnapshotOptions): StateSnapshot;
|
package/dist/advisory/types.d.ts
CHANGED
|
@@ -42,6 +42,12 @@ export interface ProviderSnapshot {
|
|
|
42
42
|
exclusionReason: ExclusionReason;
|
|
43
43
|
advisory: Advisory | null;
|
|
44
44
|
}
|
|
45
|
+
export interface UpdateStatus {
|
|
46
|
+
current: string;
|
|
47
|
+
latest: string | null;
|
|
48
|
+
upToDate: boolean;
|
|
49
|
+
checkedAt: string | null;
|
|
50
|
+
}
|
|
45
51
|
export interface StateSnapshot {
|
|
46
52
|
asOf: string;
|
|
47
53
|
runtime: {
|
|
@@ -50,6 +56,7 @@ export interface StateSnapshot {
|
|
|
50
56
|
polling: "idle" | "in-progress" | "cooldown";
|
|
51
57
|
lastCompletedPollAt: string | null;
|
|
52
58
|
version: string;
|
|
59
|
+
update?: UpdateStatus;
|
|
53
60
|
};
|
|
54
61
|
providers: ProviderSnapshot[];
|
|
55
62
|
recommendation: Recommendation;
|
package/dist/cli/advise.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { projectRecommendationResponse } from "../advisory/snapshot.js";
|
|
2
2
|
import { validateTask } from "../advisory/validate.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ensureConfig, getDbPath } from "../config.js";
|
|
4
4
|
import { createServiceClient } from "../runtime/client.js";
|
|
5
|
-
import {
|
|
5
|
+
import { VERSION } from "../version.js";
|
|
6
|
+
import { isServiceManaged, runServiceCommand } from "../service/index.js";
|
|
7
|
+
import { detectChannel, printUpdateFooter, readUpdateCache, refreshUpdateCache, updateFooter, } from "../runtime/updates.js";
|
|
8
|
+
import { ClientError, OFFLINE_LABEL, emptySnapshot, offlineOnlyClient, resolveSnapshot } from "./snapshot-source.js";
|
|
9
|
+
import { checkSkew, execSkewWarning, olderCliWarning, takeoverManaged, unmanagedSkewWarning, } from "./takeover.js";
|
|
6
10
|
export function registerAdviseCommand(program, deps) {
|
|
7
11
|
const createClient = deps.createClient ?? ((o) => createServiceClient(o));
|
|
8
12
|
const openDb = deps.openDb;
|
|
9
13
|
const now = deps.now ?? (() => new Date());
|
|
10
14
|
const exit = deps.exit ?? process.exit;
|
|
15
|
+
const execService = deps.execService ?? ((args, o) => runServiceCommand(args, o ?? {}, {}));
|
|
16
|
+
const isManaged = deps.isManaged ?? isServiceManaged;
|
|
17
|
+
const takeoverOpts = deps.takeoverOpts ?? {};
|
|
18
|
+
const checkUpdates = deps.checkUpdates ??
|
|
19
|
+
(() => refreshUpdateCache({ channel: detectChannel(), current: VERSION }));
|
|
11
20
|
program
|
|
12
21
|
.command("advise")
|
|
13
22
|
.description("show which provider to use next (same advice for every task)")
|
|
@@ -26,8 +35,61 @@ export function registerAdviseCommand(program, deps) {
|
|
|
26
35
|
return;
|
|
27
36
|
}
|
|
28
37
|
const t = now();
|
|
29
|
-
const cfg = await
|
|
30
|
-
|
|
38
|
+
const { config: cfg } = await ensureConfig();
|
|
39
|
+
let client = createClient({ port: cfg.port, timeoutMs: 2000 });
|
|
40
|
+
// Skew pre-check mirrors status: warn on exec-only and older-CLI skew,
|
|
41
|
+
// take the managed path when newer, and never spawn a foreground daemon
|
|
42
|
+
// here — unmanaged skew serves the stored snapshot instead of failing.
|
|
43
|
+
let health = null;
|
|
44
|
+
try {
|
|
45
|
+
health = await client.get("/health");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
health = null;
|
|
49
|
+
}
|
|
50
|
+
if (health?.ok) {
|
|
51
|
+
const skew = checkSkew(health, VERSION, process.execPath);
|
|
52
|
+
if (skew === "exec-only") {
|
|
53
|
+
console.error(execSkewWarning(String(health.exec)));
|
|
54
|
+
}
|
|
55
|
+
else if (skew === "cli-older") {
|
|
56
|
+
console.error(olderCliWarning(String(health.version ?? "unknown")));
|
|
57
|
+
}
|
|
58
|
+
else if (skew === "cli-newer") {
|
|
59
|
+
const managed = await isManaged().catch(() => false);
|
|
60
|
+
if (managed) {
|
|
61
|
+
try {
|
|
62
|
+
const r = await takeoverManaged({
|
|
63
|
+
port: cfg.port,
|
|
64
|
+
health,
|
|
65
|
+
createClient,
|
|
66
|
+
execService,
|
|
67
|
+
...takeoverOpts,
|
|
68
|
+
});
|
|
69
|
+
console.error(r.message);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
73
|
+
exit(1);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.error(unmanagedSkewWarning(String(health.version ?? "unknown")));
|
|
79
|
+
client = offlineOnlyClient();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Passive daily update signal: opportunistically refresh the shared
|
|
84
|
+
// cache, then surface staleness as a stderr footer (never in --json).
|
|
85
|
+
let updateCache = null;
|
|
86
|
+
try {
|
|
87
|
+
updateCache = await checkUpdates();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
updateCache = readUpdateCache();
|
|
91
|
+
}
|
|
92
|
+
const footer = updateFooter(VERSION, updateCache?.latest ?? null);
|
|
31
93
|
let resolved;
|
|
32
94
|
try {
|
|
33
95
|
resolved = await resolveSnapshot({
|
|
@@ -43,8 +105,10 @@ export function registerAdviseCommand(program, deps) {
|
|
|
43
105
|
const rec = projectRecommendationResponse(emptySnapshot(t), task);
|
|
44
106
|
if (o.json)
|
|
45
107
|
console.log(JSON.stringify(rec, null, 2));
|
|
46
|
-
else
|
|
108
|
+
else {
|
|
47
109
|
console.log(`${rec.use}: ${rec.reason}`);
|
|
110
|
+
printUpdateFooter(o, footer);
|
|
111
|
+
}
|
|
48
112
|
return;
|
|
49
113
|
}
|
|
50
114
|
console.error(e instanceof Error ? e.message : String(e));
|
|
@@ -56,7 +120,9 @@ export function registerAdviseCommand(program, deps) {
|
|
|
56
120
|
const rec = projectRecommendationResponse(resolved.snapshot, task);
|
|
57
121
|
if (o.json)
|
|
58
122
|
console.log(JSON.stringify(rec, null, 2));
|
|
59
|
-
else
|
|
123
|
+
else {
|
|
60
124
|
console.log(`${rec.use}: ${rec.reason}`);
|
|
125
|
+
printUpdateFooter(o, footer);
|
|
126
|
+
}
|
|
61
127
|
});
|
|
62
128
|
}
|
package/dist/cli/clients.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { ServiceClient } from "../runtime/client.js";
|
|
3
|
+
import type { SleepFn } from "./takeover.js";
|
|
4
|
+
import type { UpdateCache } from "../runtime/updates.js";
|
|
3
5
|
export interface CreateClientOptions {
|
|
4
6
|
port: number;
|
|
5
7
|
token?: string;
|
|
@@ -14,5 +16,13 @@ export interface ClientCommandDeps {
|
|
|
14
16
|
};
|
|
15
17
|
now?: () => Date;
|
|
16
18
|
exit?: (code: number) => void;
|
|
19
|
+
execService?: (args: string[], opts?: Record<string, any>) => Promise<number>;
|
|
20
|
+
isManaged?: () => Promise<boolean>;
|
|
21
|
+
takeoverOpts?: {
|
|
22
|
+
sleep?: SleepFn;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
readToken?: () => string | undefined;
|
|
25
|
+
};
|
|
26
|
+
checkUpdates?: () => Promise<UpdateCache | null>;
|
|
17
27
|
}
|
|
18
28
|
export declare function registerClientCommands(program: Command, deps?: ClientCommandDeps): void;
|
package/dist/cli/index.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { VERSION } from "../version.js";
|
|
4
|
-
import { registerRuntimeCommands } from "./runtime.js";
|
|
4
|
+
import { launchWeb, registerRuntimeCommands } from "./runtime.js";
|
|
5
5
|
import { registerClientCommands } from "./clients.js";
|
|
6
|
+
import { registerInitCommand } from "./init.js";
|
|
7
|
+
import { registerUpdateCommand } from "./update.js";
|
|
6
8
|
const program = new Command();
|
|
7
9
|
program.name("quotacap").version(VERSION);
|
|
8
10
|
program.command("version").action(() => console.log(VERSION));
|
|
9
|
-
program
|
|
10
|
-
const { readConfig, writeConfig } = await import("../config.js");
|
|
11
|
-
const c = await readConfig();
|
|
12
|
-
await writeConfig(c);
|
|
13
|
-
console.log(JSON.stringify(c, null, 2));
|
|
14
|
-
});
|
|
11
|
+
registerInitCommand(program);
|
|
15
12
|
program.command("mcp").description("start MCP server (stdio over HTTP)").action(async () => {
|
|
16
13
|
const mod = await import("../mcp/server.js");
|
|
17
14
|
// if run with --help, commander handles it before action; this is the real server
|
|
@@ -19,4 +16,12 @@ program.command("mcp").description("start MCP server (stdio over HTTP)").action(
|
|
|
19
16
|
});
|
|
20
17
|
registerRuntimeCommands(program);
|
|
21
18
|
registerClientCommands(program);
|
|
22
|
-
program
|
|
19
|
+
registerUpdateCommand(program);
|
|
20
|
+
// Bare `quotacap` with no arguments at all opens the dashboard through the
|
|
21
|
+
// shared launcher; flags and subcommands keep Commander behavior.
|
|
22
|
+
if (process.argv.length === 2) {
|
|
23
|
+
launchWeb({});
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
program.parseAsync();
|
|
27
|
+
}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import { defaultConfig, ensureConfig, getConfigPath, readConfig, writeConfig, } from "../config.js";
|
|
3
|
+
const NEXT_STEP = "next: run 'quotacap web' to open the dashboard, 'quotacap service status' to check the background service";
|
|
4
|
+
export function registerInitCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("init")
|
|
7
|
+
.description("show or reset the config file")
|
|
8
|
+
.option("--quiet", "print nothing on success")
|
|
9
|
+
.option("--force", "rewrite defaults after moving the existing file to config.json.bak")
|
|
10
|
+
.action(async (o) => {
|
|
11
|
+
const p = getConfigPath();
|
|
12
|
+
if (o.force && fsSync.existsSync(p)) {
|
|
13
|
+
fsSync.renameSync(p, `${p}.bak`);
|
|
14
|
+
await writeConfig(defaultConfig());
|
|
15
|
+
if (o.quiet)
|
|
16
|
+
return;
|
|
17
|
+
console.log(JSON.stringify(await readConfig(), null, 2));
|
|
18
|
+
console.error(`rewrote default config at ${p} (previous saved to ${p}.bak)\n${NEXT_STEP}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const r = await ensureConfig();
|
|
22
|
+
if (o.quiet)
|
|
23
|
+
return;
|
|
24
|
+
console.log(JSON.stringify(r.config, null, 2));
|
|
25
|
+
console.error(r.created
|
|
26
|
+
? `wrote default config to ${p}\n${NEXT_STEP}`
|
|
27
|
+
: `config already exists at ${p} (use --force to rewrite defaults)\n${NEXT_STEP}`);
|
|
28
|
+
});
|
|
29
|
+
}
|
package/dist/cli/runtime.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { startService, type StartServiceOptions } from "../runtime/service.js";
|
|
3
|
+
import { type ServiceClient } from "../runtime/client.js";
|
|
4
|
+
import { type SleepFn } from "./takeover.js";
|
|
3
5
|
export type StartServiceFn = (opts?: StartServiceOptions) => ReturnType<typeof startService>;
|
|
4
6
|
export type OpenBrowserFn = (url: string) => Promise<void> | void;
|
|
5
7
|
export type ExecServiceFn = (args: string[], opts?: Record<string, any>) => Promise<number>;
|
|
@@ -8,5 +10,24 @@ export interface RuntimeCommandDeps {
|
|
|
8
10
|
openBrowser?: OpenBrowserFn;
|
|
9
11
|
execService?: ExecServiceFn;
|
|
10
12
|
exit?: (code: number) => void;
|
|
13
|
+
createClient?: (opts: {
|
|
14
|
+
port: number;
|
|
15
|
+
token?: string;
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
}) => ServiceClient;
|
|
18
|
+
isManaged?: () => Promise<boolean>;
|
|
19
|
+
takeoverOpts?: {
|
|
20
|
+
sleep?: SleepFn;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
readToken?: () => string | undefined;
|
|
23
|
+
};
|
|
24
|
+
checkUpdates?: () => Promise<unknown>;
|
|
25
|
+
isInstalled?: () => Promise<boolean>;
|
|
26
|
+
serviceSupported?: () => boolean;
|
|
11
27
|
}
|
|
28
|
+
export interface WebLaunchOptions {
|
|
29
|
+
port?: string;
|
|
30
|
+
foreground?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function launchWeb(o: WebLaunchOptions, deps?: RuntimeCommandDeps): Promise<void>;
|
|
12
33
|
export declare function registerRuntimeCommands(program: Command, deps?: RuntimeCommandDeps): void;
|