glance-cli 0.12.0 → 0.13.1
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/CHANGELOG.md +44 -0
- package/dist/cli.js +139 -1064
- package/package.json +12 -11
- package/src/cli/commands.ts +832 -0
- package/src/cli/config.ts +24 -0
- package/src/cli/display.ts +269 -0
- package/src/cli/errors.ts +31 -0
- package/src/cli/index.ts +237 -0
- package/src/cli/logger.ts +43 -0
- package/src/cli/types.ts +114 -0
- package/src/cli/utils.ts +239 -0
- package/src/cli/validators.ts +176 -0
- package/src/cli.ts +17 -0
- package/src/core/compat.ts +96 -0
- package/src/core/extractor.ts +532 -0
- package/src/core/fetcher.ts +592 -0
- package/src/core/formatter.ts +742 -0
- package/src/core/language-detector.ts +382 -0
- package/src/core/screenshot.ts +444 -0
- package/src/core/service-detector.ts +411 -0
- package/src/core/summarizer.ts +656 -0
- package/src/core/text-cleaner.ts +150 -0
- package/src/core/voice.ts +708 -0
package/src/cli/types.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for CLI modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Service detection types
|
|
6
|
+
export interface ServiceStatus {
|
|
7
|
+
ollama: {
|
|
8
|
+
available: boolean;
|
|
9
|
+
models?: string[];
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
openai: {
|
|
13
|
+
available: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
gemini: {
|
|
17
|
+
available: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
elevenlabs: {
|
|
21
|
+
available: boolean;
|
|
22
|
+
voices?: string[];
|
|
23
|
+
error?: string;
|
|
24
|
+
};
|
|
25
|
+
defaultModel?: string;
|
|
26
|
+
priority?: string;
|
|
27
|
+
recommendations?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Voice synthesis types
|
|
31
|
+
export interface VoiceOptions {
|
|
32
|
+
voice?: string;
|
|
33
|
+
language?: string;
|
|
34
|
+
speed?: number;
|
|
35
|
+
pitch?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cache options types
|
|
39
|
+
export interface CacheOptions {
|
|
40
|
+
noCache?: boolean;
|
|
41
|
+
ttl?: number;
|
|
42
|
+
compress?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Model provider types
|
|
46
|
+
export type ModelProvider = "openai" | "google" | "ollama" | "auto";
|
|
47
|
+
|
|
48
|
+
// Summary types
|
|
49
|
+
export interface SummaryOptions {
|
|
50
|
+
tldr?: boolean;
|
|
51
|
+
keyPoints?: boolean;
|
|
52
|
+
eli5?: boolean;
|
|
53
|
+
full?: boolean;
|
|
54
|
+
customQuestion?: string;
|
|
55
|
+
language?: string;
|
|
56
|
+
stream?: boolean;
|
|
57
|
+
maxTokens?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fetch options types
|
|
61
|
+
export interface FetchOptions {
|
|
62
|
+
fullRender?: boolean;
|
|
63
|
+
timeout?: number;
|
|
64
|
+
userAgent?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Format options types
|
|
68
|
+
export interface FormatOptions {
|
|
69
|
+
url?: string;
|
|
70
|
+
language?: string;
|
|
71
|
+
customQuestion?: string;
|
|
72
|
+
customTitle?: string;
|
|
73
|
+
isFullContent?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// CLI parsed values types
|
|
77
|
+
export interface CliValues {
|
|
78
|
+
help?: boolean;
|
|
79
|
+
version?: boolean;
|
|
80
|
+
tldr?: boolean;
|
|
81
|
+
"key-points"?: boolean;
|
|
82
|
+
eli5?: boolean;
|
|
83
|
+
full?: boolean;
|
|
84
|
+
ask?: string;
|
|
85
|
+
language?: string;
|
|
86
|
+
read?: boolean;
|
|
87
|
+
voice?: string;
|
|
88
|
+
"list-voices"?: boolean;
|
|
89
|
+
"audio-output"?: string;
|
|
90
|
+
model?: string;
|
|
91
|
+
"list-models"?: boolean;
|
|
92
|
+
stream?: boolean;
|
|
93
|
+
"max-tokens"?: string;
|
|
94
|
+
"check-services"?: boolean;
|
|
95
|
+
"free-only"?: boolean;
|
|
96
|
+
"prefer-quality"?: boolean;
|
|
97
|
+
"no-cache"?: boolean;
|
|
98
|
+
"clear-cache"?: boolean;
|
|
99
|
+
"full-render"?: boolean;
|
|
100
|
+
screenshot?: string;
|
|
101
|
+
metadata?: boolean;
|
|
102
|
+
links?: boolean;
|
|
103
|
+
debug?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Error types with proper structure
|
|
107
|
+
export interface GlanceErrorDetails {
|
|
108
|
+
code: string;
|
|
109
|
+
message: string;
|
|
110
|
+
userMessage: string;
|
|
111
|
+
recoverable?: boolean;
|
|
112
|
+
hint?: string;
|
|
113
|
+
cause?: Error;
|
|
114
|
+
}
|
package/src/cli/utils.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for Glance CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import ora, { type Ora } from "ora";
|
|
6
|
+
import {
|
|
7
|
+
hasBinaryArtifacts,
|
|
8
|
+
nuclearCleanText,
|
|
9
|
+
sanitizeAIResponse,
|
|
10
|
+
} from "../core/text-cleaner";
|
|
11
|
+
import { CONFIG } from "./config";
|
|
12
|
+
import { GlanceError } from "./errors";
|
|
13
|
+
import { logger } from "./logger";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sanitize text output for terminal display
|
|
17
|
+
*/
|
|
18
|
+
export function sanitizeOutputForTerminal(text: string): string {
|
|
19
|
+
// Preserve terminal color codes while cleaning text
|
|
20
|
+
const ansiRegex = /\x1b\[[0-9;]*m/g;
|
|
21
|
+
const ansiCodes: string[] = [];
|
|
22
|
+
let cleanText = text;
|
|
23
|
+
|
|
24
|
+
// Extract ANSI codes
|
|
25
|
+
let match: RegExpExecArray | null;
|
|
26
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: RegExp.exec pattern is idiomatic
|
|
27
|
+
while ((match = ansiRegex.exec(text)) !== null) {
|
|
28
|
+
ansiCodes.push(match[0]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if nuclear cleaning is needed
|
|
32
|
+
const needsNuclearCleaning = hasBinaryArtifacts(text);
|
|
33
|
+
|
|
34
|
+
if (needsNuclearCleaning) {
|
|
35
|
+
// Apply nuclear cleaning for binary artifacts
|
|
36
|
+
logger.warn("Binary artifacts detected, applying nuclear cleaning");
|
|
37
|
+
cleanText = nuclearCleanText(sanitizeAIResponse(text));
|
|
38
|
+
} else {
|
|
39
|
+
// Light cleaning for normal text
|
|
40
|
+
cleanText = text
|
|
41
|
+
// Remove null bytes and other control characters
|
|
42
|
+
.replace(/\0/g, "")
|
|
43
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
|
|
44
|
+
// Fix common encoding issues
|
|
45
|
+
.replace(/’/g, "'")
|
|
46
|
+
.replace(/â€"/g, "—")
|
|
47
|
+
.replace(/“/g, '"')
|
|
48
|
+
.replace(/�/g, '"')
|
|
49
|
+
.replace(/‘/g, "'")
|
|
50
|
+
.replace(/…/g, "...")
|
|
51
|
+
.replace(/é/g, "é")
|
|
52
|
+
.replace(/è/g, "è")
|
|
53
|
+
.replace(/Ã /g, "à")
|
|
54
|
+
.replace(/â/g, "â")
|
|
55
|
+
.replace(/î/g, "î")
|
|
56
|
+
.replace(/ô/g, "ô")
|
|
57
|
+
.replace(/ç/g, "ç")
|
|
58
|
+
// Fix spacing
|
|
59
|
+
.replace(/\r\n/g, "\n")
|
|
60
|
+
.replace(/\r/g, "\n")
|
|
61
|
+
.replace(/\n{4,}/g, "\n\n\n")
|
|
62
|
+
.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return cleanText;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retry logic for async operations
|
|
70
|
+
*/
|
|
71
|
+
export async function withRetry<T>(
|
|
72
|
+
operation: () => Promise<T>,
|
|
73
|
+
options: {
|
|
74
|
+
attempts?: number;
|
|
75
|
+
delay?: number;
|
|
76
|
+
backoff?: number;
|
|
77
|
+
onRetry?: (attempt: number, error: unknown) => void;
|
|
78
|
+
} = {},
|
|
79
|
+
): Promise<T> {
|
|
80
|
+
const {
|
|
81
|
+
attempts = CONFIG.RETRY_ATTEMPTS,
|
|
82
|
+
delay = CONFIG.RETRY_DELAY,
|
|
83
|
+
backoff = 2,
|
|
84
|
+
onRetry,
|
|
85
|
+
} = options;
|
|
86
|
+
|
|
87
|
+
let lastError: unknown;
|
|
88
|
+
|
|
89
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
90
|
+
try {
|
|
91
|
+
return await operation();
|
|
92
|
+
} catch (error: unknown) {
|
|
93
|
+
lastError = error;
|
|
94
|
+
|
|
95
|
+
// Don't retry on non-recoverable errors
|
|
96
|
+
if (error instanceof GlanceError && !error.recoverable) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check for specific non-recoverable HTTP status codes
|
|
101
|
+
if (
|
|
102
|
+
error &&
|
|
103
|
+
typeof error === "object" &&
|
|
104
|
+
"status" in error &&
|
|
105
|
+
typeof error.status === "number" &&
|
|
106
|
+
[400, 401, 403, 404].includes(error.status)
|
|
107
|
+
) {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (attempt < attempts) {
|
|
112
|
+
const waitTime = delay * backoff ** (attempt - 1);
|
|
113
|
+
|
|
114
|
+
if (onRetry) {
|
|
115
|
+
onRetry(attempt, error);
|
|
116
|
+
} else {
|
|
117
|
+
logger.debug(
|
|
118
|
+
`Retry attempt ${attempt}/${attempts} after ${waitTime}ms`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw lastError;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a spinner with consistent styling
|
|
132
|
+
*/
|
|
133
|
+
export function createSpinner(text: string): Ora {
|
|
134
|
+
return ora({
|
|
135
|
+
text,
|
|
136
|
+
spinner: "dots",
|
|
137
|
+
color: "cyan",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Format file size for display
|
|
143
|
+
*/
|
|
144
|
+
export function formatFileSize(bytes: number): string {
|
|
145
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
146
|
+
let size = bytes;
|
|
147
|
+
let unitIndex = 0;
|
|
148
|
+
|
|
149
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
150
|
+
size /= 1024;
|
|
151
|
+
unitIndex++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Format duration for display
|
|
159
|
+
*/
|
|
160
|
+
export function formatDuration(ms: number): string {
|
|
161
|
+
if (ms < 1000) {
|
|
162
|
+
return `${ms}ms`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const seconds = Math.floor(ms / 1000);
|
|
166
|
+
const minutes = Math.floor(seconds / 60);
|
|
167
|
+
|
|
168
|
+
if (minutes > 0) {
|
|
169
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return `${seconds}s`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if running in TTY environment
|
|
177
|
+
*/
|
|
178
|
+
export function isTTY(): boolean {
|
|
179
|
+
return process.stdout.isTTY || false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get terminal width
|
|
184
|
+
*/
|
|
185
|
+
export function getTerminalWidth(): number {
|
|
186
|
+
return process.stdout.columns || 80;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Truncate text to fit terminal width
|
|
191
|
+
*/
|
|
192
|
+
export function truncateToWidth(text: string, maxWidth?: number): string {
|
|
193
|
+
const width = maxWidth || getTerminalWidth() - 4; // Leave some margin
|
|
194
|
+
|
|
195
|
+
if (text.length <= width) {
|
|
196
|
+
return text;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return `${text.substring(0, width - 3)}...`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Parse file extension for export
|
|
204
|
+
*/
|
|
205
|
+
export function getFileExtension(filename: string): string {
|
|
206
|
+
const parts = filename.split(".");
|
|
207
|
+
return parts.length > 1 ? (parts[parts.length - 1]?.toLowerCase() ?? "") : "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Check if model is a premium model
|
|
212
|
+
*/
|
|
213
|
+
export function isPremiumModel(model: string): boolean {
|
|
214
|
+
const premiumPrefixes = ["gpt-4", "claude", "gemini-pro", "gemini-ultra"];
|
|
215
|
+
return premiumPrefixes.some((prefix) =>
|
|
216
|
+
model.toLowerCase().startsWith(prefix),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Format model name for display
|
|
222
|
+
*/
|
|
223
|
+
export function formatModelName(model: string): string {
|
|
224
|
+
// Add provider labels for clarity
|
|
225
|
+
if (model.startsWith("gpt")) {
|
|
226
|
+
return `${model} (OpenAI)`;
|
|
227
|
+
}
|
|
228
|
+
if (model.startsWith("gemini")) {
|
|
229
|
+
return `${model} (Google)`;
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
model.includes("llama") ||
|
|
233
|
+
model.includes("mistral") ||
|
|
234
|
+
model.includes("phi")
|
|
235
|
+
) {
|
|
236
|
+
return `${model} (Local)`;
|
|
237
|
+
}
|
|
238
|
+
return model;
|
|
239
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utilities for Glance CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { LANGUAGE_MAP } from "./config";
|
|
6
|
+
|
|
7
|
+
export function validateURL(urlString: string): {
|
|
8
|
+
valid: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
} {
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(urlString);
|
|
13
|
+
|
|
14
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
15
|
+
return {
|
|
16
|
+
valid: false,
|
|
17
|
+
error: "Only HTTP and HTTPS URLs are supported",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!url.hostname || url.hostname.length < 3) {
|
|
22
|
+
return {
|
|
23
|
+
valid: false,
|
|
24
|
+
error: "Invalid hostname",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { valid: true };
|
|
29
|
+
} catch (_error) {
|
|
30
|
+
return {
|
|
31
|
+
valid: false,
|
|
32
|
+
error:
|
|
33
|
+
"Invalid URL format. Please provide a valid URL starting with http:// or https://",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateLanguage(lang: string): {
|
|
39
|
+
valid: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
} {
|
|
42
|
+
const supportedLanguages = Object.keys(LANGUAGE_MAP);
|
|
43
|
+
|
|
44
|
+
if (!supportedLanguages.includes(lang)) {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
error: `Language '${lang}' is not supported. Supported languages: ${supportedLanguages.join(", ")}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { valid: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validateMaxTokens(value: string | undefined): {
|
|
55
|
+
valid: boolean;
|
|
56
|
+
parsed?: number;
|
|
57
|
+
error?: string;
|
|
58
|
+
} {
|
|
59
|
+
if (!value) {
|
|
60
|
+
return { valid: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const parsed = parseInt(value, 10);
|
|
64
|
+
|
|
65
|
+
if (Number.isNaN(parsed)) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
error: "max-tokens must be a valid number",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (parsed < 1 || parsed > 100000) {
|
|
73
|
+
return {
|
|
74
|
+
valid: false,
|
|
75
|
+
error: "max-tokens must be between 1 and 100000",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { valid: true, parsed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function validateAPIKeys(
|
|
83
|
+
provider: "openai" | "google" | "ollama",
|
|
84
|
+
): Promise<{ valid: boolean; error?: string; hint?: string }> {
|
|
85
|
+
switch (provider) {
|
|
86
|
+
case "openai": {
|
|
87
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
88
|
+
if (!apiKey) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: "OpenAI API key not found",
|
|
92
|
+
hint: "Set your API key with: export OPENAI_API_KEY=sk-...",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!apiKey.startsWith("sk-")) {
|
|
97
|
+
return {
|
|
98
|
+
valid: false,
|
|
99
|
+
error: "Invalid OpenAI API key format",
|
|
100
|
+
hint: "OpenAI API keys should start with 'sk-'",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Basic length check
|
|
105
|
+
if (apiKey.length < 40) {
|
|
106
|
+
return {
|
|
107
|
+
valid: false,
|
|
108
|
+
error: "OpenAI API key appears to be incomplete",
|
|
109
|
+
hint: "Make sure you've copied the entire API key",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { valid: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "google": {
|
|
117
|
+
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
|
118
|
+
if (!apiKey) {
|
|
119
|
+
return {
|
|
120
|
+
valid: false,
|
|
121
|
+
error: "Gemini API key not found",
|
|
122
|
+
hint: "Set your API key with: export GEMINI_API_KEY=...",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Basic length check for Google API keys
|
|
127
|
+
if (apiKey.length < 30) {
|
|
128
|
+
return {
|
|
129
|
+
valid: false,
|
|
130
|
+
error: "Gemini API key appears to be incomplete",
|
|
131
|
+
hint: "Make sure you've copied the entire API key",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { valid: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "ollama": {
|
|
139
|
+
// Ollama doesn't require API keys, just check if it's running
|
|
140
|
+
const endpoint = process.env.OLLAMA_ENDPOINT || "http://localhost:11434";
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
145
|
+
|
|
146
|
+
const response = await fetch(`${endpoint}/api/version`, {
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
return {
|
|
154
|
+
valid: false,
|
|
155
|
+
error: `Ollama is not responding at ${endpoint}`,
|
|
156
|
+
hint: "Make sure Ollama is running. Start it with: ollama serve",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { valid: true };
|
|
161
|
+
} catch (_error) {
|
|
162
|
+
return {
|
|
163
|
+
valid: false,
|
|
164
|
+
error: `Cannot connect to Ollama at ${endpoint}`,
|
|
165
|
+
hint: "Install and start Ollama: https://ollama.ai/download",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
return {
|
|
172
|
+
valid: false,
|
|
173
|
+
error: `Unknown provider: ${provider}`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Glance CLI - Main entry point
|
|
5
|
+
*
|
|
6
|
+
* This file serves as a thin wrapper that imports and runs
|
|
7
|
+
* the modularized CLI from the cli/ directory.
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { runCli } from "./cli/index";
|
|
12
|
+
|
|
13
|
+
// Run the CLI
|
|
14
|
+
runCli().catch((error) => {
|
|
15
|
+
console.error("Fatal error:", error);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility layer for Bun and Node.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import * as fs from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
// Runtime detection
|
|
9
|
+
export const isBun = typeof Bun !== "undefined";
|
|
10
|
+
|
|
11
|
+
// Process arguments
|
|
12
|
+
export const argv = isBun ? Bun.argv : process.argv;
|
|
13
|
+
|
|
14
|
+
// Environment variables
|
|
15
|
+
export const env = isBun ? Bun.env : process.env;
|
|
16
|
+
|
|
17
|
+
// File operations
|
|
18
|
+
export async function writeFile(
|
|
19
|
+
filePath: string,
|
|
20
|
+
content: string | Buffer,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
if (isBun) {
|
|
23
|
+
await Bun.write(filePath, content);
|
|
24
|
+
} else {
|
|
25
|
+
await fs.writeFile(filePath, content);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function readFile(filePath: string): Promise<string> {
|
|
30
|
+
if (isBun) {
|
|
31
|
+
const file = Bun.file(filePath);
|
|
32
|
+
return (await file.exists()) ? await file.text() : "";
|
|
33
|
+
} else {
|
|
34
|
+
try {
|
|
35
|
+
return await fs.readFile(filePath, "utf-8");
|
|
36
|
+
} catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function readFileBuffer(filePath: string): Promise<Buffer> {
|
|
43
|
+
if (isBun) {
|
|
44
|
+
const file = Bun.file(filePath);
|
|
45
|
+
const buffer = await file.arrayBuffer();
|
|
46
|
+
return Buffer.from(buffer);
|
|
47
|
+
} else {
|
|
48
|
+
return await fs.readFile(filePath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
53
|
+
if (isBun) {
|
|
54
|
+
return Bun.file(filePath).exists();
|
|
55
|
+
} else {
|
|
56
|
+
try {
|
|
57
|
+
await fs.access(filePath);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Shell operations
|
|
66
|
+
export async function shell(command: string): Promise<void> {
|
|
67
|
+
if (isBun) {
|
|
68
|
+
await Bun.$`sh -c ${command}`.quiet();
|
|
69
|
+
} else {
|
|
70
|
+
execSync(command, { stdio: "ignore" });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function mkdirp(dirPath: string): Promise<void> {
|
|
75
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function rm(filePath: string): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
await fs.unlink(filePath);
|
|
81
|
+
} catch {
|
|
82
|
+
// Ignore if file doesn't exist
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function rmrf(dirPath: string): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
await fs.rm(dirPath, { recursive: true, force: true });
|
|
89
|
+
} catch {
|
|
90
|
+
// Ignore if directory doesn't exist
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function mv(src: string, dest: string): Promise<void> {
|
|
95
|
+
await fs.rename(src, dest);
|
|
96
|
+
}
|