From 97c453ba40d53edd42437205ff0b12f5e279aeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Fri, 5 Sep 2025 12:52:01 +0200 Subject: [PATCH 1/4] feat(status-bar): improve logic that shows "start", "stop" and "setup wizard" commands --- src/plugins/logs.ts | 8 +++++ src/plugins/manage.ts | 6 ---- src/plugins/status-bar.ts | 65 ++++++++++++++++----------------------- 3 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/plugins/logs.ts b/src/plugins/logs.ts index e7c1671..8b1faae 100644 --- a/src/plugins/logs.ts +++ b/src/plugins/logs.ts @@ -1,12 +1,20 @@ import { spawn } from "node:child_process"; import type { ChildProcess } from "node:child_process"; +import { commands } from "vscode"; + import { createPlugin } from "../plugins.ts"; import { pipeToLogOutputChannel } from "../utils/spawn.ts"; export default createPlugin( "logs", ({ context, outputChannel, containerStatusTracker }) => { + context.subscriptions.push( + commands.registerCommand("localstack.viewLogs", () => { + outputChannel.show(true); + }), + ); + let logsProcess: ChildProcess | undefined; const startLogging = () => { diff --git a/src/plugins/manage.ts b/src/plugins/manage.ts index 0f02549..126cd74 100644 --- a/src/plugins/manage.ts +++ b/src/plugins/manage.ts @@ -10,12 +10,6 @@ import { export default createPlugin( "manage", ({ context, outputChannel, telemetry, localStackStatusTracker }) => { - context.subscriptions.push( - commands.registerCommand("localstack.viewLogs", () => { - outputChannel.show(true); - }), - ); - context.subscriptions.push( commands.registerCommand("localstack.start", async () => { if (localStackStatusTracker.status() !== "stopped") { diff --git a/src/plugins/status-bar.ts b/src/plugins/status-bar.ts index 16d9332..d7fc4f8 100644 --- a/src/plugins/status-bar.ts +++ b/src/plugins/status-bar.ts @@ -2,13 +2,19 @@ import { commands, QuickPickItemKind, ThemeColor, window } from "vscode"; import type { QuickPickItem } from "vscode"; import { createPlugin } from "../plugins.ts"; -import { checkIsProfileConfigured } from "../utils/configure-aws.ts"; export default createPlugin( "status-bar", ({ context, statusBarItem, localStackStatusTracker, setupStatusTracker }) => { context.subscriptions.push( commands.registerCommand("localstack.showCommands", async () => { + const shouldShowLocalStackStart = () => + localStackStatusTracker.status() === "stopped"; + const shouldShowLocalStackStop = () => + localStackStatusTracker.status() === "running"; + const shouldShowRunSetupWizard = () => + setupStatusTracker.status() === "setup_required"; + const getCommands = async () => { const commands: (QuickPickItem & { command: string })[] = []; commands.push({ @@ -16,54 +22,37 @@ export default createPlugin( command: "", kind: QuickPickItemKind.Separator, }); - const setupStatus = setupStatusTracker.status(); - - if (setupStatus === "ok") { - if (localStackStatusTracker.status() === "stopped") { - commands.push({ - label: "Start LocalStack", - command: "localstack.start", - }); - } else { - commands.push({ - label: "Stop LocalStack", - command: "localstack.stop", - }); - } + + if (shouldShowLocalStackStart()) { + commands.push({ + label: "Start LocalStack", + command: "localstack.start", + }); + } + + if (shouldShowLocalStackStop()) { + commands.push({ + label: "Stop LocalStack", + command: "localstack.stop", + }); } + commands.push({ + label: "View Logs", + command: "localstack.viewLogs", + }); + commands.push({ label: "Configure", command: "", kind: QuickPickItemKind.Separator, }); - if (setupStatus === "setup_required") { + if (shouldShowRunSetupWizard()) { commands.push({ label: "Run LocalStack setup Wizard", command: "localstack.setup", }); - - // show start command if stopped or stop command when running, even if setup_required (in authentication, or profile) - if (localStackStatusTracker.status() === "stopped") { - commands.push({ - label: "Start LocalStack", - command: "localstack.start", - }); - } else if (localStackStatusTracker.status() === "running") { - commands.push({ - label: "Stop LocalStack", - command: "localstack.stop", - }); - } - } - - const isProfileConfigured = await checkIsProfileConfigured(); - if (!isProfileConfigured) { - commands.push({ - label: "Configure AWS Profiles", - command: "localstack.configureAwsProfiles", - }); } return commands; @@ -74,7 +63,7 @@ export default createPlugin( }); if (selected) { - commands.executeCommand(selected.command); + void commands.executeCommand(selected.command); } }), ); From 505f11ae05416bc346e38f600d4183b26348da6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Fri, 5 Sep 2025 14:37:42 +0200 Subject: [PATCH 2/4] wip --- src/plugins/status-bar.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/plugins/status-bar.ts b/src/plugins/status-bar.ts index d7fc4f8..c806dda 100644 --- a/src/plugins/status-bar.ts +++ b/src/plugins/status-bar.ts @@ -2,15 +2,24 @@ import { commands, QuickPickItemKind, ThemeColor, window } from "vscode"; import type { QuickPickItem } from "vscode"; import { createPlugin } from "../plugins.ts"; +import { checkLocalstackInstalled } from "../utils/install.ts"; export default createPlugin( "status-bar", - ({ context, statusBarItem, localStackStatusTracker, setupStatusTracker }) => { + ({ + context, + statusBarItem, + localStackStatusTracker, + setupStatusTracker, + outputChannel, + }) => { context.subscriptions.push( commands.registerCommand("localstack.showCommands", async () => { - const shouldShowLocalStackStart = () => + const shouldShowLocalStackStart = async () => + (await checkLocalstackInstalled(outputChannel)) && localStackStatusTracker.status() === "stopped"; - const shouldShowLocalStackStop = () => + const shouldShowLocalStackStop = async () => + (await checkLocalstackInstalled(outputChannel)) && localStackStatusTracker.status() === "running"; const shouldShowRunSetupWizard = () => setupStatusTracker.status() === "setup_required"; @@ -23,14 +32,14 @@ export default createPlugin( kind: QuickPickItemKind.Separator, }); - if (shouldShowLocalStackStart()) { + if (await shouldShowLocalStackStart()) { commands.push({ label: "Start LocalStack", command: "localstack.start", }); } - if (shouldShowLocalStackStop()) { + if (await shouldShowLocalStackStop()) { commands.push({ label: "Stop LocalStack", command: "localstack.stop", @@ -70,6 +79,7 @@ export default createPlugin( context.subscriptions.push( commands.registerCommand("localstack.refreshStatusBar", () => { + // TODO const setupStatus = setupStatusTracker.status(); if (setupStatus === "setup_required") { From 3b7a2b67112103c2231363230d8ea0e8a1b1e6d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Fri, 5 Sep 2025 16:07:48 +0200 Subject: [PATCH 3/4] feat(status-bar): improve status bar display and actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The status bar will always show the LS instance status (running, stopping, etc) if the LS CLI is installed — it doesn’t need to have the full setup complete - The status bar commands will always allow starting and stopping LS if the LS CLI is installed — it doesn’t need to have the full setup complete - The status bar will be red if any part of the setup is incomplete --- src/plugins/status-bar.ts | 67 ++++++++++++++++++++------------------- src/utils/promises.ts | 5 +++ src/utils/setup-status.ts | 16 ++++++++-- src/utils/setup.ts | 13 ++++---- 4 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/plugins/status-bar.ts b/src/plugins/status-bar.ts index c806dda..a7f881b 100644 --- a/src/plugins/status-bar.ts +++ b/src/plugins/status-bar.ts @@ -15,16 +15,16 @@ export default createPlugin( }) => { context.subscriptions.push( commands.registerCommand("localstack.showCommands", async () => { - const shouldShowLocalStackStart = async () => - (await checkLocalstackInstalled(outputChannel)) && + const shouldShowLocalStackStart = () => + setupStatusTracker.statuses().isInstalled && localStackStatusTracker.status() === "stopped"; - const shouldShowLocalStackStop = async () => - (await checkLocalstackInstalled(outputChannel)) && + const shouldShowLocalStackStop = () => + setupStatusTracker.statuses().isInstalled && localStackStatusTracker.status() === "running"; const shouldShowRunSetupWizard = () => setupStatusTracker.status() === "setup_required"; - const getCommands = async () => { + const getCommands = () => { const commands: (QuickPickItem & { command: string })[] = []; commands.push({ label: "Manage", @@ -32,14 +32,14 @@ export default createPlugin( kind: QuickPickItemKind.Separator, }); - if (await shouldShowLocalStackStart()) { + if (shouldShowLocalStackStart()) { commands.push({ label: "Start LocalStack", command: "localstack.start", }); } - if (await shouldShowLocalStackStop()) { + if (shouldShowLocalStackStop()) { commands.push({ label: "Stop LocalStack", command: "localstack.stop", @@ -59,7 +59,7 @@ export default createPlugin( if (shouldShowRunSetupWizard()) { commands.push({ - label: "Run LocalStack setup Wizard", + label: "Run LocalStack Setup Wizard", command: "localstack.setup", }); } @@ -81,30 +81,30 @@ export default createPlugin( commands.registerCommand("localstack.refreshStatusBar", () => { // TODO const setupStatus = setupStatusTracker.status(); - - if (setupStatus === "setup_required") { - statusBarItem.command = "localstack.showCommands"; - statusBarItem.text = "$(error) LocalStack"; - statusBarItem.backgroundColor = new ThemeColor( - "statusBarItem.errorBackground", - ); - } else { - statusBarItem.command = "localstack.showCommands"; - statusBarItem.backgroundColor = undefined; - const localStackStatus = localStackStatusTracker.status(); - if ( - localStackStatus === "starting" || - localStackStatus === "stopping" - ) { - statusBarItem.text = `$(sync~spin) LocalStack (${localStackStatus})`; - } else if ( - localStackStatus === "running" || - localStackStatus === "stopped" - ) { - statusBarItem.text = `$(localstack-logo) LocalStack (${localStackStatus})`; - } - } - + const localStackStatus = localStackStatusTracker.status(); + const localStackInstalled = setupStatusTracker.statuses().isInstalled; + + statusBarItem.command = "localstack.showCommands"; + statusBarItem.backgroundColor = + setupStatus === "setup_required" + ? new ThemeColor("statusBarItem.errorBackground") + : undefined; + + const shouldSpin = + localStackStatus === "starting" || localStackStatus === "stopping"; + const icon = + setupStatus === "setup_required" + ? "$(error)" + : shouldSpin + ? "$(sync~spin)" + : "$(localstack-logo)"; + + const statusText = localStackInstalled + ? `${localStackStatus}` + : "not installed"; + statusBarItem.text = `${icon} LocalStack: ${statusText}`; + + statusBarItem.tooltip = "Show LocalStack commands"; statusBarItem.show(); }), ); @@ -118,6 +118,7 @@ export default createPlugin( }); } }; + context.subscriptions.push({ dispose() { clearImmediate(refreshStatusBarImmediateId); @@ -127,10 +128,12 @@ export default createPlugin( refreshStatusBarImmediate(); localStackStatusTracker.onChange(() => { + outputChannel.trace("[status-bar]: localStackStatusTracker changed"); refreshStatusBarImmediate(); }); setupStatusTracker.onChange(() => { + outputChannel.trace("[status-bar]: setupStatusTracker changed"); refreshStatusBarImmediate(); }); }, diff --git a/src/utils/promises.ts b/src/utils/promises.ts index 157133a..725e59e 100644 --- a/src/utils/promises.ts +++ b/src/utils/promises.ts @@ -32,3 +32,8 @@ export function minDelay( MIN_TIME_BETWEEN_STEPS_MS, ); } + +/** + * Extracts the resolved type from a Promise. + */ +export type UnwrapPromise = T extends Promise ? U : T; diff --git a/src/utils/setup-status.ts b/src/utils/setup-status.ts index ffff689..7470c57 100644 --- a/src/utils/setup-status.ts +++ b/src/utils/setup-status.ts @@ -2,13 +2,15 @@ import ms from "ms"; import type { Disposable, LogOutputChannel } from "vscode"; import { createEmitter } from "./emitter.ts"; -import { checkIsSetupRequired } from "./setup.ts"; +import type { UnwrapPromise } from "./promises.ts"; +import { checkSetupStatus } from "./setup.ts"; import type { TimeTracker } from "./time-tracker.ts"; export type SetupStatus = "ok" | "setup_required"; export interface SetupStatusTracker extends Disposable { status(): SetupStatus; + statuses(): UnwrapPromise>; onChange(callback: (status: SetupStatus) => void): void; } @@ -20,6 +22,7 @@ export async function createSetupStatusTracker( timeTracker: TimeTracker, ): Promise { const start = Date.now(); + let statuses: UnwrapPromise> | undefined; let status: SetupStatus | undefined; const emitter = createEmitter(outputChannel); const end = Date.now(); @@ -29,13 +32,18 @@ export async function createSetupStatusTracker( let timeout: NodeJS.Timeout | undefined; const startChecking = async () => { - const setupRequired = await checkIsSetupRequired(outputChannel); + statuses = await checkSetupStatus(outputChannel); + + const setupRequired = Object.values(statuses).some( + (check) => check === false, + ); const newStatus = setupRequired ? "setup_required" : "ok"; if (status !== newStatus) { status = newStatus; await emitter.emit(status); } + // TODO: Find a smarter way to check the status (e.g. watch for changes in AWS credentials or LocalStack installation) timeout = setTimeout(() => void startChecking(), 1_000); }; @@ -48,6 +56,10 @@ export async function createSetupStatusTracker( // biome-ignore lint/style/noNonNullAssertion: false positive return status!; }, + statuses() { + // biome-ignore lint/style/noNonNullAssertion: false positive + return statuses!; + }, onChange(callback) { emitter.on(callback); if (status) { diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 2a46ab5..721b79c 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -5,9 +5,7 @@ import { checkIsProfileConfigured } from "./configure-aws.ts"; import { checkLocalstackInstalled } from "./install.ts"; import { checkIsLicenseValid } from "./license.ts"; -export async function checkIsSetupRequired( - outputChannel: LogOutputChannel, -): Promise { +export async function checkSetupStatus(outputChannel: LogOutputChannel) { const [isInstalled, isAuthenticated, isLicenseValid, isProfileConfigured] = await Promise.all([ checkLocalstackInstalled(outputChannel), @@ -16,7 +14,10 @@ export async function checkIsSetupRequired( checkIsProfileConfigured(), ]); - return ( - !isInstalled || !isAuthenticated || !isLicenseValid || !isProfileConfigured - ); + return { + isInstalled, + isAuthenticated, + isLicenseValid, + isProfileConfigured, + }; } From c6ff15634a0fed1b461eb2dad1d653fe35eb81b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Fri, 5 Sep 2025 17:53:56 +0200 Subject: [PATCH 4/4] move "run setup wizard" command to the top --- src/plugins/status-bar.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/plugins/status-bar.ts b/src/plugins/status-bar.ts index a7f881b..3f69330 100644 --- a/src/plugins/status-bar.ts +++ b/src/plugins/status-bar.ts @@ -26,6 +26,20 @@ export default createPlugin( const getCommands = () => { const commands: (QuickPickItem & { command: string })[] = []; + + commands.push({ + label: "Configure", + command: "", + kind: QuickPickItemKind.Separator, + }); + + if (shouldShowRunSetupWizard()) { + commands.push({ + label: "Run LocalStack Setup Wizard", + command: "localstack.setup", + }); + } + commands.push({ label: "Manage", command: "", @@ -51,19 +65,6 @@ export default createPlugin( command: "localstack.viewLogs", }); - commands.push({ - label: "Configure", - command: "", - kind: QuickPickItemKind.Separator, - }); - - if (shouldShowRunSetupWizard()) { - commands.push({ - label: "Run LocalStack Setup Wizard", - command: "localstack.setup", - }); - } - return commands; }; @@ -79,7 +80,6 @@ export default createPlugin( context.subscriptions.push( commands.registerCommand("localstack.refreshStatusBar", () => { - // TODO const setupStatus = setupStatusTracker.status(); const localStackStatus = localStackStatusTracker.status(); const localStackInstalled = setupStatusTracker.statuses().isInstalled;