taskchef 7.22.3 → 7.22.5
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/.codex-plugin/plugin.json +1 -1
- package/README.md +40 -28
- package/docs/dashboard-lifecycle.md +168 -0
- package/docs/spec.md +117 -61
- package/docs/workflows.md +30 -18
- package/mcp/dashboard-session.js +51 -0
- package/package.json +2 -1
- package/src/dashboard-manager.js +633 -157
- package/src/dashboard-ownership.js +124 -13
- package/src/dashboard-session-process.js +179 -0
- package/src/dashboard-session.js +91 -0
- package/src/dashboard.js +288 -8
- package/src/mcp.js +2 -2
- package/src/usage.js +31 -3
- package/src/version.js +1 -1
package/src/dashboard.js
CHANGED
|
@@ -25,9 +25,18 @@ import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
|
25
25
|
import { taskGitHubProjection } from "./dashboard/github-links.js";
|
|
26
26
|
import { CODEX_CHAT_ARCHIVE_ENABLED } from "./dashboard/state.js";
|
|
27
27
|
import { createUsageTracker } from "./usage-tracker.js";
|
|
28
|
+
import {
|
|
29
|
+
MAX_DASHBOARD_SESSION_PIDS,
|
|
30
|
+
MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS,
|
|
31
|
+
validSessionPid,
|
|
32
|
+
} from "./dashboard-session.js";
|
|
28
33
|
import {
|
|
29
34
|
DASHBOARD_CONTROL_CHALLENGE_PATH,
|
|
35
|
+
DASHBOARD_CONTROL_HANDOFF_COMMIT_PATH,
|
|
36
|
+
DASHBOARD_CONTROL_SESSION_PATH,
|
|
37
|
+
DASHBOARD_CONTROL_HANDOFF_PATH,
|
|
30
38
|
DASHBOARD_CONTROL_SHUTDOWN_PATH,
|
|
39
|
+
createDashboardControlNonce,
|
|
31
40
|
dashboardControlProof,
|
|
32
41
|
validDashboardControlNonce,
|
|
33
42
|
validDashboardControlSecret,
|
|
@@ -41,6 +50,11 @@ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1"]);
|
|
|
41
50
|
const DEFAULT_MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
42
51
|
const DEFAULT_MAX_TASKS = 2_000;
|
|
43
52
|
const DEFAULT_MAX_EVENT_CLIENTS = 16;
|
|
53
|
+
const DASHBOARD_HANDOFF_PREPARE_TTL_MS = 5_000;
|
|
54
|
+
const DASHBOARD_HANDOFF_COMMIT_GRACE_MS = 250;
|
|
55
|
+
const DASHBOARD_HANDOFF_COMMIT_RETRY_MS = 1_000;
|
|
56
|
+
const DASHBOARD_CONTROL_NONCE_LIMIT = 4_096;
|
|
57
|
+
const DASHBOARD_CONTROL_NONCE_TTL_MS = 5 * 60_000;
|
|
44
58
|
const MAX_MANUAL_TRANSITION_BODY_BYTES = 4 * 1024;
|
|
45
59
|
export const DASHBOARD_HEALTH_PATH = "/api/health";
|
|
46
60
|
export const DASHBOARD_HEALTH_MAX_BYTES = 8 * 1024;
|
|
@@ -537,6 +551,33 @@ function publicMonitorError() {
|
|
|
537
551
|
};
|
|
538
552
|
}
|
|
539
553
|
|
|
554
|
+
export function createDashboardControlReplayCache({
|
|
555
|
+
maximum = DASHBOARD_CONTROL_NONCE_LIMIT,
|
|
556
|
+
ttlMs = DASHBOARD_CONTROL_NONCE_TTL_MS,
|
|
557
|
+
now = Date.now,
|
|
558
|
+
} = {}) {
|
|
559
|
+
if (!Number.isSafeInteger(maximum) || maximum <= 0) {
|
|
560
|
+
throw new Error("dashboard control replay limit must be a positive integer");
|
|
561
|
+
}
|
|
562
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0 || typeof now !== "function") {
|
|
563
|
+
throw new Error("dashboard control replay window is invalid");
|
|
564
|
+
}
|
|
565
|
+
const entries = new Map();
|
|
566
|
+
return {
|
|
567
|
+
accept(nonce) {
|
|
568
|
+
const timestamp = now();
|
|
569
|
+
for (const [candidate, expiresAt] of entries) {
|
|
570
|
+
if (expiresAt > timestamp) break;
|
|
571
|
+
entries.delete(candidate);
|
|
572
|
+
}
|
|
573
|
+
if (entries.has(nonce) || entries.size >= maximum) return false;
|
|
574
|
+
entries.set(nonce, timestamp + ttlMs);
|
|
575
|
+
return true;
|
|
576
|
+
},
|
|
577
|
+
get size() { return entries.size; },
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
540
581
|
export async function createDashboardServer({
|
|
541
582
|
archiveEnabled = CODEX_CHAT_ARCHIVE_ENABLED,
|
|
542
583
|
archiveThread = archiveThreadInCodex,
|
|
@@ -553,6 +594,7 @@ export async function createDashboardServer({
|
|
|
553
594
|
serverVersion = DASHBOARD_SERVER_VERSION,
|
|
554
595
|
usageTracker = null,
|
|
555
596
|
control = null,
|
|
597
|
+
controlReplayCache = createDashboardControlReplayCache(),
|
|
556
598
|
} = {}) {
|
|
557
599
|
if (!LOOPBACK_HOSTS.has(host)) {
|
|
558
600
|
throw new Error("dashboard host must be a loopback address");
|
|
@@ -563,13 +605,15 @@ export async function createDashboardServer({
|
|
|
563
605
|
if (!Number.isInteger(maxEventClients) || maxEventClients < 0) {
|
|
564
606
|
throw new Error("dashboard event-client limit must be a non-negative integer");
|
|
565
607
|
}
|
|
566
|
-
if (!new Set(["mcp", "standalone"]).has(launcher)) {
|
|
567
|
-
throw new Error("dashboard launcher must be mcp or standalone");
|
|
608
|
+
if (!new Set(["mcp", "session", "standalone"]).has(launcher)) {
|
|
609
|
+
throw new Error("dashboard launcher must be mcp, session, or standalone");
|
|
568
610
|
}
|
|
569
|
-
if (control !== null && (
|
|
611
|
+
if (control !== null && (!new Set(["mcp", "session"]).has(launcher)
|
|
570
612
|
|| !validDashboardControlSecret(control?.secret)
|
|
571
|
-
|| typeof control?.onShutdown !== "function"
|
|
572
|
-
|
|
613
|
+
|| typeof control?.onShutdown !== "function"
|
|
614
|
+
|| (launcher === "session" && (typeof control?.onSession !== "function"
|
|
615
|
+
|| typeof control?.onHandoff !== "function")))) {
|
|
616
|
+
throw new Error("dashboard control requires a valid TaskChef ownership controller");
|
|
573
617
|
}
|
|
574
618
|
const monitor = new DashboardMonitor(workspace, monitorOptions);
|
|
575
619
|
await monitor.start();
|
|
@@ -588,7 +632,8 @@ export async function createDashboardServer({
|
|
|
588
632
|
}
|
|
589
633
|
const clients = new Set();
|
|
590
634
|
const archiveRequests = new Set();
|
|
591
|
-
|
|
635
|
+
let controlHandoff = null;
|
|
636
|
+
let controlHandoffTimer = null;
|
|
592
637
|
let allowedAuthority;
|
|
593
638
|
let allowedOrigin;
|
|
594
639
|
|
|
@@ -655,16 +700,251 @@ export async function createDashboardServer({
|
|
|
655
700
|
sendJson(response, 403, { message: "Dashboard control authentication failed." });
|
|
656
701
|
return;
|
|
657
702
|
}
|
|
658
|
-
if (
|
|
703
|
+
if (!controlReplayCache.accept(nonce)) {
|
|
659
704
|
sendJson(response, 409, { message: "Dashboard control credential was already used." });
|
|
660
705
|
return;
|
|
661
706
|
}
|
|
662
|
-
controlNonces.add(nonce);
|
|
663
707
|
sendJson(response, 202, { schemaVersion: 1, accepted: true });
|
|
664
708
|
setImmediate(() => { void control.onShutdown().catch(() => {}); });
|
|
665
709
|
return;
|
|
666
710
|
}
|
|
667
711
|
|
|
712
|
+
if (url.pathname === DASHBOARD_CONTROL_SESSION_PATH && method === "POST") {
|
|
713
|
+
if (!control || typeof control.onSession !== "function") {
|
|
714
|
+
sendJson(response, 404, { message: "Not found." });
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const body = await readBoundedJsonBody(request);
|
|
718
|
+
const nonce = body?.nonce;
|
|
719
|
+
const pid = body?.pid;
|
|
720
|
+
if (!Number.isSafeInteger(pid) || pid <= 1
|
|
721
|
+
|| !verifyDashboardControlProof(
|
|
722
|
+
control.secret, `session:${pid}`, nonce, body?.proof,
|
|
723
|
+
)) {
|
|
724
|
+
sendJson(response, 403, { message: "Dashboard control authentication failed." });
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (controlHandoff) {
|
|
728
|
+
sendJson(response, 409, {
|
|
729
|
+
message: "Dashboard handoff is already in progress.",
|
|
730
|
+
reason: "retiring",
|
|
731
|
+
});
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (!controlReplayCache.accept(nonce)) {
|
|
735
|
+
sendJson(response, 409, { message: "Dashboard control credential was already used." });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
await control.onSession(pid);
|
|
740
|
+
sendJson(response, 200, {
|
|
741
|
+
schemaVersion: 1,
|
|
742
|
+
accepted: true,
|
|
743
|
+
nonce,
|
|
744
|
+
proof: dashboardControlProof(control.secret, `session-accepted:${pid}`, nonce),
|
|
745
|
+
});
|
|
746
|
+
} catch (error) {
|
|
747
|
+
sendJson(response, 409, {
|
|
748
|
+
message: "Dashboard session registration was refused.",
|
|
749
|
+
reason: error?.code === "TASKCHEF_DASHBOARD_SESSION_RETIRING"
|
|
750
|
+
? "retiring"
|
|
751
|
+
: "refused",
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (url.pathname === DASHBOARD_CONTROL_HANDOFF_PATH && method === "POST") {
|
|
758
|
+
if (!control || typeof control.onHandoff !== "function") {
|
|
759
|
+
sendJson(response, 404, { message: "Not found." });
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const body = await readBoundedJsonBody(request);
|
|
763
|
+
const nonce = body?.nonce;
|
|
764
|
+
const pid = body?.pid;
|
|
765
|
+
if (!Number.isSafeInteger(pid) || pid <= 1
|
|
766
|
+
|| !verifyDashboardControlProof(
|
|
767
|
+
control.secret, `handoff:${pid}`, nonce, body?.proof,
|
|
768
|
+
)) {
|
|
769
|
+
sendJson(response, 403, { message: "Dashboard control authentication failed." });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (!controlReplayCache.accept(nonce)) {
|
|
773
|
+
sendJson(response, 409, { message: "Dashboard control credential was already used." });
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (controlHandoff) {
|
|
777
|
+
if (controlHandoff.pending || controlHandoff.finalizing || controlHandoff.finalized) {
|
|
778
|
+
sendJson(response, 409, {
|
|
779
|
+
message: "Dashboard handoff is already in progress.",
|
|
780
|
+
reason: "retiring",
|
|
781
|
+
});
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
const pids = controlHandoff.participants.has(pid)
|
|
786
|
+
? controlHandoff.pids
|
|
787
|
+
: await control.onHandoff(pid);
|
|
788
|
+
if (!Array.isArray(pids) || pids.length > MAX_DASHBOARD_SESSION_PIDS
|
|
789
|
+
|| pids.some((value) => !validSessionPid(value))
|
|
790
|
+
|| new Set(pids).size !== pids.length
|
|
791
|
+
|| !pids.includes(pid)) {
|
|
792
|
+
throw new Error("dashboard handoff returned invalid session leases");
|
|
793
|
+
}
|
|
794
|
+
controlHandoff.pids = pids;
|
|
795
|
+
controlHandoff.participants.add(pid);
|
|
796
|
+
const { id } = controlHandoff;
|
|
797
|
+
sendJson(response, 200, {
|
|
798
|
+
schemaVersion: 1,
|
|
799
|
+
accepted: true,
|
|
800
|
+
id,
|
|
801
|
+
pids,
|
|
802
|
+
nonce,
|
|
803
|
+
proof: dashboardControlProof(
|
|
804
|
+
control.secret,
|
|
805
|
+
`handoff-prepared:${pid}:${id}:${JSON.stringify(pids)}`,
|
|
806
|
+
nonce,
|
|
807
|
+
),
|
|
808
|
+
});
|
|
809
|
+
} catch {
|
|
810
|
+
sendJson(response, 409, {
|
|
811
|
+
message: "Dashboard handoff was refused.",
|
|
812
|
+
reason: "refused",
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
controlHandoff = { pid, pending: true, committed: false };
|
|
818
|
+
try {
|
|
819
|
+
const pids = await control.onHandoff(pid);
|
|
820
|
+
if (!Array.isArray(pids) || pids.length > MAX_DASHBOARD_SESSION_PIDS
|
|
821
|
+
|| pids.some((value) => !validSessionPid(value))
|
|
822
|
+
|| new Set(pids).size !== pids.length
|
|
823
|
+
|| !pids.includes(pid)) {
|
|
824
|
+
throw new Error("dashboard handoff returned invalid session leases");
|
|
825
|
+
}
|
|
826
|
+
const id = createDashboardControlNonce();
|
|
827
|
+
controlHandoff = {
|
|
828
|
+
id,
|
|
829
|
+
pid,
|
|
830
|
+
pids,
|
|
831
|
+
participants: new Set([pid]),
|
|
832
|
+
pending: false,
|
|
833
|
+
committed: false,
|
|
834
|
+
};
|
|
835
|
+
controlHandoffTimer = setTimeout(() => {
|
|
836
|
+
if (controlHandoff?.id === id && !controlHandoff.committed) controlHandoff = null;
|
|
837
|
+
controlHandoffTimer = null;
|
|
838
|
+
}, DASHBOARD_HANDOFF_PREPARE_TTL_MS);
|
|
839
|
+
controlHandoffTimer.unref?.();
|
|
840
|
+
sendJson(response, 200, {
|
|
841
|
+
schemaVersion: 1,
|
|
842
|
+
accepted: true,
|
|
843
|
+
id,
|
|
844
|
+
pids,
|
|
845
|
+
nonce,
|
|
846
|
+
proof: dashboardControlProof(
|
|
847
|
+
control.secret,
|
|
848
|
+
`handoff-prepared:${pid}:${id}:${JSON.stringify(pids)}`,
|
|
849
|
+
nonce,
|
|
850
|
+
),
|
|
851
|
+
});
|
|
852
|
+
} catch {
|
|
853
|
+
if (controlHandoff?.pid === pid && controlHandoff.pending) controlHandoff = null;
|
|
854
|
+
sendJson(response, 409, {
|
|
855
|
+
message: "Dashboard handoff was refused.",
|
|
856
|
+
reason: "refused",
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (url.pathname === DASHBOARD_CONTROL_HANDOFF_COMMIT_PATH && method === "POST") {
|
|
863
|
+
if (!control || typeof control.onHandoff !== "function") {
|
|
864
|
+
sendJson(response, 404, { message: "Not found." });
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const body = await readBoundedJsonBody(request);
|
|
868
|
+
const nonce = body?.nonce;
|
|
869
|
+
const id = body?.id;
|
|
870
|
+
if (!validDashboardControlNonce(id)
|
|
871
|
+
|| !verifyDashboardControlProof(
|
|
872
|
+
control.secret, `handoff-commit:${id}`, nonce, body?.proof,
|
|
873
|
+
)) {
|
|
874
|
+
sendJson(response, 403, { message: "Dashboard control authentication failed." });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (!controlReplayCache.accept(nonce)) {
|
|
878
|
+
sendJson(response, 409, { message: "Dashboard control credential was already used." });
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (!controlHandoff || controlHandoff.id !== id) {
|
|
882
|
+
sendJson(response, 409, {
|
|
883
|
+
message: "Dashboard handoff preparation expired or did not match.",
|
|
884
|
+
reason: "refused",
|
|
885
|
+
});
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
if (!controlHandoff.commitPromise) {
|
|
889
|
+
controlHandoff.committed = true;
|
|
890
|
+
if (controlHandoffTimer) clearTimeout(controlHandoffTimer);
|
|
891
|
+
controlHandoffTimer = null;
|
|
892
|
+
controlHandoff.commitPromise = new Promise((resolve, reject) => {
|
|
893
|
+
const finalizationTimer = setTimeout(async () => {
|
|
894
|
+
try {
|
|
895
|
+
controlHandoff.finalizing = true;
|
|
896
|
+
const pids = await control.onHandoff(controlHandoff.pid);
|
|
897
|
+
if (!Array.isArray(pids)
|
|
898
|
+
|| pids.length > MAX_TRANSFERRED_DASHBOARD_SESSION_PIDS
|
|
899
|
+
|| pids.some((value) => !validSessionPid(value))
|
|
900
|
+
|| new Set(pids).size !== pids.length
|
|
901
|
+
|| !pids.includes(controlHandoff.pid)) {
|
|
902
|
+
throw new Error("dashboard handoff returned invalid final session leases");
|
|
903
|
+
}
|
|
904
|
+
controlHandoff.pids = pids;
|
|
905
|
+
await control.onHandoffFinalized?.({ id, pids });
|
|
906
|
+
controlHandoff.finalized = true;
|
|
907
|
+
resolve(pids);
|
|
908
|
+
} catch (error) {
|
|
909
|
+
reject(error);
|
|
910
|
+
}
|
|
911
|
+
}, DASHBOARD_HANDOFF_COMMIT_GRACE_MS);
|
|
912
|
+
finalizationTimer.unref?.();
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
let pids;
|
|
916
|
+
try {
|
|
917
|
+
pids = await controlHandoff.commitPromise;
|
|
918
|
+
} catch {
|
|
919
|
+
if (controlHandoff?.id === id) controlHandoff = null;
|
|
920
|
+
sendJson(response, 409, {
|
|
921
|
+
message: "Dashboard handoff finalization was refused.",
|
|
922
|
+
reason: "refused",
|
|
923
|
+
});
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
sendJson(response, 202, {
|
|
927
|
+
schemaVersion: 1,
|
|
928
|
+
accepted: true,
|
|
929
|
+
id,
|
|
930
|
+
pids,
|
|
931
|
+
nonce,
|
|
932
|
+
proof: dashboardControlProof(
|
|
933
|
+
control.secret,
|
|
934
|
+
`handoff-committed:${id}:${JSON.stringify(pids)}`,
|
|
935
|
+
nonce,
|
|
936
|
+
),
|
|
937
|
+
});
|
|
938
|
+
if (!controlHandoff.shutdownScheduled) {
|
|
939
|
+
controlHandoff.shutdownScheduled = true;
|
|
940
|
+
const shutdownTimer = setTimeout(() => {
|
|
941
|
+
void control.onShutdown().catch(() => {});
|
|
942
|
+
}, DASHBOARD_HANDOFF_COMMIT_RETRY_MS);
|
|
943
|
+
shutdownTimer.unref?.();
|
|
944
|
+
}
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
668
948
|
if (url.pathname === "/api/snapshot" && (method === "GET" || method === "HEAD")) {
|
|
669
949
|
if (method === "HEAD") {
|
|
670
950
|
response.writeHead(200, securityHeaders("application/json; charset=utf-8"));
|
package/src/mcp.js
CHANGED
|
@@ -107,7 +107,7 @@ const preparationSchema = z.object({
|
|
|
107
107
|
|
|
108
108
|
const dashboardSchema = z.object({
|
|
109
109
|
action: z.enum(["started", "reused"]),
|
|
110
|
-
launcher: z.literal("
|
|
110
|
+
launcher: z.literal("session"),
|
|
111
111
|
url: z.string().url(),
|
|
112
112
|
workspace: z.string(),
|
|
113
113
|
taskchefVersion: z.string(),
|
|
@@ -201,7 +201,7 @@ export function createTaskChefMcpServer({
|
|
|
201
201
|
{
|
|
202
202
|
title: "Ensure TaskChef dashboard",
|
|
203
203
|
description:
|
|
204
|
-
"Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts
|
|
204
|
+
"Best-effort ensure the canonical TaskChef dashboard is available on 127.0.0.1:3210. Starts or reuses the authenticated TaskChef dashboard for this Codex session and canonical workspace; verified older TaskChef session listeners are handed off to the installed version, while standalone and unknown listeners are never terminated or replaced.",
|
|
205
205
|
inputSchema: {},
|
|
206
206
|
outputSchema: { dashboard: dashboardSchema },
|
|
207
207
|
annotations: {
|
package/src/usage.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants as fsConstants,
|
|
5
|
+
existsSync,
|
|
6
|
+
fchmodSync,
|
|
7
|
+
fstatSync,
|
|
8
|
+
openSync,
|
|
9
|
+
} from "node:fs";
|
|
3
10
|
import { lstat, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
11
|
import { createRequire } from "node:module";
|
|
5
12
|
import path from "node:path";
|
|
@@ -356,10 +363,28 @@ export function managedCcusageInvocation({
|
|
|
356
363
|
};
|
|
357
364
|
}
|
|
358
365
|
|
|
366
|
+
export function ensureCcusageExecutable(command, {
|
|
367
|
+
platform = process.platform,
|
|
368
|
+
} = {}) {
|
|
369
|
+
if (platform === "win32") return;
|
|
370
|
+
const descriptor = openSync(command, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
371
|
+
try {
|
|
372
|
+
const file = fstatSync(descriptor);
|
|
373
|
+
if (!file.isFile()) throw new Error("ccusage native binary is not a regular file");
|
|
374
|
+
if ((file.mode & 0o100) === 0) fchmodSync(descriptor, file.mode | 0o100);
|
|
375
|
+
} finally {
|
|
376
|
+
closeSync(descriptor);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
359
380
|
async function resolveManagedCcusageInvocation(invocation, run, timeoutMs) {
|
|
360
|
-
if (invocation.resolvesNative !== true)
|
|
381
|
+
if (invocation.resolvesNative !== true) {
|
|
382
|
+
ensureCcusageExecutable(invocation.command);
|
|
383
|
+
return invocation;
|
|
384
|
+
}
|
|
361
385
|
if (npxResolvedCcusageInvocation !== null
|
|
362
386
|
&& existsSync(npxResolvedCcusageInvocation.command)) {
|
|
387
|
+
ensureCcusageExecutable(npxResolvedCcusageInvocation.command);
|
|
363
388
|
return npxResolvedCcusageInvocation;
|
|
364
389
|
}
|
|
365
390
|
npxResolvedCcusageInvocation = null;
|
|
@@ -374,6 +399,7 @@ async function resolveManagedCcusageInvocation(invocation, run, timeoutMs) {
|
|
|
374
399
|
|| !existsSync(command)) {
|
|
375
400
|
throw new Error("npx returned an invalid ccusage executable");
|
|
376
401
|
}
|
|
402
|
+
ensureCcusageExecutable(command);
|
|
377
403
|
npxResolvedCcusageInvocation = {
|
|
378
404
|
command,
|
|
379
405
|
args: [],
|
|
@@ -504,7 +530,9 @@ export async function readCcusageThreadUsage(threadId, {
|
|
|
504
530
|
const invocation = command === null
|
|
505
531
|
? managedCcusageInvocation()
|
|
506
532
|
: { command, args: commandArgs, version: null };
|
|
507
|
-
const resolvedInvocation =
|
|
533
|
+
const resolvedInvocation = command === null
|
|
534
|
+
? await resolveManagedCcusageInvocation(invocation, run, timeoutMs)
|
|
535
|
+
: invocation;
|
|
508
536
|
const invoke = (args, options) => run(
|
|
509
537
|
resolvedInvocation.command,
|
|
510
538
|
[...resolvedInvocation.args, ...args],
|
package/src/version.js
CHANGED