Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to the ALCops extension will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.1] - 2026-09-03

### Added
- "ALCops: Copy Version Information" command — copies extension, analyzers, AL Language, VS Code, and OS version details to the clipboard for issue reports
- "ALCops" output channel with extension/analyzers version banner on activation and logging of install/update activity
- Status bar tooltip now shows the extension, analyzers (with channel), and AL Language versions

## [1.4.0] - 2026-08-20

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
{
"command": "alcops.selectCodeAnalyzers",
"title": "ALCops: Select Code Analyzers"
},
{
"command": "alcops.copyVersionInfo",
"title": "ALCops: Copy Version Information"
}
],
"jsonValidation": [
Expand Down
3 changes: 2 additions & 1 deletion src/al-extension-handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from 'vscode';
import { checkDirectoryForLockedFiles } from './file-lock-handler.js';
import { resolveAnalyzersDir } from './analyzers-layout.js';
import { log } from './logger.js';

const AL_EXTENSION_ID = 'ms-dynamics-smb.al';

Expand Down Expand Up @@ -35,7 +36,7 @@ function checkALExtensionStatus(analyzerPath: string): ALExtensionStatus {
const alExtension = getALExtension();
isRunning = alExtension?.isActive ?? false;
} catch (error) {
console.warn('Error checking AL extension status:', error);
log.warn('Error checking AL extension status:', error);
}
const lockedFiles = checkDirectoryForLockedFiles(analyzerPath).lockedFiles;
const hasLocks = lockedFiles.length > 0;
Expand Down
17 changes: 9 additions & 8 deletions src/auto-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { queryLatestVersion, downloadALCopsAnalyzers } from './downloader.js';
import { getPendingUpdate } from './manifest-manager.js';
import { getAnalyzersPath, getALExtension } from './al-extension-handler.js';
import { formatError, showTimedMessage } from './utils.js';
import { log } from './logger.js';

export class AutoUpdater {
private readonly _onDidInstallAnalyzers = new vscode.EventEmitter<string>();
Expand All @@ -27,7 +28,7 @@ export class AutoUpdater {

await this.performUpdateCheck();
} catch (error) {
console.error('Error checking for ALCops updates:', error);
log.error('Error checking for ALCops updates:', error);
}
}

Expand Down Expand Up @@ -123,18 +124,18 @@ export class AutoUpdater {
private async installVersion(version: string | null, reason: string): Promise<boolean> {
const targetVersion = version ?? await queryLatestVersion(this.getVersionChannel());
if (!targetVersion) {
console.error(`Could not determine version to install (${reason})`);
log.error(`Could not determine version to install (${reason})`);
return false;
}

console.log(`Installing ALCops v${targetVersion} (${reason})...`);
log.info(`Installing ALCops v${targetVersion} (${reason})...`);
try {
await downloadALCopsAnalyzers(targetVersion);
this._onDidInstallAnalyzers.fire(targetVersion);
showTimedMessage(`ALCops v${targetVersion} installed successfully.`);
return true;
} catch (error) {
console.error(`Failed to install ALCops v${targetVersion}:`, error);
log.error(`Failed to install ALCops v${targetVersion}:`, error);
vscode.window.showErrorMessage(`Failed to install ALCops: ${formatError(error)}`);
return false;
}
Expand All @@ -146,7 +147,7 @@ export class AutoUpdater {
private async performUpdateCheck(): Promise<void> {
const latestVersion = await queryLatestVersion(this.getVersionChannel());
if (!latestVersion) {
console.log('Could not determine latest ALCops version');
log.info('Could not determine latest ALCops version');
return;
}

Expand Down Expand Up @@ -203,7 +204,7 @@ export class AutoUpdater {
return false;
}

console.log(`Found pending ALCops installation for v${pendingVersion}. Attempting installation...`);
log.info(`Found pending ALCops installation for v${pendingVersion}. Attempting installation...`);
return this.installVersion(pendingVersion, 'pending deferred installation');
}

Expand All @@ -213,7 +214,7 @@ export class AutoUpdater {
async performStartupChecks(): Promise<void> {
try {
if (!getALExtension()) {
console.log('AL extension is not installed. Skipping ALCops startup checks.');
log.info('AL extension is not installed. Skipping ALCops startup checks.');
return;
}

Expand All @@ -227,7 +228,7 @@ export class AutoUpdater {

await this.checkAndNotifyUpdates();
} catch (error) {
console.error('Error during startup checks:', error);
log.error('Error during startup checks:', error);
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getALExtension, promptUserForLockedFiles } from './al-extension-handler
import { resolveAnalyzersDir, getAnalyzersDirCandidates, CODE_ANALYSIS_DLL } from './analyzers-layout.js';
import { launchNewVSCodeWindow } from './vscode-launcher.js';
import { formatError, showTimedMessage } from './utils.js';
import { log } from './logger.js';

const PACKAGE_NAME = 'ALCops.Analyzers';

Expand Down Expand Up @@ -113,7 +114,7 @@ export async function queryLatestVersion(channel: 'stable' | 'beta' | 'alpha'):

return filtered.sort((a, b) => compare(a.version, b.version)).at(-1)!.version;
} catch (error) {
console.error('Error querying NuGet for latest version:', error);
log.error('Error querying NuGet for latest version:', error);
return null;
}
}
Expand Down Expand Up @@ -306,7 +307,7 @@ async function downloadALCopsAnalyzersInternal(version: string): Promise<void> {
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (err) {
console.warn(`Failed to clean up temp directory: ${err}`);
log.warn(`Failed to clean up temp directory: ${err}`);
}
}
}
Expand All @@ -316,7 +317,7 @@ async function handleLockedFiles(targetPath: string, version: string): Promise<'
const lockCheck = checkDirectoryForLockedFiles(targetPath);
if (!lockCheck.isLocked) { return 'proceed'; }

console.warn(`Locked files detected: ${lockCheck.lockedFiles.join(', ')}`);
log.warn(`Locked files detected: ${lockCheck.lockedFiles.join(', ')}`);
const userChoice = await promptUserForLockedFiles(targetPath, version);

if (userChoice === 'cancel') {
Expand Down
32 changes: 26 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@ import * as vscode from 'vscode';
import { VersionManager } from './version-manager.js';
import { AutoUpdater } from './auto-updater.js';
import { StatusBarManager } from './status-bar-manager.js';
import { initLogger, log } from './logger.js';
import { gatherVersionInfo, formatClipboardText, formatVersionBanner } from './version-info.js';
import { showTimedMessage } from './utils.js';

// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export async function activate(context: vscode.ExtensionContext) {

// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "ALCops" is now active!');
// Create the "ALCops" output channel before anything logs
initLogger(context);
log.info(`ALCops is now active. ${formatVersionBanner(gatherVersionInfo())}`);

// Initialize version manager and auto updater first; StatusBarManager subscribes to its event
const versionManager = new VersionManager(context);
const autoUpdater = new AutoUpdater(versionManager);

// Log the resulting versions after every successful installation
const installLogDisposable = autoUpdater.onDidInstallAnalyzers((version) => {
log.info(`Installed ALCops.Analyzers v${version}. ${formatVersionBanner(gatherVersionInfo())}`);
});

// Initialize status bar manager and wire up the installation event
const statusBarManager = new StatusBarManager(context, autoUpdater.onDidInstallAnalyzers);

Expand All @@ -28,7 +36,7 @@ export async function activate(context: vscode.ExtensionContext) {
try {
await autoUpdater.checkUpdatesManually();
} catch (error) {
console.error('Check updates command failed:', error);
log.error('Check updates command failed:', error);
}
});

Expand All @@ -37,15 +45,27 @@ export async function activate(context: vscode.ExtensionContext) {
try {
await autoUpdater.installLatestVersion();
} catch (error) {
console.error('Install update command failed:', error);
log.error('Install update command failed:', error);
}
});

// Register the copy version information command
const copyVersionInfoDisposable = vscode.commands.registerCommand('alcops.copyVersionInfo', async () => {
try {
await vscode.env.clipboard.writeText(formatClipboardText(gatherVersionInfo()));
showTimedMessage('ALCops version information copied to clipboard.');
} catch (error) {
log.error('Copy version information command failed:', error);
}
});

context.subscriptions.push(
statusBarManager,
autoUpdater,
installLogDisposable,
checkUpdatesDisposable,
installDisposable
installDisposable,
copyVersionInfoDisposable
);
}

Expand Down
13 changes: 7 additions & 6 deletions src/file-staging.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { formatError } from './utils.js';
import { log } from './logger.js';

/**
* Result of staging and replacing files
Expand Down Expand Up @@ -63,7 +64,7 @@ export function stageAndReplaceFiles(
replacedFiles.push(file);
} catch (error) {
failedFiles.push(file);
console.error(`Failed to replace file ${file}:`, error);
log.error(`Failed to replace file ${file}:`, error);
// Don't continue - we want all-or-nothing
break;
}
Expand All @@ -80,7 +81,7 @@ export function stageAndReplaceFiles(
}

// Step 5: Rollback on partial failure
console.warn(`Partial failure detected (${failedFiles.length}/${sourceFiles.length}). Rolling back...`);
log.warn(`Partial failure detected (${failedFiles.length}/${sourceFiles.length}). Rolling back...`);
rollbackFiles(backupDir, targetDir);

return {
Expand Down Expand Up @@ -118,14 +119,14 @@ function rollbackFiles(backupDir: string, targetDir: string): void {
try {
fs.copyFileSync(backupFile, targetFile);
} catch (error) {
console.error(`Failed to rollback file ${file}:`, error);
log.error(`Failed to rollback file ${file}:`, error);
}
}

// Clean up backup directory
fs.rmSync(backupDir, { recursive: true, force: true });
} catch (error) {
console.error('Failed to complete rollback:', error);
log.error('Failed to complete rollback:', error);
}
}

Expand All @@ -145,11 +146,11 @@ export function cleanupOldBackups(targetDir: string, maxAge: number = 24 * 60 *

if (age > maxAge) {
fs.rmSync(backupPath, { recursive: true, force: true });
console.log(`Cleaned up old backup: ${file}`);
log.info(`Cleaned up old backup: ${file}`);
}
}
}
} catch (error) {
console.warn('Failed to cleanup old backups:', error);
log.warn('Failed to cleanup old backups:', error);
}
}
56 changes: 56 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as vscode from 'vscode';

let outputChannel: vscode.LogOutputChannel | undefined;

/**
* Create the "ALCops" output channel and register it for disposal.
* Call this first thing during activation, before anything logs.
*/
export function initLogger(context: vscode.ExtensionContext): vscode.LogOutputChannel {
if (!outputChannel) {
outputChannel = vscode.window.createOutputChannel('ALCops', { log: true });
context.subscriptions.push(outputChannel);
}
return outputChannel;
}

/**
* Logging facade used across the extension.
*
* Writes to the "ALCops" output channel once {@link initLogger} has run, and
* falls back to the console otherwise. The fallback keeps pure-Node modules
* importable in unit tests, where no VS Code window exists.
*/
export const log = {
info(message: string, ...args: unknown[]): void {
if (outputChannel) {
outputChannel.info(message, ...args);
} else {
console.log(message, ...args);
}
},

warn(message: string, ...args: unknown[]): void {
if (outputChannel) {
outputChannel.warn(message, ...args);
} else {
console.warn(message, ...args);
}
},

error(message: string | Error, ...args: unknown[]): void {
if (outputChannel) {
outputChannel.error(message, ...args);
} else {
console.error(message, ...args);
}
},

debug(message: string, ...args: unknown[]): void {
if (outputChannel) {
outputChannel.debug(message, ...args);
} else {
console.debug(message, ...args);
}
},
};
13 changes: 7 additions & 6 deletions src/manifest-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { formatError } from './utils.js';
import { log } from './logger.js';

/**
* Represents the manifest metadata for ALCops analyzer installation
Expand Down Expand Up @@ -46,7 +47,7 @@ export function readManifest(targetPath: string): ALCopsManifest | null {
const data = fs.readFileSync(manifestPath, 'utf-8');
return JSON.parse(data) as ALCopsManifest;
} catch (error) {
console.warn(`Failed to read manifest file: ${formatError(error)}`);
log.warn(`Failed to read manifest file: ${formatError(error)}`);
return null;
}
}
Expand Down Expand Up @@ -98,9 +99,9 @@ export function markAsPendingUpdate(
}

writeManifest(targetPath, manifest);
console.log(`Marked ALCops v${pendingVersion} as pending for next startup`);
log.info(`Marked ALCops v${pendingVersion} as pending for next startup`);
} catch (error) {
console.warn(`Failed to mark pending update: ${formatError(error)}`);
log.warn(`Failed to mark pending update: ${formatError(error)}`);
}
}

Expand All @@ -110,12 +111,12 @@ export function markAsPendingUpdate(
export function getPendingUpdate(targetPath: string): string | null {
try {
const manifest = readManifest(targetPath);
console.log(`getPendingUpdate: manifest exists=${!!manifest}, pendingUpdate=${manifest?.pendingUpdate}, version=${manifest?.pendingVersion}`);
log.info(`getPendingUpdate: manifest exists=${!!manifest}, pendingUpdate=${manifest?.pendingUpdate}, version=${manifest?.pendingVersion}`);
if (manifest?.pendingUpdate && manifest?.pendingVersion) {
return manifest.pendingVersion;
}
} catch (error) {
console.warn(`Failed to get pending update: ${formatError(error)}`);
log.warn(`Failed to get pending update: ${formatError(error)}`);
}
return null;
}
Expand All @@ -132,6 +133,6 @@ export function clearPendingUpdate(targetPath: string): void {
writeManifest(targetPath, manifest);
}
} catch (error) {
console.warn(`Failed to clear pending update: ${formatError(error)}`);
log.warn(`Failed to clear pending update: ${formatError(error)}`);
}
}
Loading