openpets 1.0.4 → 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/ai-client-base/index.ts +112 -0
- package/cli.ts +94 -5
- package/package.json +12 -3
- package/plugin-factory.ts +1 -10
- package/validate-pet.ts +159 -4
package/ai-client-base/index.ts
CHANGED
|
@@ -44,6 +44,27 @@ export abstract class BaseAIClient {
|
|
|
44
44
|
|
|
45
45
|
abstract get providerName(): string
|
|
46
46
|
|
|
47
|
+
protected log(message: string, level: "info" | "warn" | "error" | "debug" = "info"): void {
|
|
48
|
+
if (!this.debug && level === "debug") return
|
|
49
|
+
|
|
50
|
+
const timestamp = new Date().toISOString()
|
|
51
|
+
const prefix = `[${this.providerName.toUpperCase()}] ${timestamp}`
|
|
52
|
+
|
|
53
|
+
switch (level) {
|
|
54
|
+
case "error":
|
|
55
|
+
console.error(`${prefix} ERROR:`, message)
|
|
56
|
+
break
|
|
57
|
+
case "warn":
|
|
58
|
+
console.warn(`${prefix} WARN:`, message)
|
|
59
|
+
break
|
|
60
|
+
case "debug":
|
|
61
|
+
console.debug(`${prefix} DEBUG:`, message)
|
|
62
|
+
break
|
|
63
|
+
default:
|
|
64
|
+
console.log(`${prefix} INFO:`, message)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
protected ensureApiKey(envVarName: string): void {
|
|
48
69
|
if (!this.apiKey || this.apiKey.trim() === "") {
|
|
49
70
|
throw new Error(`${envVarName} environment variable is required but not set`)
|
|
@@ -114,4 +135,95 @@ export abstract class BaseAIClient {
|
|
|
114
135
|
}
|
|
115
136
|
return mimeTypes[ext] || 'application/octet-stream'
|
|
116
137
|
}
|
|
138
|
+
|
|
139
|
+
protected async downloadAndOpen<T extends AIImageResult | AIVideoResult>(
|
|
140
|
+
result: T,
|
|
141
|
+
mediaType: "image" | "video",
|
|
142
|
+
fileExtension: string,
|
|
143
|
+
openInViewer: boolean = true
|
|
144
|
+
): Promise<T & { localPath?: string }> {
|
|
145
|
+
if (!result.success) {
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const mediaArray = mediaType === "image"
|
|
150
|
+
? (result as AIImageResult).images
|
|
151
|
+
: (result as AIVideoResult).videos
|
|
152
|
+
|
|
153
|
+
if (!mediaArray || mediaArray.length === 0) {
|
|
154
|
+
return result
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const downloadsDir = path.join(process.cwd(), "output")
|
|
159
|
+
if (!fs.existsSync(downloadsDir)) {
|
|
160
|
+
fs.mkdirSync(downloadsDir, { recursive: true })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const timestamp = Date.now()
|
|
164
|
+
const filename = `${this.providerName}-${mediaType}-${timestamp}.${fileExtension}`
|
|
165
|
+
const localPath = path.join(downloadsDir, filename)
|
|
166
|
+
|
|
167
|
+
const response = await fetch(mediaArray[0].url)
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
throw new Error(`Failed to download: ${response.statusText}`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const buffer = await response.arrayBuffer()
|
|
173
|
+
fs.writeFileSync(localPath, Buffer.from(buffer))
|
|
174
|
+
|
|
175
|
+
logger.info(`${mediaType} saved to ${localPath}`)
|
|
176
|
+
|
|
177
|
+
if (openInViewer) {
|
|
178
|
+
await this.openInDefaultViewer(localPath)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...result,
|
|
183
|
+
localPath
|
|
184
|
+
}
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
this.log(
|
|
187
|
+
`${mediaType.charAt(0).toUpperCase() + mediaType.slice(1)} generated successfully but post-processing failed: ${error.message}`,
|
|
188
|
+
"error"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
...result,
|
|
193
|
+
localPath: undefined
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
protected extractCreditsFromResponse(response: any): any {
|
|
199
|
+
const credits: any = {}
|
|
200
|
+
|
|
201
|
+
if (response?.credits !== undefined) {
|
|
202
|
+
credits.balance = response.credits.balance
|
|
203
|
+
credits.cost = response.credits.cost
|
|
204
|
+
credits.currency = response.credits.currency
|
|
205
|
+
credits.remaining = response.credits.remaining
|
|
206
|
+
credits.limit = response.credits.limit
|
|
207
|
+
credits.used = response.credits.used
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (response?.billing !== undefined) {
|
|
211
|
+
credits.balance = response.billing.balance
|
|
212
|
+
credits.cost = response.billing.cost
|
|
213
|
+
credits.currency = response.billing.currency
|
|
214
|
+
credits.remaining = response.billing.remaining
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (response?.usage !== undefined) {
|
|
218
|
+
credits.used = response.usage.total_tokens || response.usage.used
|
|
219
|
+
credits.limit = response.usage.limit
|
|
220
|
+
credits.remaining = response.usage.remaining
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (Object.keys(credits).length === 0) {
|
|
224
|
+
return undefined
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return credits
|
|
228
|
+
}
|
|
117
229
|
}
|
package/cli.ts
CHANGED
|
@@ -7,6 +7,74 @@ import { spawn } from 'child_process'
|
|
|
7
7
|
import { resolve } from 'path'
|
|
8
8
|
import { readFileSync, existsSync } from 'fs'
|
|
9
9
|
|
|
10
|
+
async function publishPet(petName: string | undefined, options: { preview?: boolean; channel?: string } = {}) {
|
|
11
|
+
const { preview = false, channel = 'latest' } = options
|
|
12
|
+
|
|
13
|
+
// Find the script path - try multiple locations
|
|
14
|
+
let scriptPath = resolve(__dirname, '../../scripts/publish-pet.ts')
|
|
15
|
+
if (!existsSync(scriptPath)) {
|
|
16
|
+
// We might be in a different location, try relative to cwd
|
|
17
|
+
scriptPath = resolve(process.cwd(), '../../scripts/publish-pet.ts')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!existsSync(scriptPath)) {
|
|
21
|
+
console.error(`❌ Publish script not found: ${scriptPath}`)
|
|
22
|
+
console.error(` Tried: ${resolve(__dirname, '../../scripts/publish-pet.ts')}`)
|
|
23
|
+
console.error(` And: ${resolve(process.cwd(), '../../scripts/publish-pet.ts')}`)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If no petName provided, try to infer from current directory
|
|
28
|
+
let finalPetName = petName
|
|
29
|
+
if (!finalPetName) {
|
|
30
|
+
const cwd = process.cwd()
|
|
31
|
+
const pkgPath = resolve(cwd, 'package.json')
|
|
32
|
+
|
|
33
|
+
if (existsSync(pkgPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
36
|
+
// Extract pet name from @openpets/maps -> maps
|
|
37
|
+
if (pkg.name && pkg.name.startsWith('@openpets/')) {
|
|
38
|
+
finalPetName = pkg.name.replace('@openpets/', '')
|
|
39
|
+
console.log(`📦 Detected pet: ${finalPetName}`)
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Ignore parse errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!finalPetName) {
|
|
47
|
+
console.error('❌ Could not determine pet name. Run from pet directory or specify pet name.')
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const bunArgs = [scriptPath, finalPetName]
|
|
53
|
+
if (preview) bunArgs.push('--preview')
|
|
54
|
+
if (channel && channel !== 'latest') bunArgs.push('--channel', channel)
|
|
55
|
+
|
|
56
|
+
return new Promise<void>((resolve, reject) => {
|
|
57
|
+
const child = spawn('bun', bunArgs, {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
shell: true,
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
OPENPETS_PREVIEW: preview ? 'true' : 'false',
|
|
63
|
+
OPENPETS_CHANNEL: channel
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
child.on('error', reject)
|
|
68
|
+
child.on('exit', (code) => {
|
|
69
|
+
if (code !== 0) {
|
|
70
|
+
reject(new Error(`Publish exited with code ${code}`))
|
|
71
|
+
} else {
|
|
72
|
+
resolve()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
10
78
|
const args = process.argv.slice(2)
|
|
11
79
|
const command = args[0]
|
|
12
80
|
|
|
@@ -156,6 +224,18 @@ if (command === 'build') {
|
|
|
156
224
|
log('Deploy error details:', error)
|
|
157
225
|
process.exit(1)
|
|
158
226
|
})
|
|
227
|
+
} else if (command === 'publish') {
|
|
228
|
+
const petName = args[1] && !args[1].startsWith('--') ? args[1] : undefined
|
|
229
|
+
const preview = args.includes('--preview')
|
|
230
|
+
const channelIndex = args.indexOf('--channel')
|
|
231
|
+
const channel = channelIndex >= 0 ? args[channelIndex + 1] : 'latest'
|
|
232
|
+
|
|
233
|
+
log('Publishing pet:', petName || '(auto-detect)', { preview, channel })
|
|
234
|
+
publishPet(petName, { preview, channel }).catch(error => {
|
|
235
|
+
console.error('Error publishing pet:', error)
|
|
236
|
+
log('Publish error details:', error)
|
|
237
|
+
process.exit(1)
|
|
238
|
+
})
|
|
159
239
|
} else if (!command) {
|
|
160
240
|
log('No command provided, launching manager UI')
|
|
161
241
|
launchManager()
|
|
@@ -164,16 +244,25 @@ if (command === 'build') {
|
|
|
164
244
|
console.error('Usage: pets [command] [options]')
|
|
165
245
|
console.error('')
|
|
166
246
|
console.error('Commands:')
|
|
167
|
-
console.error(' (none)
|
|
168
|
-
console.error(' build <pet-name>
|
|
169
|
-
console.error(' deploy <pet-name>
|
|
247
|
+
console.error(' (none) Launch the plugin manager UI (default)')
|
|
248
|
+
console.error(' build <pet-name> Build and validate a pet package')
|
|
249
|
+
console.error(' deploy <pet-name> Build and deploy a pet package with metadata')
|
|
250
|
+
console.error(' publish [pet-name] Publish a pet package to npm (auto-detects if in pet dir)')
|
|
251
|
+
console.error('')
|
|
252
|
+
console.error('Publish Options:')
|
|
253
|
+
console.error(' --preview Dry-run mode (no actual publish)')
|
|
254
|
+
console.error(' --channel <name> Publish to a specific npm tag (default: latest)')
|
|
170
255
|
console.error('')
|
|
171
256
|
console.error('Examples:')
|
|
172
|
-
console.error(' pets
|
|
257
|
+
console.error(' pets # Launch the desktop plugin manager')
|
|
173
258
|
console.error(' pets build postgres')
|
|
174
259
|
console.error(' pets deploy maps')
|
|
260
|
+
console.error(' pets publish maps # From root directory')
|
|
261
|
+
console.error(' cd pets/maps && pets publish # From pet directory')
|
|
262
|
+
console.error(' pets publish --preview # Dry-run from pet directory')
|
|
263
|
+
console.error(' pets publish maps --channel beta')
|
|
175
264
|
console.error('')
|
|
176
265
|
console.error('Environment:')
|
|
177
|
-
console.error(' PETS_DEBUG=true
|
|
266
|
+
console.error(' PETS_DEBUG=true Enable detailed debug logging')
|
|
178
267
|
process.exit(1)
|
|
179
268
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openpets",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core package for OpenPets - AI plugin infrastructure and shared utilities",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -15,7 +15,16 @@
|
|
|
15
15
|
"*.d.ts",
|
|
16
16
|
"ai-client-base/**/*",
|
|
17
17
|
"!**/*.test.ts",
|
|
18
|
-
"!**/*.test.js"
|
|
18
|
+
"!**/*.test.js",
|
|
19
|
+
"!.env*",
|
|
20
|
+
"!*.pem",
|
|
21
|
+
"!*.key",
|
|
22
|
+
"!credentials*.json",
|
|
23
|
+
"!*.p12",
|
|
24
|
+
"!*.pfx",
|
|
25
|
+
"!reference",
|
|
26
|
+
"!reports",
|
|
27
|
+
"!report"
|
|
19
28
|
],
|
|
20
29
|
"keywords": [
|
|
21
30
|
"ai",
|
|
@@ -42,4 +51,4 @@
|
|
|
42
51
|
"peerDependencies": {
|
|
43
52
|
"@opencode-ai/plugin": "^1.0.106"
|
|
44
53
|
}
|
|
45
|
-
}
|
|
54
|
+
}
|
package/plugin-factory.ts
CHANGED
|
@@ -285,16 +285,7 @@ export function loadEnv(petId?: string): EnvConfig {
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
if (value && value.length > 0) {
|
|
290
|
-
if (!envVars[key]) {
|
|
291
|
-
envVars[key] = value
|
|
292
|
-
logger.debug(`Loaded from process.env: ${key}`)
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
logger.info(`Loaded ${Object.keys(envVars).length} environment variables`)
|
|
288
|
+
logger.info(`Loaded ${Object.keys(envVars).length} environment variables from project config`)
|
|
298
289
|
|
|
299
290
|
} catch (error: any) {
|
|
300
291
|
logger.error(`Error loading environment variables: ${error.message}`)
|
package/validate-pet.ts
CHANGED
|
@@ -39,6 +39,10 @@ export class PluginValidator {
|
|
|
39
39
|
'scenarios'
|
|
40
40
|
]
|
|
41
41
|
|
|
42
|
+
private recommendedFields = [
|
|
43
|
+
'providers'
|
|
44
|
+
]
|
|
45
|
+
|
|
42
46
|
private requiredScripts = [
|
|
43
47
|
'test:all'
|
|
44
48
|
]
|
|
@@ -158,9 +162,27 @@ export class PluginValidator {
|
|
|
158
162
|
warnings.push('No scripts defined - consider adding test scripts')
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
// Validate providers structure if present
|
|
166
|
+
const validProviderIds = new Set<string>()
|
|
167
|
+
if (pkg.providers) {
|
|
168
|
+
const providersValidation = this.validateProviders(pkg.providers)
|
|
169
|
+
errors.push(...providersValidation.errors)
|
|
170
|
+
warnings.push(...providersValidation.warnings)
|
|
171
|
+
|
|
172
|
+
if (Array.isArray(pkg.providers)) {
|
|
173
|
+
pkg.providers.forEach((provider: any) => {
|
|
174
|
+
if (provider.id) {
|
|
175
|
+
validProviderIds.add(provider.id)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
} else if (pkg.envVariables) {
|
|
180
|
+
warnings.push('Missing recommended field "providers" - consider defining providers with credentialsUrl for better documentation')
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
// Validate envVariables structure if present
|
|
162
184
|
if (pkg.envVariables) {
|
|
163
|
-
const envValidation = this.validateEnvVariables(pkg.envVariables)
|
|
185
|
+
const envValidation = this.validateEnvVariables(pkg.envVariables, validProviderIds)
|
|
164
186
|
errors.push(...envValidation.errors)
|
|
165
187
|
warnings.push(...envValidation.warnings)
|
|
166
188
|
}
|
|
@@ -206,6 +228,9 @@ export class PluginValidator {
|
|
|
206
228
|
// Check for .env.example
|
|
207
229
|
if (pkg.envVariables && !existsSync(join(pluginPath, '.env.example'))) {
|
|
208
230
|
errors.push('envVariables defined but .env.example file missing - create this file to document required environment variables')
|
|
231
|
+
} else if (pkg.envVariables && existsSync(join(pluginPath, '.env.example'))) {
|
|
232
|
+
const envExampleValidation = this.validateEnvExampleFile(join(pluginPath, '.env.example'), pkg.envVariables, pkg.providers)
|
|
233
|
+
warnings.push(...envExampleValidation.warnings)
|
|
209
234
|
}
|
|
210
235
|
|
|
211
236
|
// Check for README
|
|
@@ -223,7 +248,72 @@ export class PluginValidator {
|
|
|
223
248
|
}
|
|
224
249
|
}
|
|
225
250
|
|
|
226
|
-
private
|
|
251
|
+
private validateProviders(providers: any): ValidationResult {
|
|
252
|
+
const errors: string[] = []
|
|
253
|
+
const warnings: string[] = []
|
|
254
|
+
|
|
255
|
+
if (!Array.isArray(providers)) {
|
|
256
|
+
errors.push('providers must be an array')
|
|
257
|
+
return { valid: false, errors, warnings }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (providers.length === 0) {
|
|
261
|
+
warnings.push('providers array is empty - add at least one provider definition')
|
|
262
|
+
return { valid: true, errors, warnings }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const seenIds = new Set<string>()
|
|
266
|
+
|
|
267
|
+
providers.forEach((provider: any, index: number) => {
|
|
268
|
+
if (typeof provider !== 'object' || provider === null) {
|
|
269
|
+
errors.push(`providers[${index}] must be an object`)
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!provider.id) {
|
|
274
|
+
errors.push(`providers[${index}] missing required field "id"`)
|
|
275
|
+
} else if (typeof provider.id !== 'string') {
|
|
276
|
+
errors.push(`providers[${index}].id must be a string`)
|
|
277
|
+
} else {
|
|
278
|
+
if (seenIds.has(provider.id)) {
|
|
279
|
+
errors.push(`providers[${index}] duplicate provider id "${provider.id}"`)
|
|
280
|
+
}
|
|
281
|
+
seenIds.add(provider.id)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!provider.name) {
|
|
285
|
+
warnings.push(`providers[${index}] missing recommended field "name"`)
|
|
286
|
+
} else if (typeof provider.name !== 'string') {
|
|
287
|
+
errors.push(`providers[${index}].name must be a string`)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!provider.credentialsUrl) {
|
|
291
|
+
warnings.push(`providers[${index}] missing recommended field "credentialsUrl" for API key generation link`)
|
|
292
|
+
} else if (typeof provider.credentialsUrl !== 'string') {
|
|
293
|
+
errors.push(`providers[${index}].credentialsUrl must be a string`)
|
|
294
|
+
} else {
|
|
295
|
+
try {
|
|
296
|
+
new URL(provider.credentialsUrl.replace(/\{[^}]+\}/g, 'placeholder'))
|
|
297
|
+
} catch {
|
|
298
|
+
warnings.push(`providers[${index}].credentialsUrl should be a valid URL pattern`)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (provider.credentialsDocUrl && typeof provider.credentialsDocUrl !== 'string') {
|
|
303
|
+
errors.push(`providers[${index}].credentialsDocUrl must be a string`)
|
|
304
|
+
} else if (provider.credentialsDocUrl) {
|
|
305
|
+
try {
|
|
306
|
+
new URL(provider.credentialsDocUrl)
|
|
307
|
+
} catch {
|
|
308
|
+
warnings.push(`providers[${index}].credentialsDocUrl should be a valid URL`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
return { valid: errors.length === 0, errors, warnings }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private validateEnvVariables(envVars: any, validProviderIds: Set<string>): ValidationResult {
|
|
227
317
|
const errors: string[] = []
|
|
228
318
|
const warnings: string[] = []
|
|
229
319
|
|
|
@@ -245,8 +335,14 @@ export class PluginValidator {
|
|
|
245
335
|
if (!envVar.description) {
|
|
246
336
|
warnings.push(`envVariables.${type}[${index}] missing description field`)
|
|
247
337
|
}
|
|
248
|
-
if (!envVar.
|
|
249
|
-
warnings.push(`envVariables.${type}[${index}] missing provider
|
|
338
|
+
if (!envVar.providerId) {
|
|
339
|
+
warnings.push(`envVariables.${type}[${index}] missing providerId field - should reference a provider in the providers array`)
|
|
340
|
+
} else if (typeof envVar.providerId === 'string') {
|
|
341
|
+
if (validProviderIds.size > 0 && !validProviderIds.has(envVar.providerId)) {
|
|
342
|
+
errors.push(`envVariables.${type}[${index}].providerId "${envVar.providerId}" does not match any provider id in the providers array`)
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
errors.push(`envVariables.${type}[${index}].providerId must be a string`)
|
|
250
346
|
}
|
|
251
347
|
if (typeof envVar.priority !== 'number') {
|
|
252
348
|
warnings.push(`envVariables.${type}[${index}] missing or invalid priority field`)
|
|
@@ -264,6 +360,65 @@ export class PluginValidator {
|
|
|
264
360
|
return { valid: errors.length === 0, errors, warnings }
|
|
265
361
|
}
|
|
266
362
|
|
|
363
|
+
private validateEnvExampleFile(envExamplePath: string, envVars: any, providers: any[]): { warnings: string[] } {
|
|
364
|
+
const warnings: string[] = []
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const envExampleContent = readFileSync(envExamplePath, 'utf-8')
|
|
368
|
+
const envExampleLines = envExampleContent.split('\n')
|
|
369
|
+
|
|
370
|
+
const documentedVars = new Set<string>()
|
|
371
|
+
const providerComments = new Map<string, boolean>()
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < envExampleLines.length; i++) {
|
|
374
|
+
const line = envExampleLines[i].trim()
|
|
375
|
+
|
|
376
|
+
if (line.startsWith('#') && (line.includes('http://') || line.includes('https://'))) {
|
|
377
|
+
const nextLine = envExampleLines[i + 1]?.trim()
|
|
378
|
+
if (nextLine && nextLine.includes('=')) {
|
|
379
|
+
const varName = nextLine.split('=')[0].trim()
|
|
380
|
+
providerComments.set(varName, true)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (line && !line.startsWith('#') && line.includes('=')) {
|
|
385
|
+
const varName = line.split('=')[0].trim()
|
|
386
|
+
documentedVars.add(varName)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const allEnvVars: any[] = [
|
|
391
|
+
...(envVars.required || []),
|
|
392
|
+
...(envVars.optional || [])
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
const providerMap = new Map<string, any>()
|
|
396
|
+
if (providers && Array.isArray(providers)) {
|
|
397
|
+
providers.forEach(provider => {
|
|
398
|
+
if (provider.id) {
|
|
399
|
+
providerMap.set(provider.id, provider)
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for (const envVar of allEnvVars) {
|
|
405
|
+
if (!documentedVars.has(envVar.name)) {
|
|
406
|
+
warnings.push(`Environment variable "${envVar.name}" defined in package.json but not found in .env.example`)
|
|
407
|
+
} else if (!providerComments.has(envVar.name) && envVar.providerId) {
|
|
408
|
+
const provider = providerMap.get(envVar.providerId)
|
|
409
|
+
if (provider && provider.credentialsUrl) {
|
|
410
|
+
warnings.push(`Environment variable "${envVar.name}" in .env.example missing comment with provider credentialsUrl (${provider.credentialsUrl})`)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
warnings.push(`Could not validate .env.example file: ${error instanceof Error ? error.message : String(error)}`)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return { warnings }
|
|
420
|
+
}
|
|
421
|
+
|
|
267
422
|
private validateScenarios(scenarios: any): ValidationResult {
|
|
268
423
|
const errors: string[] = []
|
|
269
424
|
const warnings: string[] = []
|