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,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
+ }