@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.
Files changed (89) hide show
  1. package/dist/auth/manager.js +1 -1
  2. package/dist/filesystem/__tests__/search-cache.test.js +5 -5
  3. package/dist/filesystem/search-cache.js +1 -1
  4. package/dist/index.js +42 -0
  5. package/dist/index.test.js +40 -10
  6. package/dist/opencode-update/service.js +150 -0
  7. package/dist/opencode-update/service.test.js +107 -0
  8. package/dist/permissions/auto-accept-manager.js +165 -0
  9. package/dist/permissions/auto-accept-manager.test.js +208 -0
  10. package/dist/permissions/auto-accept-store.js +5 -2
  11. package/dist/permissions/opencode-yolo-metadata.js +81 -0
  12. package/dist/permissions/opencode-yolo-metadata.test.js +54 -0
  13. package/dist/server/__tests__/remote-proxy.test.js +64 -56
  14. package/dist/server/http-server.js +12 -15
  15. package/dist/server/remote-proxy.js +68 -98
  16. package/dist/server/routes/opencode-update.js +37 -0
  17. package/dist/server/routes/opencode-update.test.js +34 -0
  18. package/dist/server/routes/remote-servers.js +1 -1
  19. package/dist/server/routes/workspaces.test.js +38 -0
  20. package/dist/server/routes/worktrees.js +19 -0
  21. package/dist/server/routes/yolo.js +2 -1
  22. package/dist/server/tls.js +2 -2
  23. package/dist/settings/binaries.test.js +2 -7
  24. package/dist/shutdown.js +67 -59
  25. package/dist/shutdown.test.js +115 -78
  26. package/dist/ui/remote-ui.js +2 -2
  27. package/dist/usage/providers/api-key.js +3 -3
  28. package/dist/usage/providers/google.js +1 -1
  29. package/dist/usage/providers/oauth.js +1 -36
  30. package/dist/usage/providers/special.js +3 -3
  31. package/dist/usage/service.js +1 -1
  32. package/dist/usage/service.test.js +5 -3
  33. package/dist/usage/shared.js +4 -4
  34. package/dist/usage/shared.test.js +4 -4
  35. package/dist/workspaces/__tests__/spawn.test.js +2 -2
  36. package/dist/workspaces/__tests__/workspace-identity.test.js +94 -286
  37. package/dist/workspaces/binary-path.test.js +10 -0
  38. package/dist/workspaces/instance-client.js +4 -3
  39. package/dist/workspaces/launch-cleanup.test.js +21 -25
  40. package/dist/workspaces/manager.js +300 -351
  41. package/dist/workspaces/manager.test.js +209 -237
  42. package/dist/workspaces/process-identity.darwin.test.js +88 -0
  43. package/dist/workspaces/process-identity.js +172 -149
  44. package/dist/workspaces/process-identity.test.js +86 -140
  45. package/dist/workspaces/runtime.js +261 -373
  46. package/dist/workspaces/runtime.test.js +222 -547
  47. package/package.json +1 -1
  48. package/public/assets/{FilesTab-BjztpSdj.js → FilesTab-ChfE8-8N.js} +2 -2
  49. package/public/assets/{GitChangesTab-B9XraSoR.js → GitChangesTab-ntRtkxIF.js} +2 -2
  50. package/public/assets/{SplitFilePanel-Cu_5-Z-E.js → SplitFilePanel-Dav7VCqK.js} +1 -1
  51. package/public/assets/StatusTab-D3toOefp.js +1 -0
  52. package/public/assets/{align-justify-BKS9gyRk.js → align-justify-D91sAsLc.js} +1 -1
  53. package/public/assets/{bundle-full-CXGXXlzR.js → bundle-full-CWkY9-v4.js} +1 -1
  54. package/public/assets/{diff-viewer-CSNOYoEB.js → diff-viewer-Cn_41zpc.js} +1 -1
  55. package/public/assets/index-3dx-hhNQ.js +1 -0
  56. package/public/assets/index-BKwfZLzw.js +1 -0
  57. package/public/assets/index-C5IioEFT.js +1 -0
  58. package/public/assets/index-CZYc0VcU.js +2 -0
  59. package/public/assets/index-CdCoXYXP.js +1 -0
  60. package/public/assets/index-CwAVops2.js +1 -0
  61. package/public/assets/index-CzvCOwKc.js +1 -0
  62. package/public/assets/index-D-loOWPv.js +1 -0
  63. package/public/assets/{index-DVc4ls8c.js → index-DM1cpVNo.js} +1 -1
  64. package/public/assets/{index-sLe9hEfQ.js → index-DOprq6Gt.js} +1 -1
  65. package/public/assets/index-L7fX3W8k.css +1 -0
  66. package/public/assets/index-w2KdHEME.js +1 -0
  67. package/public/assets/{loading-CDmt9OJU.js → loading-BfoDcWcg.js} +1 -1
  68. package/public/assets/main-CaTCcck1.js +68 -0
  69. package/public/assets/{markdown-1ZLSxCFT.js → markdown-D33YI2g2.js} +3 -3
  70. package/public/assets/{monaco-viewer-D6JgH76W.js → monaco-viewer-DM35aobI.js} +4 -4
  71. package/public/assets/{tool-call-CyXb63LH.js → tool-call-tV85Jv2i.js} +3 -3
  72. package/public/assets/{unified-picker-DCCdSVwI.js → unified-picker-DSRT9GMh.js} +1 -1
  73. package/public/assets/{wrap-text-pNMdTIq3.js → wrap-text-Dmrn2DKo.js} +1 -1
  74. package/public/index.html +4 -4
  75. package/public/loading.html +4 -4
  76. package/public/sw.js +1 -1
  77. package/dist/workspaces/launch-cleanup.js +0 -132
  78. package/public/assets/StatusTab-D_IE79r-.js +0 -1
  79. package/public/assets/index-B3YjNCh4.js +0 -1
  80. package/public/assets/index-B3ZD2_SU.js +0 -1
  81. package/public/assets/index-Bh2ofaQo.js +0 -1
  82. package/public/assets/index-CM0c9xRZ.js +0 -1
  83. package/public/assets/index-CdDxouJl.js +0 -2
  84. package/public/assets/index-D06Y2cFq.js +0 -1
  85. package/public/assets/index-PmE51vPh.js +0 -1
  86. package/public/assets/index-Qj63IiSj.css +0 -1
  87. package/public/assets/index-VY_zwmsG.js +0 -1
  88. package/public/assets/index-qiiH7egV.js +0 -1
  89. 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
  });
