promptarchitect 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Environment Tree View Provider
3
+ *
4
+ * Displays environment variables from .env files and provides
5
+ * quick copy/edit functionality.
6
+ */
7
+
8
+ import * as vscode from 'vscode';
9
+ import * as path from 'path';
10
+ import * as fs from 'fs';
11
+
12
+ export interface EnvVariable {
13
+ key: string;
14
+ value: string;
15
+ source: string; // filename like '.env', '.env.local'
16
+ isSecret: boolean; // Heuristic based on key name
17
+ lineNumber: number;
18
+ }
19
+
20
+ export class EnvironmentTreeProvider implements vscode.TreeDataProvider<EnvTreeItem> {
21
+ private _onDidChangeTreeData: vscode.EventEmitter<EnvTreeItem | undefined | null | void> =
22
+ new vscode.EventEmitter<EnvTreeItem | undefined | null | void>();
23
+ readonly onDidChangeTreeData: vscode.Event<EnvTreeItem | undefined | null | void> =
24
+ this._onDidChangeTreeData.event;
25
+
26
+ private envFiles: Map<string, EnvVariable[]> = new Map();
27
+ private revealedSecrets: Set<string> = new Set(); // key@source format
28
+
29
+ // Patterns that suggest a value is secret
30
+ private readonly SECRET_PATTERNS = [
31
+ /secret/i,
32
+ /password/i,
33
+ /api[_-]?key/i,
34
+ /token/i,
35
+ /private/i,
36
+ /auth/i,
37
+ /credential/i,
38
+ /key$/i,
39
+ ];
40
+
41
+ // Common env file names to look for
42
+ private readonly ENV_FILES = [
43
+ '.env',
44
+ '.env.local',
45
+ '.env.development',
46
+ '.env.production',
47
+ '.env.test',
48
+ '.env.example',
49
+ ];
50
+
51
+ constructor(private context: vscode.ExtensionContext) {
52
+ this.detectEnvFiles();
53
+
54
+ // Watch for .env file changes
55
+ const watcher = vscode.workspace.createFileSystemWatcher('**/.env*');
56
+ watcher.onDidChange(() => this.refresh());
57
+ watcher.onDidCreate(() => this.refresh());
58
+ watcher.onDidDelete(() => this.refresh());
59
+ context.subscriptions.push(watcher);
60
+ }
61
+
62
+ refresh(): void {
63
+ this.detectEnvFiles();
64
+ this._onDidChangeTreeData.fire();
65
+ }
66
+
67
+ getTreeItem(element: EnvTreeItem): vscode.TreeItem {
68
+ return element;
69
+ }
70
+
71
+ async getChildren(element?: EnvTreeItem): Promise<EnvTreeItem[]> {
72
+ if (!vscode.workspace.workspaceFolders) {
73
+ return [];
74
+ }
75
+
76
+ if (this.envFiles.size === 0) {
77
+ return [];
78
+ }
79
+
80
+ if (!element) {
81
+ // Root level - show env file categories
82
+ const items: EnvTreeItem[] = [];
83
+
84
+ for (const [source, vars] of this.envFiles.entries()) {
85
+ if (vars.length > 0) {
86
+ items.push(new EnvTreeItem(
87
+ source,
88
+ vscode.TreeItemCollapsibleState.Expanded,
89
+ 'envFile',
90
+ undefined,
91
+ source
92
+ ));
93
+ }
94
+ }
95
+
96
+ return items;
97
+ } else {
98
+ // Children - show variables for this file
99
+ const source = element.source || '';
100
+ const vars = this.envFiles.get(source) || [];
101
+
102
+ return vars.map(envVar => {
103
+ const isRevealed = this.revealedSecrets.has(`${envVar.key}@${envVar.source}`);
104
+ const displayValue = envVar.isSecret && !isRevealed
105
+ ? '••••••••••'
106
+ : envVar.value;
107
+
108
+ const item = new EnvTreeItem(
109
+ envVar.key,
110
+ vscode.TreeItemCollapsibleState.None,
111
+ 'envVar',
112
+ envVar,
113
+ envVar.source
114
+ );
115
+ item.description = displayValue;
116
+ item.tooltip = new vscode.MarkdownString(
117
+ `**${envVar.key}**\n\n` +
118
+ `Value: \`${envVar.isSecret && !isRevealed ? '(hidden)' : envVar.value}\`\n\n` +
119
+ `Source: ${envVar.source}\n\n` +
120
+ `Line: ${envVar.lineNumber}\n\n` +
121
+ `_Click to copy value_`
122
+ );
123
+ item.iconPath = new vscode.ThemeIcon(
124
+ envVar.isSecret ? 'key' : 'symbol-variable'
125
+ );
126
+
127
+ return item;
128
+ });
129
+ }
130
+ }
131
+
132
+ private detectEnvFiles(): void {
133
+ this.envFiles.clear();
134
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
135
+ if (!workspaceFolder) return;
136
+
137
+ const rootPath = workspaceFolder.uri.fsPath;
138
+
139
+ for (const envFileName of this.ENV_FILES) {
140
+ const envPath = path.join(rootPath, envFileName);
141
+
142
+ try {
143
+ if (fs.existsSync(envPath)) {
144
+ const variables = this.parseEnvFile(envPath, envFileName);
145
+ if (variables.length > 0) {
146
+ this.envFiles.set(envFileName, variables);
147
+ }
148
+ }
149
+ } catch (error) {
150
+ console.error(`Error reading ${envFileName}:`, error);
151
+ }
152
+ }
153
+ }
154
+
155
+ private parseEnvFile(filePath: string, source: string): EnvVariable[] {
156
+ const content = fs.readFileSync(filePath, 'utf-8');
157
+ const lines = content.split('\n');
158
+ const variables: EnvVariable[] = [];
159
+
160
+ lines.forEach((line, index) => {
161
+ // Skip comments and empty lines
162
+ const trimmedLine = line.trim();
163
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
164
+ return;
165
+ }
166
+
167
+ // Parse KEY=VALUE format
168
+ const match = trimmedLine.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
169
+ if (match) {
170
+ const key = match[1];
171
+ let value = match[2];
172
+
173
+ // Remove surrounding quotes if present
174
+ if ((value.startsWith('"') && value.endsWith('"')) ||
175
+ (value.startsWith("'") && value.endsWith("'"))) {
176
+ value = value.slice(1, -1);
177
+ }
178
+
179
+ // Check if this looks like a secret
180
+ const isSecret = this.SECRET_PATTERNS.some(pattern => pattern.test(key));
181
+
182
+ variables.push({
183
+ key,
184
+ value,
185
+ source,
186
+ isSecret,
187
+ lineNumber: index + 1,
188
+ });
189
+ }
190
+ });
191
+
192
+ return variables;
193
+ }
194
+
195
+ async copyValue(item: EnvTreeItem): Promise<void> {
196
+ if (!item.envVar) return;
197
+
198
+ await vscode.env.clipboard.writeText(item.envVar.value);
199
+ vscode.window.showInformationMessage(`📋 Copied ${item.envVar.key} value`);
200
+ }
201
+
202
+ toggleReveal(item: EnvTreeItem): void {
203
+ if (!item.envVar) return;
204
+
205
+ const key = `${item.envVar.key}@${item.envVar.source}`;
206
+ if (this.revealedSecrets.has(key)) {
207
+ this.revealedSecrets.delete(key);
208
+ } else {
209
+ this.revealedSecrets.add(key);
210
+ }
211
+ this._onDidChangeTreeData.fire();
212
+ }
213
+
214
+ async createEnvFile(): Promise<void> {
215
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
216
+ if (!workspaceFolder) {
217
+ vscode.window.showWarningMessage('No workspace folder open');
218
+ return;
219
+ }
220
+
221
+ const envPath = path.join(workspaceFolder.uri.fsPath, '.env');
222
+
223
+ if (fs.existsSync(envPath)) {
224
+ // Open existing file
225
+ const doc = await vscode.workspace.openTextDocument(envPath);
226
+ await vscode.window.showTextDocument(doc);
227
+ return;
228
+ }
229
+
230
+ // Create new .env file with template
231
+ const template = `# Environment Variables
232
+ # Add your environment-specific variables here
233
+
234
+ # Example:
235
+ # DATABASE_URL=postgres://user:pass@localhost:5432/db
236
+ # API_KEY=your-api-key-here
237
+ # NODE_ENV=development
238
+
239
+ `;
240
+
241
+ fs.writeFileSync(envPath, template);
242
+ const doc = await vscode.workspace.openTextDocument(envPath);
243
+ await vscode.window.showTextDocument(doc);
244
+
245
+ this.refresh();
246
+ vscode.window.showInformationMessage('Created .env file');
247
+ }
248
+
249
+ async openEnvFile(item: EnvTreeItem): Promise<void> {
250
+ const source = item.source;
251
+ if (!source) return;
252
+
253
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
254
+ if (!workspaceFolder) return;
255
+
256
+ const envPath = path.join(workspaceFolder.uri.fsPath, source);
257
+
258
+ try {
259
+ const doc = await vscode.workspace.openTextDocument(envPath);
260
+ await vscode.window.showTextDocument(doc);
261
+
262
+ // If we have a specific line number, go to it
263
+ if (item.envVar && item.envVar.lineNumber > 0) {
264
+ const editor = vscode.window.activeTextEditor;
265
+ if (editor) {
266
+ const line = item.envVar.lineNumber - 1;
267
+ const position = new vscode.Position(line, 0);
268
+ editor.selection = new vscode.Selection(position, position);
269
+ editor.revealRange(new vscode.Range(position, position));
270
+ }
271
+ }
272
+ } catch (error) {
273
+ vscode.window.showErrorMessage(`Could not open ${source}`);
274
+ }
275
+ }
276
+ }
277
+
278
+ export class EnvTreeItem extends vscode.TreeItem {
279
+ public envVar?: EnvVariable;
280
+ public source?: string;
281
+
282
+ constructor(
283
+ public readonly label: string,
284
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
285
+ public readonly contextValue: string,
286
+ envVar?: EnvVariable,
287
+ source?: string
288
+ ) {
289
+ super(label, collapsibleState);
290
+ this.envVar = envVar;
291
+ this.source = source;
292
+
293
+ if (contextValue === 'envFile') {
294
+ this.iconPath = new vscode.ThemeIcon('file');
295
+ this.tooltip = `Environment file: ${source}`;
296
+ // Make clickable to open the file
297
+ this.command = {
298
+ command: 'promptarchitect.openEnvFile',
299
+ title: 'Open File',
300
+ arguments: [this],
301
+ };
302
+ } else if (contextValue === 'envVar') {
303
+ // Make clickable to copy
304
+ this.command = {
305
+ command: 'promptarchitect.copyEnvValue',
306
+ title: 'Copy Value',
307
+ arguments: [this],
308
+ };
309
+ }
310
+ }
311
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Provider exports
3
+ */
4
+
5
+ export { ScriptsTreeProvider, ScriptTreeItem } from './scriptsTreeProvider';
6
+ export { QuickAccessTreeProvider, QuickAccessTreeItem } from './quickAccessTreeProvider';
7
+ export { EnvironmentTreeProvider, EnvTreeItem } from './environmentTreeProvider';
8
+ export { NotesTreeProvider, NoteTreeItem } from './notesTreeProvider';
9
+ export { AIChatViewProvider } from './aiChatViewProvider';
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Notes Tree View Provider
3
+ *
4
+ * A lightweight, project-scoped TODO list and scratchpad.
5
+ * Notes are persisted per-project in workspace state.
6
+ */
7
+
8
+ import * as vscode from 'vscode';
9
+
10
+ export interface NoteItem {
11
+ id: string;
12
+ text: string;
13
+ completed: boolean;
14
+ priority: 'none' | 'high' | 'medium' | 'low';
15
+ createdAt: number;
16
+ completedAt?: number;
17
+ }
18
+
19
+ export class NotesTreeProvider implements vscode.TreeDataProvider<NoteTreeItem> {
20
+ private _onDidChangeTreeData: vscode.EventEmitter<NoteTreeItem | undefined | null | void> =
21
+ new vscode.EventEmitter<NoteTreeItem | undefined | null | void>();
22
+ readonly onDidChangeTreeData: vscode.Event<NoteTreeItem | undefined | null | void> =
23
+ this._onDidChangeTreeData.event;
24
+
25
+ private notes: NoteItem[] = [];
26
+
27
+ constructor(private context: vscode.ExtensionContext) {
28
+ // Load notes from workspace storage
29
+ this.notes = context.workspaceState.get('projectNotes', []);
30
+ }
31
+
32
+ refresh(): void {
33
+ this._onDidChangeTreeData.fire();
34
+ }
35
+
36
+ getTreeItem(element: NoteTreeItem): vscode.TreeItem {
37
+ return element;
38
+ }
39
+
40
+ async getChildren(element?: NoteTreeItem): Promise<NoteTreeItem[]> {
41
+ if (!element) {
42
+ // Root level - show categories
43
+ const activeNotes = this.notes.filter(n => !n.completed);
44
+ const completedNotes = this.notes.filter(n => n.completed);
45
+
46
+ const items: NoteTreeItem[] = [];
47
+
48
+ // Active notes (shown directly, no category header if few)
49
+ if (activeNotes.length > 0) {
50
+ // Sort by priority then by creation date
51
+ const sorted = this.sortNotes(activeNotes);
52
+ sorted.forEach(note => {
53
+ items.push(this.createNoteTreeItem(note));
54
+ });
55
+ }
56
+
57
+ // Completed section (collapsible)
58
+ if (completedNotes.length > 0) {
59
+ items.push(new NoteTreeItem(
60
+ `Completed (${completedNotes.length})`,
61
+ vscode.TreeItemCollapsibleState.Collapsed,
62
+ 'completedCategory',
63
+ undefined
64
+ ));
65
+ }
66
+
67
+ return items;
68
+ } else if (element.contextValue === 'completedCategory') {
69
+ // Show completed notes
70
+ const completedNotes = this.notes.filter(n => n.completed);
71
+ const sorted = completedNotes.sort((a, b) => (b.completedAt || 0) - (a.completedAt || 0));
72
+ return sorted.map(note => this.createNoteTreeItem(note));
73
+ }
74
+
75
+ return [];
76
+ }
77
+
78
+ private sortNotes(notes: NoteItem[]): NoteItem[] {
79
+ const priorityOrder: Record<string, number> = {
80
+ 'high': 0,
81
+ 'medium': 1,
82
+ 'low': 2,
83
+ 'none': 3,
84
+ };
85
+
86
+ return notes.sort((a, b) => {
87
+ const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
88
+ if (priorityDiff !== 0) return priorityDiff;
89
+ return a.createdAt - b.createdAt;
90
+ });
91
+ }
92
+
93
+ private createNoteTreeItem(note: NoteItem): NoteTreeItem {
94
+ const item = new NoteTreeItem(
95
+ note.text,
96
+ vscode.TreeItemCollapsibleState.None,
97
+ note.completed ? 'noteCompleted' : 'noteActive',
98
+ note
99
+ );
100
+
101
+ // Set checkbox icon
102
+ item.iconPath = new vscode.ThemeIcon(
103
+ note.completed ? 'pass-filled' : 'circle-large-outline'
104
+ );
105
+
106
+ // Add priority indicator
107
+ const priorityIcons: Record<string, string> = {
108
+ 'high': '🔴',
109
+ 'medium': '🟡',
110
+ 'low': '🟢',
111
+ 'none': '',
112
+ };
113
+ const priorityIndicator = priorityIcons[note.priority];
114
+
115
+ if (note.completed) {
116
+ item.description = this.getRelativeTime(note.completedAt || note.createdAt);
117
+ } else if (priorityIndicator) {
118
+ item.description = priorityIndicator;
119
+ }
120
+
121
+ // Tooltip with details
122
+ item.tooltip = new vscode.MarkdownString(
123
+ `${note.completed ? '✅' : '☐'} ${note.text}\n\n` +
124
+ `Priority: ${note.priority}\n\n` +
125
+ `Created: ${new Date(note.createdAt).toLocaleString()}\n\n` +
126
+ (note.completed && note.completedAt
127
+ ? `Completed: ${new Date(note.completedAt).toLocaleString()}`
128
+ : '_Click to toggle completion_')
129
+ );
130
+
131
+ // Click to toggle
132
+ item.command = {
133
+ command: 'promptarchitect.toggleNote',
134
+ title: 'Toggle Note',
135
+ arguments: [item],
136
+ };
137
+
138
+ return item;
139
+ }
140
+
141
+ private getRelativeTime(timestamp: number): string {
142
+ const now = Date.now();
143
+ const diff = now - timestamp;
144
+ const minutes = Math.floor(diff / 60000);
145
+ const hours = Math.floor(minutes / 60);
146
+ const days = Math.floor(hours / 24);
147
+
148
+ if (days > 0) return `${days}d ago`;
149
+ if (hours > 0) return `${hours}h ago`;
150
+ if (minutes > 0) return `${minutes}m ago`;
151
+ return 'just now';
152
+ }
153
+
154
+ private generateId(): string {
155
+ return `note_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
156
+ }
157
+
158
+ private async saveNotes(): Promise<void> {
159
+ await this.context.workspaceState.update('projectNotes', this.notes);
160
+ }
161
+
162
+ async addNote(): Promise<void> {
163
+ const text = await vscode.window.showInputBox({
164
+ prompt: 'Enter your note or TODO',
165
+ placeHolder: 'e.g., Fix authentication bug',
166
+ });
167
+
168
+ if (!text) return;
169
+
170
+ // Ask for priority
171
+ const priority = await vscode.window.showQuickPick(
172
+ [
173
+ { label: '🔴 High', value: 'high' },
174
+ { label: '🟡 Medium', value: 'medium' },
175
+ { label: '🟢 Low', value: 'low' },
176
+ { label: 'No Priority', value: 'none' },
177
+ ],
178
+ {
179
+ placeHolder: 'Set priority (optional)',
180
+ }
181
+ );
182
+
183
+ const note: NoteItem = {
184
+ id: this.generateId(),
185
+ text,
186
+ completed: false,
187
+ priority: (priority?.value as NoteItem['priority']) || 'none',
188
+ createdAt: Date.now(),
189
+ };
190
+
191
+ this.notes.unshift(note);
192
+ await this.saveNotes();
193
+ this._onDidChangeTreeData.fire();
194
+ }
195
+
196
+ async toggleNote(item: NoteTreeItem): Promise<void> {
197
+ if (!item.note) return;
198
+
199
+ const noteIndex = this.notes.findIndex(n => n.id === item.note!.id);
200
+ if (noteIndex === -1) return;
201
+
202
+ this.notes[noteIndex].completed = !this.notes[noteIndex].completed;
203
+
204
+ if (this.notes[noteIndex].completed) {
205
+ this.notes[noteIndex].completedAt = Date.now();
206
+ } else {
207
+ this.notes[noteIndex].completedAt = undefined;
208
+ }
209
+
210
+ await this.saveNotes();
211
+ this._onDidChangeTreeData.fire();
212
+ }
213
+
214
+ async deleteNote(item: NoteTreeItem): Promise<void> {
215
+ if (!item.note) return;
216
+
217
+ const confirm = await vscode.window.showQuickPick(['Yes', 'No'], {
218
+ placeHolder: `Delete "${item.note.text.substring(0, 30)}..."?`,
219
+ });
220
+
221
+ if (confirm !== 'Yes') return;
222
+
223
+ this.notes = this.notes.filter(n => n.id !== item.note!.id);
224
+ await this.saveNotes();
225
+ this._onDidChangeTreeData.fire();
226
+ }
227
+
228
+ async editNote(item: NoteTreeItem): Promise<void> {
229
+ if (!item.note) return;
230
+
231
+ const newText = await vscode.window.showInputBox({
232
+ prompt: 'Edit note',
233
+ value: item.note.text,
234
+ });
235
+
236
+ if (!newText) return;
237
+
238
+ const noteIndex = this.notes.findIndex(n => n.id === item.note!.id);
239
+ if (noteIndex === -1) return;
240
+
241
+ this.notes[noteIndex].text = newText;
242
+ await this.saveNotes();
243
+ this._onDidChangeTreeData.fire();
244
+ }
245
+
246
+ async cyclePriority(item: NoteTreeItem): Promise<void> {
247
+ if (!item.note) return;
248
+
249
+ const noteIndex = this.notes.findIndex(n => n.id === item.note!.id);
250
+ if (noteIndex === -1) return;
251
+
252
+ const priorities: NoteItem['priority'][] = ['none', 'high', 'medium', 'low'];
253
+ const currentIndex = priorities.indexOf(this.notes[noteIndex].priority);
254
+ const nextIndex = (currentIndex + 1) % priorities.length;
255
+
256
+ this.notes[noteIndex].priority = priorities[nextIndex];
257
+ await this.saveNotes();
258
+ this._onDidChangeTreeData.fire();
259
+ }
260
+
261
+ async clearCompleted(): Promise<void> {
262
+ const completedCount = this.notes.filter(n => n.completed).length;
263
+ if (completedCount === 0) {
264
+ vscode.window.showInformationMessage('No completed notes to clear');
265
+ return;
266
+ }
267
+
268
+ const confirm = await vscode.window.showQuickPick(['Yes', 'No'], {
269
+ placeHolder: `Clear ${completedCount} completed note(s)?`,
270
+ });
271
+
272
+ if (confirm !== 'Yes') return;
273
+
274
+ this.notes = this.notes.filter(n => !n.completed);
275
+ await this.saveNotes();
276
+ this._onDidChangeTreeData.fire();
277
+ vscode.window.showInformationMessage(`Cleared ${completedCount} completed note(s)`);
278
+ }
279
+
280
+ getActiveCount(): number {
281
+ return this.notes.filter(n => !n.completed).length;
282
+ }
283
+ }
284
+
285
+ export class NoteTreeItem extends vscode.TreeItem {
286
+ public note?: NoteItem;
287
+
288
+ constructor(
289
+ public readonly label: string,
290
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
291
+ public readonly contextValue: string,
292
+ note?: NoteItem
293
+ ) {
294
+ super(label, collapsibleState);
295
+ this.note = note;
296
+
297
+ if (contextValue === 'completedCategory') {
298
+ this.iconPath = new vscode.ThemeIcon('checklist');
299
+ }
300
+ }
301
+ }