spindb 0.5.4 → 0.6.0
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 +46 -4
- package/cli/commands/backup.ts +269 -0
- package/cli/commands/config.ts +200 -67
- package/cli/commands/connect.ts +29 -9
- package/cli/commands/engines.ts +1 -9
- package/cli/commands/list.ts +41 -4
- package/cli/commands/menu.ts +289 -19
- package/cli/commands/restore.ts +3 -0
- package/cli/commands/self-update.ts +109 -0
- package/cli/commands/version.ts +55 -0
- package/cli/index.ts +84 -1
- package/cli/ui/prompts.ts +89 -1
- package/cli/ui/theme.ts +11 -0
- package/core/config-manager.ts +123 -37
- package/core/container-manager.ts +78 -2
- package/core/dependency-manager.ts +5 -0
- package/core/update-manager.ts +194 -0
- package/engines/base-engine.ts +20 -0
- package/engines/mysql/backup.ts +159 -0
- package/engines/mysql/index.ts +39 -0
- package/engines/mysql/restore.ts +16 -2
- package/engines/postgresql/backup.ts +93 -0
- package/engines/postgresql/index.ts +37 -0
- package/package.json +1 -1
- package/types/index.ts +26 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL Backup
|
|
3
|
+
*
|
|
4
|
+
* Creates database backups in SQL or compressed (.dump = gzipped SQL) format using mysqldump.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process'
|
|
8
|
+
import { createWriteStream } from 'fs'
|
|
9
|
+
import { stat } from 'fs/promises'
|
|
10
|
+
import { createGzip } from 'zlib'
|
|
11
|
+
import { pipeline } from 'stream/promises'
|
|
12
|
+
import { getMysqldumpPath } from './binary-detection'
|
|
13
|
+
import { getEngineDefaults } from '../../config/defaults'
|
|
14
|
+
import type { ContainerConfig, BackupOptions, BackupResult } from '../../types'
|
|
15
|
+
|
|
16
|
+
const engineDef = getEngineDefaults('mysql')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a backup of a MySQL database
|
|
20
|
+
*
|
|
21
|
+
* CLI equivalent:
|
|
22
|
+
* - SQL format: mysqldump -h 127.0.0.1 -P {port} -u root --result-file={outputPath} {database}
|
|
23
|
+
* - Dump format: mysqldump -h 127.0.0.1 -P {port} -u root {database} | gzip > {outputPath}
|
|
24
|
+
*/
|
|
25
|
+
export async function createBackup(
|
|
26
|
+
container: ContainerConfig,
|
|
27
|
+
outputPath: string,
|
|
28
|
+
options: BackupOptions,
|
|
29
|
+
): Promise<BackupResult> {
|
|
30
|
+
const { port } = container
|
|
31
|
+
const { database, format } = options
|
|
32
|
+
|
|
33
|
+
const mysqldump = await getMysqldumpPath()
|
|
34
|
+
if (!mysqldump) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'mysqldump not found. Install MySQL client tools:\n' +
|
|
37
|
+
' macOS: brew install mysql-client\n' +
|
|
38
|
+
' Ubuntu/Debian: sudo apt install mysql-client',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (format === 'sql') {
|
|
43
|
+
return createSqlBackup(mysqldump, port, database, outputPath)
|
|
44
|
+
} else {
|
|
45
|
+
return createCompressedBackup(mysqldump, port, database, outputPath)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a plain SQL backup
|
|
51
|
+
*/
|
|
52
|
+
async function createSqlBackup(
|
|
53
|
+
mysqldump: string,
|
|
54
|
+
port: number,
|
|
55
|
+
database: string,
|
|
56
|
+
outputPath: string,
|
|
57
|
+
): Promise<BackupResult> {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const args = [
|
|
60
|
+
'-h',
|
|
61
|
+
'127.0.0.1',
|
|
62
|
+
'-P',
|
|
63
|
+
String(port),
|
|
64
|
+
'-u',
|
|
65
|
+
engineDef.superuser,
|
|
66
|
+
'--result-file',
|
|
67
|
+
outputPath,
|
|
68
|
+
database,
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
const proc = spawn(mysqldump, args, {
|
|
72
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
let stderr = ''
|
|
76
|
+
|
|
77
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
78
|
+
stderr += data.toString()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
82
|
+
reject(err)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
proc.on('close', async (code) => {
|
|
86
|
+
if (code === 0) {
|
|
87
|
+
const stats = await stat(outputPath)
|
|
88
|
+
resolve({
|
|
89
|
+
path: outputPath,
|
|
90
|
+
format: 'sql',
|
|
91
|
+
size: stats.size,
|
|
92
|
+
})
|
|
93
|
+
} else {
|
|
94
|
+
const errorMessage = stderr || `mysqldump exited with code ${code}`
|
|
95
|
+
reject(new Error(errorMessage))
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a compressed (gzipped) backup
|
|
103
|
+
* Uses Node's zlib for compression instead of relying on system gzip
|
|
104
|
+
*/
|
|
105
|
+
async function createCompressedBackup(
|
|
106
|
+
mysqldump: string,
|
|
107
|
+
port: number,
|
|
108
|
+
database: string,
|
|
109
|
+
outputPath: string,
|
|
110
|
+
): Promise<BackupResult> {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const args = [
|
|
113
|
+
'-h',
|
|
114
|
+
'127.0.0.1',
|
|
115
|
+
'-P',
|
|
116
|
+
String(port),
|
|
117
|
+
'-u',
|
|
118
|
+
engineDef.superuser,
|
|
119
|
+
database,
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
const proc = spawn(mysqldump, args, {
|
|
123
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const gzip = createGzip()
|
|
127
|
+
const output = createWriteStream(outputPath)
|
|
128
|
+
|
|
129
|
+
let stderr = ''
|
|
130
|
+
|
|
131
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
132
|
+
stderr += data.toString()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Pipe mysqldump stdout -> gzip -> file
|
|
136
|
+
pipeline(proc.stdout!, gzip, output)
|
|
137
|
+
.then(async () => {
|
|
138
|
+
const stats = await stat(outputPath)
|
|
139
|
+
resolve({
|
|
140
|
+
path: outputPath,
|
|
141
|
+
format: 'compressed',
|
|
142
|
+
size: stats.size,
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
.catch(reject)
|
|
146
|
+
|
|
147
|
+
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
148
|
+
reject(err)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
proc.on('close', (code) => {
|
|
152
|
+
if (code !== 0) {
|
|
153
|
+
const errorMessage = stderr || `mysqldump exited with code ${code}`
|
|
154
|
+
reject(new Error(errorMessage))
|
|
155
|
+
}
|
|
156
|
+
// If code is 0, the pipeline promise will resolve
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
}
|
package/engines/mysql/index.ts
CHANGED
|
@@ -33,10 +33,13 @@ import {
|
|
|
33
33
|
restoreBackup,
|
|
34
34
|
parseConnectionString,
|
|
35
35
|
} from './restore'
|
|
36
|
+
import { createBackup } from './backup'
|
|
36
37
|
import type {
|
|
37
38
|
ContainerConfig,
|
|
38
39
|
ProgressCallback,
|
|
39
40
|
BackupFormat,
|
|
41
|
+
BackupOptions,
|
|
42
|
+
BackupResult,
|
|
40
43
|
RestoreResult,
|
|
41
44
|
DumpResult,
|
|
42
45
|
StatusResult,
|
|
@@ -734,6 +737,31 @@ export class MySQLEngine extends BaseEngine {
|
|
|
734
737
|
}
|
|
735
738
|
}
|
|
736
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Get the size of the container's database in bytes
|
|
742
|
+
* Uses information_schema.tables to sum data_length + index_length
|
|
743
|
+
* Returns null if container is not running or query fails
|
|
744
|
+
*/
|
|
745
|
+
async getDatabaseSize(container: ContainerConfig): Promise<number | null> {
|
|
746
|
+
const { port, database } = container
|
|
747
|
+
const db = database || 'mysql'
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
const mysql = await getMysqlClientPath()
|
|
751
|
+
if (!mysql) return null
|
|
752
|
+
|
|
753
|
+
// Query information_schema for total data + index size
|
|
754
|
+
const { stdout } = await execAsync(
|
|
755
|
+
`"${mysql}" -h 127.0.0.1 -P ${port} -u ${engineDef.superuser} -N -e "SELECT COALESCE(SUM(data_length + index_length), 0) FROM information_schema.tables WHERE table_schema = '${db}'"`,
|
|
756
|
+
)
|
|
757
|
+
const size = parseInt(stdout.trim(), 10)
|
|
758
|
+
return isNaN(size) ? null : size
|
|
759
|
+
} catch {
|
|
760
|
+
// Container not running or query failed
|
|
761
|
+
return null
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
737
765
|
/**
|
|
738
766
|
* Create a dump from a remote database using a connection string
|
|
739
767
|
* CLI wrapper: mysqldump -h {host} -P {port} -u {user} -p{pass} {db} > {file}
|
|
@@ -803,6 +831,17 @@ export class MySQLEngine extends BaseEngine {
|
|
|
803
831
|
})
|
|
804
832
|
})
|
|
805
833
|
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Create a backup of a MySQL database
|
|
837
|
+
*/
|
|
838
|
+
async backup(
|
|
839
|
+
container: ContainerConfig,
|
|
840
|
+
outputPath: string,
|
|
841
|
+
options: BackupOptions,
|
|
842
|
+
): Promise<BackupResult> {
|
|
843
|
+
return createBackup(container, outputPath, options)
|
|
844
|
+
}
|
|
806
845
|
}
|
|
807
846
|
|
|
808
847
|
export const mysqlEngine = new MySQLEngine()
|
package/engines/mysql/restore.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { spawn } from 'child_process'
|
|
8
8
|
import { createReadStream } from 'fs'
|
|
9
9
|
import { open } from 'fs/promises'
|
|
10
|
+
import { createGunzip } from 'zlib'
|
|
10
11
|
import { getMysqlClientPath } from './binary-detection'
|
|
11
12
|
import { validateRestoreCompatibility } from './version-validator'
|
|
12
13
|
import { getEngineDefaults } from '../../config/defaults'
|
|
@@ -200,6 +201,7 @@ export async function restoreBackup(
|
|
|
200
201
|
|
|
201
202
|
// Restore using mysql client
|
|
202
203
|
// CLI: mysql -h 127.0.0.1 -P {port} -u root {db} < {file}
|
|
204
|
+
// For compressed files: gunzip -c {file} | mysql ...
|
|
203
205
|
return new Promise((resolve, reject) => {
|
|
204
206
|
const args = ['-h', '127.0.0.1', '-P', String(port), '-u', user, database]
|
|
205
207
|
|
|
@@ -207,9 +209,21 @@ export async function restoreBackup(
|
|
|
207
209
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
208
210
|
})
|
|
209
211
|
|
|
210
|
-
// Pipe backup file to stdin
|
|
212
|
+
// Pipe backup file to stdin, decompressing if necessary
|
|
211
213
|
const fileStream = createReadStream(backupPath)
|
|
212
|
-
|
|
214
|
+
|
|
215
|
+
if (format.format === 'compressed') {
|
|
216
|
+
// Decompress gzipped file before piping to mysql
|
|
217
|
+
const gunzip = createGunzip()
|
|
218
|
+
fileStream.pipe(gunzip).pipe(proc.stdin)
|
|
219
|
+
|
|
220
|
+
// Handle gunzip errors
|
|
221
|
+
gunzip.on('error', (err) => {
|
|
222
|
+
reject(new Error(`Failed to decompress backup file: ${err.message}`))
|
|
223
|
+
})
|
|
224
|
+
} else {
|
|
225
|
+
fileStream.pipe(proc.stdin)
|
|
226
|
+
}
|
|
213
227
|
|
|
214
228
|
let stdout = ''
|
|
215
229
|
let stderr = ''
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Backup
|
|
3
|
+
*
|
|
4
|
+
* Creates database backups in SQL or custom (.dump) format using pg_dump.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process'
|
|
8
|
+
import { stat } from 'fs/promises'
|
|
9
|
+
import { configManager } from '../../core/config-manager'
|
|
10
|
+
import { defaults } from '../../config/defaults'
|
|
11
|
+
import type { ContainerConfig, BackupOptions, BackupResult } from '../../types'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get pg_dump path from config, with helpful error message
|
|
15
|
+
*/
|
|
16
|
+
async function getPgDumpPath(): Promise<string> {
|
|
17
|
+
const pgDumpPath = await configManager.getBinaryPath('pg_dump')
|
|
18
|
+
if (!pgDumpPath) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'pg_dump not found. Install PostgreSQL client tools:\n' +
|
|
21
|
+
' macOS: brew install libpq && brew link --force libpq\n' +
|
|
22
|
+
' Ubuntu/Debian: apt install postgresql-client\n\n' +
|
|
23
|
+
'Or configure manually: spindb config set pg_dump /path/to/pg_dump',
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
return pgDumpPath
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a backup of a PostgreSQL database
|
|
31
|
+
*
|
|
32
|
+
* CLI equivalent:
|
|
33
|
+
* - SQL format: pg_dump -Fp -h 127.0.0.1 -p {port} -U postgres -d {database} -f {outputPath}
|
|
34
|
+
* - Dump format: pg_dump -Fc -h 127.0.0.1 -p {port} -U postgres -d {database} -f {outputPath}
|
|
35
|
+
*/
|
|
36
|
+
export async function createBackup(
|
|
37
|
+
container: ContainerConfig,
|
|
38
|
+
outputPath: string,
|
|
39
|
+
options: BackupOptions,
|
|
40
|
+
): Promise<BackupResult> {
|
|
41
|
+
const { port } = container
|
|
42
|
+
const { database, format } = options
|
|
43
|
+
|
|
44
|
+
const pgDumpPath = await getPgDumpPath()
|
|
45
|
+
|
|
46
|
+
// -Fp = plain SQL format, -Fc = custom format
|
|
47
|
+
const formatFlag = format === 'sql' ? '-Fp' : '-Fc'
|
|
48
|
+
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const args = [
|
|
51
|
+
'-h',
|
|
52
|
+
'127.0.0.1',
|
|
53
|
+
'-p',
|
|
54
|
+
String(port),
|
|
55
|
+
'-U',
|
|
56
|
+
defaults.superuser,
|
|
57
|
+
'-d',
|
|
58
|
+
database,
|
|
59
|
+
formatFlag,
|
|
60
|
+
'-f',
|
|
61
|
+
outputPath,
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
const proc = spawn(pgDumpPath, args, {
|
|
65
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
let stderr = ''
|
|
69
|
+
|
|
70
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
71
|
+
stderr += data.toString()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
proc.on('error', (err: NodeJS.ErrnoException) => {
|
|
75
|
+
reject(err)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
proc.on('close', async (code) => {
|
|
79
|
+
if (code === 0) {
|
|
80
|
+
// Get file size
|
|
81
|
+
const stats = await stat(outputPath)
|
|
82
|
+
resolve({
|
|
83
|
+
path: outputPath,
|
|
84
|
+
format: format === 'sql' ? 'sql' : 'custom',
|
|
85
|
+
size: stats.size,
|
|
86
|
+
})
|
|
87
|
+
} else {
|
|
88
|
+
const errorMessage = stderr || `pg_dump exited with code ${code}`
|
|
89
|
+
reject(new Error(errorMessage))
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}
|
|
@@ -16,10 +16,13 @@ import {
|
|
|
16
16
|
FALLBACK_VERSION_MAP,
|
|
17
17
|
} from './binary-urls'
|
|
18
18
|
import { detectBackupFormat, restoreBackup } from './restore'
|
|
19
|
+
import { createBackup } from './backup'
|
|
19
20
|
import type {
|
|
20
21
|
ContainerConfig,
|
|
21
22
|
ProgressCallback,
|
|
22
23
|
BackupFormat,
|
|
24
|
+
BackupOptions,
|
|
25
|
+
BackupResult,
|
|
23
26
|
RestoreResult,
|
|
24
27
|
DumpResult,
|
|
25
28
|
StatusResult,
|
|
@@ -369,6 +372,29 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
369
372
|
}
|
|
370
373
|
}
|
|
371
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Get the size of the container's database in bytes
|
|
377
|
+
* Uses pg_database_size() to get accurate data size
|
|
378
|
+
* Returns null if container is not running or query fails
|
|
379
|
+
*/
|
|
380
|
+
async getDatabaseSize(container: ContainerConfig): Promise<number | null> {
|
|
381
|
+
const { port, database } = container
|
|
382
|
+
const db = database || 'postgres'
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const psqlPath = await this.getPsqlPath()
|
|
386
|
+
// Query pg_database_size for the specific database
|
|
387
|
+
const { stdout } = await execAsync(
|
|
388
|
+
`"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -t -A -c "SELECT pg_database_size('${db}')"`,
|
|
389
|
+
)
|
|
390
|
+
const size = parseInt(stdout.trim(), 10)
|
|
391
|
+
return isNaN(size) ? null : size
|
|
392
|
+
} catch {
|
|
393
|
+
// Container not running or query failed
|
|
394
|
+
return null
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
372
398
|
/**
|
|
373
399
|
* Create a dump from a remote database using a connection string
|
|
374
400
|
* @param connectionString PostgreSQL connection string (e.g., postgresql://user:pass@host:port/dbname)
|
|
@@ -420,6 +446,17 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
420
446
|
})
|
|
421
447
|
})
|
|
422
448
|
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Create a backup of a PostgreSQL database
|
|
452
|
+
*/
|
|
453
|
+
async backup(
|
|
454
|
+
container: ContainerConfig,
|
|
455
|
+
outputPath: string,
|
|
456
|
+
options: BackupOptions,
|
|
457
|
+
): Promise<BackupResult> {
|
|
458
|
+
return createBackup(container, outputPath, options)
|
|
459
|
+
}
|
|
423
460
|
}
|
|
424
461
|
|
|
425
462
|
export const postgresqlEngine = new PostgreSQLEngine()
|
package/package.json
CHANGED
package/types/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type ContainerConfig = {
|
|
|
4
4
|
version: string
|
|
5
5
|
port: number
|
|
6
6
|
database: string
|
|
7
|
+
databases?: string[]
|
|
7
8
|
created: string
|
|
8
9
|
status: 'created' | 'running' | 'stopped'
|
|
9
10
|
clonedFrom?: string
|
|
@@ -56,6 +57,17 @@ export type RestoreResult = {
|
|
|
56
57
|
code?: number
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
export type BackupOptions = {
|
|
61
|
+
database: string
|
|
62
|
+
format: 'sql' | 'dump'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type BackupResult = {
|
|
66
|
+
path: string
|
|
67
|
+
format: string
|
|
68
|
+
size: number
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
export type DumpResult = {
|
|
60
72
|
filePath: string
|
|
61
73
|
stdout?: string
|
|
@@ -85,6 +97,10 @@ export type BinaryTool =
|
|
|
85
97
|
| 'mysqlpump'
|
|
86
98
|
| 'mysqld'
|
|
87
99
|
| 'mysqladmin'
|
|
100
|
+
// Enhanced shells (optional)
|
|
101
|
+
| 'pgcli'
|
|
102
|
+
| 'mycli'
|
|
103
|
+
| 'usql'
|
|
88
104
|
|
|
89
105
|
/**
|
|
90
106
|
* Source of a binary - bundled (downloaded by spindb) or system (found on PATH)
|
|
@@ -118,6 +134,10 @@ export type SpinDBConfig = {
|
|
|
118
134
|
mysqlpump?: BinaryConfig
|
|
119
135
|
mysqld?: BinaryConfig
|
|
120
136
|
mysqladmin?: BinaryConfig
|
|
137
|
+
// Enhanced shells (optional)
|
|
138
|
+
pgcli?: BinaryConfig
|
|
139
|
+
mycli?: BinaryConfig
|
|
140
|
+
usql?: BinaryConfig
|
|
121
141
|
}
|
|
122
142
|
// Default settings
|
|
123
143
|
defaults?: {
|
|
@@ -127,4 +147,10 @@ export type SpinDBConfig = {
|
|
|
127
147
|
}
|
|
128
148
|
// Last updated timestamp
|
|
129
149
|
updatedAt?: string
|
|
150
|
+
// Self-update tracking
|
|
151
|
+
update?: {
|
|
152
|
+
lastCheck?: string // ISO timestamp of last npm registry check
|
|
153
|
+
latestVersion?: string // Latest version found from registry
|
|
154
|
+
autoCheckEnabled?: boolean // Default true, user can disable
|
|
155
|
+
}
|
|
130
156
|
}
|