Skip to content

Commit e32abc5

Browse files
committed
feat: do installation/version checks on startup
1 parent 5e2de16 commit e32abc5

File tree

5 files changed

+494
-9
lines changed

5 files changed

+494
-9
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,5 +251,10 @@
251251
"glob": "^8.1.0",
252252
"mocha": "^10.2.0",
253253
"typescript": "^4.9.5"
254+
},
255+
"dependencies": {
256+
"@octokit/types": "^9.0.0",
257+
"octokit": "^2.0.14",
258+
"semver": "^7.3.8"
254259
}
255260
}

src/services/taskfile.ts

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@ import * as vscode from 'vscode';
33
import * as models from '../models';
44
import * as path from 'path';
55
import * as fs from 'fs';
6+
import * as semver from 'semver';
7+
import { Octokit } from 'octokit';
8+
import { Endpoints } from "@octokit/types";
9+
10+
const octokit = new Octokit();
11+
type ReleaseRequest = Endpoints["GET /repos/{owner}/{repo}/releases/latest"]["parameters"];
12+
type ReleaseResponse = Endpoints["GET /repos/{owner}/{repo}/releases/latest"]["response"];
13+
14+
const taskCommand = 'task';
15+
const minimumRequiredVersion = '3.19.1';
16+
const minimumRecommendedVersion = '3.23.0';
617

718
class TaskfileService {
819
private static _instance: TaskfileService;
920
private static outputChannel: vscode.OutputChannel;
10-
private static readonly taskCommand = 'task';
1121
private lastTaskName: string | undefined;
1222
private lastTaskDir: string | undefined;
1323

@@ -19,9 +29,87 @@ class TaskfileService {
1929
return this._instance ?? (this._instance = new this());
2030
}
2131

32+
public async checkInstallation(): Promise<void> {
33+
return await new Promise((resolve) => {
34+
cp.exec(`${taskCommand} --version`, (_, stdout: string, stderr: string) => {
35+
36+
// If the version is a devel version, ignore all version checks
37+
if (stdout.includes("devel")) {
38+
return resolve();
39+
}
40+
41+
// Get the installed version of task (if any)
42+
let version = this.parseVersion(stdout);
43+
44+
// If there is an error fetching the version, assume task is not installed
45+
if (stderr !== "" || version === undefined) {
46+
vscode.window.showErrorMessage("Task command not found.", "Install").then(this.buttonCallback);
47+
return resolve();
48+
}
49+
50+
// If the current version is older than the minimum required version, show an error
51+
if (version && version.compare(minimumRequiredVersion) < 0) {
52+
vscode.window.showErrorMessage(`Task v${minimumRequiredVersion} is required to run this extension. Your current version is v${version}.`, "Update").then(this.buttonCallback);
53+
return resolve();
54+
}
55+
56+
// If the current version is older than the minimum recommended version, show a warning
57+
if (version && version.compare(minimumRecommendedVersion) < 0) {
58+
vscode.window.showWarningMessage(`Task v${minimumRecommendedVersion} is recommended to run this extension. Your current version is v${version} which doesn't support some features.`, "Update").then(this.buttonCallback);
59+
return resolve();
60+
}
61+
62+
// If a newer version is available, show a message
63+
// TODO: what happens if the user is offline?
64+
this.getLatestVersion().then((latestVersion) => {
65+
if (version && latestVersion && version.compare(latestVersion) < 0) {
66+
vscode.window.showInformationMessage(`A new version of Task is available. Current version: v${version}, Latest version: v${latestVersion}`, "Update").then(this.buttonCallback);
67+
}
68+
return resolve();
69+
}).catch((err) => {
70+
console.error(err);
71+
return resolve();
72+
});
73+
});
74+
});
75+
}
76+
77+
buttonCallback(value: string | undefined) {
78+
if (value === undefined) {
79+
return;
80+
}
81+
if (["Update", "Install"].includes(value)) {
82+
vscode.env.openExternal(vscode.Uri.parse("https://taskfile.dev/installation"));
83+
return;
84+
}
85+
}
86+
87+
async getLatestVersion(): Promise<semver.SemVer | null> {
88+
let request: ReleaseRequest = {
89+
owner: 'go-task',
90+
repo: 'task'
91+
};
92+
let response: ReleaseResponse = await octokit.rest.repos.getLatestRelease(request);
93+
return Promise.resolve(semver.parse(response.data.tag_name));
94+
}
95+
96+
parseVersion(stdout: string): semver.SemVer | undefined {
97+
// Extract the version string from the output
98+
let matches = stdout.match(/v(\d+\.\d+\.\d+)/);
99+
if (!matches || matches.length !== 2) {
100+
return undefined;
101+
}
102+
// Parse the version string as a semver
103+
let version = semver.parse(matches[1]);
104+
if (!version) {
105+
return undefined;
106+
}
107+
return version;
108+
}
109+
22110
public async init(dir: string): Promise<void> {
23111
return await new Promise((resolve) => {
24-
let command = 'task --init';
112+
let command = `${taskCommand} --init`;
25113
cp.exec(command, { cwd: dir }, (_, stdout: string, stderr: string) => {
26114
if (stderr) {
27115
vscode.window.showErrorMessage(stderr);
@@ -38,7 +126,6 @@ class TaskfileService {
38126
for (let i = 0; i < filenames.length; i++) {
39127
let filename = path.join(dir, filenames[i]);
40128
if (fs.existsSync(filename)) {
41-
console.log(filename);
42129
await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(filename), { preview: false });
43130
return;
44131
}
@@ -47,7 +134,7 @@ class TaskfileService {
47134

48135
public async read(dir: string): Promise<models.Taskfile> {
49136
return await new Promise((resolve) => {
50-
let command = 'task --list-all --json';
137+
let command = `${taskCommand} --list-all --json`;
51138
cp.exec(command, { cwd: dir }, (_, stdout: string) => {
52139
var taskfile: models.Taskfile = JSON.parse(stdout);
53140
taskfile.workspace = dir;
@@ -67,7 +154,7 @@ class TaskfileService {
67154
public async runTask(taskName: string, dir?: string): Promise<void> {
68155
return await new Promise((resolve) => {
69156
// Spawn a child process
70-
let child = cp.spawn(TaskfileService.taskCommand, [taskName], { cwd: dir });
157+
let child = cp.spawn(taskCommand, [taskName], { cwd: dir });
71158

72159
// Clear the output channel and show it
73160
TaskfileService.outputChannel.clear();
@@ -97,7 +184,7 @@ class TaskfileService {
97184

98185
public async goToDefinition(task: models.Task, preview: boolean = false): Promise<void> {
99186
if (task.location === undefined) {
100-
vscode.window.showErrorMessage(`Go to definition requires Task v3.23.0 or higher.`);
187+
vscode.window.showErrorMessage(`Go to definition requires Task v3.23.0 or higher.`, "Update").then(this.buttonCallback);
101188
return;
102189
}
103190

src/task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class TaskExtension {
1414
this._settings = new Settings();
1515
this._activityBar = new elements.ActivityBar();
1616
this._watcher = vscode.workspace.createFileSystemWatcher("**/*.{yml,yaml}");
17+
services.taskfile.checkInstallation();
1718
this.setTreeNesting(this._settings.treeNesting);
1819
}
1920

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"target": "ES2020",
55
"outDir": "out",
66
"lib": [
7-
"ES2020"
7+
"ES2020",
8+
"dom"
89
],
910
"sourceMap": true,
1011
"rootDir": "src",

0 commit comments

Comments
 (0)