Skip to content
Open
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
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ TypeScript using Electron Forge.
stories with Titles, Descriptions, and Acceptance Criteria.
- Ability to seamlessly write the generated stories back to Azure DevOps as
new **Product Backlog Items (PBIs)** linked under a specific Feature.
- **Persistent Settings**: Securely store Azure DevOps credentials, Confluence
tokens, and project configuration locally, and actively check the status of
local GitHub Copilot CLI authentication.
- **Persistent Settings**: Securely store Azure DevOps organization details,
Confluence tokens, and project configuration locally, and actively check the
status of local GitHub Copilot CLI authentication.

## Tech Stack

Expand All @@ -39,6 +39,7 @@ TypeScript using Electron Forge.
- **Navigation:** [React Router Dom](https://reactrouter.com/)
- **APIs & Integration**:
- `azure-devops-node-api`: For interacting with Azure DevOps REST APIs.
- `@azure/msal-node`: For OAuth 2.0 with PKCE authentication to Azure DevOps.
- `@github/copilot-sdk`: For AI-powered generation via GitHub Copilot.
- **Confluence REST API**: Utilizing internal fetches for reading Atlassian
Cloud content via Basic Auth using API Tokens.
Expand All @@ -55,8 +56,9 @@ TypeScript using Electron Forge.
- **Note for Windows Users**: You must set the `NODE_PATH` (path to Node.js
executable) and `COPILOT_SCRIPT_PATH` (path to the Copilot JS script)
environment variables for the Copilot client to initialize correctly.
- **Azure DevOps PAT**: A Personal Access Token with "Work Items: Read & Write"
permissions.
- **Azure DevOps Account**: Access to an Azure DevOps organization. The
application uses OAuth 2.0 with PKCE for secure authentication (no PAT
required).
- **Confluence API Token**: An Atlassian API Token generated from your profile
settings (to be paired with your login email) for basic authentication.

Expand Down
7 changes: 3 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ main (Node.js) process from the renderer (Chromium) process.

## Configuration Management

We use `electron-store` to persist user settings (like Azure DevOps PATs)
locally on the machine.
We use `electron-store` to persist user settings locally on the machine.

- **Encryption:** Settings are stored in the default Electron user data path.
- **IPC Access:** The renderer fetches and saves settings through the
Expand All @@ -30,8 +29,8 @@ locally on the machine.

### Azure DevOps

- **Library:** `azure-devops-node-api`
- **Method:** Uses Personal Access Tokens (PAT) via the Work Item Tracking API.
- **Libraries:** `azure-devops-node-api`, `@azure/msal-node`
- **Method:** OAuth 2.0 with PKCE via MSAL for secure authentication.
- **Scope:** Fetches work item details (ID, Title, Description, Acceptance
Criteria), and allows pushing generated test cases back to Azure DevOps as
Comments or Child Tasks.
Expand Down
126 changes: 124 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"typescript": "^5.9.3"
},
"dependencies": {
"@azure/msal-node": "^5.1.2",
"@fortawesome/fontawesome-free": "^7.2.0",
"@github/copilot-sdk": "^0.2.1",
"@popperjs/core": "^2.11.8",
Expand Down
58 changes: 45 additions & 13 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
type AppSettings = {
azureOrg?: string;
azureProject?: string;
azurePat?: string;
copilotToken?: string;
copilotModel?: string;
confluenceUrl?: string;
Expand All @@ -32,7 +31,6 @@ async function initStore() {
return store;
}

// Global service instances
// Global service instances
let azureService: AzureDevOpsService | null = null;
let confluenceService: any = null;
Expand All @@ -57,9 +55,9 @@ function trimProperties(
obj: Record<string, string | undefined>,
): Record<string, string | undefined> {
const out: Record<string, string> = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
out[key] = obj[key]?.toString().trim();
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
out[key] = obj[key]?.toString().trim() || '';
}
}
return out;
Expand All @@ -83,19 +81,53 @@ ipcMain.handle('save-settings', async (event, settings: AppSettings) => {
});

async function getAzureService() {
const s = await initStore();
const settings = trimProperties(s.get('settings') || {}) as AppSettings;
const { azureOrg } = settings;

if (!azureOrg) {
throw new Error(
'Azure DevOps Organization URL is missing in settings. Please provide it before logging in.',
);
}

if (!azureService) {
const s = await initStore();
const { azureOrg, azurePat } = trimProperties(
s.get('settings'),
) as AppSettings;
if (!azureOrg || !azurePat) {
throw new Error('Azure DevOps settings are missing.');
}
azureService = new AzureDevOpsService(azureOrg, azurePat);
azureService = new AzureDevOpsService(azureOrg);
} else if (azureService.org !== azureOrg) {
// If org changed, re-initialize
azureService = new AzureDevOpsService(azureOrg);
}

return azureService;
}

ipcMain.handle('azure-login', async () => {
const service = await getAzureService();
return service.login();
});

ipcMain.handle('azure-logout', async () => {
if (azureService) {
await azureService.logout();
azureService = null;
}
return { success: true };
});

ipcMain.handle('get-azure-status', async () => {
try {
const s = await initStore();
const settings = trimProperties(s.get('settings') || {}) as AppSettings;
if (!settings.azureOrg) return { isAuthenticated: false };

const service = await getAzureService();
const token = await service.getAccessToken();
return { isAuthenticated: !!token };
} catch (error) {
return { isAuthenticated: false };
}
});

ipcMain.handle('fetch-ticket', async (event, ticketId) => {
const service = await getAzureService();
return service.fetchTicket(ticketId);
Expand Down
3 changes: 3 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
checkCopilotAuth: () => ipcRenderer.invoke('check-copilot-auth'),
getVersion: () => ipcRenderer.invoke('get-version'),
listCopilotModels: () => ipcRenderer.invoke('list-copilot-models'),
azureLogin: () => ipcRenderer.invoke('azure-login'),
azureLogout: () => ipcRenderer.invoke('azure-logout'),
getAzureStatus: () => ipcRenderer.invoke('get-azure-status'),
});
Loading
Loading