Skip to content

Commit adfca7f

Browse files
Add automatic updates for imandrax-cli (#82)
1 parent e42a913 commit adfca7f

6 files changed

Lines changed: 211 additions & 13 deletions

File tree

package-lock.json

Lines changed: 94 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,31 @@
66
"publisher": "Imandra",
77
"icon": "assets/marketplace.png",
88
"galleryBanner": {
9-
"color": "#3B3F49",
9+
"color": "#221B32",
1010
"theme": "dark"
1111
},
1212
"license": "MIT",
13-
"version": "0.0.50",
13+
"version": "0.0.51",
1414
"repository": {
1515
"type": "git",
1616
"url": "https://github.com/imandra-ai/imandrax-vscode"
1717
},
18-
"categories": [],
18+
"categories": [
19+
"Programming Languages",
20+
"AI",
21+
"Visualization",
22+
"Other"
23+
],
24+
"keywords": [
25+
"formalization",
26+
"formal",
27+
"reasoning",
28+
"verification",
29+
"testing",
30+
"AI",
31+
"safety",
32+
"neurosymbolic"
33+
],
1934
"engines": {
2035
"vscode": "^1.96.2"
2136
},
@@ -216,6 +231,7 @@
216231
"@vscode/vsce": "^3.7.1",
217232
"async-mutex": "^0.5.0",
218233
"jest": "^30.2.0",
234+
"node-fetch": "^3.3.2",
219235
"ovsx": "^0.10.8",
220236
"prettier": "^3.7.4",
221237
"vscode-languageclient": "^8.1.0"

src/extension.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export async function activate(context: ExtensionContext) {
5656
const openUri = Uri.parse(
5757
`command:workbench.action.openWorkspaceSettingsFile?${encodeURIComponent(JSON.stringify(args))}`
5858
);
59-
await installer.promptToInstall(openUri);
59+
await installer.promptToInstall(openUri, false);
6060
} else if (languageClientConfig.binPathAvailability.status === "onWindows") {
6161
const item = { title: "Go to docs" };
6262
const itemT = await window.showErrorMessage(`ImandraX can't run natively on Windows. Please start a remote VSCode session against WSL`, item);
@@ -66,5 +66,15 @@ export async function activate(context: ExtensionContext) {
6666
}
6767
if (context.extensionMode === ExtensionMode.Test || context.extensionMode === undefined) {
6868
(global as any).testExtensionContext = context;
69+
} else {
70+
if (await installer.installedByUs()) {
71+
if (await installer.updateAvailable()) {
72+
const args = { revealSetting: { key: "imandrax.lsp.binary", edit: true } };
73+
const openUri = Uri.parse(
74+
`command:workbench.action.openWorkspaceSettingsFile?${encodeURIComponent(JSON.stringify(args))}`
75+
);
76+
await installer.promptToInstall(openUri, true);
77+
}
78+
}
6979
}
7080
}

src/installer.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import * as Which from "which";
66
import { commands, ConfigurationTarget, env, MessageItem, ProgressLocation, QuickPickItem, QuickPickOptions, Uri, window, workspace } from "vscode";
77
import { exec } from 'child_process';
88

9+
import { stat, writeFile, utimes } from 'fs/promises';
10+
11+
import fetch, { Response } from 'node-fetch';
12+
13+
914
async function getApiKeyInput() {
1015
const result = await window.showInputBox({
1116
placeHolder: 'Paste your API key here',
@@ -82,10 +87,33 @@ async function setBinaryPaths(openUri: Uri) {
8287
await config.update('terminal.binary', binaryPath, ConfigurationTarget.Global);
8388
}
8489

90+
async function markerFilename(): Promise<string> {
91+
const config = workspace.getConfiguration('imandrax');
92+
const binaryPath = await config.get('lsp.binary');
93+
const binaryDir = Path.dirname(binaryPath as string);
94+
return Path.join(binaryDir, 'imandrax-cli.installed_by_vscode');
95+
}
96+
97+
async function markInstalled(): Promise<void> {
98+
const markerFile = await markerFilename();
99+
await writeFile(markerFile, '');
100+
await utimes(markerFile, Date.now(), Date.now());
101+
}
102+
103+
export async function installedByUs(): Promise<boolean> {
104+
try {
105+
await stat(await markerFilename());
106+
return true;
107+
} catch {
108+
return false;
109+
}
110+
}
111+
85112
async function handleSuccess(openUri: Uri) {
86113
await setBinaryPaths(openUri);
87114
await promptForApiKey();
88115
await promptToReloadWindow();
116+
await markInstalled();
89117
}
90118

91119
async function runInstallerForUnix(itemT: MessageItem, title: string): Promise<void> {
@@ -127,12 +155,15 @@ async function runInstallerForUnix(itemT: MessageItem, title: string): Promise<v
127155
}
128156
}
129157

130-
export async function promptToInstall(openUri: Uri) {
158+
export async function promptToInstall(openUri: Uri, update?: boolean) {
131159
const launchInstallerItem = { title: "Launch installer" } as const;
132160
const items: readonly MessageItem[] = [launchInstallerItem];
133-
134-
const itemT: MessageItem | undefined = await window.showErrorMessage(`Could not find ImandraX. Please install it or ensure the imandrax-cli binary is in your PATH or its location is set in [Workspace Settings](${openUri.toString()}).`, ...items);
135-
161+
let itemT: MessageItem | undefined;
162+
if (update) {
163+
itemT = await window.showInformationMessage(`An updated imandrax-cli binary is available.`, ...items);
164+
} else {
165+
itemT = await window.showErrorMessage(`Could not find ImandraX. Please install it or ensure the imandrax-cli binary is in your PATH or its location is set in [Workspace Settings](${openUri.toString()}).`, ...items);
166+
}
136167
if (itemT) {
137168
await window.withProgress(
138169
{
@@ -141,7 +172,56 @@ export async function promptToInstall(openUri: Uri) {
141172
},
142173
() => runInstallerForUnix(itemT, launchInstallerItem.title)).then(
143174
() => handleSuccess(openUri),
144-
async (reason) => { await window.showErrorMessage(`ImandraX install failed\n ${reason}`); }
175+
async (reason) => { await window.showErrorMessage(`ImandraX installation failed\n ${reason}`); }
145176
);
146177
}
147178
}
179+
180+
interface ReleaseObject {
181+
name: string;
182+
generation: string; // not guaranteed to increase
183+
meta_generation: string; // guaranteed to increase, but not a time
184+
timeCreated: string;
185+
timeFinalized: string;
186+
timeStorageClassUpdated: string;
187+
updated: string
188+
}
189+
190+
export async function updateAvailable() {
191+
try {
192+
async function getFileModificationDate(filePath: string): Promise<Date> {
193+
const stats = await stat(filePath);
194+
return stats.mtime;
195+
}
196+
197+
async function getData(pkg_name: string): Promise<ReleaseObject> {
198+
const url = `https://storage.googleapis.com/storage/v1/b/imandra-prod-imandrax-releases/o/${pkg_name}`;
199+
const response: Response = await fetch(url);
200+
if (!response.ok) {
201+
throw new Error(`Response status: ${response.status}`);
202+
}
203+
return await response.json() as ReleaseObject;
204+
}
205+
206+
let pkg_name = "";
207+
if (process.platform == "linux" && process.arch == "x64")
208+
pkg_name = "imandrax-linux-x86_64-latest.tar.gz";
209+
else if (process.platform == "darwin" && process.arch == "arm64")
210+
pkg_name = "imandrax-macos-aarch64-latest.pkg";
211+
else if (process.platform == "darwin" && process.arch == "x64")
212+
pkg_name = "imandrax-macos-x64-latest.pkg";
213+
else {
214+
throw new Error(`Unsupported platform/architecture ${process.platform}-${process.arch}`);
215+
}
216+
217+
const data: ReleaseObject = await getData(pkg_name);
218+
const marker: string = await markerFilename();
219+
const remoteTime: number = Date.parse(data.updated)
220+
const markerTime: number | undefined = (await getFileModificationDate(marker))?.getTime();
221+
222+
return markerTime && remoteTime > markerTime;
223+
} catch (e) {
224+
// Don't annoy the user with a showErrorMessage about our bugs or network outages, just log them.
225+
console.log(`Update check failed: ${(e as Error).message}.`);
226+
}
227+
}

src/listeners.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class Listeners {
4545
const all_bad: DecorationOptions[] = [];
4646
const diags = languages.getDiagnostics(uri);
4747
diags.forEach(d => {
48-
if (d.source === "lsp") {
48+
if (d.source == "lsp" || d.source === "ImandraX") {
4949
const good = d.severity === DiagnosticSeverity.Information || d.severity === DiagnosticSeverity.Hint;
5050
const range = d.range.with(d.range.start, d.range.start);
5151
const decoration_options: DecorationOptions = { range: range };

src/test/commands.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ suite('Commands Test Suite', () => {
125125
resolveSawDiagnostic(true);
126126
});
127127
}
128-
// We received some diagnostics, but they were not for us
129128
else
129+
// We received some diagnostics, but they were not for us
130130
resolveSawDiagnostic(false);
131131
}
132132
}

0 commit comments

Comments
 (0)