@neuralnomads/codenomad-dev 0.18.0-dev-20260716-40cee071 → 0.18.0-dev-20260720-4c50829d
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/auth/manager.js +1 -1
- package/dist/filesystem/__tests__/search-cache.test.js +5 -5
- package/dist/filesystem/search-cache.js +1 -1
- package/dist/index.js +42 -0
- package/dist/index.test.js +40 -10
- package/dist/opencode-update/service.js +150 -0
- package/dist/opencode-update/service.test.js +107 -0
- package/dist/permissions/auto-accept-manager.js +165 -0
- package/dist/permissions/auto-accept-manager.test.js +208 -0
- package/dist/permissions/auto-accept-store.js +5 -2
- package/dist/permissions/opencode-yolo-metadata.js +81 -0
- package/dist/permissions/opencode-yolo-metadata.test.js +54 -0
- package/dist/server/__tests__/remote-proxy.test.js +64 -56
- package/dist/server/http-server.js +12 -15
- package/dist/server/remote-proxy.js +68 -98
- package/dist/server/routes/opencode-update.js +37 -0
- package/dist/server/routes/opencode-update.test.js +34 -0
- package/dist/server/routes/remote-servers.js +1 -1
- package/dist/server/routes/workspaces.test.js +38 -0
- package/dist/server/routes/worktrees.js +19 -0
- package/dist/server/routes/yolo.js +2 -1
- package/dist/server/tls.js +2 -2
- package/dist/settings/binaries.test.js +2 -7
- package/dist/shutdown.js +67 -59
- package/dist/shutdown.test.js +115 -78
- package/dist/ui/remote-ui.js +2 -2
- package/dist/usage/providers/api-key.js +3 -3
- package/dist/usage/providers/google.js +1 -1
- package/dist/usage/providers/oauth.js +1 -36
- package/dist/usage/providers/special.js +3 -3
- package/dist/usage/service.js +1 -1
- package/dist/usage/service.test.js +5 -3
- package/dist/usage/shared.js +4 -4
- package/dist/usage/shared.test.js +4 -4
- package/dist/workspaces/__tests__/spawn.test.js +2 -2
- package/dist/workspaces/__tests__/workspace-identity.test.js +94 -286
- package/dist/workspaces/binary-path.test.js +10 -0
- package/dist/workspaces/instance-client.js +4 -3
- package/dist/workspaces/launch-cleanup.test.js +21 -25
- package/dist/workspaces/manager.js +300 -351
- package/dist/workspaces/manager.test.js +209 -237
- package/dist/workspaces/process-identity.darwin.test.js +88 -0
- package/dist/workspaces/process-identity.js +172 -149
- package/dist/workspaces/process-identity.test.js +86 -140
- package/dist/workspaces/runtime.js +261 -373
- package/dist/workspaces/runtime.test.js +222 -547
- package/package.json +1 -1
- package/public/assets/{FilesTab-BjztpSdj.js → FilesTab-ChfE8-8N.js} +2 -2
- package/public/assets/{GitChangesTab-B9XraSoR.js → GitChangesTab-ntRtkxIF.js} +2 -2
- package/public/assets/{SplitFilePanel-Cu_5-Z-E.js → SplitFilePanel-Dav7VCqK.js} +1 -1
- package/public/assets/StatusTab-D3toOefp.js +1 -0
- package/public/assets/{align-justify-BKS9gyRk.js → align-justify-D91sAsLc.js} +1 -1
- package/public/assets/{bundle-full-CXGXXlzR.js → bundle-full-CWkY9-v4.js} +1 -1
- package/public/assets/{diff-viewer-CSNOYoEB.js → diff-viewer-Cn_41zpc.js} +1 -1
- package/public/assets/index-3dx-hhNQ.js +1 -0
- package/public/assets/index-BKwfZLzw.js +1 -0
- package/public/assets/index-C5IioEFT.js +1 -0
- package/public/assets/index-CZYc0VcU.js +2 -0
- package/public/assets/index-CdCoXYXP.js +1 -0
- package/public/assets/index-CwAVops2.js +1 -0
- package/public/assets/index-CzvCOwKc.js +1 -0
- package/public/assets/index-D-loOWPv.js +1 -0
- package/public/assets/{index-DVc4ls8c.js → index-DM1cpVNo.js} +1 -1
- package/public/assets/{index-sLe9hEfQ.js → index-DOprq6Gt.js} +1 -1
- package/public/assets/index-L7fX3W8k.css +1 -0
- package/public/assets/index-w2KdHEME.js +1 -0
- package/public/assets/{loading-CDmt9OJU.js → loading-BfoDcWcg.js} +1 -1
- package/public/assets/main-CaTCcck1.js +68 -0
- package/public/assets/{markdown-1ZLSxCFT.js → markdown-D33YI2g2.js} +3 -3
- package/public/assets/{monaco-viewer-D6JgH76W.js → monaco-viewer-DM35aobI.js} +4 -4
- package/public/assets/{tool-call-CyXb63LH.js → tool-call-tV85Jv2i.js} +3 -3
- package/public/assets/{unified-picker-DCCdSVwI.js → unified-picker-DSRT9GMh.js} +1 -1
- package/public/assets/{wrap-text-pNMdTIq3.js → wrap-text-Dmrn2DKo.js} +1 -1
- package/public/index.html +4 -4
- package/public/loading.html +4 -4
- package/public/sw.js +1 -1
- package/dist/workspaces/launch-cleanup.js +0 -132
- package/public/assets/StatusTab-D_IE79r-.js +0 -1
- package/public/assets/index-B3YjNCh4.js +0 -1
- package/public/assets/index-B3ZD2_SU.js +0 -1
- package/public/assets/index-Bh2ofaQo.js +0 -1
- package/public/assets/index-CM0c9xRZ.js +0 -1
- package/public/assets/index-CdDxouJl.js +0 -2
- package/public/assets/index-D06Y2cFq.js +0 -1
- package/public/assets/index-PmE51vPh.js +0 -1
- package/public/assets/index-Qj63IiSj.css +0 -1
- package/public/assets/index-VY_zwmsG.js +0 -1
- package/public/assets/index-qiiH7egV.js +0 -1
- package/public/assets/main-CTN6Wwz1.js +0 -68
|
@@ -72,4 +72,42 @@ describe("workspace routes", () => {
|
|
|
72
72
|
assert.equal(calls.length, 2);
|
|
73
73
|
await app.close();
|
|
74
74
|
});
|
|
75
|
+
it("rejects release after cancellation wins while deletion is still pending", async () => {
|
|
76
|
+
const app = Fastify({ logger: false });
|
|
77
|
+
let state = "active";
|
|
78
|
+
let cancellationStarted;
|
|
79
|
+
let finishDeletion;
|
|
80
|
+
const started = new Promise((resolve) => { cancellationStarted = resolve; });
|
|
81
|
+
const deletion = new Promise((resolve) => { finishDeletion = resolve; });
|
|
82
|
+
const workspaceManager = {
|
|
83
|
+
cancelCreationRequest: async () => {
|
|
84
|
+
state = "cancelled";
|
|
85
|
+
cancellationStarted();
|
|
86
|
+
await deletion;
|
|
87
|
+
},
|
|
88
|
+
releaseCreationRequest: () => {
|
|
89
|
+
if (state === "cancelled")
|
|
90
|
+
return false;
|
|
91
|
+
state = "released";
|
|
92
|
+
return true;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
registerWorkspaceRoutes(app, { workspaceManager });
|
|
96
|
+
const cancellation = app.inject({
|
|
97
|
+
method: "POST",
|
|
98
|
+
url: "/api/workspaces/creation/cancel",
|
|
99
|
+
payload: { requestId: "restore-request" },
|
|
100
|
+
});
|
|
101
|
+
await started;
|
|
102
|
+
const release = await app.inject({
|
|
103
|
+
method: "POST",
|
|
104
|
+
url: "/api/workspaces/workspace/creation/release",
|
|
105
|
+
payload: { requestId: "restore-request" },
|
|
106
|
+
});
|
|
107
|
+
assert.equal(release.statusCode, 404);
|
|
108
|
+
assert.equal(release.body, "Workspace creation request not found");
|
|
109
|
+
finishDeletion();
|
|
110
|
+
assert.equal((await cancellation).statusCode, 204);
|
|
111
|
+
await app.close();
|
|
112
|
+
});
|
|
75
113
|
});
|
|
@@ -10,7 +10,26 @@ const WorktreeCreateSchema = z.object({
|
|
|
10
10
|
slug: z.string().trim().min(1),
|
|
11
11
|
branch: z.string().trim().min(1).optional(),
|
|
12
12
|
});
|
|
13
|
+
const WorktreeSessionSchema = z.object({ worktreeSlug: z.string().trim().refine(isValidWorktreeSlug) });
|
|
13
14
|
export function registerWorktreeRoutes(app, deps) {
|
|
15
|
+
app.put("/api/workspaces/:id/worktrees/sessions/:sessionId", async (request, reply) => {
|
|
16
|
+
if (!deps.workspaceManager.get(request.params.id)) {
|
|
17
|
+
reply.code(404);
|
|
18
|
+
return { error: "Workspace not found" };
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const body = WorktreeSessionSchema.parse(request.body);
|
|
22
|
+
if (!await deps.sessionMetadataPersistence.hasProjectSession(request.params.id, request.params.sessionId)) {
|
|
23
|
+
reply.code(404);
|
|
24
|
+
return { error: "Session not found" };
|
|
25
|
+
}
|
|
26
|
+
const metadata = await deps.sessionMetadataPersistence.setWorktreeSlug(request.params.id, request.params.sessionId, body.worktreeSlug);
|
|
27
|
+
return { metadata };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return handleError(error, reply);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
14
33
|
app.get("/api/workspaces/:id/worktrees", async (request, reply) => {
|
|
15
34
|
const workspace = deps.workspaceManager.get(request.params.id);
|
|
16
35
|
if (!workspace) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export function registerYoloRoutes(app, deps) {
|
|
2
2
|
app.get("/workspaces/:id/yolo/sessions/:sessionId", async (request) => {
|
|
3
3
|
const { id, sessionId } = request.params;
|
|
4
|
+
await deps.yoloManager.hydrateInstance(id);
|
|
4
5
|
return { enabled: deps.yoloManager.isEnabled(id, sessionId) };
|
|
5
6
|
});
|
|
6
7
|
app.post("/workspaces/:id/yolo/sessions/:sessionId/toggle", async (request, reply) => {
|
|
7
8
|
const { id, sessionId } = request.params;
|
|
8
|
-
const enabled = deps.yoloManager.toggle(id, sessionId);
|
|
9
|
+
const enabled = await deps.yoloManager.toggle(id, sessionId);
|
|
9
10
|
reply.code(200);
|
|
10
11
|
return { enabled };
|
|
11
12
|
});
|
package/dist/server/tls.js
CHANGED
|
@@ -115,7 +115,7 @@ function generateCaCertificate() {
|
|
|
115
115
|
cert.publicKey = keys.publicKey;
|
|
116
116
|
cert.serialNumber = crypto.randomBytes(16).toString("hex");
|
|
117
117
|
const now = new Date();
|
|
118
|
-
const notBefore = new Date(now.getTime() -
|
|
118
|
+
const notBefore = new Date(now.getTime() - 60_000);
|
|
119
119
|
const notAfter = new Date(now.getTime() + CA_VALIDITY_DAYS * 24 * 60 * 60 * 1000);
|
|
120
120
|
cert.validity.notBefore = notBefore;
|
|
121
121
|
cert.validity.notAfter = notAfter;
|
|
@@ -142,7 +142,7 @@ function generateServerCertificate(args) {
|
|
|
142
142
|
cert.publicKey = keys.publicKey;
|
|
143
143
|
cert.serialNumber = crypto.randomBytes(16).toString("hex");
|
|
144
144
|
const now = new Date();
|
|
145
|
-
const notBefore = new Date(now.getTime() -
|
|
145
|
+
const notBefore = new Date(now.getTime() - 60_000);
|
|
146
146
|
const notAfter = new Date(now.getTime() + LEAF_VALIDITY_DAYS * 24 * 60 * 60 * 1000);
|
|
147
147
|
cert.validity.notBefore = notBefore;
|
|
148
148
|
cert.validity.notAfter = notAfter;
|
|
@@ -7,18 +7,13 @@ describe("BinaryResolver", () => {
|
|
|
7
7
|
getOwner(scope, owner) {
|
|
8
8
|
if (scope === "config" && owner === "server")
|
|
9
9
|
return { opencodeBinary: "default-opencode" };
|
|
10
|
-
if (scope === "state" && owner === "ui")
|
|
10
|
+
if (scope === "state" && owner === "ui")
|
|
11
11
|
return { opencodeBinaries: [{ path: "saved-opencode", label: "Saved", version: "1.2.3" }] };
|
|
12
|
-
}
|
|
13
12
|
return {};
|
|
14
13
|
},
|
|
15
14
|
};
|
|
16
15
|
const resolver = new BinaryResolver(settings);
|
|
17
|
-
assert.deepEqual(resolver.resolve("saved-opencode"), {
|
|
18
|
-
path: "saved-opencode",
|
|
19
|
-
label: "Saved",
|
|
20
|
-
version: "1.2.3",
|
|
21
|
-
});
|
|
16
|
+
assert.deepEqual(resolver.resolve("saved-opencode"), { path: "saved-opencode", label: "Saved", version: "1.2.3" });
|
|
22
17
|
assert.equal(resolver.resolveDefault().path, "default-opencode");
|
|
23
18
|
});
|
|
24
19
|
});
|
package/dist/shutdown.js
CHANGED
|
@@ -1,75 +1,83 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
super(`Server shutdown failed while stopping: ${failures.map((failure) => failure.resource).join(", ")}`);
|
|
4
|
-
this.name = "ServerShutdownError";
|
|
5
|
-
this.failures = failures;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
1
|
+
export const SERVER_SHUTDOWN_COMPLETE = "CODENOMAD_SHUTDOWN_STATUS:complete";
|
|
2
|
+
export const SERVER_SHUTDOWN_INCOMPLETE = "CODENOMAD_SHUTDOWN_STATUS:incomplete";
|
|
8
3
|
export function createServerShutdownHandler(options) {
|
|
9
|
-
const forceExit = options.forceExit ??
|
|
10
|
-
const setExitCode = options.setExitCode ?? ((code) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let shutdownPromise;
|
|
4
|
+
const forceExit = options.forceExit ?? process.exit;
|
|
5
|
+
const setExitCode = options.setExitCode ?? ((code) => { process.exitCode = code; });
|
|
6
|
+
const reportStatus = options.reportStatus ?? ((status) => console.log(status));
|
|
7
|
+
let pending;
|
|
14
8
|
return (signal) => {
|
|
15
|
-
if (
|
|
9
|
+
if (pending) {
|
|
16
10
|
options.logger.error({ signal }, "Additional shutdown signal received; forcing nonzero exit");
|
|
17
11
|
forceExit(1);
|
|
18
|
-
return
|
|
12
|
+
return pending;
|
|
19
13
|
}
|
|
20
14
|
options.logger.info({ signal }, "Received shutdown signal, stopping workspaces and server");
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
pending = Promise.resolve().then(options.shutdown).then(() => {
|
|
16
|
+
options.logger.info({}, "Shutdown complete");
|
|
17
|
+
reportStatus(SERVER_SHUTDOWN_COMPLETE);
|
|
18
|
+
setExitCode(0);
|
|
19
|
+
}, async (error) => {
|
|
20
|
+
options.logger.error({ err: error }, "Server shutdown incomplete; retrying cleanup");
|
|
21
|
+
const retryAttempts = Math.max(0, Math.floor(options.retryAttempts ?? 3));
|
|
22
|
+
for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, options.retryDelayMs ?? 250));
|
|
24
|
+
try {
|
|
25
|
+
await options.shutdown();
|
|
26
|
+
options.logger.info({}, "Shutdown cleanup retry completed");
|
|
27
|
+
reportStatus(SERVER_SHUTDOWN_COMPLETE);
|
|
28
|
+
setExitCode(0);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch (retryError) {
|
|
32
|
+
options.logger.warn({ err: retryError, attempt, attempts: retryAttempts }, "Shutdown cleanup retry remains incomplete");
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
options.logger.error({ attempts: retryAttempts }, "Shutdown cleanup retries exhausted; preserving process-tree containment");
|
|
36
|
+
reportStatus(SERVER_SHUTDOWN_INCOMPLETE);
|
|
37
|
+
setExitCode(1);
|
|
38
|
+
if (options.holdAfterFailure)
|
|
39
|
+
await options.holdAfterFailure();
|
|
40
|
+
});
|
|
41
|
+
return pending;
|
|
33
42
|
};
|
|
34
43
|
}
|
|
35
44
|
export async function orchestrateServerShutdown(operations, logger, workspaceAttempts = 2) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
const errors = [];
|
|
46
|
+
const namedError = (name, cause) => Object.assign(new Error(`${name} failed: ${cause instanceof Error ? cause.message : String(cause)}`), { cause });
|
|
47
|
+
const settle = async (pending) => {
|
|
48
|
+
const results = await Promise.allSettled(pending.map(([, run]) => Promise.resolve().then(run)));
|
|
49
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
50
|
+
const result = results[index];
|
|
51
|
+
if (result.status !== "rejected")
|
|
52
|
+
continue;
|
|
53
|
+
const error = namedError(pending[index][0], result.reason);
|
|
54
|
+
errors.push(error);
|
|
55
|
+
logger.error({ err: error }, "Server resource shutdown failed");
|
|
44
56
|
}
|
|
45
57
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
53
|
-
try {
|
|
54
|
-
await operations.stopWorkspaces();
|
|
55
|
-
workspaceFailure = undefined;
|
|
56
|
-
logger.info({ attempt }, "Workspace manager shutdown complete");
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
workspaceFailure = error;
|
|
58
|
+
const workspaceShutdown = (async () => {
|
|
59
|
+
const attempts = Math.max(1, Math.floor(workspaceAttempts));
|
|
60
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
61
|
+
const [result] = await Promise.allSettled([Promise.resolve().then(operations.stopWorkspaces)]);
|
|
62
|
+
if (result.status === "fulfilled")
|
|
63
|
+
break;
|
|
61
64
|
if (attempt < attempts) {
|
|
62
|
-
logger.warn({ err:
|
|
65
|
+
logger.warn({ err: result.reason, attempt, attempts }, "Workspace manager shutdown failed; retrying cleanup");
|
|
66
|
+
continue;
|
|
63
67
|
}
|
|
68
|
+
const error = namedError("stopWorkspaces", result.reason);
|
|
69
|
+
errors.push(error);
|
|
70
|
+
logger.error({ err: error, attempts }, "Workspace manager shutdown failed");
|
|
64
71
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
})();
|
|
73
|
+
await Promise.all([
|
|
74
|
+
settle([
|
|
75
|
+
["stopInstanceEventBridge", operations.stopInstanceEventBridge], ["stopSidecars", operations.stopSidecars],
|
|
76
|
+
["stopClientConnections", operations.stopClientConnections], ["stopRemoteProxySessions", operations.stopRemoteProxySessions],
|
|
77
|
+
]),
|
|
78
|
+
workspaceShutdown,
|
|
79
|
+
]);
|
|
80
|
+
await settle([["stopHttpServers", operations.stopHttpServers], ["stopReleaseMonitor", operations.stopReleaseMonitor]]);
|
|
81
|
+
if (errors.length)
|
|
82
|
+
throw new AggregateError(errors, "Server shutdown failed");
|
|
75
83
|
}
|
package/dist/shutdown.test.js
CHANGED
|
@@ -1,110 +1,147 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import { createServerShutdownHandler, orchestrateServerShutdown,
|
|
4
|
-
const logger = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
stopInstanceEventBridge: () => undefined,
|
|
12
|
-
stopSidecars: () => undefined,
|
|
13
|
-
stopClientConnections: () => undefined,
|
|
14
|
-
stopRemoteProxySessions: () => undefined,
|
|
15
|
-
stopWorkspaces: () => undefined,
|
|
16
|
-
stopHttpServers: () => undefined,
|
|
17
|
-
stopReleaseMonitor: () => undefined,
|
|
18
|
-
...overrides,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
3
|
+
import { createServerShutdownHandler, orchestrateServerShutdown, SERVER_SHUTDOWN_COMPLETE, SERVER_SHUTDOWN_INCOMPLETE, } from "./shutdown";
|
|
4
|
+
const logger = { info() { }, warn() { }, error() { } };
|
|
5
|
+
const operations = (overrides = {}) => ({
|
|
6
|
+
stopInstanceEventBridge() { }, stopSidecars() { }, stopClientConnections() { },
|
|
7
|
+
stopRemoteProxySessions() { }, stopWorkspaces() { }, stopHttpServers() { }, stopReleaseMonitor() { },
|
|
8
|
+
...overrides,
|
|
9
|
+
});
|
|
21
10
|
describe("server shutdown orchestration", () => {
|
|
22
|
-
it("retries workspace cleanup
|
|
11
|
+
it("retries workspace cleanup and preserves shutdown order", async () => {
|
|
12
|
+
const calls = [];
|
|
23
13
|
let attempts = 0;
|
|
24
14
|
await orchestrateServerShutdown(operations({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
},
|
|
30
|
-
}), logger);
|
|
31
|
-
assert.equal(attempts, 2);
|
|
32
|
-
});
|
|
33
|
-
it("stops remote proxy sessions before workspace and HTTP shutdown", async () => {
|
|
34
|
-
const closed = [];
|
|
35
|
-
await orchestrateServerShutdown(operations({
|
|
36
|
-
stopRemoteProxySessions: () => { closed.push("remote-proxy"); },
|
|
37
|
-
stopWorkspaces: () => { closed.push("workspaces"); },
|
|
38
|
-
stopHttpServers: () => { closed.push("http"); },
|
|
15
|
+
stopRemoteProxySessions: () => { calls.push("remote-proxy"); },
|
|
16
|
+
stopWorkspaces: () => { calls.push(`workspaces-${++attempts}`); if (attempts === 1)
|
|
17
|
+
throw new Error("still alive"); },
|
|
18
|
+
stopHttpServers: () => { calls.push("http"); },
|
|
39
19
|
}), logger);
|
|
40
|
-
assert.deepEqual(
|
|
20
|
+
assert.deepEqual(calls, ["workspaces-1", "remote-proxy", "workspaces-2", "http"]);
|
|
41
21
|
});
|
|
42
|
-
it("closes
|
|
22
|
+
it("closes remaining resources and aggregates the concrete current error", async () => {
|
|
23
|
+
const failure = new Error("workspace abc POSIX process group is still alive");
|
|
43
24
|
const closed = [];
|
|
44
|
-
const cleanupFailure = new Error("workspace abc POSIX process group is still alive");
|
|
45
25
|
let attempts = 0;
|
|
46
26
|
await assert.rejects(orchestrateServerShutdown(operations({
|
|
47
|
-
stopWorkspaces: () => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
stopHttpServers: () => {
|
|
52
|
-
closed.push("http");
|
|
53
|
-
},
|
|
54
|
-
stopReleaseMonitor: () => {
|
|
55
|
-
closed.push("release-monitor");
|
|
56
|
-
},
|
|
27
|
+
stopWorkspaces: () => { attempts++; throw failure; },
|
|
28
|
+
stopHttpServers: () => { closed.push("http"); },
|
|
29
|
+
stopReleaseMonitor: () => { closed.push("release-monitor"); },
|
|
57
30
|
}), logger), (error) => {
|
|
58
|
-
assert.ok(error instanceof
|
|
59
|
-
assert.equal(error.
|
|
60
|
-
assert.
|
|
31
|
+
assert.ok(error instanceof AggregateError);
|
|
32
|
+
assert.equal(error.errors.length, 1);
|
|
33
|
+
assert.match(error.errors[0].message, /^stopWorkspaces failed:/);
|
|
34
|
+
assert.strictEqual(error.errors[0].cause, failure);
|
|
61
35
|
return true;
|
|
62
36
|
});
|
|
63
|
-
assert.
|
|
64
|
-
|
|
37
|
+
assert.deepEqual([attempts, closed], [2, ["http", "release-monitor"]]);
|
|
38
|
+
});
|
|
39
|
+
it("starts workspace cleanup without waiting for preliminary shutdown", async () => {
|
|
40
|
+
let releasePreliminary;
|
|
41
|
+
const preliminary = new Promise((resolve) => { releasePreliminary = resolve; });
|
|
42
|
+
let workspaceStarted = false;
|
|
43
|
+
const shutdown = orchestrateServerShutdown(operations({
|
|
44
|
+
stopRemoteProxySessions: () => preliminary,
|
|
45
|
+
stopWorkspaces: () => { workspaceStarted = true; },
|
|
46
|
+
}), logger);
|
|
47
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
48
|
+
assert.equal(workspaceStarted, true);
|
|
49
|
+
releasePreliminary();
|
|
50
|
+
await shutdown;
|
|
65
51
|
});
|
|
66
52
|
});
|
|
67
53
|
describe("server shutdown signal boundary", () => {
|
|
68
|
-
it("
|
|
54
|
+
it("reports incomplete cleanup and holds for final tree enforcement", async () => {
|
|
69
55
|
const calls = [];
|
|
70
56
|
const exits = [];
|
|
57
|
+
const statuses = [];
|
|
58
|
+
let releaseHold;
|
|
59
|
+
const hold = new Promise((resolve) => { releaseHold = resolve; });
|
|
71
60
|
const handler = createServerShutdownHandler({
|
|
72
|
-
shutdown: async () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
error: () => calls.push("error"),
|
|
80
|
-
},
|
|
81
|
-
forceExit: (code) => {
|
|
82
|
-
calls.push("force-exit");
|
|
83
|
-
exits.push(code);
|
|
84
|
-
},
|
|
61
|
+
shutdown: async () => { calls.push("cleanup"); throw new Error("retained child survived"); },
|
|
62
|
+
logger: { info: () => calls.push("info"), warn() { }, error: () => calls.push("error") },
|
|
63
|
+
forceExit: (code) => { calls.push("force-exit"); exits.push(code); },
|
|
64
|
+
setExitCode: () => undefined,
|
|
65
|
+
reportStatus: (status) => statuses.push(status),
|
|
66
|
+
holdAfterFailure: () => hold,
|
|
67
|
+
retryAttempts: 0,
|
|
85
68
|
});
|
|
86
|
-
|
|
69
|
+
const pending = handler("SIGTERM");
|
|
70
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
71
|
+
assert.deepEqual([exits, statuses, calls], [[], [SERVER_SHUTDOWN_INCOMPLETE], ["info", "cleanup", "error", "error"]]);
|
|
72
|
+
handler("SIGTERM");
|
|
87
73
|
assert.deepEqual(exits, [1]);
|
|
88
|
-
|
|
74
|
+
releaseHold();
|
|
75
|
+
await pending;
|
|
89
76
|
});
|
|
90
|
-
it("
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
77
|
+
it("reports complete cleanup before allowing natural exit", async () => {
|
|
78
|
+
const statuses = [], exitCodes = [];
|
|
79
|
+
const handler = createServerShutdownHandler({
|
|
80
|
+
shutdown: async () => undefined,
|
|
81
|
+
logger,
|
|
82
|
+
reportStatus: (status) => statuses.push(status),
|
|
83
|
+
setExitCode: (code) => exitCodes.push(code),
|
|
94
84
|
});
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
await handler("stdin");
|
|
86
|
+
assert.deepEqual(statuses, [SERVER_SHUTDOWN_COMPLETE]);
|
|
87
|
+
assert.deepEqual(exitCodes, [0]);
|
|
88
|
+
});
|
|
89
|
+
it("keeps retrying identity-aware cleanup during the final enforcement budget", async () => {
|
|
90
|
+
let attempts = 0;
|
|
91
|
+
const statuses = [], exitCodes = [];
|
|
92
|
+
const handler = createServerShutdownHandler({
|
|
93
|
+
shutdown: async () => {
|
|
94
|
+
attempts++;
|
|
95
|
+
if (attempts === 1)
|
|
96
|
+
throw new Error("detached group still alive");
|
|
97
|
+
},
|
|
98
|
+
logger,
|
|
99
|
+
reportStatus: (status) => statuses.push(status),
|
|
100
|
+
setExitCode: (code) => exitCodes.push(code),
|
|
101
|
+
retryDelayMs: 0,
|
|
102
|
+
});
|
|
103
|
+
await handler("stdin");
|
|
104
|
+
assert.equal(attempts, 2);
|
|
105
|
+
assert.deepEqual(statuses, [SERVER_SHUTDOWN_COMPLETE]);
|
|
106
|
+
assert.deepEqual(exitCodes, [0]);
|
|
107
|
+
});
|
|
108
|
+
it("preserves containment after the standalone cleanup retry budget", async () => {
|
|
109
|
+
let attempts = 0;
|
|
110
|
+
const statuses = [], exitCodes = [], forcedExits = [];
|
|
111
|
+
let releaseHold;
|
|
112
|
+
const hold = new Promise((resolve) => { releaseHold = resolve; });
|
|
97
113
|
const handler = createServerShutdownHandler({
|
|
98
|
-
shutdown: () =>
|
|
114
|
+
shutdown: async () => { attempts++; throw new Error("process tree remains alive"); },
|
|
99
115
|
logger,
|
|
100
|
-
|
|
116
|
+
reportStatus: (status) => statuses.push(status),
|
|
101
117
|
setExitCode: (code) => exitCodes.push(code),
|
|
118
|
+
forceExit: (code) => forcedExits.push(code),
|
|
119
|
+
holdAfterFailure: () => hold,
|
|
120
|
+
retryDelayMs: 0,
|
|
121
|
+
retryAttempts: 2,
|
|
102
122
|
});
|
|
103
|
-
const
|
|
104
|
-
|
|
123
|
+
const pending = handler("stdin");
|
|
124
|
+
while (attempts < 3)
|
|
125
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
126
|
+
assert.equal(attempts, 3);
|
|
127
|
+
assert.deepEqual(statuses, [SERVER_SHUTDOWN_INCOMPLETE]);
|
|
128
|
+
assert.deepEqual(exitCodes, [1]);
|
|
129
|
+
assert.deepEqual(forcedExits, []);
|
|
130
|
+
handler("SIGTERM");
|
|
131
|
+
assert.deepEqual(forcedExits, [1]);
|
|
132
|
+
releaseHold();
|
|
133
|
+
await pending;
|
|
134
|
+
});
|
|
135
|
+
it("escalates a second signal while sharing first-signal cleanup", async () => {
|
|
136
|
+
let finish;
|
|
137
|
+
const cleanup = new Promise((resolve) => { finish = resolve; });
|
|
138
|
+
const exits = [], exitCodes = [];
|
|
139
|
+
const handler = createServerShutdownHandler({ shutdown: () => cleanup, logger,
|
|
140
|
+
forceExit: (code) => exits.push(code), setExitCode: (code) => exitCodes.push(code), reportStatus: () => undefined });
|
|
141
|
+
const first = handler("SIGINT"), second = handler("SIGTERM");
|
|
105
142
|
assert.strictEqual(first, second);
|
|
106
143
|
assert.deepEqual(exits, [1]);
|
|
107
|
-
|
|
144
|
+
finish();
|
|
108
145
|
await first;
|
|
109
146
|
assert.deepEqual(exitCodes, [0]);
|
|
110
147
|
});
|
package/dist/ui/remote-ui.js
CHANGED
|
@@ -7,8 +7,8 @@ import { Readable } from "stream";
|
|
|
7
7
|
import { fetch } from "undici";
|
|
8
8
|
import yauzl from "yauzl";
|
|
9
9
|
const DEFAULT_MANIFEST_URL = "https://ui.codenomad.neuralnomads.ai/version.json";
|
|
10
|
-
const MANIFEST_TIMEOUT_MS =
|
|
11
|
-
const ZIP_TIMEOUT_MS =
|
|
10
|
+
const MANIFEST_TIMEOUT_MS = 5_000;
|
|
11
|
+
const ZIP_TIMEOUT_MS = 30_000;
|
|
12
12
|
export async function resolveUi(options) {
|
|
13
13
|
const manifestUrl = options.manifestUrl ?? DEFAULT_MANIFEST_URL;
|
|
14
14
|
if (options.uiDevServerUrl) {
|
|
@@ -25,7 +25,7 @@ const kimi = {
|
|
|
25
25
|
for (const item of Array.isArray(payload?.limits) ? payload.limits : []) {
|
|
26
26
|
const duration = toNumber(item?.window?.duration);
|
|
27
27
|
const unit = item?.window?.timeUnit;
|
|
28
|
-
const multiplier = unit === "TIME_UNIT_MINUTE" ? 60 : unit === "TIME_UNIT_HOUR" ? 3600 : unit === "TIME_UNIT_DAY" ?
|
|
28
|
+
const multiplier = unit === "TIME_UNIT_MINUTE" ? 60 : unit === "TIME_UNIT_HOUR" ? 3600 : unit === "TIME_UNIT_DAY" ? 86_400 : null;
|
|
29
29
|
const seconds = duration !== null && multiplier ? duration * multiplier : null;
|
|
30
30
|
const limit = toNumber(item?.detail?.limit);
|
|
31
31
|
const remaining = toNumber(item?.detail?.remaining);
|
|
@@ -61,7 +61,7 @@ const nanoGpt = {
|
|
|
61
61
|
const limit = toNumber(source.limit ?? source.limits?.[label]);
|
|
62
62
|
windows[label] = toUsageWindow({
|
|
63
63
|
usedPercent: fraction !== null ? fraction * 100 : used !== null && limit ? (used / limit) * 100 : null,
|
|
64
|
-
windowSeconds: label === "daily" ?
|
|
64
|
+
windowSeconds: label === "daily" ? 86_400 : null,
|
|
65
65
|
resetAt: toTimestamp(source.resetAt ?? payload?.period?.currentPeriodEnd),
|
|
66
66
|
});
|
|
67
67
|
}
|
|
@@ -112,7 +112,7 @@ function createTokenLimitProvider(input) {
|
|
|
112
112
|
if (limit?.type !== "TOKENS_LIMIT" && limit?.type !== "TIME_LIMIT")
|
|
113
113
|
continue;
|
|
114
114
|
const duration = toNumber(limit.number);
|
|
115
|
-
const seconds = limit.type === "TIME_LIMIT" ? 30 *
|
|
115
|
+
const seconds = limit.type === "TIME_LIMIT" ? 30 * 86_400 : limit.unit === 3 && duration ? duration * 3600 : null;
|
|
116
116
|
const label = limit.type === "TIME_LIMIT" ? "mcp-tools" : resolveWindowLabel(seconds);
|
|
117
117
|
windows[label] = toUsageWindow({
|
|
118
118
|
usedPercent: toNumber(limit.percentage),
|
|
@@ -92,7 +92,7 @@ function transformModel(sourceId, modelId, data) {
|
|
|
92
92
|
windows: {
|
|
93
93
|
[label]: toUsageWindow({
|
|
94
94
|
usedPercent: fraction === null ? null : 100 - fraction * 100,
|
|
95
|
-
windowSeconds: label === "daily" ?
|
|
95
|
+
windowSeconds: label === "daily" ? 86_400 : 5 * 3600,
|
|
96
96
|
resetAt,
|
|
97
97
|
}),
|
|
98
98
|
},
|
|
@@ -1,32 +1,4 @@
|
|
|
1
1
|
import { fetchJson, getAuthEntry, getCredential, notConfigured, resolveWindowLabel, safeFetch, toNumber, toTimestamp, toUsageWindow, } from "../shared";
|
|
2
|
-
const claudeAliases = ["anthropic", "claude"];
|
|
3
|
-
const claude = {
|
|
4
|
-
id: "claude",
|
|
5
|
-
name: "Claude",
|
|
6
|
-
aliases: claudeAliases,
|
|
7
|
-
async fetchQuota() {
|
|
8
|
-
const token = getCredential(claudeAliases, ["access", "token"]);
|
|
9
|
-
if (!token)
|
|
10
|
-
return notConfigured(this.id, this.name);
|
|
11
|
-
return safeFetch(this.id, this.name, async () => {
|
|
12
|
-
const payload = await fetchJson("https://api.anthropic.com/api/oauth/usage", {
|
|
13
|
-
headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
|
|
14
|
-
});
|
|
15
|
-
const windows = {};
|
|
16
|
-
for (const [key, source] of [
|
|
17
|
-
["5h", payload?.five_hour],
|
|
18
|
-
["7d", payload?.seven_day],
|
|
19
|
-
["7d-sonnet", payload?.seven_day_sonnet],
|
|
20
|
-
["7d-opus", payload?.seven_day_opus],
|
|
21
|
-
]) {
|
|
22
|
-
if (!source)
|
|
23
|
-
continue;
|
|
24
|
-
windows[key] = toUsageWindow({ usedPercent: toNumber(source.utilization), resetAt: toTimestamp(source.resets_at) });
|
|
25
|
-
}
|
|
26
|
-
return { windows };
|
|
27
|
-
});
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
2
|
const codexAliases = ["openai", "codex", "chatgpt"];
|
|
31
3
|
const codex = {
|
|
32
4
|
id: "codex",
|
|
@@ -57,13 +29,6 @@ const codex = {
|
|
|
57
29
|
resetAt: toTimestamp(source.reset_at),
|
|
58
30
|
});
|
|
59
31
|
}
|
|
60
|
-
const balance = toNumber(payload?.credits?.balance);
|
|
61
|
-
if (payload?.credits?.unlimited || balance !== null) {
|
|
62
|
-
windows.credits_balance = toUsageWindow({
|
|
63
|
-
usedPercent: null,
|
|
64
|
-
valueLabel: payload?.credits?.unlimited ? "∞" : `$${balance.toFixed(2)}`,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
32
|
return { windows };
|
|
68
33
|
});
|
|
69
34
|
},
|
|
@@ -108,4 +73,4 @@ const copilot = {
|
|
|
108
73
|
});
|
|
109
74
|
},
|
|
110
75
|
};
|
|
111
|
-
export const oauthProviders = [
|
|
76
|
+
export const oauthProviders = [codex, copilot];
|
|
@@ -15,7 +15,7 @@ async function resolveCursorToken() {
|
|
|
15
15
|
const accessToken = getString(process.env.CURSOR_ACCESS_TOKEN) ?? getString(process.env.CURSOR_TOKEN);
|
|
16
16
|
const refreshToken = getString(process.env.CURSOR_REFRESH_TOKEN);
|
|
17
17
|
const expiresAt = accessToken ? decodeJwtExpiration(accessToken) : null;
|
|
18
|
-
if (accessToken && (!expiresAt || expiresAt > Date.now() + 5 *
|
|
18
|
+
if (accessToken && (!expiresAt || expiresAt > Date.now() + 5 * 60_000))
|
|
19
19
|
return accessToken;
|
|
20
20
|
if (!refreshToken)
|
|
21
21
|
return accessToken;
|
|
@@ -94,7 +94,7 @@ const ollamaCloud = {
|
|
|
94
94
|
const response = await fetch("https://ollama.com/settings", {
|
|
95
95
|
headers: { Cookie: cookie, "User-Agent": "CodeNomad usage provider" },
|
|
96
96
|
redirect: "manual",
|
|
97
|
-
signal: AbortSignal.timeout(
|
|
97
|
+
signal: AbortSignal.timeout(15_000),
|
|
98
98
|
});
|
|
99
99
|
if (!response.ok)
|
|
100
100
|
throw new Error(`Provider returned HTTP ${response.status}`);
|
|
@@ -134,7 +134,7 @@ const openCodeGo = {
|
|
|
134
134
|
const response = await fetch(`https://opencode.ai/workspace/${encodeURIComponent(workspaceId)}/go`, {
|
|
135
135
|
headers: { Cookie: `auth=${authCookie.replace(/^auth=/, "")}`, "User-Agent": "CodeNomad usage provider" },
|
|
136
136
|
redirect: "manual",
|
|
137
|
-
signal: AbortSignal.timeout(
|
|
137
|
+
signal: AbortSignal.timeout(15_000),
|
|
138
138
|
});
|
|
139
139
|
if (!response.ok)
|
|
140
140
|
throw new Error(`Provider returned HTTP ${response.status}`);
|
package/dist/usage/service.js
CHANGED
|
@@ -3,7 +3,7 @@ import { googleProviders } from "./providers/google";
|
|
|
3
3
|
import { miniMaxProviders } from "./providers/minimax";
|
|
4
4
|
import { oauthProviders } from "./providers/oauth";
|
|
5
5
|
import { specialProviders } from "./providers/special";
|
|
6
|
-
const CACHE_TTL_MS =
|
|
6
|
+
const CACHE_TTL_MS = 60_000;
|
|
7
7
|
const providers = [...oauthProviders, ...apiKeyProviders, ...miniMaxProviders, ...googleProviders, ...specialProviders];
|
|
8
8
|
const registry = new Map();
|
|
9
9
|
for (const provider of providers) {
|