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.
- package/.vscodeignore +7 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +44 -0
- package/README.md +200 -0
- package/docs/CHAT_UI_REDESIGN_PLAN.md +371 -0
- package/images/hub-icon.svg +6 -0
- package/images/prompt-lab-icon.svg +11 -0
- package/package.json +519 -0
- package/src/agentPrompts.ts +278 -0
- package/src/agentService.ts +630 -0
- package/src/api.ts +223 -0
- package/src/authService.ts +556 -0
- package/src/chatPanel.ts +979 -0
- package/src/extension.ts +822 -0
- package/src/providers/aiChatViewProvider.ts +1023 -0
- package/src/providers/environmentTreeProvider.ts +311 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/notesTreeProvider.ts +301 -0
- package/src/providers/quickAccessTreeProvider.ts +328 -0
- package/src/providers/scriptsTreeProvider.ts +324 -0
- package/src/refinerPanel.ts +620 -0
- package/src/templates.ts +61 -0
- package/src/workspaceIndexer.ts +766 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick Access Tree View Provider
|
|
3
|
+
*
|
|
4
|
+
* Displays pinned files and auto-detected important project files.
|
|
5
|
+
* Allows quick navigation to frequently used files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as vscode from 'vscode';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
|
|
12
|
+
export interface PinnedItem {
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
type: 'file' | 'folder' | 'link';
|
|
16
|
+
url?: string; // For external links
|
|
17
|
+
pinned: boolean; // true = user pinned, false = auto-detected
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class QuickAccessTreeProvider implements vscode.TreeDataProvider<QuickAccessTreeItem> {
|
|
21
|
+
private _onDidChangeTreeData: vscode.EventEmitter<QuickAccessTreeItem | undefined | null | void> =
|
|
22
|
+
new vscode.EventEmitter<QuickAccessTreeItem | undefined | null | void>();
|
|
23
|
+
readonly onDidChangeTreeData: vscode.Event<QuickAccessTreeItem | undefined | null | void> =
|
|
24
|
+
this._onDidChangeTreeData.event;
|
|
25
|
+
|
|
26
|
+
private pinnedItems: PinnedItem[] = [];
|
|
27
|
+
private detectedItems: PinnedItem[] = [];
|
|
28
|
+
|
|
29
|
+
// Common important files to auto-detect
|
|
30
|
+
private readonly AUTO_DETECT_FILES = [
|
|
31
|
+
'README.md',
|
|
32
|
+
'readme.md',
|
|
33
|
+
'README',
|
|
34
|
+
'CONTRIBUTING.md',
|
|
35
|
+
'CHANGELOG.md',
|
|
36
|
+
'LICENSE',
|
|
37
|
+
'LICENSE.md',
|
|
38
|
+
'.env.example',
|
|
39
|
+
'.env.template',
|
|
40
|
+
'docker-compose.yml',
|
|
41
|
+
'docker-compose.yaml',
|
|
42
|
+
'Dockerfile',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Common entry point patterns
|
|
46
|
+
private readonly ENTRY_POINT_PATTERNS = [
|
|
47
|
+
'src/index.ts',
|
|
48
|
+
'src/index.js',
|
|
49
|
+
'src/main.ts',
|
|
50
|
+
'src/main.js',
|
|
51
|
+
'src/App.tsx',
|
|
52
|
+
'src/App.jsx',
|
|
53
|
+
'index.ts',
|
|
54
|
+
'index.js',
|
|
55
|
+
'main.ts',
|
|
56
|
+
'main.py',
|
|
57
|
+
'app.py',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
constructor(private context: vscode.ExtensionContext) {
|
|
61
|
+
// Load pinned items from storage
|
|
62
|
+
this.pinnedItems = context.workspaceState.get('pinnedItems', []);
|
|
63
|
+
this.detectImportantFiles();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
refresh(): void {
|
|
67
|
+
this.detectImportantFiles();
|
|
68
|
+
this._onDidChangeTreeData.fire();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getTreeItem(element: QuickAccessTreeItem): vscode.TreeItem {
|
|
72
|
+
return element;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getChildren(element?: QuickAccessTreeItem): Promise<QuickAccessTreeItem[]> {
|
|
76
|
+
if (!vscode.workspace.workspaceFolders) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!element) {
|
|
81
|
+
// Root level - show categories
|
|
82
|
+
const items: QuickAccessTreeItem[] = [];
|
|
83
|
+
|
|
84
|
+
// Detected files section
|
|
85
|
+
if (this.detectedItems.length > 0) {
|
|
86
|
+
items.push(new QuickAccessTreeItem(
|
|
87
|
+
'Detected',
|
|
88
|
+
vscode.TreeItemCollapsibleState.Expanded,
|
|
89
|
+
'category',
|
|
90
|
+
undefined
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Pinned files section
|
|
95
|
+
if (this.pinnedItems.length > 0) {
|
|
96
|
+
items.push(new QuickAccessTreeItem(
|
|
97
|
+
'⭐ Pinned',
|
|
98
|
+
vscode.TreeItemCollapsibleState.Expanded,
|
|
99
|
+
'category',
|
|
100
|
+
undefined,
|
|
101
|
+
true
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If nothing exists, return empty (welcome view will show)
|
|
106
|
+
if (items.length === 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return items;
|
|
111
|
+
} else {
|
|
112
|
+
// Children based on category
|
|
113
|
+
const isPinnedCategory = element.isPinnedCategory;
|
|
114
|
+
const sourceItems = isPinnedCategory ? this.pinnedItems : this.detectedItems;
|
|
115
|
+
|
|
116
|
+
return sourceItems.map(item => {
|
|
117
|
+
const treeItem = new QuickAccessTreeItem(
|
|
118
|
+
item.name,
|
|
119
|
+
vscode.TreeItemCollapsibleState.None,
|
|
120
|
+
item.pinned ? 'pinnedFile' : 'detectedFile',
|
|
121
|
+
item
|
|
122
|
+
);
|
|
123
|
+
return treeItem;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private detectImportantFiles(): void {
|
|
129
|
+
this.detectedItems = [];
|
|
130
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
131
|
+
if (!workspaceFolder) return;
|
|
132
|
+
|
|
133
|
+
const rootPath = workspaceFolder.uri.fsPath;
|
|
134
|
+
|
|
135
|
+
// Check for auto-detect files
|
|
136
|
+
for (const fileName of this.AUTO_DETECT_FILES) {
|
|
137
|
+
const filePath = path.join(rootPath, fileName);
|
|
138
|
+
if (fs.existsSync(filePath)) {
|
|
139
|
+
// Skip if already pinned
|
|
140
|
+
if (!this.pinnedItems.some(p => p.path === filePath)) {
|
|
141
|
+
this.detectedItems.push({
|
|
142
|
+
name: fileName,
|
|
143
|
+
path: filePath,
|
|
144
|
+
type: 'file',
|
|
145
|
+
pinned: false,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for entry points
|
|
152
|
+
for (const entryPoint of this.ENTRY_POINT_PATTERNS) {
|
|
153
|
+
const filePath = path.join(rootPath, entryPoint);
|
|
154
|
+
if (fs.existsSync(filePath)) {
|
|
155
|
+
if (!this.pinnedItems.some(p => p.path === filePath) &&
|
|
156
|
+
!this.detectedItems.some(d => d.path === filePath)) {
|
|
157
|
+
this.detectedItems.push({
|
|
158
|
+
name: entryPoint,
|
|
159
|
+
path: filePath,
|
|
160
|
+
type: 'file',
|
|
161
|
+
pinned: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
break; // Only add one entry point
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async pinCurrentFile(): Promise<void> {
|
|
170
|
+
const editor = vscode.window.activeTextEditor;
|
|
171
|
+
if (!editor) {
|
|
172
|
+
vscode.window.showWarningMessage('No active file to pin');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const filePath = editor.document.uri.fsPath;
|
|
177
|
+
const fileName = path.basename(filePath);
|
|
178
|
+
|
|
179
|
+
// Check if already pinned
|
|
180
|
+
if (this.pinnedItems.some(p => p.path === filePath)) {
|
|
181
|
+
vscode.window.showInformationMessage(`"${fileName}" is already pinned`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const item: PinnedItem = {
|
|
186
|
+
name: fileName,
|
|
187
|
+
path: filePath,
|
|
188
|
+
type: 'file',
|
|
189
|
+
pinned: true,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
this.pinnedItems.push(item);
|
|
193
|
+
await this.context.workspaceState.update('pinnedItems', this.pinnedItems);
|
|
194
|
+
|
|
195
|
+
// Remove from detected if it was there
|
|
196
|
+
this.detectedItems = this.detectedItems.filter(d => d.path !== filePath);
|
|
197
|
+
|
|
198
|
+
this._onDidChangeTreeData.fire();
|
|
199
|
+
vscode.window.showInformationMessage(`📌 Pinned "${fileName}"`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async unpinFile(item: QuickAccessTreeItem): Promise<void> {
|
|
203
|
+
if (!item.pinnedItem) return;
|
|
204
|
+
|
|
205
|
+
this.pinnedItems = this.pinnedItems.filter(p => p.path !== item.pinnedItem!.path);
|
|
206
|
+
await this.context.workspaceState.update('pinnedItems', this.pinnedItems);
|
|
207
|
+
|
|
208
|
+
// Re-detect in case it should appear in detected section
|
|
209
|
+
this.detectImportantFiles();
|
|
210
|
+
|
|
211
|
+
this._onDidChangeTreeData.fire();
|
|
212
|
+
vscode.window.showInformationMessage(`Unpinned "${item.pinnedItem.name}"`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async openFile(item: QuickAccessTreeItem): Promise<void> {
|
|
216
|
+
if (!item.pinnedItem) return;
|
|
217
|
+
|
|
218
|
+
const pinnedItem = item.pinnedItem;
|
|
219
|
+
|
|
220
|
+
if (pinnedItem.type === 'link' && pinnedItem.url) {
|
|
221
|
+
vscode.env.openExternal(vscode.Uri.parse(pinnedItem.url));
|
|
222
|
+
} else if (pinnedItem.type === 'folder') {
|
|
223
|
+
// Reveal in explorer
|
|
224
|
+
const uri = vscode.Uri.file(pinnedItem.path);
|
|
225
|
+
vscode.commands.executeCommand('revealInExplorer', uri);
|
|
226
|
+
} else {
|
|
227
|
+
// Open file
|
|
228
|
+
const uri = vscode.Uri.file(pinnedItem.path);
|
|
229
|
+
const doc = await vscode.workspace.openTextDocument(uri);
|
|
230
|
+
await vscode.window.showTextDocument(doc);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async addExternalLink(): Promise<void> {
|
|
235
|
+
const name = await vscode.window.showInputBox({
|
|
236
|
+
prompt: 'Enter a name for this link',
|
|
237
|
+
placeHolder: 'e.g., Jira Board',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (!name) return;
|
|
241
|
+
|
|
242
|
+
const url = await vscode.window.showInputBox({
|
|
243
|
+
prompt: 'Enter the URL',
|
|
244
|
+
placeHolder: 'https://...',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!url) return;
|
|
248
|
+
|
|
249
|
+
const item: PinnedItem = {
|
|
250
|
+
name,
|
|
251
|
+
path: '',
|
|
252
|
+
type: 'link',
|
|
253
|
+
url,
|
|
254
|
+
pinned: true,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
this.pinnedItems.push(item);
|
|
258
|
+
await this.context.workspaceState.update('pinnedItems', this.pinnedItems);
|
|
259
|
+
this._onDidChangeTreeData.fire();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export class QuickAccessTreeItem extends vscode.TreeItem {
|
|
264
|
+
public pinnedItem?: PinnedItem;
|
|
265
|
+
public isPinnedCategory?: boolean;
|
|
266
|
+
|
|
267
|
+
constructor(
|
|
268
|
+
public readonly label: string,
|
|
269
|
+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
|
270
|
+
public readonly contextValue: string,
|
|
271
|
+
pinnedItem?: PinnedItem,
|
|
272
|
+
isPinnedCategory?: boolean
|
|
273
|
+
) {
|
|
274
|
+
super(label, collapsibleState);
|
|
275
|
+
this.pinnedItem = pinnedItem;
|
|
276
|
+
this.isPinnedCategory = isPinnedCategory;
|
|
277
|
+
|
|
278
|
+
if (pinnedItem) {
|
|
279
|
+
this.tooltip = pinnedItem.path || pinnedItem.url;
|
|
280
|
+
this.description = this.getRelativePath(pinnedItem.path);
|
|
281
|
+
this.iconPath = this.getIcon(pinnedItem);
|
|
282
|
+
|
|
283
|
+
// Make clickable to open
|
|
284
|
+
this.command = {
|
|
285
|
+
command: 'promptarchitect.openQuickAccessFile',
|
|
286
|
+
title: 'Open File',
|
|
287
|
+
arguments: [this],
|
|
288
|
+
};
|
|
289
|
+
} else if (contextValue === 'category') {
|
|
290
|
+
this.iconPath = new vscode.ThemeIcon(isPinnedCategory ? 'star-full' : 'eye');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private getRelativePath(filePath: string): string {
|
|
295
|
+
if (!filePath) return '';
|
|
296
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
297
|
+
if (workspaceFolder) {
|
|
298
|
+
const relativePath = path.relative(workspaceFolder.uri.fsPath, path.dirname(filePath));
|
|
299
|
+
return relativePath ? `./${relativePath}` : './';
|
|
300
|
+
}
|
|
301
|
+
return '';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private getIcon(item: PinnedItem): vscode.ThemeIcon {
|
|
305
|
+
if (item.type === 'link') {
|
|
306
|
+
return new vscode.ThemeIcon('link-external');
|
|
307
|
+
} else if (item.type === 'folder') {
|
|
308
|
+
return new vscode.ThemeIcon('folder');
|
|
309
|
+
} else {
|
|
310
|
+
// Use file-specific icons based on extension
|
|
311
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
312
|
+
const iconMap: Record<string, string> = {
|
|
313
|
+
'.md': 'markdown',
|
|
314
|
+
'.json': 'json',
|
|
315
|
+
'.ts': 'symbol-namespace',
|
|
316
|
+
'.tsx': 'symbol-namespace',
|
|
317
|
+
'.js': 'symbol-method',
|
|
318
|
+
'.jsx': 'symbol-method',
|
|
319
|
+
'.py': 'symbol-misc',
|
|
320
|
+
'.yml': 'settings-gear',
|
|
321
|
+
'.yaml': 'settings-gear',
|
|
322
|
+
'.env': 'symbol-key',
|
|
323
|
+
'': 'file',
|
|
324
|
+
};
|
|
325
|
+
return new vscode.ThemeIcon(iconMap[ext] || 'file');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripts Tree View Provider
|
|
3
|
+
*
|
|
4
|
+
* Detects and displays project scripts from package.json, Makefile, tasks.json, etc.
|
|
5
|
+
* Provides one-click execution and status tracking.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as vscode from 'vscode';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
|
|
12
|
+
export interface ScriptItem {
|
|
13
|
+
name: string;
|
|
14
|
+
command: string;
|
|
15
|
+
source: string; // 'npm', 'make', 'task', 'custom'
|
|
16
|
+
description?: string;
|
|
17
|
+
isRunning?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ScriptsTreeProvider implements vscode.TreeDataProvider<ScriptTreeItem> {
|
|
21
|
+
private _onDidChangeTreeData: vscode.EventEmitter<ScriptTreeItem | undefined | null | void> =
|
|
22
|
+
new vscode.EventEmitter<ScriptTreeItem | undefined | null | void>();
|
|
23
|
+
readonly onDidChangeTreeData: vscode.Event<ScriptTreeItem | undefined | null | void> =
|
|
24
|
+
this._onDidChangeTreeData.event;
|
|
25
|
+
|
|
26
|
+
private scripts: Map<string, ScriptItem[]> = new Map();
|
|
27
|
+
private runningScripts: Map<string, vscode.Terminal> = new Map();
|
|
28
|
+
private customScripts: ScriptItem[] = [];
|
|
29
|
+
|
|
30
|
+
constructor(private context: vscode.ExtensionContext) {
|
|
31
|
+
// Load custom scripts from storage
|
|
32
|
+
this.customScripts = context.workspaceState.get('customScripts', []);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
refresh(): void {
|
|
36
|
+
this.detectScripts();
|
|
37
|
+
this._onDidChangeTreeData.fire();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getTreeItem(element: ScriptTreeItem): vscode.TreeItem {
|
|
41
|
+
return element;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getChildren(element?: ScriptTreeItem): Promise<ScriptTreeItem[]> {
|
|
45
|
+
if (!vscode.workspace.workspaceFolders) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If no scripts detected yet, detect them
|
|
50
|
+
if (this.scripts.size === 0 && this.customScripts.length === 0) {
|
|
51
|
+
await this.detectScripts();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!element) {
|
|
55
|
+
// Root level - return source categories
|
|
56
|
+
const categories: ScriptTreeItem[] = [];
|
|
57
|
+
|
|
58
|
+
for (const [source, scripts] of this.scripts.entries()) {
|
|
59
|
+
if (scripts.length > 0) {
|
|
60
|
+
categories.push(new ScriptTreeItem(
|
|
61
|
+
this.getSourceLabel(source),
|
|
62
|
+
vscode.TreeItemCollapsibleState.Expanded,
|
|
63
|
+
'category',
|
|
64
|
+
source
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add custom scripts category if any exist
|
|
70
|
+
if (this.customScripts.length > 0) {
|
|
71
|
+
categories.push(new ScriptTreeItem(
|
|
72
|
+
'⚡ Custom Commands',
|
|
73
|
+
vscode.TreeItemCollapsibleState.Expanded,
|
|
74
|
+
'category',
|
|
75
|
+
'custom'
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return categories;
|
|
80
|
+
} else {
|
|
81
|
+
// Child level - return scripts for this category
|
|
82
|
+
const source = element.source;
|
|
83
|
+
let sourceScripts: ScriptItem[] = [];
|
|
84
|
+
|
|
85
|
+
if (source === 'custom') {
|
|
86
|
+
sourceScripts = this.customScripts;
|
|
87
|
+
} else {
|
|
88
|
+
sourceScripts = this.scripts.get(source || '') || [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return sourceScripts.map(script => {
|
|
92
|
+
const isRunning = this.runningScripts.has(script.name);
|
|
93
|
+
const item = new ScriptTreeItem(
|
|
94
|
+
script.name,
|
|
95
|
+
vscode.TreeItemCollapsibleState.None,
|
|
96
|
+
isRunning ? 'runningScript' : 'script',
|
|
97
|
+
script.source
|
|
98
|
+
);
|
|
99
|
+
item.script = script;
|
|
100
|
+
item.description = isRunning ? '● Running' : script.description;
|
|
101
|
+
item.tooltip = `${script.command}\n\nClick to run`;
|
|
102
|
+
item.command = {
|
|
103
|
+
command: 'promptarchitect.runScript',
|
|
104
|
+
title: 'Run Script',
|
|
105
|
+
arguments: [item]
|
|
106
|
+
};
|
|
107
|
+
item.iconPath = new vscode.ThemeIcon(isRunning ? 'loading~spin' : 'play');
|
|
108
|
+
return item;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private getSourceLabel(source: string): string {
|
|
114
|
+
const labels: Record<string, string> = {
|
|
115
|
+
'npm': '📦 package.json',
|
|
116
|
+
'make': '🔧 Makefile',
|
|
117
|
+
'task': '⚙️ tasks.json',
|
|
118
|
+
'cargo': '🦀 Cargo.toml',
|
|
119
|
+
'python': '🐍 pyproject.toml',
|
|
120
|
+
};
|
|
121
|
+
return labels[source] || source;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async detectScripts(): Promise<void> {
|
|
125
|
+
this.scripts.clear();
|
|
126
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
127
|
+
if (!workspaceFolder) return;
|
|
128
|
+
|
|
129
|
+
const rootPath = workspaceFolder.uri.fsPath;
|
|
130
|
+
|
|
131
|
+
// Detect npm scripts from package.json
|
|
132
|
+
await this.detectNpmScripts(rootPath);
|
|
133
|
+
|
|
134
|
+
// Detect tasks from tasks.json
|
|
135
|
+
await this.detectTasksJson(rootPath);
|
|
136
|
+
|
|
137
|
+
// Detect Makefile targets
|
|
138
|
+
await this.detectMakefileTargets(rootPath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async detectNpmScripts(rootPath: string): Promise<void> {
|
|
142
|
+
const packageJsonPath = path.join(rootPath, 'package.json');
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
146
|
+
const content = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
147
|
+
const pkg = JSON.parse(content);
|
|
148
|
+
|
|
149
|
+
if (pkg.scripts) {
|
|
150
|
+
const scripts: ScriptItem[] = Object.entries(pkg.scripts).map(([name, command]) => ({
|
|
151
|
+
name,
|
|
152
|
+
command: `npm run ${name}`,
|
|
153
|
+
source: 'npm',
|
|
154
|
+
description: String(command).substring(0, 50) + (String(command).length > 50 ? '...' : ''),
|
|
155
|
+
}));
|
|
156
|
+
this.scripts.set('npm', scripts);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error detecting npm scripts:', error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async detectTasksJson(rootPath: string): Promise<void> {
|
|
165
|
+
const tasksPath = path.join(rootPath, '.vscode', 'tasks.json');
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (fs.existsSync(tasksPath)) {
|
|
169
|
+
const content = fs.readFileSync(tasksPath, 'utf-8');
|
|
170
|
+
// Remove comments from JSON (VS Code allows them)
|
|
171
|
+
const cleanContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
172
|
+
const tasks = JSON.parse(cleanContent);
|
|
173
|
+
|
|
174
|
+
if (tasks.tasks && Array.isArray(tasks.tasks)) {
|
|
175
|
+
const scripts: ScriptItem[] = tasks.tasks.map((task: any) => ({
|
|
176
|
+
name: task.label || task.taskName || 'Unnamed Task',
|
|
177
|
+
command: task.command || task.script || '',
|
|
178
|
+
source: 'task',
|
|
179
|
+
description: task.detail || task.type,
|
|
180
|
+
}));
|
|
181
|
+
this.scripts.set('task', scripts);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('Error detecting tasks.json:', error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async detectMakefileTargets(rootPath: string): Promise<void> {
|
|
190
|
+
const makefilePath = path.join(rootPath, 'Makefile');
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
if (fs.existsSync(makefilePath)) {
|
|
194
|
+
const content = fs.readFileSync(makefilePath, 'utf-8');
|
|
195
|
+
const targetRegex = /^([a-zA-Z_][a-zA-Z0-9_-]*):/gm;
|
|
196
|
+
const targets: ScriptItem[] = [];
|
|
197
|
+
|
|
198
|
+
let match;
|
|
199
|
+
while ((match = targetRegex.exec(content)) !== null) {
|
|
200
|
+
const targetName = match[1];
|
|
201
|
+
// Skip common non-runnable targets
|
|
202
|
+
if (!targetName.startsWith('.') && !['PHONY', 'FORCE'].includes(targetName)) {
|
|
203
|
+
targets.push({
|
|
204
|
+
name: targetName,
|
|
205
|
+
command: `make ${targetName}`,
|
|
206
|
+
source: 'make',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (targets.length > 0) {
|
|
212
|
+
this.scripts.set('make', targets);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('Error detecting Makefile targets:', error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async runScript(item: ScriptTreeItem): Promise<void> {
|
|
221
|
+
if (!item.script) return;
|
|
222
|
+
|
|
223
|
+
const script = item.script;
|
|
224
|
+
const terminalName = `PromptArchitect: ${script.name}`;
|
|
225
|
+
|
|
226
|
+
// Check if already running
|
|
227
|
+
if (this.runningScripts.has(script.name)) {
|
|
228
|
+
const existingTerminal = this.runningScripts.get(script.name);
|
|
229
|
+
existingTerminal?.show();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Create new terminal and run
|
|
234
|
+
const terminal = vscode.window.createTerminal({
|
|
235
|
+
name: terminalName,
|
|
236
|
+
cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
terminal.show();
|
|
240
|
+
terminal.sendText(script.command);
|
|
241
|
+
|
|
242
|
+
// Track running script
|
|
243
|
+
this.runningScripts.set(script.name, terminal);
|
|
244
|
+
this._onDidChangeTreeData.fire();
|
|
245
|
+
|
|
246
|
+
// Listen for terminal close
|
|
247
|
+
const disposable = vscode.window.onDidCloseTerminal((closedTerminal) => {
|
|
248
|
+
if (closedTerminal === terminal) {
|
|
249
|
+
this.runningScripts.delete(script.name);
|
|
250
|
+
this._onDidChangeTreeData.fire();
|
|
251
|
+
disposable.dispose();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
stopScript(item: ScriptTreeItem): void {
|
|
257
|
+
if (!item.script) return;
|
|
258
|
+
|
|
259
|
+
const terminal = this.runningScripts.get(item.script.name);
|
|
260
|
+
if (terminal) {
|
|
261
|
+
terminal.dispose();
|
|
262
|
+
this.runningScripts.delete(item.script.name);
|
|
263
|
+
this._onDidChangeTreeData.fire();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async addCustomScript(): Promise<void> {
|
|
268
|
+
const name = await vscode.window.showInputBox({
|
|
269
|
+
prompt: 'Enter a name for this command',
|
|
270
|
+
placeHolder: 'e.g., deploy-staging',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (!name) return;
|
|
274
|
+
|
|
275
|
+
const command = await vscode.window.showInputBox({
|
|
276
|
+
prompt: 'Enter the command to run',
|
|
277
|
+
placeHolder: 'e.g., npm run build && firebase deploy',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (!command) return;
|
|
281
|
+
|
|
282
|
+
const script: ScriptItem = {
|
|
283
|
+
name,
|
|
284
|
+
command,
|
|
285
|
+
source: 'custom',
|
|
286
|
+
description: command.substring(0, 50),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
this.customScripts.push(script);
|
|
290
|
+
await this.context.workspaceState.update('customScripts', this.customScripts);
|
|
291
|
+
this._onDidChangeTreeData.fire();
|
|
292
|
+
|
|
293
|
+
vscode.window.showInformationMessage(`Custom command "${name}" added!`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export class ScriptTreeItem extends vscode.TreeItem {
|
|
298
|
+
public script?: ScriptItem;
|
|
299
|
+
public source?: string;
|
|
300
|
+
|
|
301
|
+
constructor(
|
|
302
|
+
public readonly label: string,
|
|
303
|
+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
|
304
|
+
public readonly contextValue: string,
|
|
305
|
+
source?: string
|
|
306
|
+
) {
|
|
307
|
+
super(label, collapsibleState);
|
|
308
|
+
this.source = source;
|
|
309
|
+
|
|
310
|
+
if (contextValue === 'category') {
|
|
311
|
+
this.iconPath = this.getCategoryIcon(source || '');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private getCategoryIcon(source: string): vscode.ThemeIcon {
|
|
316
|
+
const icons: Record<string, string> = {
|
|
317
|
+
'npm': 'package',
|
|
318
|
+
'make': 'tools',
|
|
319
|
+
'task': 'tasklist',
|
|
320
|
+
'custom': 'zap',
|
|
321
|
+
};
|
|
322
|
+
return new vscode.ThemeIcon(icons[source] || 'folder');
|
|
323
|
+
}
|
|
324
|
+
}
|