@larkiny/astro-github-loader 0.9.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/LICENSE +21 -0
- package/README.md +675 -0
- package/dist/github.cleanup.d.ts +5 -0
- package/dist/github.cleanup.js +216 -0
- package/dist/github.constants.d.ts +24 -0
- package/dist/github.constants.js +24 -0
- package/dist/github.content.d.ts +138 -0
- package/dist/github.content.js +1016 -0
- package/dist/github.dryrun.d.ts +72 -0
- package/dist/github.dryrun.js +247 -0
- package/dist/github.link-transform.d.ts +77 -0
- package/dist/github.link-transform.js +321 -0
- package/dist/github.loader.d.ts +14 -0
- package/dist/github.loader.js +143 -0
- package/dist/github.loader.spec.d.ts +1 -0
- package/dist/github.loader.spec.js +96 -0
- package/dist/github.logger.d.ts +132 -0
- package/dist/github.logger.js +260 -0
- package/dist/github.sync.d.ts +5 -0
- package/dist/github.sync.js +292 -0
- package/dist/github.types.d.ts +315 -0
- package/dist/github.types.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/package.json +66 -0
- package/src/github.cleanup.ts +243 -0
- package/src/github.constants.ts +25 -0
- package/src/github.content.ts +1205 -0
- package/src/github.dryrun.ts +339 -0
- package/src/github.link-transform.ts +452 -0
- package/src/github.loader.spec.ts +106 -0
- package/src/github.loader.ts +189 -0
- package/src/github.logger.ts +324 -0
- package/src/github.types.ts +339 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { toCollectionEntry } from "./github.content.js";
|
|
2
|
+
import { performSelectiveCleanup } from "./github.cleanup.js";
|
|
3
|
+
import { performDryRun, displayDryRunResults, updateImportState } from "./github.dryrun.js";
|
|
4
|
+
import { createLogger, type Logger, type ImportSummary } from "./github.logger.js";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Loader,
|
|
8
|
+
GithubLoaderOptions,
|
|
9
|
+
ImportOptions,
|
|
10
|
+
SyncStats,
|
|
11
|
+
} from "./github.types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Performs selective cleanup for configurations with basePath
|
|
15
|
+
* @param configs - Array of configuration objects
|
|
16
|
+
* @param context - Loader context
|
|
17
|
+
* @param octokit - GitHub API client
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
async function performSelectiveCleanups(
|
|
21
|
+
configs: ImportOptions[],
|
|
22
|
+
context: any,
|
|
23
|
+
octokit: any
|
|
24
|
+
): Promise<SyncStats[]> {
|
|
25
|
+
const results: SyncStats[] = [];
|
|
26
|
+
|
|
27
|
+
// Process each config sequentially to avoid overwhelming Astro's file watcher
|
|
28
|
+
for (const config of configs) {
|
|
29
|
+
if (config.enabled === false) {
|
|
30
|
+
context.logger.debug(`Skipping disabled config: ${config.name || `${config.owner}/${config.repo}`}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const stats = await performSelectiveCleanup(config, context, octokit);
|
|
36
|
+
results.push(stats);
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
context.logger.error(`Selective cleanup failed for ${config.name || `${config.owner}/${config.repo}`}: ${error}`);
|
|
39
|
+
// Continue with other configs even if one fails
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Loads data from GitHub repositories based on the provided configurations and options.
|
|
48
|
+
*
|
|
49
|
+
* Features:
|
|
50
|
+
* - Sequential processing with spinner feedback for long-running operations
|
|
51
|
+
* - Dry run mode for change detection without actual imports
|
|
52
|
+
* - Configurable logging levels per configuration
|
|
53
|
+
* - Import state tracking for incremental updates
|
|
54
|
+
* - Content store management with optional clearing
|
|
55
|
+
*
|
|
56
|
+
* @return A loader object responsible for managing the data loading process.
|
|
57
|
+
*/
|
|
58
|
+
export function githubLoader({
|
|
59
|
+
octokit,
|
|
60
|
+
configs,
|
|
61
|
+
fetchOptions = {},
|
|
62
|
+
clear = false,
|
|
63
|
+
dryRun = false,
|
|
64
|
+
logLevel,
|
|
65
|
+
force = false,
|
|
66
|
+
}: GithubLoaderOptions): Loader {
|
|
67
|
+
return {
|
|
68
|
+
name: "github-loader",
|
|
69
|
+
load: async (context) => {
|
|
70
|
+
const { store } = context;
|
|
71
|
+
|
|
72
|
+
// Create global logger with specified level or default
|
|
73
|
+
const globalLogger = createLogger(logLevel || 'default');
|
|
74
|
+
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
globalLogger.info("š Dry run mode enabled - checking for changes only");
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const results = await performDryRun(configs, context, octokit);
|
|
80
|
+
displayDryRunResults(results, context.logger);
|
|
81
|
+
|
|
82
|
+
globalLogger.info("\nš« Dry run complete - no imports performed");
|
|
83
|
+
globalLogger.info("š” Set dryRun: false to perform actual imports");
|
|
84
|
+
|
|
85
|
+
return; // Exit without importing
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
globalLogger.error(`Dry run failed: ${error.message}`);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
globalLogger.debug(`Loading data from ${configs.length} sources`);
|
|
93
|
+
|
|
94
|
+
// Always use standard processing - no file deletions to avoid Astro issues
|
|
95
|
+
globalLogger.info(clear ? "Processing with content store clear" : "Processing without content store clear");
|
|
96
|
+
|
|
97
|
+
if (clear) {
|
|
98
|
+
store.clear();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Process each config sequentially to avoid overwhelming GitHub API/CDN
|
|
102
|
+
for (let i = 0; i < configs.length; i++) {
|
|
103
|
+
const config = configs[i];
|
|
104
|
+
|
|
105
|
+
if (config.enabled === false) {
|
|
106
|
+
globalLogger.debug(`Skipping disabled config: ${config.name || `${config.owner}/${config.repo}`}`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add small delay between configs to be gentler on GitHub's CDN
|
|
111
|
+
if (i > 0) {
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Determine the effective log level for this config
|
|
116
|
+
const effectiveLogLevel = logLevel || config.logLevel || 'default';
|
|
117
|
+
const configLogger = createLogger(effectiveLogLevel);
|
|
118
|
+
|
|
119
|
+
const configName = config.name || `${config.owner}/${config.repo}`;
|
|
120
|
+
const repository = `${config.owner}/${config.repo}`;
|
|
121
|
+
|
|
122
|
+
let summary: ImportSummary = {
|
|
123
|
+
configName,
|
|
124
|
+
repository,
|
|
125
|
+
ref: config.ref,
|
|
126
|
+
filesProcessed: 0,
|
|
127
|
+
filesUpdated: 0,
|
|
128
|
+
filesUnchanged: 0,
|
|
129
|
+
duration: 0,
|
|
130
|
+
status: 'error',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const startTime = Date.now();
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Perform the import with spinner
|
|
137
|
+
const stats = await globalLogger.withSpinner(
|
|
138
|
+
`š Importing ${configName}...`,
|
|
139
|
+
() => toCollectionEntry({
|
|
140
|
+
context: { ...context, logger: configLogger as any },
|
|
141
|
+
octokit,
|
|
142
|
+
options: config,
|
|
143
|
+
fetchOptions,
|
|
144
|
+
force,
|
|
145
|
+
}),
|
|
146
|
+
`ā
${configName} imported successfully`,
|
|
147
|
+
`ā ${configName} import failed`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
summary.duration = Date.now() - startTime;
|
|
151
|
+
summary.filesProcessed = stats?.processed || 0;
|
|
152
|
+
summary.filesUpdated = stats?.updated || 0;
|
|
153
|
+
summary.filesUnchanged = stats?.unchanged || 0;
|
|
154
|
+
summary.assetsDownloaded = stats?.assetsDownloaded || 0;
|
|
155
|
+
summary.assetsCached = stats?.assetsCached || 0;
|
|
156
|
+
summary.status = 'success';
|
|
157
|
+
|
|
158
|
+
// Log structured summary
|
|
159
|
+
configLogger.logImportSummary(summary);
|
|
160
|
+
|
|
161
|
+
// Update state tracking for future dry runs
|
|
162
|
+
try {
|
|
163
|
+
// Get the latest commit info to track state
|
|
164
|
+
const { data } = await octokit.rest.repos.listCommits({
|
|
165
|
+
owner: config.owner,
|
|
166
|
+
repo: config.repo,
|
|
167
|
+
sha: config.ref || 'main',
|
|
168
|
+
per_page: 1
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (data.length > 0) {
|
|
172
|
+
await updateImportState(process.cwd(), config, data[0].sha);
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// Don't fail the import if state tracking fails
|
|
176
|
+
configLogger.debug(`Failed to update import state for ${configName}: ${error}`);
|
|
177
|
+
}
|
|
178
|
+
} catch (error: any) {
|
|
179
|
+
summary.duration = Date.now() - startTime;
|
|
180
|
+
summary.status = 'error';
|
|
181
|
+
summary.error = error.message;
|
|
182
|
+
|
|
183
|
+
configLogger.logImportSummary(summary);
|
|
184
|
+
// Continue with other configs even if one fails
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-level logging system for astro-github-loader
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type LogLevel = 'silent' | 'default' | 'verbose' | 'debug';
|
|
6
|
+
|
|
7
|
+
export interface LoggerOptions {
|
|
8
|
+
level: LogLevel;
|
|
9
|
+
prefix?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ImportSummary {
|
|
13
|
+
configName: string;
|
|
14
|
+
repository: string;
|
|
15
|
+
ref?: string;
|
|
16
|
+
filesProcessed: number;
|
|
17
|
+
filesUpdated: number;
|
|
18
|
+
filesUnchanged: number;
|
|
19
|
+
assetsDownloaded?: number;
|
|
20
|
+
assetsCached?: number;
|
|
21
|
+
duration: number;
|
|
22
|
+
status: 'success' | 'error' | 'cancelled';
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SyncSummary {
|
|
27
|
+
added: number;
|
|
28
|
+
updated: number;
|
|
29
|
+
deleted: number;
|
|
30
|
+
unchanged: number;
|
|
31
|
+
duration: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CleanupSummary {
|
|
35
|
+
deleted: number;
|
|
36
|
+
duration: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Centralized logger with configurable verbosity levels and spinner support for long-running operations
|
|
41
|
+
*/
|
|
42
|
+
export class Logger {
|
|
43
|
+
private level: LogLevel;
|
|
44
|
+
private prefix: string;
|
|
45
|
+
private spinnerInterval?: NodeJS.Timeout;
|
|
46
|
+
private spinnerChars = ['ā ', 'ā ', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ', 'ā '];
|
|
47
|
+
private spinnerIndex = 0;
|
|
48
|
+
private spinnerStartTime?: number;
|
|
49
|
+
|
|
50
|
+
constructor(options: LoggerOptions) {
|
|
51
|
+
this.level = options.level;
|
|
52
|
+
this.prefix = options.prefix || '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set the logging level
|
|
57
|
+
*/
|
|
58
|
+
setLevel(level: LogLevel): void {
|
|
59
|
+
this.level = level;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the current logging level
|
|
64
|
+
*/
|
|
65
|
+
getLevel(): LogLevel {
|
|
66
|
+
return this.level;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a specific level should be logged
|
|
71
|
+
*/
|
|
72
|
+
private shouldLog(level: LogLevel): boolean {
|
|
73
|
+
const levels: Record<LogLevel, number> = {
|
|
74
|
+
silent: 0,
|
|
75
|
+
default: 1,
|
|
76
|
+
verbose: 2,
|
|
77
|
+
debug: 3,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return levels[this.level] >= levels[level];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Format message with prefix
|
|
85
|
+
*/
|
|
86
|
+
private formatMessage(message: string): string {
|
|
87
|
+
return this.prefix ? `${this.prefix} ${message}` : message;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Silent level - no output
|
|
92
|
+
*/
|
|
93
|
+
silent(): void {
|
|
94
|
+
// Intentionally empty
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Default level - summary information only
|
|
99
|
+
*/
|
|
100
|
+
info(message: string): void {
|
|
101
|
+
if (this.shouldLog('default')) {
|
|
102
|
+
console.log(this.formatMessage(message));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Verbose level - detailed operation information
|
|
108
|
+
*/
|
|
109
|
+
verbose(message: string): void {
|
|
110
|
+
if (this.shouldLog('verbose')) {
|
|
111
|
+
console.log(this.formatMessage(message));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Debug level - all information including diagnostics
|
|
117
|
+
*/
|
|
118
|
+
debug(message: string): void {
|
|
119
|
+
if (this.shouldLog('debug')) {
|
|
120
|
+
console.log(this.formatMessage(message));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Error - always shown unless silent
|
|
126
|
+
*/
|
|
127
|
+
error(message: string): void {
|
|
128
|
+
if (this.shouldLog('default')) {
|
|
129
|
+
console.error(this.formatMessage(message));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Warning - shown at default level and above
|
|
135
|
+
*/
|
|
136
|
+
warn(message: string): void {
|
|
137
|
+
if (this.shouldLog('default')) {
|
|
138
|
+
console.warn(this.formatMessage(message));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Log structured import summary (default level)
|
|
144
|
+
*/
|
|
145
|
+
logImportSummary(summary: ImportSummary): void {
|
|
146
|
+
if (!this.shouldLog('default')) return;
|
|
147
|
+
|
|
148
|
+
const statusIcon = summary.status === 'success' ? 'ā
' : summary.status === 'error' ? 'ā' : 'š«';
|
|
149
|
+
|
|
150
|
+
this.info('');
|
|
151
|
+
this.info(`š Import Summary: ${summary.configName}`);
|
|
152
|
+
this.info(`āā Repository: ${summary.repository}${summary.ref ? `@${summary.ref}` : ''}`);
|
|
153
|
+
this.info(`āā Files: ${summary.filesProcessed} processed, ${summary.filesUpdated} updated, ${summary.filesUnchanged} unchanged`);
|
|
154
|
+
|
|
155
|
+
if (summary.assetsDownloaded !== undefined || summary.assetsCached !== undefined) {
|
|
156
|
+
const downloaded = summary.assetsDownloaded || 0;
|
|
157
|
+
const cached = summary.assetsCached || 0;
|
|
158
|
+
this.info(`āā Assets: ${downloaded} downloaded, ${cached} cached`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.info(`āā Duration: ${(summary.duration / 1000).toFixed(1)}s`);
|
|
162
|
+
this.info(`āā Status: ${statusIcon} ${summary.status === 'success' ? 'Success' : summary.status === 'error' ? `Error: ${summary.error}` : 'Cancelled'}`);
|
|
163
|
+
this.info('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Log sync operation summary (default level)
|
|
168
|
+
*/
|
|
169
|
+
logSyncSummary(configName: string, summary: SyncSummary): void {
|
|
170
|
+
if (!this.shouldLog('default')) return;
|
|
171
|
+
|
|
172
|
+
if (summary.added > 0 || summary.updated > 0 || summary.deleted > 0) {
|
|
173
|
+
this.info(`Sync completed for ${configName}: ${summary.added} added, ${summary.updated} updated, ${summary.deleted} deleted (${summary.duration}ms)`);
|
|
174
|
+
} else {
|
|
175
|
+
this.info(`No changes needed for ${configName} (${summary.duration}ms)`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Log cleanup operation summary (default level)
|
|
181
|
+
*/
|
|
182
|
+
logCleanupSummary(configName: string, summary: CleanupSummary): void {
|
|
183
|
+
if (!this.shouldLog('default')) return;
|
|
184
|
+
|
|
185
|
+
if (summary.deleted > 0) {
|
|
186
|
+
this.info(`Cleanup completed for ${configName}: ${summary.deleted} obsolete files deleted (${summary.duration}ms)`);
|
|
187
|
+
} else {
|
|
188
|
+
this.debug(`No cleanup needed for ${configName} (${summary.duration}ms)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Log file-level processing (verbose level)
|
|
194
|
+
*/
|
|
195
|
+
logFileProcessing(action: string, filePath: string, details?: string): void {
|
|
196
|
+
const message = details ? `${action}: ${filePath} - ${details}` : `${action}: ${filePath}`;
|
|
197
|
+
this.verbose(message);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Log asset processing (verbose level)
|
|
202
|
+
*/
|
|
203
|
+
logAssetProcessing(action: string, assetPath: string, details?: string): void {
|
|
204
|
+
const message = details ? `Asset ${action}: ${assetPath} - ${details}` : `Asset ${action}: ${assetPath}`;
|
|
205
|
+
this.verbose(message);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a child logger with additional prefix
|
|
210
|
+
*/
|
|
211
|
+
child(prefix: string): Logger {
|
|
212
|
+
return new Logger({
|
|
213
|
+
level: this.level,
|
|
214
|
+
prefix: this.prefix ? `${this.prefix}${prefix}` : prefix,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Time a function execution and log the result
|
|
220
|
+
*/
|
|
221
|
+
async time<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
222
|
+
const startTime = Date.now();
|
|
223
|
+
this.debug(`ā±ļø Starting: ${label}`);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const result = await fn();
|
|
227
|
+
const duration = Date.now() - startTime;
|
|
228
|
+
this.verbose(`ā
Completed: ${label} (${duration}ms)`);
|
|
229
|
+
return result;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const duration = Date.now() - startTime;
|
|
232
|
+
this.error(`ā Failed: ${label} (${duration}ms): ${error}`);
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Format duration in human-readable format
|
|
239
|
+
*/
|
|
240
|
+
private formatDuration(seconds: number): string {
|
|
241
|
+
if (seconds < 60) {
|
|
242
|
+
return `${seconds}s`;
|
|
243
|
+
} else if (seconds < 3600) {
|
|
244
|
+
const mins = Math.floor(seconds / 60);
|
|
245
|
+
const secs = seconds % 60;
|
|
246
|
+
return `${mins}m ${secs}s`;
|
|
247
|
+
} else {
|
|
248
|
+
const hours = Math.floor(seconds / 3600);
|
|
249
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
250
|
+
return `${hours}h ${mins}m`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Start a spinner with duration timer for long-running operations
|
|
256
|
+
*/
|
|
257
|
+
startSpinner(message: string = 'Processing...'): void {
|
|
258
|
+
if (this.level === 'silent') return;
|
|
259
|
+
|
|
260
|
+
this.spinnerStartTime = Date.now();
|
|
261
|
+
this.spinnerIndex = 0;
|
|
262
|
+
|
|
263
|
+
const updateSpinner = () => {
|
|
264
|
+
const elapsed = Math.floor((Date.now() - this.spinnerStartTime!) / 1000);
|
|
265
|
+
const spinner = this.spinnerChars[this.spinnerIndex];
|
|
266
|
+
const duration = this.formatDuration(elapsed);
|
|
267
|
+
const formattedMessage = this.formatMessage(`${message} ${spinner} (${duration})`);
|
|
268
|
+
process.stdout.write(`\r${formattedMessage}`);
|
|
269
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerChars.length;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Initial display
|
|
273
|
+
updateSpinner();
|
|
274
|
+
|
|
275
|
+
// Update every 100ms
|
|
276
|
+
this.spinnerInterval = setInterval(updateSpinner, 100);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Stop the spinner and optionally show a final message
|
|
281
|
+
*/
|
|
282
|
+
stopSpinner(finalMessage?: string): void {
|
|
283
|
+
if (this.spinnerInterval) {
|
|
284
|
+
clearInterval(this.spinnerInterval);
|
|
285
|
+
this.spinnerInterval = undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (finalMessage && this.spinnerStartTime) {
|
|
289
|
+
const totalTime = Math.floor((Date.now() - this.spinnerStartTime) / 1000);
|
|
290
|
+
const duration = this.formatDuration(totalTime);
|
|
291
|
+
const formattedMessage = this.formatMessage(`${finalMessage} (${duration})`);
|
|
292
|
+
process.stdout.write(`\r${formattedMessage}\n`);
|
|
293
|
+
} else if (finalMessage) {
|
|
294
|
+
const formattedMessage = this.formatMessage(finalMessage);
|
|
295
|
+
process.stdout.write(`\r${formattedMessage}\n`);
|
|
296
|
+
} else {
|
|
297
|
+
process.stdout.write('\r\x1b[K'); // Clear the line
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.spinnerStartTime = undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Execute a function with spinner feedback
|
|
305
|
+
*/
|
|
306
|
+
async withSpinner<T>(message: string, fn: () => Promise<T>, successMessage?: string, errorMessage?: string): Promise<T> {
|
|
307
|
+
this.startSpinner(message);
|
|
308
|
+
try {
|
|
309
|
+
const result = await fn();
|
|
310
|
+
this.stopSpinner(successMessage || `ā
${message.replace(/^[šā³]?\s*/, '')} completed`);
|
|
311
|
+
return result;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
this.stopSpinner(errorMessage || `ā ${message.replace(/^[šā³]?\s*/, '')} failed`);
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Create a logger instance with the specified level
|
|
321
|
+
*/
|
|
322
|
+
export function createLogger(level: LogLevel = 'default', prefix?: string): Logger {
|
|
323
|
+
return new Logger({ level, prefix });
|
|
324
|
+
}
|