@@ -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() - 60000);
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() - 60000);
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 class ServerShutdownError extends Error {
2
- constructor(failures) {
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 ?? ((code) => process.exit(code));
10
- const setExitCode = options.setExitCode ?? ((code) => {
11
- process.exitCode = code;
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 (shutdownPromise) {
9
+ if (pending) {
16
10
  options.logger.error({ signal }, "Additional shutdown signal received; forcing nonzero exit");
17
11
  forceExit(1);
18
- return shutdownPromise;
12
+ return pending;
19
13
  }
20
14
  options.logger.info({ signal }, "Received shutdown signal, stopping workspaces and server");
21
- shutdownPromise = (async () => {
22
- try {
23
- await options.shutdown();
24
- options.logger.info({}, "Shutdown complete");
25
- setExitCode(0);
26
- }
27
- catch (error) {
28
- options.logger.error({ err: error }, "Server shutdown incomplete; forcing nonzero exit");
29
- forceExit(1);
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
- return shutdownPromise;
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 failures = [];
37
- const run = async (resource, operation) => {
38
- try {
39
- await operation();
40
- }
41
- catch (error) {
42
- failures.push({ resource, error });
43
- logger.error({ err: error, resource }, `${resource} shutdown failed`);
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
- await run("Instance event bridge", operations.stopInstanceEventBridge);
47
- await run("SideCar manager", operations.stopSidecars);
48
- await run("Client connection manager", operations.stopClientConnections);
49
- await run("Remote proxy session manager", operations.stopRemoteProxySessions);
50
- const attempts = Math.max(1, Math.floor(workspaceAttempts));
51
- let workspaceFailure;
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: error, attempt, attempts }, "Workspace manager shutdown failed; retrying cleanup");
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
- if (workspaceFailure !== undefined) {
67
- failures.push({ resource: "Workspace manager", error: workspaceFailure });
68
- logger.error({ err: workspaceFailure, attempts }, "Workspace manager shutdown failed");
69
- }
70
- await run("HTTP servers", operations.stopHttpServers);
71
- await run("Release monitor", operations.stopReleaseMonitor);
72
- if (failures.length > 0) {
73
- throw new ServerShutdownError(failures);
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
  }
@@ -1,110 +1,147 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
- import { createServerShutdownHandler, orchestrateServerShutdown, ServerShutdownError, } from "./shutdown";
4
- const logger = {
5
- info: () => undefined,
6
- warn: () => undefined,
7
- error: () => undefined,
8
- };
9
- function operations(overrides = {}) {
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 before reporting success", async () => {
11
+ it("retries workspace cleanup and preserves shutdown order", async () => {
12
+ const calls = [];
23
13
  let attempts = 0;
24
14
  await orchestrateServerShutdown(operations({
25
- stopWorkspaces: () => {
26
- attempts += 1;
27
- if (attempts === 1)
28
- throw new Error("detached process still alive");
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(closed, ["remote-proxy", "workspaces", "http"]);
20
+ assert.deepEqual(calls, ["workspaces-1", "remote-proxy", "workspaces-2", "http"]);
41
21
  });
42
- it("closes other resources and rejects with the concrete workspace failure", async () => {
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
- attempts += 1;
49
- throw cleanupFailure;
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 ServerShutdownError);
59
- assert.equal(error.failures[0]?.resource, "Workspace manager");
60
- assert.strictEqual(error.failures[0]?.error, cleanupFailure);
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.equal(attempts, 2);
64
- assert.deepEqual(closed, ["http", "release-monitor"]);
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("forces a nonzero exit after failed bounded cleanup returns", async () => {
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
- calls.push("cleanup");
74
- throw new Error("retained child survived");
75
- },
76
- logger: {
77
- info: () => calls.push("info"),
78
- warn: () => undefined,
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
- await handler("SIGTERM");
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
- assert.deepEqual(calls, ["info", "cleanup", "error", "force-exit"]);
74
+ releaseHold();
75
+ await pending;
89
76
  });
90
- it("escalates a second signal while shutdown is still pending", async () => {
91
- let finishCleanup;
92
- const cleanup = new Promise((resolve) => {
93
- finishCleanup = resolve;
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
- const exits = [];
96
- const exitCodes = [];
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: () => cleanup,
114
+ shutdown: async () => { attempts++; throw new Error("process tree remains alive"); },
99
115
  logger,
100
- forceExit: (code) => exits.push(code),
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 first = handler("SIGINT");
104
- const second = handler("SIGTERM");
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
- finishCleanup();
144
+ finish();
108
145
  await first;
109
146
  assert.deepEqual(exitCodes, [0]);
110
147
  });
@@ -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 = 5000;
11
- const ZIP_TIMEOUT_MS = 30000;
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" ? 86400 : null;
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" ? 86400 : null,
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 * 86400 : limit.unit === 3 && duration ? duration * 3600 : null;
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" ? 86400 : 5 * 3600,
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 = [claude, codex, copilot];
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 * 60000))
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(15000),
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(15000),
137
+ signal: AbortSignal.timeout(15_000),
138
138
  });
139
139
  if (!response.ok)
140
140
  throw new Error(`Provider returned HTTP ${response.status}`);
@@ -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 = 60000;
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) {