@devlln/helm
Advanced tools
| { | ||
| "name": "codex-voice-remote-bridge", | ||
| "version": "0.2.1", | ||
| "version": "0.3.0", | ||
| "lockfileVersion": 3, | ||
@@ -9,3 +9,3 @@ "requires": true, | ||
| "name": "codex-voice-remote-bridge", | ||
| "version": "0.2.1", | ||
| "version": "0.3.0", | ||
| "dependencies": { | ||
@@ -12,0 +12,0 @@ "dotenv": "^16.6.1", |
| { | ||
| "name": "codex-voice-remote-bridge", | ||
| "version": "0.2.1", | ||
| "version": "0.3.0", | ||
| "private": true, | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -9,2 +9,3 @@ import test from "node:test"; | ||
| detectBridgeInstallMethod, | ||
| getBridgeUpdateStatus, | ||
| shouldEnableBridgeAutoUpdate, | ||
@@ -105,1 +106,19 @@ } from "./bridgeAutoUpdater.js"; | ||
| }); | ||
| test("getBridgeUpdateStatus reports available updates with a user-facing link and command", async () => { | ||
| const result = await getBridgeUpdateStatus({ | ||
| rootDir: "/opt/homebrew/Cellar/helm/0.2.0/libexec", | ||
| packageInfo: { name: "@devlln/helm", version: "0.2.0" }, | ||
| hasGitDir: false, | ||
| env: {}, | ||
| installMethod: "homebrew", | ||
| fetchLatestVersion: async () => "0.2.1", | ||
| scriptExists: () => true, | ||
| }); | ||
| assert.equal(result.status, "available"); | ||
| assert.equal(result.currentVersion, "0.2.0"); | ||
| assert.equal(result.latestVersion, "0.2.1"); | ||
| assert.equal(result.updateURL, "https://www.npmjs.com/package/@devlln/helm/v/0.2.1"); | ||
| assert.equal(result.updateCommand, "brew update && brew upgrade devlln/helm/helm"); | ||
| }); |
@@ -23,2 +23,4 @@ import { spawn } from "node:child_process"; | ||
| installMethod?: BridgeInstallMethod; | ||
| updateURL?: string; | ||
| updateCommand?: string; | ||
| } | ||
@@ -38,2 +40,12 @@ | ||
| export interface BridgeUpdateStatus { | ||
| status: "disabled" | "current" | "available" | "skipped"; | ||
| reason?: string; | ||
| currentVersion?: string; | ||
| latestVersion?: string; | ||
| installMethod?: BridgeInstallMethod; | ||
| updateURL?: string; | ||
| updateCommand?: string; | ||
| } | ||
| export interface StartBridgeAutoUpdaterOptions extends BridgeUpdateCheckOptions { | ||
@@ -133,2 +145,24 @@ setTimeoutFn?: typeof setTimeout; | ||
| function updateCommandForDisplay(installMethod: BridgeInstallMethod): string { | ||
| switch (installMethod) { | ||
| case "homebrew": | ||
| return "brew update && brew upgrade devlln/helm/helm"; | ||
| case "npm": | ||
| return "npm install -g @devlln/helm@latest"; | ||
| case "git": | ||
| return "git pull && npm --prefix bridge install"; | ||
| case "unknown": | ||
| default: | ||
| return "helm update"; | ||
| } | ||
| } | ||
| function updateURLForVersion(version: string | undefined): string | undefined { | ||
| if (!version) { | ||
| return undefined; | ||
| } | ||
| return `https://www.npmjs.com/package/${PUBLIC_PACKAGE_NAME}/v/${version}`; | ||
| } | ||
| function readPackageInfo(rootDir: string): BridgePackageInfo | null { | ||
@@ -175,2 +209,34 @@ try { | ||
| export async function checkForBridgeUpdate(options: BridgeUpdateCheckOptions): Promise<BridgeUpdateCheckResult> { | ||
| const status = await getBridgeUpdateStatus(options); | ||
| switch (status.status) { | ||
| case "disabled": | ||
| case "current": | ||
| case "skipped": | ||
| return { | ||
| ...status, | ||
| status: status.status, | ||
| }; | ||
| case "available": | ||
| break; | ||
| } | ||
| const installMethod = status.installMethod ?? "unknown"; | ||
| const updateCommand = buildBridgeUpdateCommand({ rootDir: options.rootDir, installMethod }); | ||
| const runUpdate = options.runUpdate ?? runDetachedUpdate; | ||
| runUpdate(updateCommand.command, updateCommand.args); | ||
| options.logger?.log( | ||
| `[bridge] Helm ${status.currentVersion ?? "<unknown>"} is older than ${status.latestVersion ?? "<unknown>"}; started ${installMethod} update.` | ||
| ); | ||
| return { | ||
| status: "started", | ||
| currentVersion: status.currentVersion, | ||
| latestVersion: status.latestVersion, | ||
| installMethod, | ||
| updateURL: status.updateURL, | ||
| updateCommand: status.updateCommand, | ||
| }; | ||
| } | ||
| export async function getBridgeUpdateStatus(options: BridgeUpdateCheckOptions): Promise<BridgeUpdateStatus> { | ||
| const env = options.env ?? process.env; | ||
@@ -195,2 +261,3 @@ const packageInfo = options.packageInfo ?? readPackageInfo(options.rootDir); | ||
| installMethod, | ||
| updateCommand: updateCommandForDisplay(installMethod), | ||
| }; | ||
@@ -207,2 +274,3 @@ } | ||
| installMethod, | ||
| updateCommand: updateCommandForDisplay(installMethod), | ||
| }; | ||
@@ -222,2 +290,3 @@ } | ||
| installMethod, | ||
| updateCommand: updateCommandForDisplay(installMethod), | ||
| }; | ||
@@ -232,2 +301,3 @@ } | ||
| installMethod, | ||
| updateCommand: updateCommandForDisplay(installMethod), | ||
| }; | ||
@@ -242,14 +312,14 @@ } | ||
| installMethod, | ||
| updateURL: updateURLForVersion(latestVersion), | ||
| updateCommand: updateCommandForDisplay(installMethod), | ||
| }; | ||
| } | ||
| const runUpdate = options.runUpdate ?? runDetachedUpdate; | ||
| runUpdate(updateCommand.command, updateCommand.args); | ||
| options.logger?.log(`[bridge] Helm ${packageInfo.version} is older than ${latestVersion}; started ${installMethod} update.`); | ||
| return { | ||
| status: "started", | ||
| status: "available", | ||
| currentVersion: packageInfo.version, | ||
| latestVersion, | ||
| installMethod, | ||
| updateURL: updateURLForVersion(latestVersion), | ||
| updateCommand: updateCommandForDisplay(installMethod), | ||
| }; | ||
@@ -256,0 +326,0 @@ } |
@@ -84,2 +84,8 @@ import test from "node:test"; | ||
| request(method: string, params?: JSONValue): Promise<JSONValue | undefined>; | ||
| sendRequestInBackground( | ||
| method: string, | ||
| params?: JSONValue, | ||
| timeoutMs?: number, | ||
| onSettle?: (settlement: { result?: JSONValue; error?: Error }) => void | ||
| ): Promise<string | number>; | ||
| readCodexDesktopActiveWorkspaceRoots(): Promise<string[]>; | ||
@@ -142,2 +148,9 @@ loadThreadDeliverySummary(threadId: string): Promise<{ sourceKind?: string | null; status?: string | null } | null>; | ||
| ): Promise<JSONValue | undefined>; | ||
| startCodexDesktopMobileDeliveryViaAppServerFirst( | ||
| threadId: string, | ||
| text: string, | ||
| options: TestStartTurnOptions, | ||
| needsSteer: boolean, | ||
| baseline: unknown | ||
| ): Promise<JSONValue | undefined>; | ||
| ensureAppServerThreadLoadedForDelivery( | ||
@@ -332,2 +345,4 @@ threadId: string, | ||
| let startCalls = 0; | ||
| const ensureCalls: Array<{ threadId: string; options?: { forceResume?: boolean } }> = []; | ||
| const backgroundRequestCalls: Array<{ method: string; params?: JSONValue; timeoutMs?: number }> = []; | ||
@@ -352,11 +367,11 @@ hooks.loadThreadDeliverySummary = async () => ({ | ||
| startCalls += 1; | ||
| assert.equal(threadId, "thread-1"); | ||
| assert.equal(text, "from mobile"); | ||
| assert.equal(options.deliveryMode, "queue"); | ||
| return { | ||
| ok: true, | ||
| mode: "codexDesktopIpcStart", | ||
| threadId, | ||
| }; | ||
| throw new Error(`idle desktop sends should use app-server first: ${threadId} ${text} ${options.deliveryMode}`); | ||
| }; | ||
| hooks.ensureAppServerThreadLoadedForDelivery = async (threadId, options) => { | ||
| ensureCalls.push({ threadId, options }); | ||
| }; | ||
| hooks.sendRequestInBackground = async (method: string, params?: JSONValue, timeoutMs?: number) => { | ||
| backgroundRequestCalls.push({ method, params, timeoutMs }); | ||
| return 42; | ||
| }; | ||
@@ -368,7 +383,31 @@ const result = await client.startTurn("thread-1", "from mobile", { | ||
| assert.equal(enqueueCalls, 0); | ||
| assert.equal(startCalls, 1); | ||
| assert.equal(startCalls, 0); | ||
| assert.deepEqual(ensureCalls, [ | ||
| { | ||
| threadId: "thread-1", | ||
| options: undefined, | ||
| }, | ||
| ]); | ||
| assert.deepEqual(backgroundRequestCalls, [ | ||
| { | ||
| method: "turn/start", | ||
| timeoutMs: 90_000, | ||
| params: { | ||
| threadId: "thread-1", | ||
| input: [ | ||
| { | ||
| type: "text", | ||
| text: "from mobile", | ||
| text_elements: [], | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| ]); | ||
| assert.deepEqual(result, { | ||
| ok: true, | ||
| mode: "codexDesktopIpcStart", | ||
| accepted: true, | ||
| mode: "appServerStartAcceptedForCodexDesktopMobileDelivery", | ||
| threadId: "thread-1", | ||
| requestId: 42, | ||
| }); | ||
@@ -424,8 +463,10 @@ }); | ||
| test("idle Codex desktop steer retries as an app-server start when route-refresh IPC has no loaded client", async () => { | ||
| test("idle Codex desktop steer uses app-server start before route-refresh IPC", async () => { | ||
| const client = new CodexAppServerClient("ws://127.0.0.1:0"); | ||
| const hooks = client as unknown as CodexClientPrivateHooks; | ||
| const requestCalls: Array<{ method: string; params?: JSONValue }> = []; | ||
| const backgroundRequestCalls: Array<{ method: string; params?: JSONValue; timeoutMs?: number }> = []; | ||
| const ensureCalls: Array<{ threadId: string; options?: { forceResume?: boolean } }> = []; | ||
| const refreshCalls: Array<{ threadId: string; reason?: string }> = []; | ||
| let desktopStartCalls = 0; | ||
| let desktopSteerCalls = 0; | ||
@@ -446,6 +487,8 @@ hooks.loadThreadDeliverySummary = async () => ({ | ||
| hooks.startTurnViaCodexDesktopIpc = async () => { | ||
| throw new Error("Codex Desktop IPC thread-follower-start-turn failed: no-client-found"); | ||
| desktopStartCalls += 1; | ||
| throw new Error("idle desktop sends should use app-server first"); | ||
| }; | ||
| hooks.steerTurnViaCodexDesktopIpc = async () => { | ||
| throw new Error("Codex Desktop IPC thread-follower-steer-turn failed: no-client-found"); | ||
| desktopSteerCalls += 1; | ||
| throw new Error("idle desktop sends should use app-server first"); | ||
| }; | ||
@@ -464,7 +507,5 @@ hooks.enqueueTurnViaCodexDesktopIpc = async () => { | ||
| hooks.waitForThreadDelivery = async () => false; | ||
| hooks.request = async (method: string, params?: JSONValue) => { | ||
| requestCalls.push({ method, params }); | ||
| return { | ||
| ok: true, | ||
| }; | ||
| hooks.sendRequestInBackground = async (method: string, params?: JSONValue, timeoutMs?: number) => { | ||
| backgroundRequestCalls.push({ method, params, timeoutMs }); | ||
| return 43; | ||
| }; | ||
@@ -478,14 +519,19 @@ | ||
| ok: true, | ||
| mode: "appServerStartAfterDesktopIpcNoClientWithDesktopRefresh", | ||
| accepted: true, | ||
| mode: "appServerStartAcceptedForCodexDesktopMobileDelivery", | ||
| threadId: "thread-1", | ||
| requestId: 43, | ||
| }); | ||
| assert.equal(desktopStartCalls, 0); | ||
| assert.equal(desktopSteerCalls, 0); | ||
| assert.deepEqual(ensureCalls, [ | ||
| { | ||
| threadId: "thread-1", | ||
| options: { forceResume: true }, | ||
| options: undefined, | ||
| }, | ||
| ]); | ||
| assert.deepEqual(requestCalls, [ | ||
| assert.deepEqual(backgroundRequestCalls, [ | ||
| { | ||
| method: "turn/start", | ||
| timeoutMs: 90_000, | ||
| params: { | ||
@@ -503,6 +549,3 @@ threadId: "thread-1", | ||
| ]); | ||
| assert.deepEqual(refreshCalls, [ | ||
| { threadId: "thread-1", reason: "desktop-ipc-start-unavailable" }, | ||
| { threadId: "thread-1", reason: "desktop-ipc-no-client" }, | ||
| ]); | ||
| assert.deepEqual(refreshCalls, []); | ||
| }); | ||
@@ -529,2 +572,3 @@ | ||
| }); | ||
| hooks.startCodexDesktopMobileDeliveryViaAppServerFirst = async () => undefined; | ||
| hooks.startTurnViaCodexDesktopIpc = async () => { | ||
@@ -584,2 +628,3 @@ throw new Error("Codex Desktop IPC thread-follower-start-turn failed: no-client-found"); | ||
| }); | ||
| hooks.startCodexDesktopMobileDeliveryViaAppServerFirst = async () => undefined; | ||
| hooks.startTurnViaCodexDesktopIpc = async () => { | ||
@@ -646,2 +691,3 @@ throw new Error("Codex Desktop IPC thread-follower-start-turn failed: no-client-found"); | ||
| }); | ||
| hooks.startCodexDesktopMobileDeliveryViaAppServerFirst = async () => undefined; | ||
| hooks.startTurnViaCodexDesktopIpc = async () => { | ||
@@ -710,2 +756,3 @@ throw new Error("Codex Desktop IPC request timed out: thread-follower-start-turn"); | ||
| }); | ||
| hooks.startCodexDesktopMobileDeliveryViaAppServerFirst = async () => undefined; | ||
| hooks.startTurnViaCodexDesktopIpc = async () => { | ||
@@ -766,2 +813,3 @@ throw new Error("Codex Desktop IPC request timed out: thread-follower-start-turn"); | ||
| }); | ||
| hooks.startCodexDesktopMobileDeliveryViaAppServerFirst = async () => undefined; | ||
| hooks.startTurnViaCodexDesktopIpc = async () => { | ||
@@ -959,10 +1007,8 @@ throw new Error("Codex Desktop IPC request timed out: thread-follower-start-turn"); | ||
| test("running Codex desktop steer falls back to app-server steer when desktop IPC has no loaded client", async () => { | ||
| test("running Codex desktop steer acknowledges after background app-server steer handoff", async () => { | ||
| const client = new CodexAppServerClient("ws://127.0.0.1:0"); | ||
| const hooks = client as unknown as CodexClientPrivateHooks; | ||
| const appServerSteerCalls: Array<{ | ||
| threadId: string; | ||
| text: string; | ||
| activeTurnId: string | null; | ||
| }> = []; | ||
| const backgroundRequestCalls: Array<{ method: string; params?: JSONValue; timeoutMs?: number }> = []; | ||
| let desktopSteerCalls = 0; | ||
| let desktopStartCalls = 0; | ||
@@ -982,5 +1028,7 @@ hooks.loadThreadDeliverySummary = async () => ({ | ||
| hooks.steerTurnViaCodexDesktopIpc = async () => { | ||
| desktopSteerCalls += 1; | ||
| throw new Error("Codex Desktop IPC thread-follower-steer-turn failed: no-client-found"); | ||
| }; | ||
| hooks.startTurnViaCodexDesktopIpc = async () => { | ||
| desktopStartCalls += 1; | ||
| throw new Error("running steer must not start a side turn"); | ||
@@ -992,13 +1040,5 @@ }; | ||
| }; | ||
| hooks.startTurnViaAppServerSteer = async (threadId, text, baseline) => { | ||
| appServerSteerCalls.push({ | ||
| threadId, | ||
| text, | ||
| activeTurnId: baseline.activeTurnId, | ||
| }); | ||
| return { | ||
| ok: true, | ||
| mode: "appServerSteerQueued", | ||
| threadId, | ||
| }; | ||
| hooks.sendRequestInBackground = async (method: string, params?: JSONValue, timeoutMs?: number) => { | ||
| backgroundRequestCalls.push({ method, params, timeoutMs }); | ||
| return 44; | ||
| }; | ||
@@ -1012,10 +1052,24 @@ | ||
| ok: true, | ||
| mode: "appServerSteerQueued", | ||
| accepted: true, | ||
| mode: "appServerSteerAcceptedForCodexDesktopMobileDelivery", | ||
| threadId: "thread-1", | ||
| requestId: 44, | ||
| }); | ||
| assert.deepEqual(appServerSteerCalls, [ | ||
| assert.equal(desktopSteerCalls, 0); | ||
| assert.equal(desktopStartCalls, 0); | ||
| assert.deepEqual(backgroundRequestCalls, [ | ||
| { | ||
| threadId: "thread-1", | ||
| text: "testing mobile", | ||
| activeTurnId: "turn-1", | ||
| method: "turn/steer", | ||
| timeoutMs: 90_000, | ||
| params: { | ||
| threadId: "thread-1", | ||
| input: [ | ||
| { | ||
| type: "text", | ||
| text: "testing mobile", | ||
| text_elements: [], | ||
| }, | ||
| ], | ||
| expectedTurnId: "turn-1", | ||
| }, | ||
| }, | ||
@@ -1022,0 +1076,0 @@ ]); |
@@ -9,3 +9,3 @@ import { dirname, resolve } from "node:path"; | ||
| const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); | ||
| const server = new BridgeServer(); | ||
| const server = new BridgeServer({ rootDir }); | ||
| await server.start(); | ||
@@ -12,0 +12,0 @@ startBridgeAutoUpdater({ rootDir, logger: console }); |
+1
-1
| { | ||
| "name": "@devlln/helm", | ||
| "version": "0.2.1", | ||
| "version": "0.3.0", | ||
| "private": false, | ||
@@ -5,0 +5,0 @@ "description": "Helm CLI bridge installer and runtime helpers.", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Explicitly Unlicensed Item
LicenseSomething was found which is explicitly marked as unlicensed.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
916918
1.46%23749
1.62%63
-1.56%