import * as childProcess from "child_process"; import { commands, env, extensions, QuickPickItemKind, ShellExecution, TaskGroup, TaskPanelKind, TaskRevealKind, tasks, TaskScope, ThemeIcon, Uri, window } from "vscode"; import { Act } from "./act"; import { ConfigurationManager, Platform, Section } from "./configurationManager"; import { act, componentsTreeDataProvider } from "./extension"; import ComponentsTreeDataProvider from "./views/components/componentsTreeDataProvider"; export interface Component { name: string, icon: string, version?: string, path?: string, status: T, required: boolean, information: string, installation: () => Promise, start?: () => Promise, message?: string } export enum CliStatus { Installed = 'Installed', NotInstalled = 'Not Installed', Running = 'Running', NotRunning = 'Not Running' } export enum ExtensionStatus { Activated = 'Activated', NotActivated = 'Not Activated' } export class ComponentsManager { async getComponents(): Promise[]> { const components: Component[] = []; const actCliInfo = await this.getCliInfo(`${Act.getActCommand()} --version`, /act version (.+)/, false, false); components.push({ name: 'nektos/act', icon: 'terminal', version: actCliInfo.version, status: actCliInfo.status, required: true, information: 'https://github.com/nektos/act', installation: async () => { const installationMethods: any[] = [ { label: 'Software Package Managers', kind: QuickPickItemKind.Separator } ]; Object.entries(act.installationCommands).map(([packageManager, command]) => { installationMethods.push({ label: packageManager, description: command, iconPath: new ThemeIcon('terminal'), }); }); installationMethods.push( { label: 'Pre-built Artifacts', kind: QuickPickItemKind.Separator }, { label: 'Install Pre-built Executable', description: 'Install pre-built executable', iconPath: new ThemeIcon('package') }, { label: 'Bash Script Installation', description: 'Install pre-built act executable using bash script', iconPath: new ThemeIcon('code'), link: 'https://nektosact.com/installation/index.html#bash-script' }, { label: 'Build From Source', description: 'Build nektos/act yourself', iconPath: new ThemeIcon('tools'), link: 'https://nektosact.com/installation/index.html#build-from-source' } ); const selectedInstallationMethod = await window.showQuickPick(installationMethods, { title: 'Select the method of installation', placeHolder: 'Installation Method' }); if (selectedInstallationMethod) { if (selectedInstallationMethod.label === 'Install Pre-built Executable') { const prebuiltExecutables = Object.entries(act.prebuiltExecutables).map(([architecture, link]) => { return { label: architecture, iconPath: new ThemeIcon('package'), link: link }; }); const selectedPrebuiltExecutable = await window.showQuickPick(prebuiltExecutables, { title: 'Select the prebuilt executable to download', placeHolder: 'Prebuilt executable' }); if (selectedPrebuiltExecutable) { await env.openExternal(Uri.parse(selectedPrebuiltExecutable.link)); window.showInformationMessage('Unpack the executable and move it to your desired location. Once nektos/act is successfully installed, add it to your shell\'s PATH and then refresh the components view.', 'Refresh').then(async value => { if (value === 'Refresh') { componentsTreeDataProvider.refresh(); } }); } } else if (selectedInstallationMethod.link) { await env.openExternal(Uri.parse(selectedInstallationMethod.link)); window.showInformationMessage('Once nektos/act is successfully installed, add it to your shell\'s PATH and then refresh the components view.', 'Refresh').then(async value => { if (value === 'Refresh') { componentsTreeDataProvider.refresh(); } }); } else { await act.install(selectedInstallationMethod.label); } } } }); const dockerCliInfo = await this.getCliInfo('docker version', /Client:\n.+\n\sVersion:\s+(.+)/, true, true); const dockerDesktopPath = ConfigurationManager.get(Section.dockerDesktopPath); components.push({ name: 'Docker Engine', icon: 'dashboard', version: dockerCliInfo.version, path: dockerDesktopPath, status: dockerCliInfo.status, required: true, information: 'https://docs.docker.com/engine', installation: async () => { await env.openExternal(Uri.parse('https://docs.docker.com/engine/install')); }, start: async () => { const dockerDesktopPath = ConfigurationManager.get(Section.dockerDesktopPath); if (dockerDesktopPath) { await env.openExternal(Uri.parse(dockerDesktopPath)); } else { if (process.platform === Platform.linux) { await tasks.executeTask({ name: 'Docker Engine', detail: 'Start Docker Engine', definition: { type: 'GitHub Local Actions' }, source: 'GitHub Local Actions', scope: TaskScope.Workspace, isBackground: true, presentationOptions: { reveal: TaskRevealKind.Always, focus: false, clear: true, close: false, echo: true, panel: TaskPanelKind.Shared, showReuseMessage: false }, problemMatchers: [], runOptions: {}, group: TaskGroup.Build, execution: new ShellExecution('sudo dockerd', { executable: env.shell }) }); } else { window.showErrorMessage(`Invalid environment: ${process.platform}`, 'Report an Issue').then(async value => { if (value === 'Report an Issue') { await commands.executeCommand('githubLocalActions.reportAnIssue'); } }); return; } } window.withProgress({ location: { viewId: ComponentsTreeDataProvider.VIEW_ID } }, async () => { // Delay 4 seconds for Docker Desktop to be started const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); await delay(4000); // Check again for docker status const newDockerCliInfo = await this.getCliInfo('docker version', /Client:\n.+\n\sVersion:\s+(.+)/, true, true); if (dockerCliInfo.status !== newDockerCliInfo.status) { componentsTreeDataProvider.refresh(); } else { const verificationMessage = process.platform === Platform.linux ? 'If it failed to start, start it manually.' : 'If it failed to start, configure your Docker Desktop path or start it manually.'; const options = process.platform === Platform.linux ? ['Refresh'] : ['Refresh', 'Configure Docker Desktop Path']; window.showInformationMessage(`Once Docker Engine is successfully started, refresh the components view. ${verificationMessage}`, ...options).then(async value => { if (value === 'Refresh') { componentsTreeDataProvider.refresh(); } else if (value === 'Configure Docker Desktop Path') { await commands.executeCommand('workbench.action.openSettings', ConfigurationManager.getSearchTerm(Section.dockerDesktopPath)); } }); } }); } }); // const githubActionsInfo = await this.getExtensionInfo('github.vscode-github-actions'); // components.push({ // name: 'GitHub Actions Extension', // icon: 'extensions', // version: githubActionsInfo.version, // status: githubActionsInfo.status, // required: false, // information: 'https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions', // installation: async () => { // await env.openExternal(Uri.parse('https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-github-actions')); // }, // message: 'GitHub Actions extension is not required, but is recommended to take advantage of workflow editor features.' // }); // const githubCliInfo = await this.getCliInfo('gh', /gh version (.+)/, false, false); // components.push({ // name: 'GitHub CLI', // icon: 'terminal', // version: githubCliInfo.version, // status: githubCliInfo.status, // required: false, // information: 'https://cli.github.com', // installation: async () => { // await env.openExternal(Uri.parse('https://cli.github.com')); // }, // message: 'GitHub CLI is not required, but is recommended if you plan to use it to retrieve GitHub tokens.' // }); return components; } async getUnreadyComponents(): Promise[]> { const components = await this.getComponents(); return components.filter(component => component.required && [CliStatus.NotInstalled, CliStatus.NotRunning, ExtensionStatus.NotActivated].includes(component.status)); } async getCliInfo(command: string, versionRegex: RegExp, ignoreError: boolean, checksIfRunning: boolean): Promise<{ version?: string, status: CliStatus }> { return new Promise<{ version?: string, status: CliStatus }>((resolve, reject) => { childProcess.exec(command, (error, stdout, stderr) => { const version = stdout?.match(versionRegex); if (error) { if (ignoreError && version) { resolve({ version: version[1], status: CliStatus.NotRunning }); } else { resolve({ status: CliStatus.NotInstalled }); } } else { if (checksIfRunning) { resolve({ version: version ? version[1] : undefined, status: CliStatus.Running }); } else { resolve({ version: version ? version[1] : undefined, status: CliStatus.Installed }); } } }); }); } async getExtensionInfo(extensionId: string): Promise<{ version?: string, status: ExtensionStatus }> { const allExtensions = extensions.all; const extension = allExtensions.find(extension => extension.id === extensionId); if (extension) { if (!extension.isActive) { await extension.activate(); } return { status: ExtensionStatus.Activated, version: extension.packageJSON.version }; } else { return { status: ExtensionStatus.NotActivated }; } } }