github-archiver 1.0.2 → 1.0.5
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/.releaserc.json +9 -1
- package/CHANGELOG.md +25 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +2 -2
- package/package.json +6 -2
- package/.claude/CLAUDE.md +0 -248
- package/.claude/settings.json +0 -15
- package/.github/dependabot.yml +0 -76
- package/.github/workflows/ci.yml +0 -64
- package/.github/workflows/release.yml +0 -55
- package/AGENTS.md +0 -248
- package/CONTRIBUTING.md +0 -343
- package/QUICKSTART.md +0 -221
- package/bun.lock +0 -1380
- package/issues.txt +0 -1105
- package/notes.md +0 -0
- package/scripts/build.ts +0 -14
- package/src/commands/archive.ts +0 -344
- package/src/commands/auth.ts +0 -184
- package/src/constants/defaults.ts +0 -6
- package/src/constants/messages.ts +0 -24
- package/src/constants/paths.ts +0 -12
- package/src/index.ts +0 -42
- package/src/services/archiver.ts +0 -192
- package/src/services/auth-manager.ts +0 -79
- package/src/services/github.ts +0 -211
- package/src/types/config.ts +0 -24
- package/src/types/error.ts +0 -35
- package/src/types/github.ts +0 -34
- package/src/types/index.ts +0 -4
- package/src/utils/colors.ts +0 -79
- package/src/utils/config.ts +0 -95
- package/src/utils/errors.ts +0 -93
- package/src/utils/formatting.ts +0 -65
- package/src/utils/input-handler.ts +0 -163
- package/src/utils/logger.ts +0 -65
- package/src/utils/parser.ts +0 -125
- package/src/utils/progress.ts +0 -87
- package/tests/unit/formatting.test.ts +0 -93
- package/tests/unit/parser.test.ts +0 -140
- package/tsconfig.json +0 -36
package/src/utils/config.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_CONCURRENCY,
|
|
5
|
-
DEFAULT_LOG_LEVEL,
|
|
6
|
-
DEFAULT_TIMEOUT,
|
|
7
|
-
} from "../constants/defaults";
|
|
8
|
-
import { PATHS } from "../constants/paths";
|
|
9
|
-
import type { Config, StoredCredentials } from "../types";
|
|
10
|
-
import { createConfigError, createFileError } from "./errors";
|
|
11
|
-
|
|
12
|
-
export class ConfigManager {
|
|
13
|
-
private readonly configDir: string;
|
|
14
|
-
private readonly configFile: string;
|
|
15
|
-
|
|
16
|
-
constructor(configDir: string = PATHS.APP_DIR) {
|
|
17
|
-
this.configDir = configDir;
|
|
18
|
-
this.configFile = join(configDir, "config.json");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async loadConfig(): Promise<Config> {
|
|
22
|
-
try {
|
|
23
|
-
const token = process.env.GH_TOKEN;
|
|
24
|
-
const fileCredentials = await this.loadFileCredentials();
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
token: token || fileCredentials?.token,
|
|
28
|
-
concurrency: DEFAULT_CONCURRENCY,
|
|
29
|
-
timeout: DEFAULT_TIMEOUT,
|
|
30
|
-
logLevel: DEFAULT_LOG_LEVEL,
|
|
31
|
-
logDir: join(this.configDir, "logs"),
|
|
32
|
-
configDir: this.configDir,
|
|
33
|
-
};
|
|
34
|
-
} catch (error) {
|
|
35
|
-
throw createConfigError(
|
|
36
|
-
`Failed to load configuration: ${error instanceof Error ? error.message : String(error)}`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async saveConfig(credentials: StoredCredentials): Promise<void> {
|
|
42
|
-
try {
|
|
43
|
-
await this.ensureConfigDir();
|
|
44
|
-
await fs.writeFile(
|
|
45
|
-
this.configFile,
|
|
46
|
-
JSON.stringify(credentials, null, 2),
|
|
47
|
-
"utf-8"
|
|
48
|
-
);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
throw createFileError(
|
|
51
|
-
`Failed to save configuration: ${error instanceof Error ? error.message : String(error)}`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async ensureConfigDir(): Promise<void> {
|
|
57
|
-
try {
|
|
58
|
-
await fs.mkdir(this.configDir, { recursive: true });
|
|
59
|
-
} catch (error) {
|
|
60
|
-
throw createFileError(
|
|
61
|
-
`Failed to create config directory: ${error instanceof Error ? error.message : String(error)}`
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async getStoredCredentials(): Promise<StoredCredentials | null> {
|
|
67
|
-
try {
|
|
68
|
-
const data = await fs.readFile(this.configFile, "utf-8");
|
|
69
|
-
return JSON.parse(data) as StoredCredentials;
|
|
70
|
-
} catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async removeStoredCredentials(): Promise<void> {
|
|
76
|
-
try {
|
|
77
|
-
await fs.unlink(this.configFile);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
80
|
-
throw createFileError(
|
|
81
|
-
`Failed to remove stored credentials: ${error instanceof Error ? error.message : String(error)}`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private async loadFileCredentials(): Promise<StoredCredentials | null> {
|
|
88
|
-
try {
|
|
89
|
-
const data = await fs.readFile(this.configFile, "utf-8");
|
|
90
|
-
return JSON.parse(data) as StoredCredentials;
|
|
91
|
-
} catch {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
package/src/utils/errors.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { MESSAGES } from "../constants/messages";
|
|
2
|
-
import { ArchiveError, ErrorCode } from "../types";
|
|
3
|
-
|
|
4
|
-
export function isArchiveError(error: unknown): error is ArchiveError {
|
|
5
|
-
return error instanceof ArchiveError;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function getErrorMessage(error: unknown): string {
|
|
9
|
-
if (isArchiveError(error)) {
|
|
10
|
-
return error.message;
|
|
11
|
-
}
|
|
12
|
-
if (error instanceof Error) {
|
|
13
|
-
return error.message;
|
|
14
|
-
}
|
|
15
|
-
return String(error);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function createAuthError(
|
|
19
|
-
message: string,
|
|
20
|
-
statusCode?: number
|
|
21
|
-
): ArchiveError {
|
|
22
|
-
return new ArchiveError(ErrorCode.INVALID_AUTH, message, statusCode, true);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function createRepoNotFoundError(
|
|
26
|
-
owner: string,
|
|
27
|
-
repo: string
|
|
28
|
-
): ArchiveError {
|
|
29
|
-
return new ArchiveError(
|
|
30
|
-
ErrorCode.REPO_NOT_FOUND,
|
|
31
|
-
`Repository ${owner}/${repo} not found`,
|
|
32
|
-
404,
|
|
33
|
-
false
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function createAlreadyArchivedError(
|
|
38
|
-
owner: string,
|
|
39
|
-
repo: string
|
|
40
|
-
): ArchiveError {
|
|
41
|
-
return new ArchiveError(
|
|
42
|
-
ErrorCode.ALREADY_ARCHIVED,
|
|
43
|
-
`Repository ${owner}/${repo} is already archived`,
|
|
44
|
-
422,
|
|
45
|
-
false
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function createPermissionError(
|
|
50
|
-
owner: string,
|
|
51
|
-
repo: string
|
|
52
|
-
): ArchiveError {
|
|
53
|
-
return new ArchiveError(
|
|
54
|
-
ErrorCode.PERMISSION_DENIED,
|
|
55
|
-
`Permission denied for ${owner}/${repo}. You must be the repository owner or have push access.`,
|
|
56
|
-
403,
|
|
57
|
-
false
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function createRateLimitError(): ArchiveError {
|
|
62
|
-
return new ArchiveError(
|
|
63
|
-
ErrorCode.RATE_LIMITED,
|
|
64
|
-
MESSAGES.RATE_LIMITED,
|
|
65
|
-
429,
|
|
66
|
-
true
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function createNetworkError(message?: string): ArchiveError {
|
|
71
|
-
return new ArchiveError(
|
|
72
|
-
ErrorCode.NETWORK_ERROR,
|
|
73
|
-
message || MESSAGES.NETWORK_ERROR,
|
|
74
|
-
undefined,
|
|
75
|
-
true
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function createInvalidUrlError(url: string): ArchiveError {
|
|
80
|
-
return new ArchiveError(ErrorCode.INVALID_URL, `Invalid GitHub URL: ${url}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function createConfigError(message: string): ArchiveError {
|
|
84
|
-
return new ArchiveError(ErrorCode.CONFIG_ERROR, message);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function createFileError(message: string): ArchiveError {
|
|
88
|
-
return new ArchiveError(ErrorCode.FILE_ERROR, message);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function createEditorError(message: string): ArchiveError {
|
|
92
|
-
return new ArchiveError(ErrorCode.EDITOR_ERROR, message);
|
|
93
|
-
}
|
package/src/utils/formatting.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
function formatDuration(ms: number): string {
|
|
2
|
-
if (ms < 1000) {
|
|
3
|
-
return `${ms}ms`;
|
|
4
|
-
}
|
|
5
|
-
if (ms < 60_000) {
|
|
6
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
7
|
-
}
|
|
8
|
-
const minutes = Math.floor(ms / 60_000);
|
|
9
|
-
const seconds = ((ms % 60_000) / 1000).toFixed(0);
|
|
10
|
-
return `${minutes}m ${seconds}s`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function formatBytes(bytes: number): string {
|
|
14
|
-
if (bytes === 0) {
|
|
15
|
-
return "0 B";
|
|
16
|
-
}
|
|
17
|
-
const k = 1024;
|
|
18
|
-
const sizes = ["B", "KB", "MB", "GB"];
|
|
19
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
20
|
-
return `${(bytes / k ** i).toFixed(2)} ${sizes[i]}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function formatPercent(value: number, total: number): string {
|
|
24
|
-
if (total === 0) {
|
|
25
|
-
return "0%";
|
|
26
|
-
}
|
|
27
|
-
return `${((value / total) * 100).toFixed(1)}%`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function createProgressBar(
|
|
31
|
-
completed: number,
|
|
32
|
-
total: number,
|
|
33
|
-
width = 30
|
|
34
|
-
): string {
|
|
35
|
-
if (total === 0) {
|
|
36
|
-
return "[ ]";
|
|
37
|
-
}
|
|
38
|
-
const filledWidth = Math.round((completed / total) * width);
|
|
39
|
-
const emptyWidth = width - filledWidth;
|
|
40
|
-
const filled = "=".repeat(filledWidth);
|
|
41
|
-
const empty = " ".repeat(emptyWidth);
|
|
42
|
-
const percentage = ((completed / total) * 100).toFixed(0);
|
|
43
|
-
return `[${filled}${empty}] ${percentage}%`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function centerText(text: string, width: number): string {
|
|
47
|
-
const padding = Math.max(0, (width - text.length) / 2);
|
|
48
|
-
return " ".repeat(Math.floor(padding)) + text;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function truncate(text: string, maxLength: number): string {
|
|
52
|
-
if (text.length <= maxLength) {
|
|
53
|
-
return text;
|
|
54
|
-
}
|
|
55
|
-
return `${text.substring(0, maxLength - 3)}...`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const Formatting = {
|
|
59
|
-
formatDuration,
|
|
60
|
-
formatBytes,
|
|
61
|
-
formatPercent,
|
|
62
|
-
createProgressBar,
|
|
63
|
-
centerText,
|
|
64
|
-
truncate,
|
|
65
|
-
};
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
|
-
import type { RepositoryIdentifier } from "../types";
|
|
4
|
-
import { getLogger } from "./logger";
|
|
5
|
-
import { URLParser } from "./parser";
|
|
6
|
-
|
|
7
|
-
const logger = getLogger();
|
|
8
|
-
|
|
9
|
-
export class InputHandler {
|
|
10
|
-
getRepositoriesFromInteractive(): Promise<{
|
|
11
|
-
repos: RepositoryIdentifier[];
|
|
12
|
-
errors: Array<{ url: string; error: string; line: number }>;
|
|
13
|
-
}> {
|
|
14
|
-
logger.info("Starting interactive CLI input for repositories");
|
|
15
|
-
|
|
16
|
-
return new Promise((resolve) => {
|
|
17
|
-
const rl = createInterface({
|
|
18
|
-
input: process.stdin,
|
|
19
|
-
output: process.stdout,
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
console.log("");
|
|
23
|
-
console.log("📝 Enter repository URLs one at a time:");
|
|
24
|
-
console.log(
|
|
25
|
-
' (Type "done" when finished, or leave empty and press Enter to skip)'
|
|
26
|
-
);
|
|
27
|
-
console.log("");
|
|
28
|
-
|
|
29
|
-
const urls: string[] = [];
|
|
30
|
-
let lineNumber = 1;
|
|
31
|
-
|
|
32
|
-
const promptNext = () => {
|
|
33
|
-
rl.question(`[${lineNumber}] > `, (input) => {
|
|
34
|
-
const trimmedInput = input.trim();
|
|
35
|
-
|
|
36
|
-
if (trimmedInput.toLowerCase() === "done") {
|
|
37
|
-
finishInput();
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (trimmedInput === "") {
|
|
42
|
-
lineNumber++;
|
|
43
|
-
promptNext();
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
urls.push(trimmedInput);
|
|
48
|
-
lineNumber++;
|
|
49
|
-
promptNext();
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const finishInput = () => {
|
|
54
|
-
rl.close();
|
|
55
|
-
const result = URLParser.parseRepositoriesBatch(urls);
|
|
56
|
-
|
|
57
|
-
if (result.invalid.length > 0) {
|
|
58
|
-
logger.warn(`Found ${result.invalid.length} invalid repositories`, {
|
|
59
|
-
errors: result.invalid,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
logger.info(`Parsed ${result.valid.length} valid repositories`);
|
|
64
|
-
resolve({
|
|
65
|
-
repos: result.valid,
|
|
66
|
-
errors: result.invalid,
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
rl.on("close", () => {
|
|
71
|
-
finishInput();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
promptNext();
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async getRepositoriesFromFile(filePath: string): Promise<{
|
|
79
|
-
repos: RepositoryIdentifier[];
|
|
80
|
-
errors: Array<{ url: string; error: string; line: number }>;
|
|
81
|
-
}> {
|
|
82
|
-
logger.info(`Reading repositories from file: ${filePath}`);
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
86
|
-
const lines = content.split("\n");
|
|
87
|
-
|
|
88
|
-
const result = URLParser.parseRepositoriesBatch(lines);
|
|
89
|
-
|
|
90
|
-
if (result.invalid.length > 0) {
|
|
91
|
-
logger.warn(`Found ${result.invalid.length} invalid entries in file`, {
|
|
92
|
-
errors: result.invalid,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
logger.info(`Parsed ${result.valid.length} valid repositories from file`);
|
|
97
|
-
return {
|
|
98
|
-
repos: result.valid,
|
|
99
|
-
errors: result.invalid,
|
|
100
|
-
};
|
|
101
|
-
} catch (error) {
|
|
102
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
-
logger.error(`Failed to read file ${filePath}: ${message}`);
|
|
104
|
-
throw new Error(`Failed to read file: ${message}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
getRepositoriesFromStdin(): Promise<{
|
|
109
|
-
repos: RepositoryIdentifier[];
|
|
110
|
-
errors: Array<{ url: string; error: string; line: number }>;
|
|
111
|
-
}> {
|
|
112
|
-
logger.info("Reading repositories from stdin");
|
|
113
|
-
|
|
114
|
-
return new Promise((resolve) => {
|
|
115
|
-
const lines: string[] = [];
|
|
116
|
-
const rl = createInterface({
|
|
117
|
-
input: process.stdin,
|
|
118
|
-
output: process.stdout,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
rl.on("line", (line) => {
|
|
122
|
-
lines.push(line);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
rl.on("close", () => {
|
|
126
|
-
const result = URLParser.parseRepositoriesBatch(lines);
|
|
127
|
-
|
|
128
|
-
if (result.invalid.length > 0) {
|
|
129
|
-
logger.warn(
|
|
130
|
-
`Found ${result.invalid.length} invalid entries from stdin`,
|
|
131
|
-
{
|
|
132
|
-
errors: result.invalid,
|
|
133
|
-
}
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
logger.info(
|
|
138
|
-
`Parsed ${result.valid.length} valid repositories from stdin`
|
|
139
|
-
);
|
|
140
|
-
resolve({
|
|
141
|
-
repos: result.valid,
|
|
142
|
-
errors: result.invalid,
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
promptForConfirmation(message: string): Promise<boolean> {
|
|
149
|
-
return new Promise((resolve) => {
|
|
150
|
-
const rl = createInterface({
|
|
151
|
-
input: process.stdin,
|
|
152
|
-
output: process.stdout,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
rl.question(`${message} [y/N]: `, (answer) => {
|
|
156
|
-
rl.close();
|
|
157
|
-
const confirmed = answer.toLowerCase() === "y";
|
|
158
|
-
logger.info(`User confirmation: ${confirmed}`);
|
|
159
|
-
resolve(confirmed);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
package/src/utils/logger.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import winston from "winston";
|
|
3
|
-
import { DEFAULT_LOG_LEVEL } from "../constants/defaults";
|
|
4
|
-
import { PATHS } from "../constants/paths";
|
|
5
|
-
import type { Config } from "../types";
|
|
6
|
-
|
|
7
|
-
let loggerInstance: winston.Logger;
|
|
8
|
-
|
|
9
|
-
export async function initializeLogger(logDir: string): Promise<void> {
|
|
10
|
-
try {
|
|
11
|
-
await fs.mkdir(logDir, { recursive: true });
|
|
12
|
-
} catch (error) {
|
|
13
|
-
console.error("Failed to create log directory:", error);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function createLogger(config?: Partial<Config>): winston.Logger {
|
|
18
|
-
const level = config?.logLevel || DEFAULT_LOG_LEVEL;
|
|
19
|
-
const logDir = config?.logDir || PATHS.LOG_DIR;
|
|
20
|
-
|
|
21
|
-
return winston.createLogger({
|
|
22
|
-
level,
|
|
23
|
-
format: winston.format.combine(
|
|
24
|
-
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
25
|
-
winston.format.errors({ stack: true }),
|
|
26
|
-
winston.format.json()
|
|
27
|
-
),
|
|
28
|
-
defaultMeta: { service: "github-archiver" },
|
|
29
|
-
transports: [
|
|
30
|
-
new winston.transports.File({
|
|
31
|
-
filename: `${logDir}/errors.log`,
|
|
32
|
-
level: "error",
|
|
33
|
-
}),
|
|
34
|
-
new winston.transports.File({
|
|
35
|
-
filename: `${logDir}/combined.log`,
|
|
36
|
-
}),
|
|
37
|
-
],
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function createConsoleLogger(): winston.Logger {
|
|
42
|
-
return winston.createLogger({
|
|
43
|
-
format: winston.format.combine(
|
|
44
|
-
winston.format.colorize(),
|
|
45
|
-
winston.format.printf(({ level, message, ...metadata }) => {
|
|
46
|
-
const meta = Object.keys(metadata).length
|
|
47
|
-
? JSON.stringify(metadata)
|
|
48
|
-
: "";
|
|
49
|
-
return `${level}: ${message} ${meta}`;
|
|
50
|
-
})
|
|
51
|
-
),
|
|
52
|
-
transports: [new winston.transports.Console()],
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function getLogger(): winston.Logger {
|
|
57
|
-
if (!loggerInstance) {
|
|
58
|
-
loggerInstance = createLogger();
|
|
59
|
-
}
|
|
60
|
-
return loggerInstance;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function setLogger(logger: winston.Logger): void {
|
|
64
|
-
loggerInstance = logger;
|
|
65
|
-
}
|
package/src/utils/parser.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { RepositoryIdentifier } from "../types";
|
|
2
|
-
import { ArchiveError, ErrorCode } from "../types";
|
|
3
|
-
import { getLogger } from "./logger";
|
|
4
|
-
|
|
5
|
-
const logger = getLogger();
|
|
6
|
-
const NAME_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/;
|
|
7
|
-
|
|
8
|
-
const GITHUB_URL_PATTERNS: RegExp[] = [
|
|
9
|
-
// https://github.com/owner/repo or https://github.com/owner/repo.git
|
|
10
|
-
/^(?:https?:\/\/)?(?:www\.)?github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:\/)?$/i,
|
|
11
|
-
// git@github.com:owner/repo.git or git@github.com:owner/repo
|
|
12
|
-
/^git@github\.com:([\w.-]+)\/([\w.-]+?)(?:\.git)?$/i,
|
|
13
|
-
// owner/repo (shorthand)
|
|
14
|
-
/^([\w.-]+)\/([\w.-]+)$/,
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
function parseRepositoryUrl(url: string): RepositoryIdentifier {
|
|
18
|
-
const trimmed = url.trim();
|
|
19
|
-
|
|
20
|
-
if (!trimmed) {
|
|
21
|
-
throw new ArchiveError(ErrorCode.INVALID_URL, "URL cannot be empty");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
for (const pattern of GITHUB_URL_PATTERNS) {
|
|
25
|
-
const match = trimmed.match(pattern);
|
|
26
|
-
if (match) {
|
|
27
|
-
const owner = match[1];
|
|
28
|
-
const repo = match[2];
|
|
29
|
-
|
|
30
|
-
if (!(owner && repo)) {
|
|
31
|
-
throw new ArchiveError(
|
|
32
|
-
ErrorCode.INVALID_URL,
|
|
33
|
-
`Invalid GitHub URL format: ${url}`
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!isValidName(owner)) {
|
|
38
|
-
throw new ArchiveError(
|
|
39
|
-
ErrorCode.INVALID_URL,
|
|
40
|
-
`Invalid owner name: ${owner}. Owner names must contain only alphanumeric characters, hyphens, or periods.`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!isValidName(repo)) {
|
|
45
|
-
throw new ArchiveError(
|
|
46
|
-
ErrorCode.INVALID_URL,
|
|
47
|
-
`Invalid repository name: ${repo}. Repository names must contain only alphanumeric characters, hyphens, underscores, or periods.`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const normalizedUrl = `https://github.com/${owner}/${repo}`;
|
|
52
|
-
|
|
53
|
-
logger.debug("Parsed repository URL", {
|
|
54
|
-
original: trimmed,
|
|
55
|
-
owner,
|
|
56
|
-
repo,
|
|
57
|
-
normalized: normalizedUrl,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
owner,
|
|
62
|
-
repo,
|
|
63
|
-
url: normalizedUrl,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
throw new ArchiveError(ErrorCode.INVALID_URL, `Invalid GitHub URL: ${url}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function isValidName(name: string): boolean {
|
|
72
|
-
return NAME_REGEX.test(name);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function parseRepositoriesBatch(urls: string[]): {
|
|
76
|
-
valid: RepositoryIdentifier[];
|
|
77
|
-
invalid: Array<{ url: string; error: string; line: number }>;
|
|
78
|
-
} {
|
|
79
|
-
const valid: RepositoryIdentifier[] = [];
|
|
80
|
-
const invalid: Array<{ url: string; error: string; line: number }> = [];
|
|
81
|
-
|
|
82
|
-
for (const [index, url] of urls.entries()) {
|
|
83
|
-
const lineNumber = index + 1;
|
|
84
|
-
const trimmed = url.trim();
|
|
85
|
-
|
|
86
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const parsed = parseRepositoryUrl(trimmed);
|
|
92
|
-
valid.push(parsed);
|
|
93
|
-
logger.debug(`Line ${lineNumber}: Valid repository`, {
|
|
94
|
-
owner: parsed.owner,
|
|
95
|
-
repo: parsed.repo,
|
|
96
|
-
});
|
|
97
|
-
} catch (error) {
|
|
98
|
-
const errorMessage =
|
|
99
|
-
error instanceof Error ? error.message : String(error);
|
|
100
|
-
invalid.push({
|
|
101
|
-
url: trimmed,
|
|
102
|
-
error: errorMessage,
|
|
103
|
-
line: lineNumber,
|
|
104
|
-
});
|
|
105
|
-
logger.warn(`Line ${lineNumber}: Invalid repository`, {
|
|
106
|
-
url: trimmed,
|
|
107
|
-
error: errorMessage,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
logger.info("Batch parsing complete", {
|
|
113
|
-
total: urls.length,
|
|
114
|
-
valid: valid.length,
|
|
115
|
-
invalid: invalid.length,
|
|
116
|
-
skipped: urls.length - valid.length - invalid.length,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return { valid, invalid };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export const URLParser = {
|
|
123
|
-
parseRepositoryUrl,
|
|
124
|
-
parseRepositoriesBatch,
|
|
125
|
-
};
|
package/src/utils/progress.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { Formatting } from "./formatting";
|
|
2
|
-
|
|
3
|
-
export interface ProgressUpdate {
|
|
4
|
-
completed: number;
|
|
5
|
-
failed: number;
|
|
6
|
-
total: number;
|
|
7
|
-
current?: {
|
|
8
|
-
owner: string;
|
|
9
|
-
repo: string;
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class ProgressDisplay {
|
|
14
|
-
private readonly startTime: number;
|
|
15
|
-
private lastUpdate = 0;
|
|
16
|
-
private readonly updateInterval = 500; // ms
|
|
17
|
-
|
|
18
|
-
constructor() {
|
|
19
|
-
this.startTime = Date.now();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
shouldUpdate(): boolean {
|
|
23
|
-
const now = Date.now();
|
|
24
|
-
if (now - this.lastUpdate >= this.updateInterval) {
|
|
25
|
-
this.lastUpdate = now;
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
getProgressBar(progress: ProgressUpdate): string {
|
|
32
|
-
const { completed, total, current } = progress;
|
|
33
|
-
const percent = total > 0 ? (completed / total) * 100 : 0;
|
|
34
|
-
const bar = Formatting.createProgressBar(completed, total, 25);
|
|
35
|
-
|
|
36
|
-
let line = `${bar} ${completed}/${total} (${percent.toFixed(0)}%)`;
|
|
37
|
-
|
|
38
|
-
if (current) {
|
|
39
|
-
line += ` - ${current.owner}/${current.repo}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return line;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
getSummaryBox(summary: {
|
|
46
|
-
successful: number;
|
|
47
|
-
failed: number;
|
|
48
|
-
skipped: number;
|
|
49
|
-
totalDuration: number;
|
|
50
|
-
}): string {
|
|
51
|
-
const { successful, failed, skipped, totalDuration } = summary;
|
|
52
|
-
const total = successful + failed + skipped;
|
|
53
|
-
const duration = Formatting.formatDuration(totalDuration);
|
|
54
|
-
|
|
55
|
-
const lines = [
|
|
56
|
-
"╔════════════════════════════════════╗",
|
|
57
|
-
"║ Archive Operation Summary ║",
|
|
58
|
-
"╠════════════════════════════════════╣",
|
|
59
|
-
`║ ✅ Successful: ${String(successful).padEnd(20)} ║`,
|
|
60
|
-
`║ ⚠️ Skipped: ${String(skipped).padEnd(20)} ║`,
|
|
61
|
-
`║ ❌ Failed: ${String(failed).padEnd(20)} ║`,
|
|
62
|
-
"╠════════════════════════════════════╣",
|
|
63
|
-
`║ Total: ${String(total).padEnd(20)} ║`,
|
|
64
|
-
`║ Duration: ${String(duration).padEnd(20)} ║`,
|
|
65
|
-
"╚════════════════════════════════════╝",
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
return lines.join("\n");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
getElapsedTime(): string {
|
|
72
|
-
const elapsed = Date.now() - this.startTime;
|
|
73
|
-
return Formatting.formatDuration(elapsed);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
getEstimatedTimeRemaining(completed: number, total: number): string | null {
|
|
77
|
-
if (completed === 0) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const elapsed = Date.now() - this.startTime;
|
|
82
|
-
const avgTime = elapsed / completed;
|
|
83
|
-
const remaining = (total - completed) * avgTime;
|
|
84
|
-
|
|
85
|
-
return remaining > 0 ? Formatting.formatDuration(remaining) : null;
|
|
86
|
-
}
|
|
87
|
-
}
|