ubersearch 0.0.0-development
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 +374 -0
- package/package.json +76 -0
- package/src/app/index.ts +30 -0
- package/src/bootstrap/container.ts +157 -0
- package/src/cli.ts +380 -0
- package/src/config/defineConfig.ts +176 -0
- package/src/config/load.ts +368 -0
- package/src/config/types.ts +86 -0
- package/src/config/validation.ts +148 -0
- package/src/core/cache.ts +74 -0
- package/src/core/container.ts +268 -0
- package/src/core/credits/CreditManager.ts +158 -0
- package/src/core/credits/CreditStateProvider.ts +151 -0
- package/src/core/credits/FileCreditStateProvider.ts +137 -0
- package/src/core/credits/index.ts +3 -0
- package/src/core/docker/dockerComposeHelper.ts +177 -0
- package/src/core/docker/dockerLifecycleManager.ts +361 -0
- package/src/core/docker/index.ts +8 -0
- package/src/core/logger.ts +146 -0
- package/src/core/orchestrator.ts +103 -0
- package/src/core/paths.ts +157 -0
- package/src/core/provider/ILifecycleProvider.ts +120 -0
- package/src/core/provider/ProviderFactory.ts +120 -0
- package/src/core/provider.ts +61 -0
- package/src/core/serviceKeys.ts +45 -0
- package/src/core/strategy/AllProvidersStrategy.ts +245 -0
- package/src/core/strategy/FirstSuccessStrategy.ts +98 -0
- package/src/core/strategy/ISearchStrategy.ts +94 -0
- package/src/core/strategy/StrategyFactory.ts +204 -0
- package/src/core/strategy/index.ts +9 -0
- package/src/core/strategy/types.ts +56 -0
- package/src/core/types.ts +58 -0
- package/src/index.ts +1 -0
- package/src/plugin/PluginRegistry.ts +336 -0
- package/src/plugin/builtin.ts +130 -0
- package/src/plugin/index.ts +33 -0
- package/src/plugin/types.ts +212 -0
- package/src/providers/BaseProvider.ts +49 -0
- package/src/providers/brave.ts +66 -0
- package/src/providers/constants.ts +13 -0
- package/src/providers/helpers/index.ts +24 -0
- package/src/providers/helpers/lifecycleHelpers.ts +110 -0
- package/src/providers/helpers/resultMappers.ts +168 -0
- package/src/providers/index.ts +6 -0
- package/src/providers/linkup.ts +114 -0
- package/src/providers/retry.ts +95 -0
- package/src/providers/searchxng.ts +163 -0
- package/src/providers/tavily.ts +73 -0
- package/src/providers/types/index.ts +185 -0
- package/src/providers/utils.ts +182 -0
- package/src/tool/allSearchTool.ts +110 -0
- package/src/tool/interface.ts +71 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Compose Helper
|
|
3
|
+
*
|
|
4
|
+
* Manages Docker Compose services for local providers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { exec } from "node:child_process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import { getSearxngPaths } from "../paths";
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
|
|
14
|
+
export interface DockerComposeOptions {
|
|
15
|
+
cwd?: string;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper for managing Docker Compose services
|
|
21
|
+
*/
|
|
22
|
+
export class DockerComposeHelper {
|
|
23
|
+
constructor(private composeFile: string) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute docker compose command
|
|
27
|
+
*/
|
|
28
|
+
private async execDockerCompose(
|
|
29
|
+
args: string[],
|
|
30
|
+
options: DockerComposeOptions = {},
|
|
31
|
+
): Promise<string> {
|
|
32
|
+
const cmd = `docker compose -f "${this.composeFile}" ${args.join(" ")}`;
|
|
33
|
+
const cwd = options.cwd || this.getComposeDir();
|
|
34
|
+
const timeout = options.timeout || 30000;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Get SearXNG paths and ensure directories exist
|
|
38
|
+
const { configDir, dataDir } = getSearxngPaths();
|
|
39
|
+
|
|
40
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
41
|
+
cwd,
|
|
42
|
+
timeout,
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
SEARXNG_CONFIG: configDir,
|
|
46
|
+
SEARXNG_DATA: dataDir,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (stderr && !stderr.includes("warning")) {
|
|
51
|
+
console.warn(`Docker Compose warning: ${stderr}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return stdout;
|
|
55
|
+
} catch (error: unknown) {
|
|
56
|
+
const err = error as { message?: string; stdout?: string; stderr?: string };
|
|
57
|
+
const errorMessage = err.message ?? "Unknown error";
|
|
58
|
+
const timedOut = errorMessage.includes("timed out") || errorMessage.includes("ETIMEDOUT");
|
|
59
|
+
|
|
60
|
+
const errorDetails = [
|
|
61
|
+
`Docker Compose command ${timedOut ? "timed out" : "failed"}: ${errorMessage}`,
|
|
62
|
+
`Command: ${cmd}`,
|
|
63
|
+
];
|
|
64
|
+
if (err.stdout) {
|
|
65
|
+
errorDetails.push(`Output: ${err.stdout}`);
|
|
66
|
+
}
|
|
67
|
+
if (err.stderr) {
|
|
68
|
+
errorDetails.push(`Error: ${err.stderr}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(errorDetails.join("\n"));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get directory containing compose file
|
|
77
|
+
*/
|
|
78
|
+
private getComposeDir(): string {
|
|
79
|
+
return require("node:path").dirname(this.composeFile);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start services
|
|
84
|
+
* @param services Optional list of services to start
|
|
85
|
+
*/
|
|
86
|
+
async up(services?: string[], options?: DockerComposeOptions): Promise<void> {
|
|
87
|
+
const args = ["up", "-d"];
|
|
88
|
+
if (services && services.length > 0) {
|
|
89
|
+
args.push(...services);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await this.execDockerCompose(args, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Stop services
|
|
97
|
+
* @param services Optional list of services to stop
|
|
98
|
+
*/
|
|
99
|
+
async stop(services?: string[], options?: DockerComposeOptions): Promise<void> {
|
|
100
|
+
const args = ["stop"];
|
|
101
|
+
if (services && services.length > 0) {
|
|
102
|
+
args.push(...services);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await this.execDockerCompose(args, options);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Stop and remove containers
|
|
110
|
+
*/
|
|
111
|
+
async down(options?: DockerComposeOptions): Promise<void> {
|
|
112
|
+
await this.execDockerCompose(["down"], options);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get service logs
|
|
117
|
+
* @param services Optional list of services
|
|
118
|
+
* @param tail Number of lines to show (default: 50)
|
|
119
|
+
*/
|
|
120
|
+
async logs(
|
|
121
|
+
services?: string[],
|
|
122
|
+
tail: number = 50,
|
|
123
|
+
options?: DockerComposeOptions,
|
|
124
|
+
): Promise<string> {
|
|
125
|
+
const args = ["logs", "--tail", String(tail)];
|
|
126
|
+
if (services && services.length > 0) {
|
|
127
|
+
args.push(...services);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return await this.execDockerCompose(args, options);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List running services
|
|
135
|
+
*/
|
|
136
|
+
async ps(options?: DockerComposeOptions): Promise<string> {
|
|
137
|
+
return await this.execDockerCompose(["ps"], options);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if services are running
|
|
142
|
+
* @param service Optional specific service to check
|
|
143
|
+
*/
|
|
144
|
+
async isRunning(service?: string, options?: DockerComposeOptions): Promise<boolean> {
|
|
145
|
+
try {
|
|
146
|
+
const output = await this.ps(options);
|
|
147
|
+
|
|
148
|
+
if (service) {
|
|
149
|
+
return output.includes(service) && !output.includes("Exit");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if any services are running
|
|
153
|
+
return output.includes("Up");
|
|
154
|
+
} catch (_error) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if Docker is available
|
|
161
|
+
*/
|
|
162
|
+
static async isDockerAvailable(): Promise<boolean> {
|
|
163
|
+
try {
|
|
164
|
+
await execAsync("docker version", { timeout: 5000 });
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if compose file exists
|
|
173
|
+
*/
|
|
174
|
+
composeFileExists(): boolean {
|
|
175
|
+
return existsSync(this.composeFile);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Lifecycle Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles Docker container lifecycle operations independently from search logic.
|
|
5
|
+
* Enables composition over inheritance for providers that need Docker management.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const dockerManager = new DockerLifecycleManager({
|
|
10
|
+
* composeFile: './docker-compose.yml',
|
|
11
|
+
* containerName: 'my-service',
|
|
12
|
+
* healthEndpoint: 'http://localhost:8080/health',
|
|
13
|
+
* autoStart: true,
|
|
14
|
+
* autoStop: true
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* await dockerManager.init();
|
|
18
|
+
* const isHealthy = await dockerManager.healthcheck();
|
|
19
|
+
* await dockerManager.shutdown();
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createLogger } from "../logger";
|
|
24
|
+
import { bootstrapSearxngConfig } from "../paths";
|
|
25
|
+
import { DockerComposeHelper } from "./dockerComposeHelper";
|
|
26
|
+
|
|
27
|
+
const log = createLogger("DockerLifecycle");
|
|
28
|
+
|
|
29
|
+
export interface DockerLifecycleConfig {
|
|
30
|
+
containerName?: string;
|
|
31
|
+
composeFile?: string;
|
|
32
|
+
healthEndpoint?: string;
|
|
33
|
+
autoStart: boolean;
|
|
34
|
+
autoStop: boolean;
|
|
35
|
+
initTimeoutMs?: number;
|
|
36
|
+
projectRoot?: string; // Base path for resolving relative compose file paths
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Manages Docker container lifecycle operations for providers
|
|
41
|
+
*/
|
|
42
|
+
export class DockerLifecycleManager {
|
|
43
|
+
private config: DockerLifecycleConfig;
|
|
44
|
+
private dockerHelper?: DockerComposeHelper;
|
|
45
|
+
private initialized = false;
|
|
46
|
+
private initPromise: Promise<void> | null = null;
|
|
47
|
+
|
|
48
|
+
constructor(config: DockerLifecycleConfig) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
|
|
51
|
+
// Initialize Docker helper if compose file is provided
|
|
52
|
+
if (config.composeFile) {
|
|
53
|
+
this.dockerHelper = new DockerComposeHelper(config.composeFile);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initialize the Docker lifecycle manager
|
|
59
|
+
*
|
|
60
|
+
* Auto-starts containers if configured and performs health checks.
|
|
61
|
+
* Thread-safe - multiple calls return the same promise.
|
|
62
|
+
*
|
|
63
|
+
* @throws Error if container startup fails
|
|
64
|
+
*/
|
|
65
|
+
async init(): Promise<void> {
|
|
66
|
+
if (this.initPromise) {
|
|
67
|
+
return this.initPromise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!this.config.autoStart || !this.dockerHelper) {
|
|
71
|
+
this.initialized = true;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.initPromise = this.performInit().catch((error) => {
|
|
76
|
+
log.error("Initialization failed:", error);
|
|
77
|
+
this.initialized = false;
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
return this.initPromise;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wrap a promise with a timeout
|
|
85
|
+
*/
|
|
86
|
+
private async withTimeout<T>(
|
|
87
|
+
promise: Promise<T>,
|
|
88
|
+
timeoutMs: number,
|
|
89
|
+
operation: string,
|
|
90
|
+
): Promise<T> {
|
|
91
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
92
|
+
setTimeout(() => reject(new Error(`${operation} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return Promise.race([promise, timeoutPromise]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async performInit(): Promise<void> {
|
|
99
|
+
if (!this.dockerHelper) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Bootstrap SearXNG config if needed (copy default settings.yml)
|
|
104
|
+
bootstrapSearxngConfig();
|
|
105
|
+
|
|
106
|
+
// Check if Docker is available (with timeout)
|
|
107
|
+
const dockerAvailable = await this.withTimeout(
|
|
108
|
+
DockerComposeHelper.isDockerAvailable(),
|
|
109
|
+
10000,
|
|
110
|
+
"Docker availability check",
|
|
111
|
+
);
|
|
112
|
+
if (!dockerAvailable) {
|
|
113
|
+
log.warn("Docker is not available. Cannot auto-start container.");
|
|
114
|
+
this.initialized = true;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check if container is already running (with timeout)
|
|
119
|
+
const isRunning = await this.withTimeout(this.healthcheck(), 5000, "Initial health check");
|
|
120
|
+
if (isRunning) {
|
|
121
|
+
log.info("Container is already running.");
|
|
122
|
+
this.initialized = true;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Auto-start container
|
|
127
|
+
log.info("Starting Docker container...");
|
|
128
|
+
try {
|
|
129
|
+
// Run from project root to ensure correct path resolution
|
|
130
|
+
const projectRoot = this.config.projectRoot || process.cwd();
|
|
131
|
+
await this.dockerHelper.up(
|
|
132
|
+
this.config.containerName ? [this.config.containerName] : undefined,
|
|
133
|
+
{ cwd: projectRoot },
|
|
134
|
+
);
|
|
135
|
+
log.info("Container started successfully.");
|
|
136
|
+
|
|
137
|
+
// Wait for health check if endpoint is configured
|
|
138
|
+
if (this.config.healthEndpoint) {
|
|
139
|
+
await this.waitForHealth();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.initialized = true;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
log.error("Failed to start container:", error);
|
|
145
|
+
throw error;
|
|
146
|
+
} finally {
|
|
147
|
+
this.initPromise = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if container is healthy
|
|
153
|
+
*
|
|
154
|
+
* Checks both container running status and health endpoint if configured.
|
|
155
|
+
*
|
|
156
|
+
* @returns true if container is healthy, false otherwise
|
|
157
|
+
*/
|
|
158
|
+
async healthcheck(): Promise<boolean> {
|
|
159
|
+
// Check if container is running
|
|
160
|
+
if (this.dockerHelper) {
|
|
161
|
+
const projectRoot = this.config.projectRoot || process.cwd();
|
|
162
|
+
const isRunning = await this.dockerHelper.isRunning(this.config.containerName, {
|
|
163
|
+
cwd: projectRoot,
|
|
164
|
+
});
|
|
165
|
+
if (!isRunning) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check health endpoint if configured
|
|
171
|
+
if (this.config.healthEndpoint) {
|
|
172
|
+
try {
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
175
|
+
|
|
176
|
+
const response = await fetch(this.config.healthEndpoint, {
|
|
177
|
+
signal: controller.signal,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
clearTimeout(timeoutId);
|
|
181
|
+
return response.ok;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// If no health endpoint, assume healthy if initialized
|
|
188
|
+
return this.initialized;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Wait for health endpoint to be ready
|
|
193
|
+
*/
|
|
194
|
+
private async waitForHealth(timeoutMs: number = 30000): Promise<void> {
|
|
195
|
+
if (!this.config.healthEndpoint) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
log.info("Waiting for health check...");
|
|
200
|
+
|
|
201
|
+
const startTime = Date.now();
|
|
202
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
203
|
+
if (await this.healthcheck()) {
|
|
204
|
+
log.info("Health check passed.");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await this.sleep(1000);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw new Error(
|
|
212
|
+
`[DockerLifecycleManager] Health check failed after ${timeoutMs}ms. ` +
|
|
213
|
+
`Endpoint: ${this.config.healthEndpoint}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Shutdown Docker lifecycle manager
|
|
219
|
+
*
|
|
220
|
+
* Stops containers if autoStop is enabled. Errors during shutdown are logged
|
|
221
|
+
* but not thrown to prevent cleanup failures from propagating.
|
|
222
|
+
*/
|
|
223
|
+
async shutdown(): Promise<void> {
|
|
224
|
+
if (!this.config.autoStop || !this.dockerHelper) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const projectRoot = this.config.projectRoot || process.cwd();
|
|
229
|
+
const isRunning = await this.dockerHelper.isRunning(this.config.containerName, {
|
|
230
|
+
cwd: projectRoot,
|
|
231
|
+
});
|
|
232
|
+
if (!isRunning) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
log.info("Stopping Docker container...");
|
|
237
|
+
try {
|
|
238
|
+
await this.dockerHelper.stop(
|
|
239
|
+
this.config.containerName ? [this.config.containerName] : undefined,
|
|
240
|
+
{ cwd: projectRoot },
|
|
241
|
+
);
|
|
242
|
+
log.info("Container stopped.");
|
|
243
|
+
} catch (error) {
|
|
244
|
+
log.error("Failed to stop container:", error);
|
|
245
|
+
// Don't throw on shutdown errors
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Validate Docker configuration
|
|
251
|
+
*
|
|
252
|
+
* Performs comprehensive validation of Docker configuration including:
|
|
253
|
+
* - Docker availability
|
|
254
|
+
* - Compose file existence and validity
|
|
255
|
+
* - Health endpoint URL validation
|
|
256
|
+
* - Container name format validation
|
|
257
|
+
*
|
|
258
|
+
* @returns Validation results with errors and warnings
|
|
259
|
+
*/
|
|
260
|
+
async validateDockerConfig(): Promise<{
|
|
261
|
+
valid: boolean;
|
|
262
|
+
errors: string[];
|
|
263
|
+
warnings: string[];
|
|
264
|
+
}> {
|
|
265
|
+
const errors: string[] = [];
|
|
266
|
+
const warnings: string[] = [];
|
|
267
|
+
|
|
268
|
+
// Check Docker is available
|
|
269
|
+
const dockerAvailable = await DockerComposeHelper.isDockerAvailable();
|
|
270
|
+
if (!dockerAvailable) {
|
|
271
|
+
errors.push("Docker is not available or not running");
|
|
272
|
+
return { valid: false, errors, warnings };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check compose file exists
|
|
276
|
+
if (this.config.composeFile) {
|
|
277
|
+
const composeExists = await this.dockerHelper?.composeFileExists();
|
|
278
|
+
if (!composeExists) {
|
|
279
|
+
errors.push(`Compose file not found: ${this.config.composeFile}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if compose file is valid
|
|
284
|
+
if (this.dockerHelper) {
|
|
285
|
+
try {
|
|
286
|
+
const projectRoot = this.config.projectRoot || process.cwd();
|
|
287
|
+
await this.dockerHelper.ps({ cwd: projectRoot });
|
|
288
|
+
} catch (error: unknown) {
|
|
289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
290
|
+
if (message.includes("config")) {
|
|
291
|
+
errors.push(`Invalid compose file: ${message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check health endpoint if configured
|
|
297
|
+
if (this.config.healthEndpoint) {
|
|
298
|
+
try {
|
|
299
|
+
// Try to parse the URL
|
|
300
|
+
new URL(this.config.healthEndpoint);
|
|
301
|
+
} catch {
|
|
302
|
+
warnings.push(`Health endpoint URL is invalid: ${this.config.healthEndpoint}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check container name if specified
|
|
307
|
+
if (this.config.containerName && !/^[a-zA-Z0-9_-]+$/.test(this.config.containerName)) {
|
|
308
|
+
warnings.push(`Container name contains invalid characters: ${this.config.containerName}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
valid: errors.length === 0,
|
|
313
|
+
errors,
|
|
314
|
+
warnings,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Helper: Sleep for ms milliseconds
|
|
320
|
+
*/
|
|
321
|
+
private sleep(ms: number): Promise<void> {
|
|
322
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if lifecycle manager is initialized
|
|
327
|
+
*/
|
|
328
|
+
isInitialized(): boolean {
|
|
329
|
+
return this.initialized;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if container is running
|
|
334
|
+
*
|
|
335
|
+
* @returns true if container is running, false otherwise
|
|
336
|
+
*/
|
|
337
|
+
async isRunning(): Promise<boolean> {
|
|
338
|
+
if (!this.dockerHelper) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const projectRoot = this.config.projectRoot || process.cwd();
|
|
344
|
+
return await this.dockerHelper.isRunning(this.config.containerName, {
|
|
345
|
+
cwd: projectRoot,
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
log.warn("Error checking if container is running:", error);
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get the current configuration
|
|
355
|
+
*
|
|
356
|
+
* @returns Current Docker lifecycle configuration
|
|
357
|
+
*/
|
|
358
|
+
getConfig(): DockerLifecycleConfig {
|
|
359
|
+
return { ...this.config };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker core module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { DockerComposeOptions } from "./dockerComposeHelper";
|
|
6
|
+
export { DockerComposeHelper } from "./dockerComposeHelper";
|
|
7
|
+
export type { DockerLifecycleConfig } from "./dockerLifecycleManager";
|
|
8
|
+
export { DockerLifecycleManager } from "./dockerLifecycleManager";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Abstraction
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent logging interface across the codebase.
|
|
5
|
+
* Supports log levels and can be configured for different environments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "none";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Logger configuration
|
|
12
|
+
*/
|
|
13
|
+
export interface LoggerConfig {
|
|
14
|
+
/** Minimum log level to output */
|
|
15
|
+
level: LogLevel;
|
|
16
|
+
/** Prefix for all log messages */
|
|
17
|
+
prefix?: string;
|
|
18
|
+
/** Whether to include timestamps */
|
|
19
|
+
timestamp?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
23
|
+
debug: 0,
|
|
24
|
+
info: 1,
|
|
25
|
+
warn: 2,
|
|
26
|
+
error: 3,
|
|
27
|
+
none: 4,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Simple logger implementation
|
|
32
|
+
*/
|
|
33
|
+
export class Logger {
|
|
34
|
+
private config: LoggerConfig;
|
|
35
|
+
|
|
36
|
+
constructor(config: Partial<LoggerConfig> = {}) {
|
|
37
|
+
this.config = {
|
|
38
|
+
level: config.level ?? (process.env.DEBUG ? "debug" : "info"),
|
|
39
|
+
prefix: config.prefix,
|
|
40
|
+
timestamp: config.timestamp ?? false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a log level should be output
|
|
46
|
+
*/
|
|
47
|
+
private shouldLog(level: LogLevel): boolean {
|
|
48
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[this.config.level];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format a log message
|
|
53
|
+
*/
|
|
54
|
+
private format(_level: LogLevel, message: string): string {
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
|
|
57
|
+
if (this.config.timestamp) {
|
|
58
|
+
parts.push(new Date().toISOString());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.config.prefix) {
|
|
62
|
+
parts.push(`[${this.config.prefix}]`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
parts.push(message);
|
|
66
|
+
return parts.join(" ");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Log a debug message
|
|
71
|
+
*/
|
|
72
|
+
debug(message: string, ...args: unknown[]): void {
|
|
73
|
+
if (this.shouldLog("debug")) {
|
|
74
|
+
console.log(this.format("debug", message), ...args);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Log an info message
|
|
80
|
+
*/
|
|
81
|
+
info(message: string, ...args: unknown[]): void {
|
|
82
|
+
if (this.shouldLog("info")) {
|
|
83
|
+
console.log(this.format("info", message), ...args);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Log a warning message
|
|
89
|
+
*/
|
|
90
|
+
warn(message: string, ...args: unknown[]): void {
|
|
91
|
+
if (this.shouldLog("warn")) {
|
|
92
|
+
console.warn(this.format("warn", message), ...args);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Log an error message
|
|
98
|
+
*/
|
|
99
|
+
error(message: string, ...args: unknown[]): void {
|
|
100
|
+
if (this.shouldLog("error")) {
|
|
101
|
+
console.error(this.format("error", message), ...args);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a child logger with a prefix
|
|
107
|
+
*/
|
|
108
|
+
child(prefix: string): Logger {
|
|
109
|
+
const parentPrefix = this.config.prefix;
|
|
110
|
+
return new Logger({
|
|
111
|
+
...this.config,
|
|
112
|
+
prefix: parentPrefix ? `${parentPrefix}:${prefix}` : prefix,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Set the log level
|
|
118
|
+
*/
|
|
119
|
+
setLevel(level: LogLevel): void {
|
|
120
|
+
this.config.level = level;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Default logger instance
|
|
126
|
+
* Can be configured globally via environment variables:
|
|
127
|
+
* - DEBUG: Set to any value to enable debug logging
|
|
128
|
+
* - LOG_LEVEL: Set to debug, info, warn, error, or none
|
|
129
|
+
*/
|
|
130
|
+
export const logger = new Logger({
|
|
131
|
+
level: (process.env.LOG_LEVEL as LogLevel) ?? (process.env.DEBUG ? "debug" : "warn"),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a logger with a specific prefix
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* const log = createLogger('DockerLifecycle');
|
|
140
|
+
* log.info('Starting container...');
|
|
141
|
+
* // Output: [DockerLifecycle] Starting container...
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function createLogger(prefix: string): Logger {
|
|
145
|
+
return logger.child(prefix);
|
|
146
|
+
}
|