@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,339 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { ImportOptions, LoaderContext } from "./github.types.js";
|
|
5
|
+
|
|
6
|
+
const STATE_FILENAME = '.github-import-state.json';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents the state of a single import configuration
|
|
10
|
+
*/
|
|
11
|
+
export interface ImportState {
|
|
12
|
+
/** Configuration name for identification */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Repository owner/name/path identifier */
|
|
15
|
+
repoId: string;
|
|
16
|
+
/** Last known commit SHA */
|
|
17
|
+
lastCommitSha?: string;
|
|
18
|
+
/** Last import timestamp */
|
|
19
|
+
lastImported?: string;
|
|
20
|
+
/** Git reference being tracked */
|
|
21
|
+
ref: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* State file structure
|
|
26
|
+
*/
|
|
27
|
+
export interface StateFile {
|
|
28
|
+
/** Map of config identifiers to their state */
|
|
29
|
+
imports: Record<string, ImportState>;
|
|
30
|
+
/** Last check timestamp */
|
|
31
|
+
lastChecked: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Information about repository changes
|
|
36
|
+
*/
|
|
37
|
+
export interface RepositoryChangeInfo {
|
|
38
|
+
/** Configuration details */
|
|
39
|
+
config: ImportOptions;
|
|
40
|
+
/** Current state */
|
|
41
|
+
state: ImportState;
|
|
42
|
+
/** Whether repository needs to be re-imported */
|
|
43
|
+
needsReimport: boolean;
|
|
44
|
+
/** Latest commit SHA from remote */
|
|
45
|
+
latestCommitSha?: string;
|
|
46
|
+
/** Latest commit message */
|
|
47
|
+
latestCommitMessage?: string;
|
|
48
|
+
/** Latest commit date */
|
|
49
|
+
latestCommitDate?: string;
|
|
50
|
+
/** Error message if check failed */
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a unique identifier for an import configuration
|
|
56
|
+
*/
|
|
57
|
+
export function createConfigId(config: ImportOptions): string {
|
|
58
|
+
return `${config.owner}/${config.repo}@${config.ref || 'main'}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Loads the import state from the state file
|
|
63
|
+
*/
|
|
64
|
+
export async function loadImportState(workingDir: string): Promise<StateFile> {
|
|
65
|
+
const statePath = join(workingDir, STATE_FILENAME);
|
|
66
|
+
|
|
67
|
+
if (!existsSync(statePath)) {
|
|
68
|
+
return {
|
|
69
|
+
imports: {},
|
|
70
|
+
lastChecked: new Date().toISOString()
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const content = await fs.readFile(statePath, 'utf-8');
|
|
76
|
+
return JSON.parse(content);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.warn(`Failed to load import state from ${statePath}, starting fresh:`, error);
|
|
79
|
+
return {
|
|
80
|
+
imports: {},
|
|
81
|
+
lastChecked: new Date().toISOString()
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Saves the import state to the state file
|
|
88
|
+
*/
|
|
89
|
+
async function saveImportState(workingDir: string, state: StateFile): Promise<void> {
|
|
90
|
+
const statePath = join(workingDir, STATE_FILENAME);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.warn(`Failed to save import state to ${statePath}:`, error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Gets the latest commit information for a repository path
|
|
101
|
+
*/
|
|
102
|
+
export async function getLatestCommitInfo(
|
|
103
|
+
octokit: any,
|
|
104
|
+
config: ImportOptions,
|
|
105
|
+
signal?: AbortSignal
|
|
106
|
+
): Promise<{ sha: string; message: string; date: string } | null> {
|
|
107
|
+
const { owner, repo, ref = "main" } = config;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Get commits for the entire repository
|
|
111
|
+
const { data } = await octokit.rest.repos.listCommits({
|
|
112
|
+
owner,
|
|
113
|
+
repo,
|
|
114
|
+
sha: ref,
|
|
115
|
+
per_page: 1,
|
|
116
|
+
request: { signal }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (data.length === 0) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const latestCommit = data[0];
|
|
124
|
+
return {
|
|
125
|
+
sha: latestCommit.sha,
|
|
126
|
+
message: latestCommit.commit.message.split('\n')[0], // First line only
|
|
127
|
+
date: latestCommit.commit.committer?.date || latestCommit.commit.author?.date || new Date().toISOString()
|
|
128
|
+
};
|
|
129
|
+
} catch (error: any) {
|
|
130
|
+
if (error.status === 404) {
|
|
131
|
+
throw new Error(`Repository not found: ${owner}/${repo}`);
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Checks a single repository for changes
|
|
139
|
+
*/
|
|
140
|
+
async function checkRepositoryForChanges(
|
|
141
|
+
octokit: any,
|
|
142
|
+
config: ImportOptions,
|
|
143
|
+
currentState: ImportState,
|
|
144
|
+
signal?: AbortSignal
|
|
145
|
+
): Promise<RepositoryChangeInfo> {
|
|
146
|
+
const configName = config.name || `${config.owner}/${config.repo}`;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const latestCommit = await getLatestCommitInfo(octokit, config, signal);
|
|
150
|
+
|
|
151
|
+
if (!latestCommit) {
|
|
152
|
+
return {
|
|
153
|
+
config,
|
|
154
|
+
state: currentState,
|
|
155
|
+
needsReimport: false,
|
|
156
|
+
error: "No commits found in repository"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const needsReimport = !currentState.lastCommitSha || currentState.lastCommitSha !== latestCommit.sha;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
config,
|
|
164
|
+
state: currentState,
|
|
165
|
+
needsReimport,
|
|
166
|
+
latestCommitSha: latestCommit.sha,
|
|
167
|
+
latestCommitMessage: latestCommit.message,
|
|
168
|
+
latestCommitDate: latestCommit.date
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
} catch (error: any) {
|
|
172
|
+
return {
|
|
173
|
+
config,
|
|
174
|
+
state: currentState,
|
|
175
|
+
needsReimport: false,
|
|
176
|
+
error: error.message
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Updates the import state after a successful import
|
|
183
|
+
*/
|
|
184
|
+
export async function updateImportState(
|
|
185
|
+
workingDir: string,
|
|
186
|
+
config: ImportOptions,
|
|
187
|
+
commitSha?: string
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
const state = await loadImportState(workingDir);
|
|
190
|
+
const configId = createConfigId(config);
|
|
191
|
+
const configName = config.name || `${config.owner}/${config.repo}`;
|
|
192
|
+
|
|
193
|
+
state.imports[configId] = {
|
|
194
|
+
name: configName,
|
|
195
|
+
repoId: configId,
|
|
196
|
+
lastCommitSha: commitSha,
|
|
197
|
+
lastImported: new Date().toISOString(),
|
|
198
|
+
ref: config.ref || 'main'
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await saveImportState(workingDir, state);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Performs a dry run check on all configured repositories
|
|
206
|
+
*/
|
|
207
|
+
export async function performDryRun(
|
|
208
|
+
configs: ImportOptions[],
|
|
209
|
+
context: LoaderContext,
|
|
210
|
+
octokit: any,
|
|
211
|
+
workingDir: string = process.cwd(),
|
|
212
|
+
signal?: AbortSignal
|
|
213
|
+
): Promise<RepositoryChangeInfo[]> {
|
|
214
|
+
const { logger } = context;
|
|
215
|
+
|
|
216
|
+
logger.info("š Performing dry run - checking for repository changes...");
|
|
217
|
+
|
|
218
|
+
// Load current state
|
|
219
|
+
const state = await loadImportState(workingDir);
|
|
220
|
+
const results: RepositoryChangeInfo[] = [];
|
|
221
|
+
|
|
222
|
+
// Check each configuration
|
|
223
|
+
for (const config of configs) {
|
|
224
|
+
if (config.enabled === false) {
|
|
225
|
+
logger.debug(`Skipping disabled config: ${config.name || `${config.owner}/${config.repo}`}`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const configId = createConfigId(config);
|
|
230
|
+
const configName = config.name || `${config.owner}/${config.repo}`;
|
|
231
|
+
|
|
232
|
+
// Get current state for this config
|
|
233
|
+
const currentState: ImportState = state.imports[configId] || {
|
|
234
|
+
name: configName,
|
|
235
|
+
repoId: configId,
|
|
236
|
+
ref: config.ref || 'main'
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
logger.debug(`Checking ${configName}...`);
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const changeInfo = await checkRepositoryForChanges(octokit, config, currentState, signal);
|
|
243
|
+
results.push(changeInfo);
|
|
244
|
+
} catch (error: any) {
|
|
245
|
+
if (signal?.aborted) throw error;
|
|
246
|
+
|
|
247
|
+
results.push({
|
|
248
|
+
config,
|
|
249
|
+
state: currentState,
|
|
250
|
+
needsReimport: false,
|
|
251
|
+
error: `Failed to check repository: ${error.message}`
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Update last checked time
|
|
257
|
+
state.lastChecked = new Date().toISOString();
|
|
258
|
+
await saveImportState(workingDir, state);
|
|
259
|
+
|
|
260
|
+
return results;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Formats and displays the dry run results
|
|
265
|
+
*/
|
|
266
|
+
export function displayDryRunResults(results: RepositoryChangeInfo[], logger: any): void {
|
|
267
|
+
logger.info("\nš Repository Import Status:");
|
|
268
|
+
logger.info("=" .repeat(50));
|
|
269
|
+
|
|
270
|
+
let needsReimportCount = 0;
|
|
271
|
+
let errorCount = 0;
|
|
272
|
+
|
|
273
|
+
for (const result of results) {
|
|
274
|
+
const configName = result.config.name || `${result.config.owner}/${result.config.repo}`;
|
|
275
|
+
|
|
276
|
+
if (result.error) {
|
|
277
|
+
logger.info(`ā ${configName}: ${result.error}`);
|
|
278
|
+
errorCount++;
|
|
279
|
+
} else if (result.needsReimport) {
|
|
280
|
+
logger.info(`š ${configName}: Needs re-import`);
|
|
281
|
+
if (result.latestCommitMessage) {
|
|
282
|
+
logger.info(` Latest commit: ${result.latestCommitMessage}`);
|
|
283
|
+
}
|
|
284
|
+
if (result.latestCommitDate) {
|
|
285
|
+
const date = new Date(result.latestCommitDate);
|
|
286
|
+
const timeAgo = getTimeAgo(date);
|
|
287
|
+
logger.info(` Committed: ${timeAgo}`);
|
|
288
|
+
}
|
|
289
|
+
if (result.state.lastImported) {
|
|
290
|
+
const lastImported = new Date(result.state.lastImported);
|
|
291
|
+
const importTimeAgo = getTimeAgo(lastImported);
|
|
292
|
+
logger.info(` Last imported: ${importTimeAgo}`);
|
|
293
|
+
} else {
|
|
294
|
+
logger.info(` Last imported: Never`);
|
|
295
|
+
}
|
|
296
|
+
needsReimportCount++;
|
|
297
|
+
} else {
|
|
298
|
+
logger.info(`ā
${configName}: Up to date`);
|
|
299
|
+
if (result.state.lastImported) {
|
|
300
|
+
const lastImported = new Date(result.state.lastImported);
|
|
301
|
+
const timeAgo = getTimeAgo(lastImported);
|
|
302
|
+
logger.info(` Last imported: ${timeAgo}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
logger.info("=" .repeat(50));
|
|
308
|
+
logger.info(`š Summary: ${needsReimportCount} of ${results.length} repositories need re-import, ${errorCount} errors`);
|
|
309
|
+
|
|
310
|
+
if (needsReimportCount > 0) {
|
|
311
|
+
logger.info("\nš” To import updated repositories:");
|
|
312
|
+
logger.info("1. Delete the target import folders for repositories that need re-import");
|
|
313
|
+
logger.info("2. Run the import process normally (dryRun: false)");
|
|
314
|
+
logger.info("3. Fresh content will be imported automatically");
|
|
315
|
+
} else {
|
|
316
|
+
logger.info("\nš All repositories are up to date!");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Helper function to format time differences in a human-readable way
|
|
322
|
+
*/
|
|
323
|
+
function getTimeAgo(date: Date): string {
|
|
324
|
+
const now = new Date();
|
|
325
|
+
const diffMs = now.getTime() - date.getTime();
|
|
326
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
327
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
328
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
329
|
+
|
|
330
|
+
if (diffMins < 60) {
|
|
331
|
+
return `${diffMins} minutes ago`;
|
|
332
|
+
} else if (diffHours < 24) {
|
|
333
|
+
return `${diffHours} hours ago`;
|
|
334
|
+
} else if (diffDays < 7) {
|
|
335
|
+
return `${diffDays} days ago`;
|
|
336
|
+
} else {
|
|
337
|
+
return date.toLocaleDateString();
|
|
338
|
+
}
|
|
339
|
+
}
|