openpets 1.0.4
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/README.md +373 -0
- package/ai-client-base/index.ts +117 -0
- package/browser.ts +10 -0
- package/build-pet.ts +429 -0
- package/cli.ts +179 -0
- package/config-manager.ts +82 -0
- package/deploy-pet.ts +91 -0
- package/index.ts +10 -0
- package/local-cache.ts +280 -0
- package/logger.d.ts +32 -0
- package/logger.js +120 -0
- package/logger.ts +143 -0
- package/mcp-factory.ts +180 -0
- package/mcp-server.ts +69 -0
- package/migrate-plugin.ts +220 -0
- package/package.json +45 -0
- package/pets-registry.ts +160 -0
- package/plugin-factory.ts +309 -0
- package/prompt-utils.ts +130 -0
- package/schema-helpers.ts +59 -0
- package/search-pets.ts +267 -0
- package/types.ts +68 -0
- package/validate-pet.ts +594 -0
package/deploy-pet.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { join, dirname } from 'path'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, statSync, mkdirSync } from 'fs'
|
|
4
|
+
import { buildPet } from './build-pet.js'
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = dirname(__filename)
|
|
8
|
+
const projectRoot = dirname(dirname(__dirname))
|
|
9
|
+
|
|
10
|
+
interface PetData {
|
|
11
|
+
name: string
|
|
12
|
+
version: string
|
|
13
|
+
description: string
|
|
14
|
+
source_code_url: string
|
|
15
|
+
[key: string]: any
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function deployPet(petName?: string): Promise<void> {
|
|
19
|
+
if (!petName) {
|
|
20
|
+
const cwd = process.cwd()
|
|
21
|
+
const petsDir = join(projectRoot, 'pets')
|
|
22
|
+
|
|
23
|
+
if (cwd.includes('/pets/')) {
|
|
24
|
+
petName = cwd.split('/pets/')[1].split('/')[0]
|
|
25
|
+
console.log(`📦 Auto-detected pet: ${petName}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!petName) {
|
|
29
|
+
console.error('Usage: pets deploy <pet-name>')
|
|
30
|
+
console.error('Example: pets deploy postgres')
|
|
31
|
+
console.error('')
|
|
32
|
+
console.error('Available pets:')
|
|
33
|
+
if (existsSync(petsDir)) {
|
|
34
|
+
const pets = readdirSync(petsDir).filter(dir => {
|
|
35
|
+
const petPath = join(petsDir, dir)
|
|
36
|
+
return statSync(petPath).isDirectory() && dir !== '_TEMPLATE_'
|
|
37
|
+
})
|
|
38
|
+
pets.forEach(pet => console.error(` - ${pet}`))
|
|
39
|
+
}
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(`🚀 Deploying ${petName}...`)
|
|
45
|
+
|
|
46
|
+
await buildPet(petName)
|
|
47
|
+
|
|
48
|
+
const petDir = join(projectRoot, 'pets', petName)
|
|
49
|
+
const packageJsonPath = join(petDir, 'package.json')
|
|
50
|
+
|
|
51
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
52
|
+
|
|
53
|
+
const sourceCodeUrl = `https://github.com/raggle-ai/pets/tree/main/pets/${petName}`
|
|
54
|
+
|
|
55
|
+
const fieldsToExclude = ['$schema', 'version', 'main', 'types', 'scripts', 'license', 'repository', 'dependencies', 'author', 'utils', 'devDependencies']
|
|
56
|
+
|
|
57
|
+
const filteredData = Object.keys(packageJson).reduce((acc, key) => {
|
|
58
|
+
if (!fieldsToExclude.includes(key)) {
|
|
59
|
+
acc[key] = packageJson[key]
|
|
60
|
+
}
|
|
61
|
+
return acc
|
|
62
|
+
}, {} as Record<string, any>)
|
|
63
|
+
|
|
64
|
+
const petData = {
|
|
65
|
+
...filteredData,
|
|
66
|
+
name: packageJson.name,
|
|
67
|
+
version: packageJson.version,
|
|
68
|
+
description: packageJson.description,
|
|
69
|
+
source_code_url: sourceCodeUrl,
|
|
70
|
+
} as PetData
|
|
71
|
+
|
|
72
|
+
const dataDir = join(projectRoot, 'data')
|
|
73
|
+
if (!existsSync(dataDir)) {
|
|
74
|
+
mkdirSync(dataDir, { recursive: true })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const petsJsonPath = join(dataDir, 'pets.json')
|
|
78
|
+
let petsData: Record<string, PetData> = {}
|
|
79
|
+
|
|
80
|
+
if (existsSync(petsJsonPath)) {
|
|
81
|
+
petsData = JSON.parse(readFileSync(petsJsonPath, 'utf8'))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
petsData[packageJson.name] = petData
|
|
85
|
+
|
|
86
|
+
writeFileSync(petsJsonPath, JSON.stringify(petsData, null, 2), 'utf8')
|
|
87
|
+
|
|
88
|
+
console.log(`✅ ${petName} deployed successfully!`)
|
|
89
|
+
console.log(` Source URL: ${sourceCodeUrl}`)
|
|
90
|
+
console.log(` Data saved to: ${petsJsonPath}`)
|
|
91
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './plugin-factory'
|
|
2
|
+
export * from './schema-helpers'
|
|
3
|
+
export * from './validate-pet'
|
|
4
|
+
export * from './pets-registry'
|
|
5
|
+
export * from './build-pet'
|
|
6
|
+
export * from './types'
|
|
7
|
+
export * from './ai-client-base'
|
|
8
|
+
export * from './search-pets'
|
|
9
|
+
export * from './logger'
|
|
10
|
+
export * from './local-cache'
|
package/local-cache.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
export interface CacheEntry<T> {
|
|
2
|
+
data: T
|
|
3
|
+
timestamp: number
|
|
4
|
+
hits: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CacheStats {
|
|
8
|
+
hits: number
|
|
9
|
+
misses: number
|
|
10
|
+
size: number
|
|
11
|
+
oldestEntry?: number
|
|
12
|
+
newestEntry?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CacheConfig {
|
|
16
|
+
ttl?: number
|
|
17
|
+
maxSize?: number
|
|
18
|
+
cleanupInterval?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class LocalCache<T = any> {
|
|
22
|
+
private cache: Map<string, CacheEntry<T>> = new Map()
|
|
23
|
+
private stats = { hits: 0, misses: 0 }
|
|
24
|
+
private ttl: number
|
|
25
|
+
private maxSize: number
|
|
26
|
+
private cleanupInterval: number | null
|
|
27
|
+
private cleanupTimer: NodeJS.Timeout | null = null
|
|
28
|
+
|
|
29
|
+
constructor(config: CacheConfig = {}) {
|
|
30
|
+
this.ttl = config.ttl || 3600000
|
|
31
|
+
this.maxSize = config.maxSize || 1000
|
|
32
|
+
this.cleanupInterval = config.cleanupInterval || null
|
|
33
|
+
|
|
34
|
+
if (this.cleanupInterval) {
|
|
35
|
+
this.startCleanup()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private startCleanup(): void {
|
|
40
|
+
if (this.cleanupInterval) {
|
|
41
|
+
this.cleanupTimer = setInterval(() => {
|
|
42
|
+
this.cleanup()
|
|
43
|
+
}, this.cleanupInterval)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private stopCleanup(): void {
|
|
48
|
+
if (this.cleanupTimer) {
|
|
49
|
+
clearInterval(this.cleanupTimer)
|
|
50
|
+
this.cleanupTimer = null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get(key: string): T | null {
|
|
55
|
+
const entry = this.cache.get(key)
|
|
56
|
+
|
|
57
|
+
if (!entry) {
|
|
58
|
+
this.stats.misses++
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const now = Date.now()
|
|
63
|
+
if (now - entry.timestamp > this.ttl) {
|
|
64
|
+
this.cache.delete(key)
|
|
65
|
+
this.stats.misses++
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
entry.hits++
|
|
70
|
+
this.stats.hits++
|
|
71
|
+
return entry.data
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
set(key: string, data: T): void {
|
|
75
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
76
|
+
this.evictOldest()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.cache.set(key, {
|
|
80
|
+
data,
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
hits: 0
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
has(key: string): boolean {
|
|
87
|
+
const entry = this.cache.get(key)
|
|
88
|
+
if (!entry) return false
|
|
89
|
+
|
|
90
|
+
const now = Date.now()
|
|
91
|
+
if (now - entry.timestamp > this.ttl) {
|
|
92
|
+
this.cache.delete(key)
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
delete(key: string): boolean {
|
|
100
|
+
return this.cache.delete(key)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
clear(): void {
|
|
104
|
+
this.cache.clear()
|
|
105
|
+
this.stats = { hits: 0, misses: 0 }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
cleanup(): number {
|
|
109
|
+
const now = Date.now()
|
|
110
|
+
let removed = 0
|
|
111
|
+
|
|
112
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
113
|
+
if (now - entry.timestamp > this.ttl) {
|
|
114
|
+
this.cache.delete(key)
|
|
115
|
+
removed++
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return removed
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private evictOldest(): void {
|
|
123
|
+
let oldestKey: string | null = null
|
|
124
|
+
let oldestTime = Infinity
|
|
125
|
+
|
|
126
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
127
|
+
if (entry.timestamp < oldestTime) {
|
|
128
|
+
oldestTime = entry.timestamp
|
|
129
|
+
oldestKey = key
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (oldestKey) {
|
|
134
|
+
this.cache.delete(oldestKey)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getStats(): CacheStats {
|
|
139
|
+
const entries = Array.from(this.cache.values())
|
|
140
|
+
const timestamps = entries.map(e => e.timestamp)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
hits: this.stats.hits,
|
|
144
|
+
misses: this.stats.misses,
|
|
145
|
+
size: this.cache.size,
|
|
146
|
+
oldestEntry: timestamps.length > 0 ? Math.min(...timestamps) : undefined,
|
|
147
|
+
newestEntry: timestamps.length > 0 ? Math.max(...timestamps) : undefined
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
keys(): string[] {
|
|
152
|
+
return Array.from(this.cache.keys())
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
values(): T[] {
|
|
156
|
+
return Array.from(this.cache.values()).map(entry => entry.data)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
entries(): Array<[string, T]> {
|
|
160
|
+
return Array.from(this.cache.entries()).map(([key, entry]) => [key, entry.data])
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getHitRate(): number {
|
|
164
|
+
const total = this.stats.hits + this.stats.misses
|
|
165
|
+
return total === 0 ? 0 : this.stats.hits / total
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
destroy(): void {
|
|
169
|
+
this.stopCleanup()
|
|
170
|
+
this.clear()
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export class SingleValueCache<T = any> {
|
|
175
|
+
private data: T | null = null
|
|
176
|
+
private timestamp: number = 0
|
|
177
|
+
private ttl: number
|
|
178
|
+
private hits: number = 0
|
|
179
|
+
private misses: number = 0
|
|
180
|
+
|
|
181
|
+
constructor(config: { ttl?: number } = {}) {
|
|
182
|
+
this.ttl = config.ttl || 3600000
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get(): T | null {
|
|
186
|
+
if (!this.data) {
|
|
187
|
+
this.misses++
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const now = Date.now()
|
|
192
|
+
if (now - this.timestamp > this.ttl) {
|
|
193
|
+
this.data = null
|
|
194
|
+
this.misses++
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.hits++
|
|
199
|
+
return this.data
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
set(data: T): void {
|
|
203
|
+
this.data = data
|
|
204
|
+
this.timestamp = Date.now()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
has(): boolean {
|
|
208
|
+
if (!this.data) return false
|
|
209
|
+
|
|
210
|
+
const now = Date.now()
|
|
211
|
+
if (now - this.timestamp > this.ttl) {
|
|
212
|
+
this.data = null
|
|
213
|
+
return false
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
clear(): void {
|
|
220
|
+
this.data = null
|
|
221
|
+
this.timestamp = 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getAge(): number {
|
|
225
|
+
if (!this.data) return -1
|
|
226
|
+
return Date.now() - this.timestamp
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
getStats(): { hits: number; misses: number; hasData: boolean; age: number } {
|
|
230
|
+
return {
|
|
231
|
+
hits: this.hits,
|
|
232
|
+
misses: this.misses,
|
|
233
|
+
hasData: this.has(),
|
|
234
|
+
age: this.getAge()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getHitRate(): number {
|
|
239
|
+
const total = this.hits + this.misses
|
|
240
|
+
return total === 0 ? 0 : this.hits / total
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function withCache<T>(
|
|
245
|
+
cache: LocalCache<T> | SingleValueCache<T>,
|
|
246
|
+
key: string | null,
|
|
247
|
+
fetcher: () => Promise<T>
|
|
248
|
+
): Promise<T> {
|
|
249
|
+
if (cache instanceof SingleValueCache) {
|
|
250
|
+
const cached = cache.get()
|
|
251
|
+
if (cached !== null) {
|
|
252
|
+
return cached
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const data = await fetcher()
|
|
256
|
+
cache.set(data)
|
|
257
|
+
return data
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (key === null) {
|
|
261
|
+
throw new Error('Key is required for LocalCache')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const cached = cache.get(key)
|
|
265
|
+
if (cached !== null) {
|
|
266
|
+
return cached
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const data = await fetcher()
|
|
270
|
+
cache.set(key, data)
|
|
271
|
+
return data
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function createCache<T = any>(config: CacheConfig = {}): LocalCache<T> {
|
|
275
|
+
return new LocalCache<T>(config)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function createSingleValueCache<T = any>(config: { ttl?: number } = {}): SingleValueCache<T> {
|
|
279
|
+
return new SingleValueCache<T>(config)
|
|
280
|
+
}
|
package/logger.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare enum LogLevel {
|
|
2
|
+
DEBUG = 0,
|
|
3
|
+
INFO = 1,
|
|
4
|
+
WARN = 2,
|
|
5
|
+
ERROR = 3,
|
|
6
|
+
NONE = 4
|
|
7
|
+
}
|
|
8
|
+
interface LoggerOptions {
|
|
9
|
+
namespace: string;
|
|
10
|
+
level?: LogLevel;
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class Logger {
|
|
14
|
+
private namespace;
|
|
15
|
+
private level;
|
|
16
|
+
private enabled;
|
|
17
|
+
constructor(options: LoggerOptions);
|
|
18
|
+
private detectLoggingEnabled;
|
|
19
|
+
private detectLogLevel;
|
|
20
|
+
private getArgLogLevel;
|
|
21
|
+
private shouldLog;
|
|
22
|
+
private formatTimestamp;
|
|
23
|
+
private formatMessage;
|
|
24
|
+
debug(message: string, ...args: any[]): void;
|
|
25
|
+
info(message: string, ...args: any[]): void;
|
|
26
|
+
warn(message: string, ...args: any[]): void;
|
|
27
|
+
error(message: string, ...args: any[]): void;
|
|
28
|
+
child(subNamespace: string): Logger;
|
|
29
|
+
}
|
|
30
|
+
export declare function createLogger(namespace: string, options?: Partial<LoggerOptions>): Logger;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=logger.d.ts.map
|
package/logger.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Logger = exports.LogLevel = void 0;
|
|
4
|
+
exports.createLogger = createLogger;
|
|
5
|
+
var LogLevel;
|
|
6
|
+
(function (LogLevel) {
|
|
7
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
8
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
9
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
10
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
11
|
+
LogLevel[LogLevel["NONE"] = 4] = "NONE";
|
|
12
|
+
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
13
|
+
const LOG_LEVEL_NAMES = {
|
|
14
|
+
[LogLevel.DEBUG]: 'DEBUG',
|
|
15
|
+
[LogLevel.INFO]: 'INFO',
|
|
16
|
+
[LogLevel.WARN]: 'WARN',
|
|
17
|
+
[LogLevel.ERROR]: 'ERROR',
|
|
18
|
+
[LogLevel.NONE]: 'NONE'
|
|
19
|
+
};
|
|
20
|
+
class Logger {
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.namespace = options.namespace;
|
|
23
|
+
this.enabled = options.enabled ?? this.detectLoggingEnabled();
|
|
24
|
+
this.level = options.level ?? this.detectLogLevel();
|
|
25
|
+
}
|
|
26
|
+
detectLoggingEnabled() {
|
|
27
|
+
return process.env.ENABLE_LOGGING === 'true' ||
|
|
28
|
+
process.env.DEBUG === 'true' ||
|
|
29
|
+
process.argv.includes('--print-logs');
|
|
30
|
+
}
|
|
31
|
+
detectLogLevel() {
|
|
32
|
+
const envLevel = process.env.LOG_LEVEL?.toUpperCase();
|
|
33
|
+
const argLevel = this.getArgLogLevel();
|
|
34
|
+
const levelStr = argLevel || envLevel || 'INFO';
|
|
35
|
+
switch (levelStr) {
|
|
36
|
+
case 'DEBUG':
|
|
37
|
+
return LogLevel.DEBUG;
|
|
38
|
+
case 'INFO':
|
|
39
|
+
return LogLevel.INFO;
|
|
40
|
+
case 'WARN':
|
|
41
|
+
return LogLevel.WARN;
|
|
42
|
+
case 'ERROR':
|
|
43
|
+
return LogLevel.ERROR;
|
|
44
|
+
case 'NONE':
|
|
45
|
+
return LogLevel.NONE;
|
|
46
|
+
default:
|
|
47
|
+
return LogLevel.INFO;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
getArgLogLevel() {
|
|
51
|
+
const logLevelIndex = process.argv.indexOf('--log-level');
|
|
52
|
+
if (logLevelIndex !== -1 && process.argv[logLevelIndex + 1]) {
|
|
53
|
+
return process.argv[logLevelIndex + 1].toUpperCase();
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
shouldLog(level) {
|
|
58
|
+
return this.enabled && level >= this.level;
|
|
59
|
+
}
|
|
60
|
+
formatTimestamp() {
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const year = now.getFullYear();
|
|
63
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
64
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
65
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
66
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
67
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
68
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
69
|
+
}
|
|
70
|
+
formatMessage(level, message, ...args) {
|
|
71
|
+
const timestamp = this.formatTimestamp();
|
|
72
|
+
const levelName = LOG_LEVEL_NAMES[level].padEnd(5, ' ');
|
|
73
|
+
const prefix = `${levelName} ${timestamp} service=${this.namespace}`;
|
|
74
|
+
if (args.length === 0) {
|
|
75
|
+
return `${prefix} ${message}`;
|
|
76
|
+
}
|
|
77
|
+
const formattedArgs = args.map(arg => {
|
|
78
|
+
if (typeof arg === 'object') {
|
|
79
|
+
return JSON.stringify(arg, null, 2);
|
|
80
|
+
}
|
|
81
|
+
return String(arg);
|
|
82
|
+
}).join(' ');
|
|
83
|
+
return `${prefix} ${message} ${formattedArgs}`;
|
|
84
|
+
}
|
|
85
|
+
debug(message, ...args) {
|
|
86
|
+
if (this.shouldLog(LogLevel.DEBUG)) {
|
|
87
|
+
console.log(this.formatMessage(LogLevel.DEBUG, message, ...args));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
info(message, ...args) {
|
|
91
|
+
if (this.shouldLog(LogLevel.INFO)) {
|
|
92
|
+
console.log(this.formatMessage(LogLevel.INFO, message, ...args));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
warn(message, ...args) {
|
|
96
|
+
if (this.shouldLog(LogLevel.WARN)) {
|
|
97
|
+
console.warn(this.formatMessage(LogLevel.WARN, message, ...args));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
error(message, ...args) {
|
|
101
|
+
if (this.shouldLog(LogLevel.ERROR)) {
|
|
102
|
+
console.error(this.formatMessage(LogLevel.ERROR, message, ...args));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
child(subNamespace) {
|
|
106
|
+
return new Logger({
|
|
107
|
+
namespace: `${this.namespace}:${subNamespace}`,
|
|
108
|
+
level: this.level,
|
|
109
|
+
enabled: this.enabled
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.Logger = Logger;
|
|
114
|
+
function createLogger(namespace, options) {
|
|
115
|
+
return new Logger({
|
|
116
|
+
namespace,
|
|
117
|
+
level: options?.level,
|
|
118
|
+
enabled: options?.enabled
|
|
119
|
+
});
|
|
120
|
+
}
|
package/logger.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export enum LogLevel {
|
|
2
|
+
DEBUG = 0,
|
|
3
|
+
INFO = 1,
|
|
4
|
+
WARN = 2,
|
|
5
|
+
ERROR = 3,
|
|
6
|
+
NONE = 4
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const LOG_LEVEL_NAMES: Record<LogLevel, string> = {
|
|
10
|
+
[LogLevel.DEBUG]: 'DEBUG',
|
|
11
|
+
[LogLevel.INFO]: 'INFO',
|
|
12
|
+
[LogLevel.WARN]: 'WARN',
|
|
13
|
+
[LogLevel.ERROR]: 'ERROR',
|
|
14
|
+
[LogLevel.NONE]: 'NONE'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface LoggerOptions {
|
|
18
|
+
namespace: string
|
|
19
|
+
level?: LogLevel
|
|
20
|
+
enabled?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Logger {
|
|
24
|
+
private namespace: string
|
|
25
|
+
private level: LogLevel
|
|
26
|
+
private enabled: boolean
|
|
27
|
+
|
|
28
|
+
constructor(options: LoggerOptions) {
|
|
29
|
+
this.namespace = options.namespace
|
|
30
|
+
this.enabled = options.enabled ?? this.detectLoggingEnabled()
|
|
31
|
+
this.level = options.level ?? this.detectLogLevel()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private detectLoggingEnabled(): boolean {
|
|
35
|
+
return process.env.ENABLE_LOGGING === 'true' ||
|
|
36
|
+
process.env.DEBUG === 'true' ||
|
|
37
|
+
process.argv.includes('--print-logs')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private detectLogLevel(): LogLevel {
|
|
41
|
+
const envLevel = process.env.LOG_LEVEL?.toUpperCase()
|
|
42
|
+
const argLevel = this.getArgLogLevel()
|
|
43
|
+
|
|
44
|
+
const levelStr = argLevel || envLevel || 'INFO'
|
|
45
|
+
|
|
46
|
+
switch (levelStr) {
|
|
47
|
+
case 'DEBUG':
|
|
48
|
+
return LogLevel.DEBUG
|
|
49
|
+
case 'INFO':
|
|
50
|
+
return LogLevel.INFO
|
|
51
|
+
case 'WARN':
|
|
52
|
+
return LogLevel.WARN
|
|
53
|
+
case 'ERROR':
|
|
54
|
+
return LogLevel.ERROR
|
|
55
|
+
case 'NONE':
|
|
56
|
+
return LogLevel.NONE
|
|
57
|
+
default:
|
|
58
|
+
return LogLevel.INFO
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private getArgLogLevel(): string | undefined {
|
|
63
|
+
const logLevelIndex = process.argv.indexOf('--log-level')
|
|
64
|
+
if (logLevelIndex !== -1 && process.argv[logLevelIndex + 1]) {
|
|
65
|
+
return process.argv[logLevelIndex + 1].toUpperCase()
|
|
66
|
+
}
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private shouldLog(level: LogLevel): boolean {
|
|
71
|
+
return this.enabled && level >= this.level
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private formatTimestamp(): string {
|
|
75
|
+
const now = new Date()
|
|
76
|
+
const year = now.getFullYear()
|
|
77
|
+
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
78
|
+
const day = String(now.getDate()).padStart(2, '0')
|
|
79
|
+
const hours = String(now.getHours()).padStart(2, '0')
|
|
80
|
+
const minutes = String(now.getMinutes()).padStart(2, '0')
|
|
81
|
+
const seconds = String(now.getSeconds()).padStart(2, '0')
|
|
82
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private formatMessage(level: LogLevel, message: string, ...args: any[]): string {
|
|
86
|
+
const timestamp = this.formatTimestamp()
|
|
87
|
+
const levelName = LOG_LEVEL_NAMES[level].padEnd(5, ' ')
|
|
88
|
+
const prefix = `${levelName} ${timestamp} service=${this.namespace}`
|
|
89
|
+
|
|
90
|
+
if (args.length === 0) {
|
|
91
|
+
return `${prefix} ${message}`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const formattedArgs = args.map(arg => {
|
|
95
|
+
if (typeof arg === 'object') {
|
|
96
|
+
return JSON.stringify(arg, null, 2)
|
|
97
|
+
}
|
|
98
|
+
return String(arg)
|
|
99
|
+
}).join(' ')
|
|
100
|
+
|
|
101
|
+
return `${prefix} ${message} ${formattedArgs}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
debug(message: string, ...args: any[]): void {
|
|
105
|
+
if (this.shouldLog(LogLevel.DEBUG)) {
|
|
106
|
+
console.log(this.formatMessage(LogLevel.DEBUG, message, ...args))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
info(message: string, ...args: any[]): void {
|
|
111
|
+
if (this.shouldLog(LogLevel.INFO)) {
|
|
112
|
+
console.log(this.formatMessage(LogLevel.INFO, message, ...args))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
warn(message: string, ...args: any[]): void {
|
|
117
|
+
if (this.shouldLog(LogLevel.WARN)) {
|
|
118
|
+
console.warn(this.formatMessage(LogLevel.WARN, message, ...args))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
error(message: string, ...args: any[]): void {
|
|
123
|
+
if (this.shouldLog(LogLevel.ERROR)) {
|
|
124
|
+
console.error(this.formatMessage(LogLevel.ERROR, message, ...args))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
child(subNamespace: string): Logger {
|
|
129
|
+
return new Logger({
|
|
130
|
+
namespace: `${this.namespace}:${subNamespace}`,
|
|
131
|
+
level: this.level,
|
|
132
|
+
enabled: this.enabled
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createLogger(namespace: string, options?: Partial<LoggerOptions>): Logger {
|
|
138
|
+
return new Logger({
|
|
139
|
+
namespace,
|
|
140
|
+
level: options?.level,
|
|
141
|
+
enabled: options?.enabled
|
|
142
|
+
})
|
|
143
|
+
}
|