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/build-pet.ts ADDED
@@ -0,0 +1,429 @@
1
+ import { join, dirname } from 'path'
2
+ import { fileURLToPath } from 'url'
3
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'
4
+ import { pathToFileURL } from 'url'
5
+ import { PluginValidator } from './validate-pet.js'
6
+
7
+ const __filename = fileURLToPath(import.meta.url)
8
+ const __dirname = dirname(__filename)
9
+ const projectRoot = dirname(dirname(__dirname))
10
+
11
+ const DEBUG = process.env.PETS_DEBUG === 'true'
12
+
13
+ function debug(...msgs: any[]) {
14
+ if (DEBUG) {
15
+ console.log('[BUILD-PET]', ...msgs)
16
+ }
17
+ }
18
+
19
+ interface ToolDefinition {
20
+ name: string
21
+ description: string
22
+ schema: any
23
+ }
24
+
25
+ async function extractToolsFromIndexFile(indexPath: string): Promise<ToolDefinition[]> {
26
+ debug('=== EXTRACTING TOOLS FROM INDEX FILE ===')
27
+ debug('Index path:', indexPath)
28
+ debug('File exists:', existsSync(indexPath))
29
+
30
+ try {
31
+ const indexUrl = pathToFileURL(indexPath).href
32
+ debug('Index URL:', indexUrl)
33
+
34
+ debug('Attempting dynamic import...')
35
+ const indexModule = await import(/* @vite-ignore */ indexUrl)
36
+ debug('Import successful')
37
+ debug('Module keys:', Object.keys(indexModule))
38
+
39
+ if (indexModule.toolsMetadata && Array.isArray(indexModule.toolsMetadata)) {
40
+ console.log(` Found ${indexModule.toolsMetadata.length} tools from exported toolsMetadata`)
41
+ debug('Tools metadata:', JSON.stringify(indexModule.toolsMetadata, null, 2))
42
+ return indexModule.toolsMetadata
43
+ } else {
44
+ debug('toolsMetadata not found or not an array')
45
+ debug('indexModule.toolsMetadata:', indexModule.toolsMetadata)
46
+ }
47
+ } catch (error) {
48
+ console.warn(` ⚠️ Could not import toolsMetadata, falling back to file parsing`)
49
+ console.warn(` Error: ${error instanceof Error ? error.message : String(error)}`)
50
+ debug('Import error details:', {
51
+ name: error instanceof Error ? error.name : 'Unknown',
52
+ message: error instanceof Error ? error.message : String(error),
53
+ stack: error instanceof Error ? error.stack : 'No stack trace'
54
+ })
55
+ }
56
+
57
+ debug('Falling back to file parsing')
58
+ return extractToolsFromIndexFileByParsing(indexPath)
59
+ }
60
+
61
+ function extractToolsFromIndexFileByParsing(indexPath: string): ToolDefinition[] {
62
+ const content = readFileSync(indexPath, 'utf8')
63
+
64
+ const toolsMatch = content.match(/const\s+tools:\s*ToolDefinition\[\]\s*=\s*\[([\s\S]*?)\n\s*\]/m)
65
+ if (!toolsMatch) {
66
+ console.warn('⚠️ Could not find tools array in index.ts')
67
+ return []
68
+ }
69
+
70
+ const toolsArray = toolsMatch[1]
71
+ const toolNames: ToolDefinition[] = []
72
+
73
+ const toolsSeparated = toolsArray.split(/\n\s*\{/).filter(part => part.includes('name:'))
74
+ const toolBlocks = toolsSeparated.map((part, idx) => {
75
+ if (idx === 0) return part
76
+ return '{' + part
77
+ })
78
+
79
+ for (const block of toolBlocks) {
80
+ const nameMatch = block.match(/name:\s*["']([^"']+)["']/)
81
+ const descMatch = block.match(/description:\s*["']([^"']+)["']/)
82
+
83
+ const schemaMatch = block.match(/schema:\s*z\.object\(\{([\s\S]*?)\}\),?\s*(?:async\s+execute|$)/)
84
+
85
+ if (nameMatch && descMatch) {
86
+ let schemaProperties: any = {}
87
+
88
+ if (schemaMatch) {
89
+ const schemaContent = schemaMatch[1]
90
+
91
+ const lines = schemaContent.split('\n').filter(l => l.trim() && !l.trim().startsWith('//'))
92
+
93
+ for (const line of lines) {
94
+ const propMatch = line.match(/^\s*(\w+):\s*z\.(.+?),?\s*$/)
95
+ if (!propMatch) continue
96
+
97
+ const [, propName, zodDef] = propMatch
98
+ let propSchema: any = {}
99
+
100
+ if (zodDef.includes('string(')) {
101
+ propSchema.type = 'string'
102
+ } else if (zodDef.includes('number(')) {
103
+ propSchema.type = 'number'
104
+ const minMatch = zodDef.match(/\.min\((\d+)\)/)
105
+ const maxMatch = zodDef.match(/\.max\((\d+)\)/)
106
+ if (minMatch) propSchema.minimum = parseInt(minMatch[1])
107
+ if (maxMatch) propSchema.maximum = parseInt(maxMatch[1])
108
+ } else if (zodDef.includes('boolean(')) {
109
+ propSchema.type = 'boolean'
110
+ } else if (zodDef.includes('enum(')) {
111
+ propSchema.type = 'string'
112
+ const enumMatch = zodDef.match(/enum\(\[([^\]]+)\]\)/)
113
+ if (enumMatch) {
114
+ const enumValues = enumMatch[1].match(/["']([^"']+)["']/g)?.map(v => v.replace(/["']/g, ''))
115
+ if (enumValues) propSchema.enum = enumValues
116
+ }
117
+ } else if (zodDef.includes('array(')) {
118
+ propSchema.type = 'array'
119
+ const arrayTypeMatch = zodDef.match(/array\(z\.(\w+)\(\)\)/)
120
+ if (arrayTypeMatch) {
121
+ propSchema.items = { type: arrayTypeMatch[1].toLowerCase() }
122
+ }
123
+ }
124
+
125
+ if (zodDef.includes('.optional()')) {
126
+ propSchema.optional = true
127
+ }
128
+
129
+ const descMatch = zodDef.match(/\.describe\(["']([^"']+)["']\)/)
130
+ if (descMatch) {
131
+ propSchema.description = descMatch[1]
132
+ }
133
+
134
+ schemaProperties[propName] = propSchema
135
+ }
136
+ }
137
+
138
+ toolNames.push({
139
+ name: nameMatch[1],
140
+ description: descMatch[1],
141
+ schema: {
142
+ type: 'object',
143
+ properties: schemaProperties
144
+ }
145
+ })
146
+ }
147
+ }
148
+
149
+ console.log(` Found ${toolNames.length} tools in index.ts`)
150
+ return toolNames
151
+ }
152
+
153
+ function formatZodSchema(schema: any, depth: number = 0): any {
154
+ if (!schema || !schema._def) {
155
+ return { type: 'object', properties: {} }
156
+ }
157
+
158
+ const def = schema._def
159
+ const zodType = def.typeName || def.type || schema.constructor?.name || 'unknown'
160
+
161
+ if (zodType === 'ZodObject' || zodType === 'object') {
162
+ const properties: any = {}
163
+ const shapeObj = typeof def.shape === 'function' ? def.shape() : def.shape || {}
164
+
165
+ for (const [key, value] of Object.entries(shapeObj)) {
166
+ properties[key] = formatZodSchema(value, depth + 1)
167
+ }
168
+
169
+ return {
170
+ type: 'object',
171
+ properties
172
+ }
173
+ }
174
+
175
+ if (zodType === 'ZodString' || zodType === 'string') {
176
+ return { type: 'string' }
177
+ }
178
+
179
+ if (zodType === 'ZodNumber' || zodType === 'number') {
180
+ const result: any = { type: 'number' }
181
+ if (def.checks) {
182
+ for (const check of def.checks) {
183
+ if (check.kind === 'min') result.minimum = check.value
184
+ if (check.kind === 'max') result.maximum = check.value
185
+ }
186
+ }
187
+ return result
188
+ }
189
+
190
+ if (zodType === 'ZodBoolean' || zodType === 'boolean') {
191
+ return { type: 'boolean' }
192
+ }
193
+
194
+ if (zodType === 'ZodArray' || zodType === 'array') {
195
+ return {
196
+ type: 'array',
197
+ items: formatZodSchema(def.type, depth + 1)
198
+ }
199
+ }
200
+
201
+ if (zodType === 'ZodEnum' || zodType === 'enum') {
202
+ let values = def.values
203
+ if (!values && schema._def?.values) {
204
+ values = schema._def.values
205
+ }
206
+ if (!values && schema.options) {
207
+ values = schema.options
208
+ }
209
+
210
+ const enumArray = Array.isArray(values) ? values : Object.values(values || {})
211
+ return {
212
+ type: 'string',
213
+ enum: enumArray
214
+ }
215
+ }
216
+
217
+ if (zodType === 'ZodOptional' || zodType === 'optional') {
218
+ return {
219
+ ...formatZodSchema(def.innerType, depth + 1),
220
+ optional: true
221
+ }
222
+ }
223
+
224
+ return { type: zodType }
225
+ }
226
+
227
+ function generateToolsMetadata(tools: ToolDefinition[]): Array<{ name: string; description: string; schema: any }> {
228
+ return tools.map(tool => ({
229
+ name: tool.name,
230
+ description: tool.description,
231
+ schema: tool.schema._def ? formatZodSchema(tool.schema) : tool.schema
232
+ }))
233
+ }
234
+
235
+ function generateScriptsFromTools(tools: ToolDefinition[], petName: string): Record<string, string> {
236
+ const scripts: Record<string, string> = {
237
+ build: 'pets build',
238
+ 'build:tsc': 'tsc',
239
+ quickstart: `opencode run "list available ${petName} commands"`,
240
+ }
241
+
242
+ for (const tool of tools) {
243
+ let scriptName = tool.name
244
+
245
+ if (scriptName.startsWith(`${petName}-`)) {
246
+ scriptName = scriptName.replace(new RegExp(`^${petName}-`), 'test:')
247
+ } else {
248
+ scriptName = `test:${scriptName}`
249
+ }
250
+
251
+ const command = `opencode run "${tool.description}"`
252
+ scripts[scriptName] = command
253
+ }
254
+
255
+ scripts['test:all'] = 'echo "Run individual test scripts to test specific tools"'
256
+
257
+ return scripts
258
+ }
259
+
260
+ export async function buildPet(petName?: string): Promise<void> {
261
+ debug('=== BUILD PET STARTING ===')
262
+ debug('Initial petName:', petName)
263
+ debug('CWD:', process.cwd())
264
+ debug('Project root:', projectRoot)
265
+
266
+ if (!petName) {
267
+ const cwd = process.cwd()
268
+ const petsDir = join(projectRoot, 'pets')
269
+
270
+ debug('Auto-detecting pet from CWD:', cwd)
271
+ debug('Pets directory:', petsDir)
272
+
273
+ if (cwd.includes('/pets/')) {
274
+ petName = cwd.split('/pets/')[1].split('/')[0]
275
+ console.log(`📦 Auto-detected pet: ${petName}`)
276
+ debug('Auto-detected pet:', petName)
277
+ }
278
+
279
+ if (!petName) {
280
+ console.error('Usage: pets build <pet-name>')
281
+ console.error('Example: pets build postgres')
282
+ console.error('')
283
+ console.error('Available pets:')
284
+ if (existsSync(petsDir)) {
285
+ const pets = readdirSync(petsDir).filter(dir => {
286
+ const petPath = join(petsDir, dir)
287
+ return statSync(petPath).isDirectory() && dir !== '_TEMPLATE_'
288
+ })
289
+ pets.forEach(pet => console.error(` - ${pet}`))
290
+ debug('Available pets:', pets)
291
+ } else {
292
+ debug('Pets directory does not exist:', petsDir)
293
+ }
294
+ process.exit(1)
295
+ }
296
+ }
297
+
298
+ const petDir = join(projectRoot, 'pets', petName)
299
+ const packageJsonPath = join(petDir, 'package.json')
300
+ const indexPath = join(petDir, 'index.ts')
301
+
302
+ debug('Pet directory:', petDir)
303
+ debug('Package.json path:', packageJsonPath)
304
+ debug('Index.ts path:', indexPath)
305
+
306
+ if (!existsSync(petDir)) {
307
+ console.error(`❌ Pet '${petName}' not found in pets/ directory`)
308
+ debug('Pet directory does not exist:', petDir)
309
+ process.exit(1)
310
+ }
311
+
312
+ if (!existsSync(packageJsonPath)) {
313
+ console.error(`❌ package.json not found for pet '${petName}'`)
314
+ debug('Package.json does not exist:', packageJsonPath)
315
+ process.exit(1)
316
+ }
317
+
318
+ if (!existsSync(indexPath)) {
319
+ console.error(`❌ index.ts not found for pet '${petName}'`)
320
+ debug('Index.ts does not exist:', indexPath)
321
+ process.exit(1)
322
+ }
323
+
324
+ console.log(`🔨 Building ${petName}...`)
325
+ debug('All required files found, proceeding with build')
326
+
327
+ const packageJsonContent = readFileSync(packageJsonPath, 'utf8')
328
+ debug('Package.json content length:', packageJsonContent.length)
329
+ const packageJson = JSON.parse(packageJsonContent)
330
+ debug('Package.json parsed successfully')
331
+ debug('Package name:', packageJson.name)
332
+ debug('Package version:', packageJson.version)
333
+
334
+ // Auto-generate repository field if missing
335
+ if (!packageJson.repository) {
336
+ packageJson.repository = {
337
+ type: 'git',
338
+ url: `https://github.com/raggle-ai/pets/pets/${petName}`
339
+ }
340
+ debug('Auto-generated repository field:', packageJson.repository.url)
341
+ }
342
+
343
+ console.log(` Name: ${packageJson.name}`)
344
+ console.log(` Version: ${packageJson.version}`)
345
+ console.log(` Description: ${packageJson.description || 'No description'}`)
346
+
347
+ console.log(`\n🔍 Validating plugin structure...`)
348
+ const validator = new PluginValidator()
349
+ const validation = validator.validatePlugin(petDir)
350
+
351
+ if (validation.result.errors.length > 0) {
352
+ console.log(`\n❌ Validation failed with ${validation.result.errors.length} error(s):`)
353
+ validation.result.errors.forEach(error => {
354
+ console.log(` ❌ ${error}`)
355
+ })
356
+ console.log(`\n💥 Build aborted due to validation errors.`)
357
+ process.exit(1)
358
+ }
359
+
360
+ if (validation.result.warnings.length > 0) {
361
+ console.log(`\n⚠️ Found ${validation.result.warnings.length} warning(s):`)
362
+ validation.result.warnings.forEach(warning => {
363
+ console.log(` ⚠️ ${warning}`)
364
+ })
365
+ }
366
+
367
+ if (validation.result.codePatternWarnings && validation.result.codePatternWarnings.length > 0) {
368
+ console.log(`\n📋 Code pattern analysis:`)
369
+ validation.result.codePatternWarnings.forEach(warning => {
370
+ console.log(` ${warning}`)
371
+ })
372
+ }
373
+
374
+ if (validation.result.errors.length === 0 && validation.result.warnings.length === 0 && (!validation.result.codePatternWarnings || validation.result.codePatternWarnings.length === 0)) {
375
+ console.log(` ✓ All validation checks passed`)
376
+ }
377
+
378
+ debug('Extracting tools from index file...')
379
+ const tools = await extractToolsFromIndexFile(indexPath)
380
+ debug(`Extracted ${tools.length} tools`)
381
+
382
+ if (tools.length > 0) {
383
+ console.log(`\n📝 Generating scripts and metadata for ${tools.length} tools...`)
384
+ debug('Tools extracted:', tools.map(t => t.name))
385
+
386
+ const scripts = generateScriptsFromTools(tools, petName)
387
+ debug('Generated scripts:', Object.keys(scripts))
388
+
389
+ const toolsMetadata = generateToolsMetadata(tools)
390
+ debug('Generated tools metadata count:', toolsMetadata.length)
391
+
392
+ packageJson.scripts = scripts
393
+ delete packageJson.tools
394
+
395
+ const commandsJsonPath = join(petDir, 'commands.json')
396
+ debug('Commands.json path:', commandsJsonPath)
397
+
398
+ const commandsData = {
399
+ name: packageJson.name,
400
+ version: packageJson.version,
401
+ description: packageJson.description,
402
+ tools: toolsMetadata,
403
+ generatedAt: new Date().toISOString()
404
+ }
405
+
406
+ debug('Writing package.json...')
407
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8')
408
+ debug('Package.json written successfully')
409
+
410
+ debug('Writing commands.json...')
411
+ writeFileSync(commandsJsonPath, JSON.stringify(commandsData, null, 2) + '\n', 'utf8')
412
+ debug('Commands.json written successfully')
413
+
414
+ console.log(` ✓ Updated package.json with ${Object.keys(scripts).length} scripts`)
415
+ console.log(` ✓ Generated commands.json with ${tools.length} tools`)
416
+
417
+ console.log(`\n🔧 Generated test scripts:`)
418
+ Object.entries(scripts).forEach(([name, cmd]) => {
419
+ if (name.startsWith('test:')) {
420
+ console.log(` - npm run ${name}`)
421
+ }
422
+ })
423
+ } else {
424
+ debug('No tools found in index file')
425
+ }
426
+
427
+ console.log(`\n✅ ${petName} built successfully!`)
428
+ debug('=== BUILD COMPLETE ===')
429
+ }
package/cli.ts ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { buildPet } from './build-pet.js'
4
+ import { deployPet } from './deploy-pet.js'
5
+ import { addFolderToHistory } from './config-manager.js'
6
+ import { spawn } from 'child_process'
7
+ import { resolve } from 'path'
8
+ import { readFileSync, existsSync } from 'fs'
9
+
10
+ const args = process.argv.slice(2)
11
+ const command = args[0]
12
+
13
+ const DEBUG = process.env.PETS_DEBUG === 'true'
14
+
15
+ function log(...msgs: any[]) {
16
+ if (DEBUG) {
17
+ console.log('[PETS-CLI]', ...msgs)
18
+ }
19
+ }
20
+
21
+ function displayInstalledPlugins() {
22
+ const opencodeJsonPath = resolve(process.cwd(), 'opencode.json')
23
+
24
+ log('Looking for opencode.json at:', opencodeJsonPath)
25
+
26
+ if (!existsSync(opencodeJsonPath)) {
27
+ log('opencode.json not found')
28
+ return
29
+ }
30
+
31
+ try {
32
+ const content = readFileSync(opencodeJsonPath, 'utf-8')
33
+ log('opencode.json content length:', content.length)
34
+
35
+ const config = JSON.parse(content)
36
+ log('Parsed opencode.json config:', Object.keys(config))
37
+
38
+ if (config.plugin && Array.isArray(config.plugin) && config.plugin.length > 0) {
39
+ const plugins = config.plugin.filter((p: string) => typeof p === 'string' && p.trim())
40
+ log('Found plugins:', plugins)
41
+
42
+ if (plugins.length > 0) {
43
+ console.log('\x1b[36m%s\x1b[0m', '📦 Installed plugins:')
44
+ console.log('\x1b[2m%s\x1b[0m', ` ${plugins.join(', ')}`)
45
+ console.log('')
46
+ }
47
+ } else {
48
+ log('No plugins found in config')
49
+ }
50
+ } catch (error) {
51
+ log('Error reading opencode.json:', error)
52
+ }
53
+ }
54
+
55
+ function killPort(port: number) {
56
+ log(`Attempting to kill processes on port ${port}`)
57
+ try {
58
+ const { execSync } = require('child_process')
59
+ execSync(`lsof -ti:${port} | xargs kill -9 2>/dev/null || true`, { stdio: 'ignore' })
60
+ log(`Successfully killed processes on port ${port}`)
61
+ } catch (error) {
62
+ log(`No processes to kill on port ${port} or error occurred:`, error)
63
+ }
64
+ }
65
+
66
+ function launchManager() {
67
+ const startTime = Date.now()
68
+ const uiDir = resolve(__dirname, '../../apps/desktop')
69
+ const projectDir = process.cwd()
70
+
71
+ addFolderToHistory(projectDir)
72
+
73
+ console.log('Launching OpenPets Plugin Manager...')
74
+ console.log(`Project directory: ${projectDir}`)
75
+ console.log(`UI directory: ${uiDir}`)
76
+
77
+ log('Launch details:', {
78
+ __dirname,
79
+ uiDir,
80
+ projectDir,
81
+ uiDirExists: existsSync(uiDir),
82
+ timestamp: new Date().toISOString()
83
+ })
84
+
85
+ if (!existsSync(uiDir)) {
86
+ console.error(`❌ UI directory not found: ${uiDir}`)
87
+ log('Contents of parent directory:', require('fs').readdirSync(resolve(__dirname, '../../apps')))
88
+ process.exit(1)
89
+ }
90
+
91
+ log('Checking for package.json in UI directory')
92
+ const packageJsonPath = resolve(uiDir, 'package.json')
93
+ if (!existsSync(packageJsonPath)) {
94
+ console.error(`❌ package.json not found in UI directory: ${packageJsonPath}`)
95
+ process.exit(1)
96
+ }
97
+
98
+ killPort(1420)
99
+
100
+ log('Spawning npm process with tauri:dev')
101
+ const child = spawn('npm', ['run', 'tauri:dev'], {
102
+ cwd: uiDir,
103
+ stdio: 'inherit',
104
+ shell: true,
105
+ env: {
106
+ ...process.env,
107
+ OPENPETS_PROJECT_DIR: projectDir,
108
+ PETS_DEBUG: DEBUG ? 'true' : 'false'
109
+ }
110
+ })
111
+
112
+ log('Child process spawned with PID:', child.pid)
113
+
114
+ child.on('error', (error) => {
115
+ console.error('Failed to launch manager:', error)
116
+ log('Error details:', {
117
+ message: error.message,
118
+ stack: error.stack,
119
+ elapsed: Date.now() - startTime
120
+ })
121
+ process.exit(1)
122
+ })
123
+
124
+ child.on('exit', (code) => {
125
+ const elapsed = Date.now() - startTime
126
+ log(`Manager process exited with code ${code} after ${elapsed}ms`)
127
+ if (code !== 0) {
128
+ console.error(`Manager exited with code ${code}`)
129
+ process.exit(code || 1)
130
+ }
131
+ })
132
+ }
133
+
134
+ log('=== PETS CLI STARTUP ===')
135
+ log('Command:', command)
136
+ log('Args:', args)
137
+ log('CWD:', process.cwd())
138
+ log('ENV.OPENPETS_PROJECT_DIR:', process.env.OPENPETS_PROJECT_DIR)
139
+ log('ENV.PETS_DEBUG:', process.env.PETS_DEBUG)
140
+
141
+ displayInstalledPlugins()
142
+
143
+ if (command === 'build') {
144
+ const petName = args[1]
145
+ log('Building pet:', petName)
146
+ buildPet(petName).catch(error => {
147
+ console.error('Error building pet:', error)
148
+ log('Build error details:', error)
149
+ process.exit(1)
150
+ })
151
+ } else if (command === 'deploy') {
152
+ const petName = args[1]
153
+ log('Deploying pet:', petName)
154
+ deployPet(petName).catch(error => {
155
+ console.error('Error deploying pet:', error)
156
+ log('Deploy error details:', error)
157
+ process.exit(1)
158
+ })
159
+ } else if (!command) {
160
+ log('No command provided, launching manager UI')
161
+ launchManager()
162
+ } else {
163
+ log('Unknown command:', command)
164
+ console.error('Usage: pets [command] [options]')
165
+ console.error('')
166
+ console.error('Commands:')
167
+ console.error(' (none) Launch the plugin manager UI (default)')
168
+ console.error(' build <pet-name> Build and validate a pet package')
169
+ console.error(' deploy <pet-name> Build and deploy a pet package with metadata')
170
+ console.error('')
171
+ console.error('Examples:')
172
+ console.error(' pets # Launch the desktop plugin manager')
173
+ console.error(' pets build postgres')
174
+ console.error(' pets deploy maps')
175
+ console.error('')
176
+ console.error('Environment:')
177
+ console.error(' PETS_DEBUG=true Enable detailed debug logging')
178
+ process.exit(1)
179
+ }
@@ -0,0 +1,82 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
2
+ import { join } from 'path'
3
+ import { homedir } from 'os'
4
+
5
+ export interface PetsConfig {
6
+ folderHistory: Array<{
7
+ path: string
8
+ timestamp: number
9
+ name?: string
10
+ }>
11
+ }
12
+
13
+ const CONFIG_DIR = join(homedir(), '.pets')
14
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
15
+ const MAX_HISTORY_ITEMS = 20
16
+
17
+ function ensureConfigDir(): void {
18
+ if (!existsSync(CONFIG_DIR)) {
19
+ mkdirSync(CONFIG_DIR, { recursive: true })
20
+ }
21
+ }
22
+
23
+ export function readConfig(): PetsConfig {
24
+ ensureConfigDir()
25
+
26
+ if (!existsSync(CONFIG_FILE)) {
27
+ return { folderHistory: [] }
28
+ }
29
+
30
+ try {
31
+ const content = readFileSync(CONFIG_FILE, 'utf-8')
32
+ return JSON.parse(content)
33
+ } catch (error) {
34
+ console.error('Error reading config file:', error)
35
+ return { folderHistory: [] }
36
+ }
37
+ }
38
+
39
+ export function writeConfig(config: PetsConfig): void {
40
+ ensureConfigDir()
41
+
42
+ try {
43
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8')
44
+ } catch (error) {
45
+ console.error('Error writing config file:', error)
46
+ }
47
+ }
48
+
49
+ export function addFolderToHistory(folderPath: string): void {
50
+ const config = readConfig()
51
+
52
+ const existingIndex = config.folderHistory.findIndex(
53
+ entry => entry.path === folderPath
54
+ )
55
+
56
+ if (existingIndex !== -1) {
57
+ config.folderHistory.splice(existingIndex, 1)
58
+ }
59
+
60
+ const folderName = folderPath.split('/').pop() || folderPath
61
+
62
+ config.folderHistory.unshift({
63
+ path: folderPath,
64
+ timestamp: Date.now(),
65
+ name: folderName
66
+ })
67
+
68
+ if (config.folderHistory.length > MAX_HISTORY_ITEMS) {
69
+ config.folderHistory = config.folderHistory.slice(0, MAX_HISTORY_ITEMS)
70
+ }
71
+
72
+ writeConfig(config)
73
+ }
74
+
75
+ export function getFolderHistory(): PetsConfig['folderHistory'] {
76
+ const config = readConfig()
77
+ return config.folderHistory
78
+ }
79
+
80
+ export function clearFolderHistory(): void {
81
+ writeConfig({ folderHistory: [] })
82
+ }