statusbar-quick-actions 0.0.10
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/.github/FUNDING.yml +3 -0
- package/.vscodeignore +11 -0
- package/CLAUDE.md +230 -0
- package/LICENSE +21 -0
- package/README.md +529 -0
- package/assets/icon.png +0 -0
- package/bun.lock +908 -0
- package/docs/PERFORMANCE_OPTIMIZATIONS.md +240 -0
- package/docs/PRESET_AND_DYNAMIC_LABELS.md +536 -0
- package/docs/SAMPLE-CONFIGURATIONS.md +973 -0
- package/eslint.config.mjs +41 -0
- package/package.json +605 -0
- package/src/config-cli.ts +1287 -0
- package/src/configuration.ts +530 -0
- package/src/dynamic-label.ts +360 -0
- package/src/executor.ts +406 -0
- package/src/extension.ts +1754 -0
- package/src/history.ts +175 -0
- package/src/material-icons.ts +388 -0
- package/src/notifications.ts +189 -0
- package/src/output-panel.ts +403 -0
- package/src/preset-manager.ts +406 -0
- package/src/theme.ts +318 -0
- package/src/types.ts +368 -0
- package/src/utils/debounce.ts +91 -0
- package/src/visibility.ts +283 -0
- package/tsconfig.dev.json +10 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Statusbar Quick Actions Configuration CLI
|
|
5
|
+
*
|
|
6
|
+
* A modern CLI tool for managing the Statusbar Quick Actions VSCode extension.
|
|
7
|
+
* Provides installation, uninstallation, and configuration management capabilities.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import consoleClear from "console-clear";
|
|
14
|
+
import pkg from "../package.json";
|
|
15
|
+
import type { StatusBarButtonConfig, ExtensionConfig } from "./types";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// TYPE DEFINITIONS
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/** Menu option definition */
|
|
22
|
+
interface MenuOption {
|
|
23
|
+
readonly key: string;
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly description: string;
|
|
26
|
+
readonly action: () => Promise<void> | void;
|
|
27
|
+
readonly requiresConfirmation?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** User input validation result */
|
|
31
|
+
interface ValidationResult {
|
|
32
|
+
readonly isValid: boolean;
|
|
33
|
+
readonly value?: string;
|
|
34
|
+
readonly error?: string;
|
|
35
|
+
readonly suggestion?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Application state interface */
|
|
39
|
+
interface ApplicationState {
|
|
40
|
+
isRunning: boolean;
|
|
41
|
+
operationCount: number;
|
|
42
|
+
startTime: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Settings location */
|
|
46
|
+
type SettingsLocation = "user" | "workspace";
|
|
47
|
+
|
|
48
|
+
/** VSCode Settings structure */
|
|
49
|
+
interface VSCodeSettings {
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
"statusbarQuickActions.buttons"?: StatusBarButtonConfig[];
|
|
52
|
+
"statusbarQuickActions.settings.debug"?: boolean;
|
|
53
|
+
"statusbarQuickActions.settings.theme"?: unknown;
|
|
54
|
+
"statusbarQuickActions.settings.output"?: unknown;
|
|
55
|
+
"statusbarQuickActions.settings.performance"?: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Preset definition */
|
|
59
|
+
interface Preset {
|
|
60
|
+
name: string;
|
|
61
|
+
description: string;
|
|
62
|
+
buttons: StatusBarButtonConfig[];
|
|
63
|
+
metadata?: {
|
|
64
|
+
created: Date;
|
|
65
|
+
modified: Date;
|
|
66
|
+
author?: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// CONSTANTS AND CONFIGURATION
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
const CONFIG = {
|
|
75
|
+
banner: "=".repeat(50),
|
|
76
|
+
appName: "Statusbar Quick Actions - Config CLI",
|
|
77
|
+
separator: "\n",
|
|
78
|
+
version: pkg.version,
|
|
79
|
+
maxRetries: 3,
|
|
80
|
+
timeoutMs: 30000,
|
|
81
|
+
colors: {
|
|
82
|
+
primary: "\x1b[36m", // Cyan
|
|
83
|
+
success: "\x1b[32m", // Green
|
|
84
|
+
error: "\x1b[31m", // Red
|
|
85
|
+
warning: "\x1b[33m", // Yellow
|
|
86
|
+
reset: "\x1b[0m", // Reset
|
|
87
|
+
bold: "\x1b[1m", // Bold
|
|
88
|
+
dim: "\x1b[2m", // Dim
|
|
89
|
+
},
|
|
90
|
+
} as const;
|
|
91
|
+
|
|
92
|
+
/** Preset definitions */
|
|
93
|
+
const BUILTIN_PRESETS: Record<string, Preset> = {
|
|
94
|
+
"node-dev": {
|
|
95
|
+
name: "Node.js Development",
|
|
96
|
+
description: "Common Node.js development tasks",
|
|
97
|
+
buttons: [
|
|
98
|
+
{
|
|
99
|
+
id: "npm_start",
|
|
100
|
+
text: "▶️ Start",
|
|
101
|
+
tooltip: "Start the application",
|
|
102
|
+
command: { type: "npm", script: "start" },
|
|
103
|
+
enabled: true,
|
|
104
|
+
alignment: "left",
|
|
105
|
+
priority: 100,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "npm_test",
|
|
109
|
+
text: "🧪 Test",
|
|
110
|
+
tooltip: "Run tests",
|
|
111
|
+
command: { type: "npm", script: "test" },
|
|
112
|
+
enabled: true,
|
|
113
|
+
alignment: "left",
|
|
114
|
+
priority: 99,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "npm_build",
|
|
118
|
+
text: "🔨 Build",
|
|
119
|
+
tooltip: "Build the project",
|
|
120
|
+
command: { type: "npm", script: "build" },
|
|
121
|
+
enabled: true,
|
|
122
|
+
alignment: "left",
|
|
123
|
+
priority: 98,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
metadata: {
|
|
127
|
+
created: new Date(),
|
|
128
|
+
modified: new Date(),
|
|
129
|
+
author: "StatusBar Quick Actions",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
"bun-dev": {
|
|
133
|
+
name: "Bun Development",
|
|
134
|
+
description: "Common Bun development tasks",
|
|
135
|
+
buttons: [
|
|
136
|
+
{
|
|
137
|
+
id: "bun_dev",
|
|
138
|
+
text: "⚡ Dev",
|
|
139
|
+
tooltip: "Start development server",
|
|
140
|
+
command: { type: "bun", script: "dev" },
|
|
141
|
+
enabled: true,
|
|
142
|
+
alignment: "left",
|
|
143
|
+
priority: 100,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "bun_test",
|
|
147
|
+
text: "🧪 Test",
|
|
148
|
+
tooltip: "Run tests with Bun",
|
|
149
|
+
command: { type: "bun", script: "test" },
|
|
150
|
+
enabled: true,
|
|
151
|
+
alignment: "left",
|
|
152
|
+
priority: 99,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "bun_build",
|
|
156
|
+
text: "🔨 Build",
|
|
157
|
+
tooltip: "Build with Bun",
|
|
158
|
+
command: { type: "bun", script: "build" },
|
|
159
|
+
enabled: true,
|
|
160
|
+
alignment: "left",
|
|
161
|
+
priority: 98,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
metadata: {
|
|
165
|
+
created: new Date(),
|
|
166
|
+
modified: new Date(),
|
|
167
|
+
author: "StatusBar Quick Actions",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
"git-workflow": {
|
|
171
|
+
name: "Git Workflow",
|
|
172
|
+
description: "Common Git operations",
|
|
173
|
+
buttons: [
|
|
174
|
+
{
|
|
175
|
+
id: "git_status",
|
|
176
|
+
text: "📊 Status",
|
|
177
|
+
tooltip: "Show git status",
|
|
178
|
+
command: { type: "shell", command: "git status" },
|
|
179
|
+
enabled: true,
|
|
180
|
+
alignment: "right",
|
|
181
|
+
priority: 100,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "git_pull",
|
|
185
|
+
text: "⬇️ Pull",
|
|
186
|
+
tooltip: "Pull from remote",
|
|
187
|
+
command: { type: "shell", command: "git pull" },
|
|
188
|
+
enabled: true,
|
|
189
|
+
alignment: "right",
|
|
190
|
+
priority: 99,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "git_push",
|
|
194
|
+
text: "⬆️ Push",
|
|
195
|
+
tooltip: "Push to remote",
|
|
196
|
+
command: { type: "shell", command: "git push" },
|
|
197
|
+
enabled: true,
|
|
198
|
+
alignment: "right",
|
|
199
|
+
priority: 98,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
metadata: {
|
|
203
|
+
created: new Date(),
|
|
204
|
+
modified: new Date(),
|
|
205
|
+
author: "StatusBar Quick Actions",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** Menu options with enhanced descriptions */
|
|
211
|
+
const MENU_OPTIONS: readonly MenuOption[] = [
|
|
212
|
+
{
|
|
213
|
+
key: "1",
|
|
214
|
+
label: "View Current Configuration",
|
|
215
|
+
description: "Display current button configurations",
|
|
216
|
+
action: viewCurrentConfiguration,
|
|
217
|
+
requiresConfirmation: false,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
key: "2",
|
|
221
|
+
label: "Apply Preset",
|
|
222
|
+
description: "Apply a built-in preset configuration",
|
|
223
|
+
action: applyPreset,
|
|
224
|
+
requiresConfirmation: true,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
key: "3",
|
|
228
|
+
label: "Add Button",
|
|
229
|
+
description: "Add a new button to the status bar",
|
|
230
|
+
action: addButton,
|
|
231
|
+
requiresConfirmation: false,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
key: "4",
|
|
235
|
+
label: "Remove Button",
|
|
236
|
+
description: "Remove a button from the status bar",
|
|
237
|
+
action: removeButton,
|
|
238
|
+
requiresConfirmation: true,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
key: "5",
|
|
242
|
+
label: "Toggle Debug Mode",
|
|
243
|
+
description: "Enable or disable debug logging",
|
|
244
|
+
action: toggleDebugMode,
|
|
245
|
+
requiresConfirmation: false,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
key: "6",
|
|
249
|
+
label: "Export Configuration",
|
|
250
|
+
description: "Export current configuration to a file",
|
|
251
|
+
action: exportConfiguration,
|
|
252
|
+
requiresConfirmation: false,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
key: "7",
|
|
256
|
+
label: "Import Configuration",
|
|
257
|
+
description: "Import configuration from a file",
|
|
258
|
+
action: importConfiguration,
|
|
259
|
+
requiresConfirmation: true,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
key: "8",
|
|
263
|
+
label: "Reset to Defaults",
|
|
264
|
+
description: "Reset all settings to default values",
|
|
265
|
+
action: resetToDefaults,
|
|
266
|
+
requiresConfirmation: true,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
key: "9",
|
|
270
|
+
label: "Help",
|
|
271
|
+
description: "Show help and usage information",
|
|
272
|
+
action: showHelp,
|
|
273
|
+
requiresConfirmation: false,
|
|
274
|
+
},
|
|
275
|
+
] as const;
|
|
276
|
+
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// CUSTOM ERROR TYPES
|
|
279
|
+
// ============================================================================
|
|
280
|
+
|
|
281
|
+
/** Base application error */
|
|
282
|
+
export class ConfigCLIError extends Error {
|
|
283
|
+
constructor(
|
|
284
|
+
message: string,
|
|
285
|
+
public readonly code: string,
|
|
286
|
+
public readonly suggestion?: string,
|
|
287
|
+
) {
|
|
288
|
+
super(message);
|
|
289
|
+
this.name = "ConfigCLIError";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Input validation error */
|
|
294
|
+
export class ValidationError extends ConfigCLIError {
|
|
295
|
+
constructor(message: string, suggestion?: string) {
|
|
296
|
+
super(message, "VALIDATION_ERROR", suggestion);
|
|
297
|
+
this.name = "ValidationError";
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Operation timeout error */
|
|
302
|
+
export class TimeoutError extends ConfigCLIError {
|
|
303
|
+
constructor(operation: string, timeoutMs: number) {
|
|
304
|
+
super(
|
|
305
|
+
`${operation} operation timed out after ${timeoutMs}ms`,
|
|
306
|
+
"TIMEOUT_ERROR",
|
|
307
|
+
);
|
|
308
|
+
this.name = "TimeoutError";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Settings file error */
|
|
313
|
+
export class SettingsError extends ConfigCLIError {
|
|
314
|
+
constructor(message: string, suggestion?: string) {
|
|
315
|
+
super(message, "SETTINGS_ERROR", suggestion);
|
|
316
|
+
this.name = "SettingsError";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ============================================================================
|
|
321
|
+
// UTILITY CLASSES
|
|
322
|
+
// ============================================================================
|
|
323
|
+
|
|
324
|
+
/** Enhanced console UI with colors and formatting */
|
|
325
|
+
class ConsoleUI {
|
|
326
|
+
/** Clear the console screen */
|
|
327
|
+
static clear(): void {
|
|
328
|
+
consoleClear(true);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Print colored text */
|
|
332
|
+
private static printColored(text: string, color: string): void {
|
|
333
|
+
console.log(`${color}${text}${CONFIG.colors.reset}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Print the application banner */
|
|
337
|
+
static printBanner(): void {
|
|
338
|
+
const { colors } = CONFIG;
|
|
339
|
+
console.log(`${colors.primary}${CONFIG.banner}${CONFIG.colors.reset}`);
|
|
340
|
+
console.log(
|
|
341
|
+
`${colors.bold}${colors.primary}${CONFIG.appName} v${CONFIG.version}${CONFIG.colors.reset}`,
|
|
342
|
+
);
|
|
343
|
+
console.log(`${colors.primary}${CONFIG.banner}${CONFIG.colors.reset}`);
|
|
344
|
+
console.log(CONFIG.separator);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Print the main menu */
|
|
348
|
+
static printMenu(): void {
|
|
349
|
+
console.log("What would you like to do?", CONFIG.separator);
|
|
350
|
+
|
|
351
|
+
for (const option of MENU_OPTIONS) {
|
|
352
|
+
console.log(` ${option.key}. ${option.label} - ${option.description}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log(CONFIG.banner);
|
|
356
|
+
console.log("Enter your choice (or 'q' to quit): ");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Print an error message */
|
|
360
|
+
static printError(message: string, suggestion?: string): void {
|
|
361
|
+
this.printColored(`❌ ${message}`, CONFIG.colors.error);
|
|
362
|
+
if (suggestion) {
|
|
363
|
+
this.printColored(`💡 ${suggestion}`, CONFIG.colors.warning);
|
|
364
|
+
}
|
|
365
|
+
console.log(CONFIG.separator);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Print a success message */
|
|
369
|
+
static printSuccess(message: string): void {
|
|
370
|
+
this.printColored(`✅ ${message}`, CONFIG.colors.success);
|
|
371
|
+
console.log(CONFIG.separator);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Print a warning message */
|
|
375
|
+
static printWarning(message: string): void {
|
|
376
|
+
this.printColored(`⚠️ ${message}`, CONFIG.colors.warning);
|
|
377
|
+
console.log(CONFIG.separator);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Print an info message */
|
|
381
|
+
static printInfo(message: string): void {
|
|
382
|
+
this.printColored(`ℹ️ ${message}`, CONFIG.colors.primary);
|
|
383
|
+
console.log(CONFIG.separator);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Print divider line */
|
|
387
|
+
static printDivider(char = "─"): void {
|
|
388
|
+
console.log(char.repeat(50));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Print a table */
|
|
392
|
+
static printTable(headers: string[], rows: string[][]): void {
|
|
393
|
+
const colWidths = headers.map((h, i) =>
|
|
394
|
+
Math.max(h.length, ...rows.map((r) => (r[i] || "").length)),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Print header
|
|
398
|
+
const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(" | ");
|
|
399
|
+
console.log(headerRow);
|
|
400
|
+
console.log(colWidths.map((w) => "─".repeat(w)).join("─┼─"));
|
|
401
|
+
|
|
402
|
+
// Print rows
|
|
403
|
+
rows.forEach((row) => {
|
|
404
|
+
console.log(
|
|
405
|
+
row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" | "),
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Settings manager for VSCode settings files */
|
|
412
|
+
class SettingsManager {
|
|
413
|
+
private static getUserSettingsPath(): string {
|
|
414
|
+
const platform = process.platform;
|
|
415
|
+
let settingsDir: string;
|
|
416
|
+
|
|
417
|
+
if (platform === "win32") {
|
|
418
|
+
settingsDir = join(
|
|
419
|
+
process.env.APPDATA || join(homedir(), "AppData", "Roaming"),
|
|
420
|
+
"Code",
|
|
421
|
+
"User",
|
|
422
|
+
);
|
|
423
|
+
} else if (platform === "darwin") {
|
|
424
|
+
settingsDir = join(
|
|
425
|
+
homedir(),
|
|
426
|
+
"Library",
|
|
427
|
+
"Application Support",
|
|
428
|
+
"Code",
|
|
429
|
+
"User",
|
|
430
|
+
);
|
|
431
|
+
} else {
|
|
432
|
+
settingsDir = join(homedir(), ".config", "Code", "User");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return join(settingsDir, "settings.json");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private static getWorkspaceSettingsPath(): string {
|
|
439
|
+
// Assume current working directory is the workspace
|
|
440
|
+
const workspaceRoot = process.cwd();
|
|
441
|
+
return join(workspaceRoot, ".vscode", "settings.json");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
static getSettingsPath(location: SettingsLocation): string {
|
|
445
|
+
return location === "user"
|
|
446
|
+
? this.getUserSettingsPath()
|
|
447
|
+
: this.getWorkspaceSettingsPath();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
static readSettings(location: SettingsLocation): VSCodeSettings {
|
|
451
|
+
const settingsPath = this.getSettingsPath(location);
|
|
452
|
+
|
|
453
|
+
if (!existsSync(settingsPath)) {
|
|
454
|
+
// Create settings file if it doesn't exist
|
|
455
|
+
const settingsDir = dirname(settingsPath);
|
|
456
|
+
if (!existsSync(settingsDir)) {
|
|
457
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
458
|
+
}
|
|
459
|
+
writeFileSync(settingsPath, "{}", "utf-8");
|
|
460
|
+
return {};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const content = readFileSync(settingsPath, "utf-8");
|
|
465
|
+
// Remove comments and trailing commas (VSCode allows these in settings.json)
|
|
466
|
+
const cleanedContent = content
|
|
467
|
+
.replace(/\/\/.*$/gm, "") // Remove single-line comments
|
|
468
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // Remove multi-line comments
|
|
469
|
+
.replace(/,(\s*[}\]])/g, "$1"); // Remove trailing commas
|
|
470
|
+
|
|
471
|
+
return JSON.parse(cleanedContent);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
throw new SettingsError(
|
|
474
|
+
`Failed to read settings from ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
475
|
+
"Check if the file is valid JSON",
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
static writeSettings(
|
|
481
|
+
location: SettingsLocation,
|
|
482
|
+
settings: VSCodeSettings,
|
|
483
|
+
): void {
|
|
484
|
+
const settingsPath = this.getSettingsPath(location);
|
|
485
|
+
const settingsDir = dirname(settingsPath);
|
|
486
|
+
|
|
487
|
+
if (!existsSync(settingsDir)) {
|
|
488
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const content = JSON.stringify(settings, null, 2);
|
|
493
|
+
writeFileSync(settingsPath, content, "utf-8");
|
|
494
|
+
} catch (error) {
|
|
495
|
+
throw new SettingsError(
|
|
496
|
+
`Failed to write settings to ${settingsPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
497
|
+
"Check if you have write permissions",
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
static getButtons(location: SettingsLocation): StatusBarButtonConfig[] {
|
|
503
|
+
const settings = this.readSettings(location);
|
|
504
|
+
return settings["statusbarQuickActions.buttons"] || [];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
static setButtons(
|
|
508
|
+
location: SettingsLocation,
|
|
509
|
+
buttons: StatusBarButtonConfig[],
|
|
510
|
+
): void {
|
|
511
|
+
const settings = this.readSettings(location);
|
|
512
|
+
settings["statusbarQuickActions.buttons"] = buttons;
|
|
513
|
+
this.writeSettings(location, settings);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
static getDebugMode(location: SettingsLocation): boolean {
|
|
517
|
+
const settings = this.readSettings(location);
|
|
518
|
+
return settings["statusbarQuickActions.settings.debug"] || false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
static setDebugMode(location: SettingsLocation, enabled: boolean): void {
|
|
522
|
+
const settings = this.readSettings(location);
|
|
523
|
+
settings["statusbarQuickActions.settings.debug"] = enabled;
|
|
524
|
+
this.writeSettings(location, settings);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Enhanced input validator with better error handling */
|
|
529
|
+
class InputValidator {
|
|
530
|
+
/** Validate user choice input */
|
|
531
|
+
static validateChoice(input: string): ValidationResult {
|
|
532
|
+
const trimmedInput = input.trim().toLowerCase();
|
|
533
|
+
|
|
534
|
+
// Handle empty input
|
|
535
|
+
if (!trimmedInput) {
|
|
536
|
+
return {
|
|
537
|
+
isValid: false,
|
|
538
|
+
error: "Please enter a valid choice",
|
|
539
|
+
suggestion: `Type a number (1-${MENU_OPTIONS.length}) or 'q' to quit`,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Handle quit commands
|
|
544
|
+
if (this.isQuitCommand(trimmedInput)) {
|
|
545
|
+
return { isValid: false, value: "quit" };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Validate against menu options
|
|
549
|
+
const option = MENU_OPTIONS.find((opt) => opt.key === trimmedInput);
|
|
550
|
+
|
|
551
|
+
if (option) {
|
|
552
|
+
return { isValid: true, value: trimmedInput };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Provide helpful suggestions for invalid input
|
|
556
|
+
const suggestions = this.getSuggestions(trimmedInput);
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
isValid: false,
|
|
560
|
+
error: `Invalid choice '${input}'`,
|
|
561
|
+
suggestion:
|
|
562
|
+
suggestions.length > 0
|
|
563
|
+
? `Did you mean: ${suggestions.join(", ")}?`
|
|
564
|
+
: `Please select a valid option (1-${MENU_OPTIONS.length}) or 'q' to quit`,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Check if input is a quit command */
|
|
569
|
+
private static isQuitCommand(input: string): boolean {
|
|
570
|
+
return ["q", "quit", "exit", "x"].includes(input);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Get suggestions for invalid input using fuzzy matching */
|
|
574
|
+
private static getSuggestions(input: string): string[] {
|
|
575
|
+
const suggestions: string[] = [];
|
|
576
|
+
|
|
577
|
+
// Check for numeric input
|
|
578
|
+
if (/^\d+$/.test(input)) {
|
|
579
|
+
const num = parseInt(input, 10);
|
|
580
|
+
if (num > 0 && num <= MENU_OPTIONS.length + 5) {
|
|
581
|
+
suggestions.push(`${num}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Check for partial matches with menu options
|
|
586
|
+
for (const option of MENU_OPTIONS) {
|
|
587
|
+
if (option.label.toLowerCase().includes(input) || option.key === input) {
|
|
588
|
+
suggestions.push(option.key);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return suggestions.slice(0, 3);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Enhanced input handler with timeout and retry logic */
|
|
597
|
+
class InputHandler {
|
|
598
|
+
/** Get user input with timeout */
|
|
599
|
+
static async getInput(
|
|
600
|
+
timeoutMs: number = CONFIG.timeoutMs,
|
|
601
|
+
): Promise<string | null> {
|
|
602
|
+
try {
|
|
603
|
+
// Check for command line arguments first (non-interactive mode)
|
|
604
|
+
if (process.argv.length > 2) {
|
|
605
|
+
const arg = process.argv[2];
|
|
606
|
+
return arg;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Interactive mode with stdin using Promise with timeout
|
|
610
|
+
return await Promise.race([
|
|
611
|
+
new Promise<string>((resolve) => {
|
|
612
|
+
const stdin = process.stdin;
|
|
613
|
+
stdin.setEncoding("utf-8");
|
|
614
|
+
stdin.once("data", (data) => {
|
|
615
|
+
resolve(data.toString().trim());
|
|
616
|
+
});
|
|
617
|
+
}),
|
|
618
|
+
new Promise<never>((_, reject) => {
|
|
619
|
+
setTimeout(() => {
|
|
620
|
+
reject(new TimeoutError("Input", timeoutMs));
|
|
621
|
+
}, timeoutMs);
|
|
622
|
+
}),
|
|
623
|
+
]);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (error instanceof TimeoutError) {
|
|
626
|
+
ConsoleUI.printError("Input timeout", "Please try again");
|
|
627
|
+
} else {
|
|
628
|
+
ConsoleUI.printError(
|
|
629
|
+
"Failed to read input",
|
|
630
|
+
"Please check your terminal configuration",
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Get confirmation from user */
|
|
638
|
+
static async getConfirmation(message: string): Promise<boolean> {
|
|
639
|
+
console.log(`${message} (y/N): `);
|
|
640
|
+
const input = await this.getInput(10000);
|
|
641
|
+
|
|
642
|
+
if (!input) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return ["y", "yes", "yeah", "yep"].includes(input.toLowerCase());
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Prompt for settings location */
|
|
650
|
+
static async promptLocation(): Promise<SettingsLocation> {
|
|
651
|
+
console.log("\nSelect settings location:");
|
|
652
|
+
console.log(" 1. User settings (global)");
|
|
653
|
+
console.log(" 2. Workspace settings (project-specific)");
|
|
654
|
+
console.log("\nChoice (1 or 2): ");
|
|
655
|
+
|
|
656
|
+
const input = await this.getInput(10000);
|
|
657
|
+
|
|
658
|
+
if (input === "2") {
|
|
659
|
+
return "workspace";
|
|
660
|
+
}
|
|
661
|
+
return "user";
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ============================================================================
|
|
666
|
+
// OPERATION HANDLERS
|
|
667
|
+
// ============================================================================
|
|
668
|
+
|
|
669
|
+
/** View current configuration */
|
|
670
|
+
async function viewCurrentConfiguration(): Promise<void> {
|
|
671
|
+
ConsoleUI.clear();
|
|
672
|
+
ConsoleUI.printBanner();
|
|
673
|
+
|
|
674
|
+
const location = await InputHandler.promptLocation();
|
|
675
|
+
const buttons = SettingsManager.getButtons(location);
|
|
676
|
+
const debugMode = SettingsManager.getDebugMode(location);
|
|
677
|
+
|
|
678
|
+
ConsoleUI.printDivider();
|
|
679
|
+
console.log(`Configuration (${location} settings)`);
|
|
680
|
+
ConsoleUI.printDivider();
|
|
681
|
+
console.log();
|
|
682
|
+
|
|
683
|
+
console.log(`Debug Mode: ${debugMode ? "✅ Enabled" : "❌ Disabled"}`);
|
|
684
|
+
console.log(`Total Buttons: ${buttons.length}`);
|
|
685
|
+
console.log();
|
|
686
|
+
|
|
687
|
+
if (buttons.length === 0) {
|
|
688
|
+
ConsoleUI.printWarning("No buttons configured");
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
ConsoleUI.printDivider();
|
|
693
|
+
console.log("Buttons:");
|
|
694
|
+
ConsoleUI.printDivider();
|
|
695
|
+
|
|
696
|
+
const rows = buttons.map((btn, idx) => [
|
|
697
|
+
`${idx + 1}`,
|
|
698
|
+
btn.id,
|
|
699
|
+
btn.text,
|
|
700
|
+
btn.command.type,
|
|
701
|
+
btn.enabled === false ? "❌" : "✅",
|
|
702
|
+
]);
|
|
703
|
+
|
|
704
|
+
ConsoleUI.printTable(["#", "ID", "Text", "Type", "Enabled"], rows);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Apply a preset configuration */
|
|
708
|
+
async function applyPreset(): Promise<void> {
|
|
709
|
+
ConsoleUI.clear();
|
|
710
|
+
ConsoleUI.printBanner();
|
|
711
|
+
|
|
712
|
+
console.log("Available Presets:");
|
|
713
|
+
ConsoleUI.printDivider();
|
|
714
|
+
|
|
715
|
+
const presetKeys = Object.keys(BUILTIN_PRESETS);
|
|
716
|
+
presetKeys.forEach((key, idx) => {
|
|
717
|
+
const preset = BUILTIN_PRESETS[key];
|
|
718
|
+
console.log(` ${idx + 1}. ${preset.name}`);
|
|
719
|
+
console.log(
|
|
720
|
+
` ${CONFIG.colors.dim}${preset.description}${CONFIG.colors.reset}`,
|
|
721
|
+
);
|
|
722
|
+
console.log(
|
|
723
|
+
` ${CONFIG.colors.dim}${preset.buttons.length} buttons${CONFIG.colors.reset}`,
|
|
724
|
+
);
|
|
725
|
+
console.log();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
console.log("Select preset (1-" + presetKeys.length + "): ");
|
|
729
|
+
const input = await InputHandler.getInput(10000);
|
|
730
|
+
|
|
731
|
+
if (!input) {
|
|
732
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const index = parseInt(input, 10) - 1;
|
|
737
|
+
if (index < 0 || index >= presetKeys.length) {
|
|
738
|
+
ConsoleUI.printError("Invalid selection");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const presetKey = presetKeys[index];
|
|
743
|
+
const preset = BUILTIN_PRESETS[presetKey];
|
|
744
|
+
|
|
745
|
+
ConsoleUI.printInfo(`Selected: ${preset.name}`);
|
|
746
|
+
|
|
747
|
+
const location = await InputHandler.promptLocation();
|
|
748
|
+
const existingButtons = SettingsManager.getButtons(location);
|
|
749
|
+
|
|
750
|
+
if (existingButtons.length > 0) {
|
|
751
|
+
console.log("\nMerge mode:");
|
|
752
|
+
console.log(" 1. Replace all (removes existing buttons)");
|
|
753
|
+
console.log(" 2. Append (adds to existing buttons)");
|
|
754
|
+
console.log(" 3. Merge (replaces buttons with same ID)");
|
|
755
|
+
console.log("\nChoice (1-3): ");
|
|
756
|
+
|
|
757
|
+
const mergeInput = await InputHandler.getInput(10000);
|
|
758
|
+
let buttons: StatusBarButtonConfig[] = [];
|
|
759
|
+
|
|
760
|
+
switch (mergeInput) {
|
|
761
|
+
case "1":
|
|
762
|
+
buttons = preset.buttons;
|
|
763
|
+
break;
|
|
764
|
+
case "2":
|
|
765
|
+
buttons = [...existingButtons, ...preset.buttons];
|
|
766
|
+
break;
|
|
767
|
+
case "3": {
|
|
768
|
+
const mergedMap = new Map(existingButtons.map((b) => [b.id, b]));
|
|
769
|
+
preset.buttons.forEach((b) => mergedMap.set(b.id, b));
|
|
770
|
+
buttons = Array.from(mergedMap.values());
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
default:
|
|
774
|
+
ConsoleUI.printWarning("Invalid choice, operation cancelled");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
SettingsManager.setButtons(location, buttons);
|
|
779
|
+
} else {
|
|
780
|
+
SettingsManager.setButtons(location, preset.buttons);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
ConsoleUI.printSuccess(
|
|
784
|
+
`Preset "${preset.name}" applied to ${location} settings`,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Add a new button */
|
|
789
|
+
async function addButton(): Promise<void> {
|
|
790
|
+
ConsoleUI.clear();
|
|
791
|
+
ConsoleUI.printBanner();
|
|
792
|
+
|
|
793
|
+
const location = await InputHandler.promptLocation();
|
|
794
|
+
|
|
795
|
+
console.log("Button ID (unique identifier): ");
|
|
796
|
+
const id = await InputHandler.getInput(10000);
|
|
797
|
+
if (!id) {
|
|
798
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
console.log("Button text (what shows on status bar): ");
|
|
803
|
+
const text = await InputHandler.getInput(10000);
|
|
804
|
+
if (!text) {
|
|
805
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
console.log("Tooltip (hover text): ");
|
|
810
|
+
const tooltip = (await InputHandler.getInput(10000)) || text;
|
|
811
|
+
|
|
812
|
+
console.log("\nCommand type:");
|
|
813
|
+
console.log(" 1. npm 2. yarn 3. pnpm 4. bun");
|
|
814
|
+
console.log(" 5. shell 6. vscode 7. task 8. detect");
|
|
815
|
+
console.log("\nChoice (1-8): ");
|
|
816
|
+
|
|
817
|
+
const typeInput = await InputHandler.getInput(10000);
|
|
818
|
+
const typeMap: Record<string, string> = {
|
|
819
|
+
"1": "npm",
|
|
820
|
+
"2": "yarn",
|
|
821
|
+
"3": "pnpm",
|
|
822
|
+
"4": "bun",
|
|
823
|
+
"5": "shell",
|
|
824
|
+
"6": "vscode",
|
|
825
|
+
"7": "task",
|
|
826
|
+
"8": "detect",
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
const commandType = typeMap[typeInput || ""] || "shell";
|
|
830
|
+
|
|
831
|
+
console.log(
|
|
832
|
+
`\n${commandType === "shell" || commandType === "vscode" || commandType === "task" ? "Command" : "Script name"}: `,
|
|
833
|
+
);
|
|
834
|
+
const commandValue = await InputHandler.getInput(10000);
|
|
835
|
+
if (!commandValue) {
|
|
836
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const button: StatusBarButtonConfig = {
|
|
841
|
+
id,
|
|
842
|
+
text,
|
|
843
|
+
tooltip,
|
|
844
|
+
command:
|
|
845
|
+
commandType === "shell" ||
|
|
846
|
+
commandType === "vscode" ||
|
|
847
|
+
commandType === "task"
|
|
848
|
+
? {
|
|
849
|
+
type: commandType as "shell" | "vscode" | "task",
|
|
850
|
+
command: commandValue,
|
|
851
|
+
}
|
|
852
|
+
: {
|
|
853
|
+
type: commandType as
|
|
854
|
+
| "npm"
|
|
855
|
+
| "yarn"
|
|
856
|
+
| "pnpm"
|
|
857
|
+
| "bun"
|
|
858
|
+
| "npx"
|
|
859
|
+
| "pnpx"
|
|
860
|
+
| "bunx"
|
|
861
|
+
| "detect",
|
|
862
|
+
script: commandValue,
|
|
863
|
+
},
|
|
864
|
+
enabled: true,
|
|
865
|
+
alignment: "left",
|
|
866
|
+
priority: 100,
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const buttons = SettingsManager.getButtons(location);
|
|
870
|
+
buttons.push(button);
|
|
871
|
+
SettingsManager.setButtons(location, buttons);
|
|
872
|
+
|
|
873
|
+
ConsoleUI.printSuccess(`Button "${text}" added to ${location} settings`);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** Remove a button */
|
|
877
|
+
async function removeButton(): Promise<void> {
|
|
878
|
+
ConsoleUI.clear();
|
|
879
|
+
ConsoleUI.printBanner();
|
|
880
|
+
|
|
881
|
+
const location = await InputHandler.promptLocation();
|
|
882
|
+
const buttons = SettingsManager.getButtons(location);
|
|
883
|
+
|
|
884
|
+
if (buttons.length === 0) {
|
|
885
|
+
ConsoleUI.printWarning("No buttons to remove");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
console.log("Select button to remove:");
|
|
890
|
+
ConsoleUI.printDivider();
|
|
891
|
+
|
|
892
|
+
buttons.forEach((btn, idx) => {
|
|
893
|
+
console.log(` ${idx + 1}. ${btn.text} (${btn.id})`);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
console.log(`\nChoice (1-${buttons.length}): `);
|
|
897
|
+
const input = await InputHandler.getInput(10000);
|
|
898
|
+
|
|
899
|
+
if (!input) {
|
|
900
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const index = parseInt(input, 10) - 1;
|
|
905
|
+
if (index < 0 || index >= buttons.length) {
|
|
906
|
+
ConsoleUI.printError("Invalid selection");
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const removedButton = buttons[index];
|
|
911
|
+
buttons.splice(index, 1);
|
|
912
|
+
SettingsManager.setButtons(location, buttons);
|
|
913
|
+
|
|
914
|
+
ConsoleUI.printSuccess(
|
|
915
|
+
`Button "${removedButton.text}" removed from ${location} settings`,
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Toggle debug mode */
|
|
920
|
+
async function toggleDebugMode(): Promise<void> {
|
|
921
|
+
ConsoleUI.clear();
|
|
922
|
+
ConsoleUI.printBanner();
|
|
923
|
+
|
|
924
|
+
const location = await InputHandler.promptLocation();
|
|
925
|
+
const currentDebugMode = SettingsManager.getDebugMode(location);
|
|
926
|
+
|
|
927
|
+
ConsoleUI.printInfo(
|
|
928
|
+
`Debug mode is currently ${currentDebugMode ? "enabled" : "disabled"}`,
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
const confirmed = await InputHandler.getConfirmation(
|
|
932
|
+
`${currentDebugMode ? "Disable" : "Enable"} debug mode?`,
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
if (!confirmed) {
|
|
936
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
SettingsManager.setDebugMode(location, !currentDebugMode);
|
|
941
|
+
ConsoleUI.printSuccess(
|
|
942
|
+
`Debug mode ${!currentDebugMode ? "enabled" : "disabled"} in ${location} settings`,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/** Export configuration */
|
|
947
|
+
async function exportConfiguration(): Promise<void> {
|
|
948
|
+
ConsoleUI.clear();
|
|
949
|
+
ConsoleUI.printBanner();
|
|
950
|
+
|
|
951
|
+
const location = await InputHandler.promptLocation();
|
|
952
|
+
const buttons = SettingsManager.getButtons(location);
|
|
953
|
+
|
|
954
|
+
console.log("Export file path (e.g., config.json): ");
|
|
955
|
+
const filePath = await InputHandler.getInput(10000);
|
|
956
|
+
|
|
957
|
+
if (!filePath) {
|
|
958
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const exportData: ExtensionConfig = {
|
|
963
|
+
buttons,
|
|
964
|
+
history: true,
|
|
965
|
+
autoDetect: false,
|
|
966
|
+
settings: {
|
|
967
|
+
debug: SettingsManager.getDebugMode(location),
|
|
968
|
+
},
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
try {
|
|
972
|
+
writeFileSync(filePath, JSON.stringify(exportData, null, 2), "utf-8");
|
|
973
|
+
ConsoleUI.printSuccess(`Configuration exported to ${filePath}`);
|
|
974
|
+
} catch (error) {
|
|
975
|
+
ConsoleUI.printError(
|
|
976
|
+
`Failed to export: ${error instanceof Error ? error.message : String(error)}`,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** Import configuration */
|
|
982
|
+
async function importConfiguration(): Promise<void> {
|
|
983
|
+
ConsoleUI.clear();
|
|
984
|
+
ConsoleUI.printBanner();
|
|
985
|
+
|
|
986
|
+
console.log("Import file path: ");
|
|
987
|
+
const filePath = await InputHandler.getInput(10000);
|
|
988
|
+
|
|
989
|
+
if (!filePath) {
|
|
990
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (!existsSync(filePath)) {
|
|
995
|
+
ConsoleUI.printError("File not found");
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
try {
|
|
1000
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1001
|
+
const importedConfig: ExtensionConfig = JSON.parse(content);
|
|
1002
|
+
|
|
1003
|
+
if (!importedConfig.buttons || !Array.isArray(importedConfig.buttons)) {
|
|
1004
|
+
ConsoleUI.printError("Invalid configuration file");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const location = await InputHandler.promptLocation();
|
|
1009
|
+
const existingButtons = SettingsManager.getButtons(location);
|
|
1010
|
+
|
|
1011
|
+
if (existingButtons.length > 0) {
|
|
1012
|
+
console.log("\nImport mode:");
|
|
1013
|
+
console.log(" 1. Replace all");
|
|
1014
|
+
console.log(" 2. Merge");
|
|
1015
|
+
console.log("\nChoice (1-2): ");
|
|
1016
|
+
|
|
1017
|
+
const mergeInput = await InputHandler.getInput(10000);
|
|
1018
|
+
|
|
1019
|
+
if (mergeInput === "2") {
|
|
1020
|
+
const mergedButtons = [...existingButtons, ...importedConfig.buttons];
|
|
1021
|
+
SettingsManager.setButtons(location, mergedButtons);
|
|
1022
|
+
} else {
|
|
1023
|
+
SettingsManager.setButtons(location, importedConfig.buttons);
|
|
1024
|
+
}
|
|
1025
|
+
} else {
|
|
1026
|
+
SettingsManager.setButtons(location, importedConfig.buttons);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
ConsoleUI.printSuccess(
|
|
1030
|
+
`Configuration imported to ${location} settings (${importedConfig.buttons.length} buttons)`,
|
|
1031
|
+
);
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
ConsoleUI.printError(
|
|
1034
|
+
`Failed to import: ${error instanceof Error ? error.message : String(error)}`,
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/** Reset to defaults */
|
|
1040
|
+
async function resetToDefaults(): Promise<void> {
|
|
1041
|
+
ConsoleUI.clear();
|
|
1042
|
+
ConsoleUI.printBanner();
|
|
1043
|
+
|
|
1044
|
+
const location = await InputHandler.promptLocation();
|
|
1045
|
+
const confirmed = await InputHandler.getConfirmation(
|
|
1046
|
+
`Reset ${location} settings to defaults? This will remove all buttons.`,
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
if (!confirmed) {
|
|
1050
|
+
ConsoleUI.printWarning("Operation cancelled");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
SettingsManager.setButtons(location, []);
|
|
1055
|
+
SettingsManager.setDebugMode(location, false);
|
|
1056
|
+
|
|
1057
|
+
ConsoleUI.printSuccess(`${location} settings reset to defaults`);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** Enhanced help system */
|
|
1061
|
+
async function showHelp(): Promise<void> {
|
|
1062
|
+
ConsoleUI.clear();
|
|
1063
|
+
ConsoleUI.printBanner();
|
|
1064
|
+
|
|
1065
|
+
ConsoleUI.printDivider();
|
|
1066
|
+
console.log("Help & Usage Guide");
|
|
1067
|
+
ConsoleUI.printDivider();
|
|
1068
|
+
console.log();
|
|
1069
|
+
|
|
1070
|
+
console.log("Available Operations:");
|
|
1071
|
+
ConsoleUI.printDivider();
|
|
1072
|
+
|
|
1073
|
+
for (const option of MENU_OPTIONS) {
|
|
1074
|
+
console.log(` ${option.key}. ${option.label}`);
|
|
1075
|
+
console.log(` ${option.description}`);
|
|
1076
|
+
if (option.requiresConfirmation) {
|
|
1077
|
+
ConsoleUI.printWarning(" Requires confirmation");
|
|
1078
|
+
}
|
|
1079
|
+
console.log();
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
ConsoleUI.printDivider();
|
|
1083
|
+
console.log("Settings Locations:");
|
|
1084
|
+
ConsoleUI.printDivider();
|
|
1085
|
+
console.log(" • User settings: Global settings for all workspaces");
|
|
1086
|
+
console.log(" • Workspace settings: Project-specific settings");
|
|
1087
|
+
console.log();
|
|
1088
|
+
|
|
1089
|
+
ConsoleUI.printDivider();
|
|
1090
|
+
console.log("Built-in Presets:");
|
|
1091
|
+
ConsoleUI.printDivider();
|
|
1092
|
+
Object.values(BUILTIN_PRESETS).forEach((preset) => {
|
|
1093
|
+
console.log(` • ${preset.name}: ${preset.description}`);
|
|
1094
|
+
});
|
|
1095
|
+
console.log();
|
|
1096
|
+
|
|
1097
|
+
ConsoleUI.printDivider();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ============================================================================
|
|
1101
|
+
// MAIN APPLICATION CLASS
|
|
1102
|
+
// ============================================================================
|
|
1103
|
+
|
|
1104
|
+
/** Main CLI application controller */
|
|
1105
|
+
class ConfigCLI {
|
|
1106
|
+
private state: ApplicationState = {
|
|
1107
|
+
isRunning: true,
|
|
1108
|
+
operationCount: 0,
|
|
1109
|
+
startTime: Date.now(),
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
/** Run the main application loop */
|
|
1113
|
+
async run(): Promise<void> {
|
|
1114
|
+
ConsoleUI.clear();
|
|
1115
|
+
ConsoleUI.printBanner();
|
|
1116
|
+
|
|
1117
|
+
try {
|
|
1118
|
+
while (this.state.isRunning) {
|
|
1119
|
+
this.displayMainMenu();
|
|
1120
|
+
const input = await InputHandler.getInput();
|
|
1121
|
+
|
|
1122
|
+
if (!input) {
|
|
1123
|
+
this.handleQuit("Input timeout");
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
await this.processInput(input);
|
|
1128
|
+
}
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
this.handleError(error);
|
|
1131
|
+
this.state.isRunning = false;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/** Display main menu and get user choice */
|
|
1136
|
+
private displayMainMenu(): void {
|
|
1137
|
+
ConsoleUI.printMenu();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/** Process user input and execute corresponding action */
|
|
1141
|
+
private async processInput(input: string): Promise<void> {
|
|
1142
|
+
const validation = InputValidator.validateChoice(input);
|
|
1143
|
+
|
|
1144
|
+
if (!validation.isValid) {
|
|
1145
|
+
if (validation.value === "quit") {
|
|
1146
|
+
this.handleQuit("User requested exit");
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
ConsoleUI.printError(validation.error!, validation.suggestion);
|
|
1151
|
+
console.log("Press Enter to try again...");
|
|
1152
|
+
await InputHandler.getInput(10000);
|
|
1153
|
+
ConsoleUI.clear();
|
|
1154
|
+
ConsoleUI.printBanner();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
await this.executeChoice(validation.value!);
|
|
1159
|
+
|
|
1160
|
+
// Show pause prompt for operations that modify settings
|
|
1161
|
+
if (!["1", "9"].includes(validation.value!)) {
|
|
1162
|
+
console.log("\nPress Enter to return to menu...");
|
|
1163
|
+
await InputHandler.getInput(15000);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Clear screen and show menu again
|
|
1167
|
+
ConsoleUI.clear();
|
|
1168
|
+
ConsoleUI.printBanner();
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/** Execute the selected menu option */
|
|
1172
|
+
private async executeChoice(choice: string): Promise<void> {
|
|
1173
|
+
const option = MENU_OPTIONS.find((opt) => opt.key === choice);
|
|
1174
|
+
|
|
1175
|
+
if (!option) {
|
|
1176
|
+
ConsoleUI.printError("Invalid option selected");
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Handle confirmation for destructive operations
|
|
1181
|
+
if (option.requiresConfirmation) {
|
|
1182
|
+
const confirmed = await InputHandler.getConfirmation(
|
|
1183
|
+
`Are you sure you want to ${option.label.toLowerCase()}?`,
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
if (!confirmed) {
|
|
1187
|
+
ConsoleUI.printWarning(`${option.label} cancelled`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
try {
|
|
1193
|
+
this.state.operationCount++;
|
|
1194
|
+
await option.action();
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
ConsoleUI.printError(
|
|
1197
|
+
`${option.label} operation failed`,
|
|
1198
|
+
error instanceof ConfigCLIError ? error.suggestion : "Please try again",
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** Handle application quit */
|
|
1204
|
+
private handleQuit(reason?: string): void {
|
|
1205
|
+
const duration = Date.now() - this.state.startTime;
|
|
1206
|
+
const message = reason ? `${reason}. ` : "";
|
|
1207
|
+
|
|
1208
|
+
ConsoleUI.clear();
|
|
1209
|
+
ConsoleUI.printBanner();
|
|
1210
|
+
ConsoleUI.printSuccess(
|
|
1211
|
+
`${message}Thank you for using Config CLI! Operations: ${this.state.operationCount}, Duration: ${Math.round(duration)}ms`,
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
this.state.isRunning = false;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** Handle unexpected errors */
|
|
1218
|
+
private handleError(error: unknown): void {
|
|
1219
|
+
ConsoleUI.clear();
|
|
1220
|
+
ConsoleUI.printBanner();
|
|
1221
|
+
|
|
1222
|
+
const errorMessage =
|
|
1223
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
1224
|
+
const errorCode =
|
|
1225
|
+
error instanceof ConfigCLIError ? error.code : "UNKNOWN_ERROR";
|
|
1226
|
+
|
|
1227
|
+
ConsoleUI.printError(
|
|
1228
|
+
`An unexpected error occurred: ${errorMessage}`,
|
|
1229
|
+
"Please report this issue with the error code below",
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
console.log(`Error Code: ${errorCode}`);
|
|
1233
|
+
console.log(`Timestamp: ${new Date().toISOString()}`);
|
|
1234
|
+
ConsoleUI.printDivider();
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// ============================================================================
|
|
1239
|
+
// APPLICATION ENTRY POINT
|
|
1240
|
+
// ============================================================================
|
|
1241
|
+
|
|
1242
|
+
/** Main application entry point */
|
|
1243
|
+
async function main(): Promise<void> {
|
|
1244
|
+
try {
|
|
1245
|
+
const cli = new ConfigCLI();
|
|
1246
|
+
await cli.run();
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
console.error("Failed to start application:", error);
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// ============================================================================
|
|
1254
|
+
// GLOBAL ERROR HANDLERS
|
|
1255
|
+
// ============================================================================
|
|
1256
|
+
|
|
1257
|
+
/** Handle unhandled promise rejections */
|
|
1258
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
1259
|
+
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
|
1260
|
+
console.error("This might be due to an unhandled async operation");
|
|
1261
|
+
process.exit(1);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
/** Handle uncaught exceptions */
|
|
1265
|
+
process.on("uncaughtException", (error) => {
|
|
1266
|
+
console.error("Uncaught Exception:", error);
|
|
1267
|
+
console.error("The application encountered a critical error and must exit");
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
/** Handle SIGINT (Ctrl+C) gracefully */
|
|
1272
|
+
process.on("SIGINT", () => {
|
|
1273
|
+
ConsoleUI.printWarning(
|
|
1274
|
+
"Received interrupt signal. Shutting down gracefully...",
|
|
1275
|
+
);
|
|
1276
|
+
process.exit(0);
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
// ============================================================================
|
|
1280
|
+
// START APPLICATION
|
|
1281
|
+
// ============================================================================
|
|
1282
|
+
|
|
1283
|
+
// Start the application
|
|
1284
|
+
main().catch((error) => {
|
|
1285
|
+
console.error("Fatal error during application startup:", error);
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
});
|