spindb 0.31.4 → 0.33.1
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 +8 -0
- package/README.md +107 -826
- package/cli/commands/create.ts +5 -1
- package/cli/commands/engines.ts +256 -1
- package/cli/commands/menu/backup-handlers.ts +16 -0
- package/cli/commands/menu/container-handlers.ts +170 -17
- package/cli/commands/menu/engine-handlers.ts +6 -0
- package/cli/commands/menu/settings-handlers.ts +6 -0
- package/cli/commands/menu/shell-handlers.ts +74 -14
- package/cli/commands/menu/sql-handlers.ts +8 -50
- package/cli/commands/menu/validators.ts +8 -0
- package/cli/commands/users.ts +264 -0
- package/cli/constants.ts +8 -0
- package/cli/helpers.ts +140 -0
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +24 -20
- package/config/backup-formats.ts +28 -0
- package/config/engine-defaults.ts +26 -0
- package/config/engines-registry.ts +1 -0
- package/config/engines.json +50 -0
- package/config/engines.schema.json +6 -1
- package/core/base-binary-manager.ts +6 -1
- package/core/config-manager.ts +20 -0
- package/core/credential-manager.ts +257 -0
- package/core/dependency-manager.ts +5 -0
- package/core/docker-exporter.ts +30 -0
- package/core/error-handler.ts +19 -0
- package/engines/base-engine.ts +32 -1
- package/engines/clickhouse/index.ts +99 -3
- package/engines/cockroachdb/index.ts +69 -2
- package/engines/couchdb/index.ts +149 -1
- package/engines/ferretdb/README.md +4 -0
- package/engines/ferretdb/index.ts +342 -13
- package/engines/index.ts +8 -0
- package/engines/influxdb/README.md +180 -0
- package/engines/influxdb/api-client.ts +64 -0
- package/engines/influxdb/backup.ts +160 -0
- package/engines/influxdb/binary-manager.ts +110 -0
- package/engines/influxdb/binary-urls.ts +69 -0
- package/engines/influxdb/hostdb-releases.ts +23 -0
- package/engines/influxdb/index.ts +1227 -0
- package/engines/influxdb/restore.ts +417 -0
- package/engines/influxdb/version-maps.ts +75 -0
- package/engines/influxdb/version-validator.ts +128 -0
- package/engines/mariadb/index.ts +96 -1
- package/engines/meilisearch/index.ts +97 -1
- package/engines/mongodb/index.ts +82 -0
- package/engines/mysql/index.ts +105 -1
- package/engines/postgresql/index.ts +92 -0
- package/engines/qdrant/index.ts +107 -2
- package/engines/redis/index.ts +106 -12
- package/engines/surrealdb/index.ts +102 -2
- package/engines/typedb/backup.ts +167 -0
- package/engines/typedb/binary-manager.ts +200 -0
- package/engines/typedb/binary-urls.ts +38 -0
- package/engines/typedb/cli-utils.ts +210 -0
- package/engines/typedb/hostdb-releases.ts +118 -0
- package/engines/typedb/index.ts +1275 -0
- package/engines/typedb/restore.ts +377 -0
- package/engines/typedb/version-maps.ts +48 -0
- package/engines/typedb/version-validator.ts +127 -0
- package/engines/valkey/index.ts +70 -2
- package/package.json +4 -1
- package/types/index.ts +37 -0
|
@@ -32,6 +32,8 @@ import { createSpinner } from '../../ui/spinner'
|
|
|
32
32
|
import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme'
|
|
33
33
|
import { pressEnterToContinue } from './shared'
|
|
34
34
|
import { paths } from '../../../config/paths'
|
|
35
|
+
import { getEngineConfig } from '../../../config/engines-registry'
|
|
36
|
+
import { getConsoleBaseArgs } from '../../../engines/typedb/cli-utils'
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* Open a URL in the system's default browser
|
|
@@ -216,6 +218,13 @@ export async function handleOpenShell(
|
|
|
216
218
|
engineSpecificInstalled = false
|
|
217
219
|
engineSpecificValue = null
|
|
218
220
|
engineSpecificInstallValue = null
|
|
221
|
+
} else if (config.engine === 'influxdb') {
|
|
222
|
+
// InfluxDB uses REST API, no interactive shell
|
|
223
|
+
defaultShellName = 'Web Dashboard'
|
|
224
|
+
engineSpecificCli = null
|
|
225
|
+
engineSpecificInstalled = false
|
|
226
|
+
engineSpecificValue = null
|
|
227
|
+
engineSpecificInstallValue = null
|
|
219
228
|
} else if (config.engine === 'couchdb') {
|
|
220
229
|
// CouchDB uses REST API, open Fauxton dashboard in browser
|
|
221
230
|
defaultShellName = 'Fauxton Dashboard'
|
|
@@ -246,6 +255,13 @@ export async function handleOpenShell(
|
|
|
246
255
|
engineSpecificInstalled = false
|
|
247
256
|
engineSpecificValue = null
|
|
248
257
|
engineSpecificInstallValue = null
|
|
258
|
+
} else if (config.engine === 'typedb') {
|
|
259
|
+
// TypeDB uses typedb console
|
|
260
|
+
defaultShellName = 'typedb console'
|
|
261
|
+
engineSpecificCli = null
|
|
262
|
+
engineSpecificInstalled = false
|
|
263
|
+
engineSpecificValue = null
|
|
264
|
+
engineSpecificInstallValue = null
|
|
249
265
|
} else {
|
|
250
266
|
defaultShellName = 'psql'
|
|
251
267
|
engineSpecificCli = 'pgcli'
|
|
@@ -299,6 +315,12 @@ export async function handleOpenShell(
|
|
|
299
315
|
name: `ℹ Show API info`,
|
|
300
316
|
value: 'api-info',
|
|
301
317
|
})
|
|
318
|
+
} else if (config.engine === 'influxdb') {
|
|
319
|
+
// InfluxDB: REST API only, no web dashboard or interactive shell
|
|
320
|
+
choices.push({
|
|
321
|
+
name: `ℹ Show API info`,
|
|
322
|
+
value: 'api-info',
|
|
323
|
+
})
|
|
302
324
|
} else if (config.engine === 'couchdb') {
|
|
303
325
|
// CouchDB: Fauxton dashboard is built-in at /_utils
|
|
304
326
|
choices.push({
|
|
@@ -311,7 +333,7 @@ export async function handleOpenShell(
|
|
|
311
333
|
value: 'api-info',
|
|
312
334
|
})
|
|
313
335
|
} else {
|
|
314
|
-
// Non-
|
|
336
|
+
// Non-REST-API engines: show default shell option
|
|
315
337
|
choices.push({
|
|
316
338
|
name: `>_ Use default shell (${defaultShellName})`,
|
|
317
339
|
value: 'default',
|
|
@@ -342,17 +364,9 @@ export async function handleOpenShell(
|
|
|
342
364
|
}
|
|
343
365
|
}
|
|
344
366
|
|
|
345
|
-
// usql supports SQL databases
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
config.engine === 'valkey' ||
|
|
349
|
-
config.engine === 'mongodb' ||
|
|
350
|
-
config.engine === 'ferretdb' ||
|
|
351
|
-
config.engine === 'qdrant' ||
|
|
352
|
-
config.engine === 'meilisearch' ||
|
|
353
|
-
config.engine === 'couchdb' ||
|
|
354
|
-
config.engine === 'surrealdb'
|
|
355
|
-
if (!isNonSqlEngine) {
|
|
367
|
+
// usql supports SQL databases - skip for non-SQL engines
|
|
368
|
+
const engineConfig = await getEngineConfig(config.engine)
|
|
369
|
+
if (engineConfig.queryLanguage === 'sql') {
|
|
356
370
|
if (usqlInstalled) {
|
|
357
371
|
choices.push({
|
|
358
372
|
name: '⚡ Use usql (universal SQL client)',
|
|
@@ -423,6 +437,17 @@ export async function handleOpenShell(
|
|
|
423
437
|
console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/indexes`))
|
|
424
438
|
console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/health`))
|
|
425
439
|
console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/stats`))
|
|
440
|
+
} else if (config.engine === 'influxdb') {
|
|
441
|
+
console.log(chalk.cyan('InfluxDB REST API:'))
|
|
442
|
+
console.log(chalk.white(` HTTP: http://127.0.0.1:${config.port}`))
|
|
443
|
+
console.log()
|
|
444
|
+
console.log(chalk.gray('Example curl commands:'))
|
|
445
|
+
console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/health`))
|
|
446
|
+
console.log(
|
|
447
|
+
chalk.gray(
|
|
448
|
+
` curl -H "Content-Type: application/json" http://127.0.0.1:${config.port}/api/v3/query_sql -d '{"db":"mydb","q":"SELECT 1"}'`,
|
|
449
|
+
),
|
|
450
|
+
)
|
|
426
451
|
} else if (config.engine === 'couchdb') {
|
|
427
452
|
console.log(chalk.cyan('CouchDB REST API:'))
|
|
428
453
|
console.log(chalk.white(` HTTP: http://127.0.0.1:${config.port}`))
|
|
@@ -918,6 +943,24 @@ async function launchShell(
|
|
|
918
943
|
openInBrowser(dashboardUrl)
|
|
919
944
|
await pressEnterToContinue()
|
|
920
945
|
return
|
|
946
|
+
} else if (config.engine === 'influxdb') {
|
|
947
|
+
// InfluxDB: REST API only, no web dashboard
|
|
948
|
+
// This branch shouldn't be reached since we removed the 'default' choice,
|
|
949
|
+
// but handle gracefully just in case
|
|
950
|
+
console.log()
|
|
951
|
+
console.log(chalk.cyan('InfluxDB REST API:'))
|
|
952
|
+
console.log(chalk.white(` HTTP: http://127.0.0.1:${config.port}`))
|
|
953
|
+
console.log()
|
|
954
|
+
console.log(chalk.gray('Example curl commands:'))
|
|
955
|
+
console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/health`))
|
|
956
|
+
console.log(
|
|
957
|
+
chalk.gray(
|
|
958
|
+
` curl -H "Content-Type: application/json" http://127.0.0.1:${config.port}/api/v3/query_sql -d '{"db":"mydb","q":"SELECT 1"}'`,
|
|
959
|
+
),
|
|
960
|
+
)
|
|
961
|
+
console.log()
|
|
962
|
+
await pressEnterToContinue()
|
|
963
|
+
return
|
|
921
964
|
} else if (config.engine === 'couchdb') {
|
|
922
965
|
// CouchDB: Open Fauxton dashboard in browser (served at /_utils)
|
|
923
966
|
const dashboardUrl = `http://127.0.0.1:${config.port}/_utils`
|
|
@@ -990,10 +1033,27 @@ async function launchShell(
|
|
|
990
1033
|
const questDbConnStr = `postgresql://admin:quest@127.0.0.1:${config.port}/${db}`
|
|
991
1034
|
shellArgs = [questDbConnStr]
|
|
992
1035
|
installHint = 'brew install libpq && brew link --force libpq'
|
|
1036
|
+
} else if (config.engine === 'typedb') {
|
|
1037
|
+
// TypeDB uses typedb console with address and tls-disabled flags
|
|
1038
|
+
const engine = getEngine(config.engine)
|
|
1039
|
+
const consolePath = await engine
|
|
1040
|
+
.getTypeDBConsolePath(config.version)
|
|
1041
|
+
.catch(() => null)
|
|
1042
|
+
if (consolePath) {
|
|
1043
|
+
shellCmd = consolePath
|
|
1044
|
+
shellArgs = getConsoleBaseArgs(config.port)
|
|
1045
|
+
} else {
|
|
1046
|
+
// Fallback: use the typedb launcher with 'console' subcommand
|
|
1047
|
+
shellCmd = 'typedb'
|
|
1048
|
+
shellArgs = ['console', ...getConsoleBaseArgs(config.port)]
|
|
1049
|
+
}
|
|
1050
|
+
installHint = 'spindb engines download typedb'
|
|
993
1051
|
} else {
|
|
994
|
-
|
|
1052
|
+
// PostgreSQL default shell - look up downloaded binary path
|
|
1053
|
+
const psqlPath = await configManager.getBinaryPath('psql')
|
|
1054
|
+
shellCmd = psqlPath || 'psql'
|
|
995
1055
|
shellArgs = [connectionString]
|
|
996
|
-
installHint = '
|
|
1056
|
+
installHint = 'spindb engines download postgresql'
|
|
997
1057
|
}
|
|
998
1058
|
|
|
999
1059
|
const shellProcess = spawn(shellCmd, shellArgs, {
|
|
@@ -10,7 +10,7 @@ import { promptInstallDependencies, escapeablePrompt } from '../../ui/prompts'
|
|
|
10
10
|
import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme'
|
|
11
11
|
import { pressEnterToContinue } from './shared'
|
|
12
12
|
import { followFile, getLastNLines } from '../../utils/file-follower'
|
|
13
|
-
import {
|
|
13
|
+
import { getEngineConfig } from '../../../config/engines-registry'
|
|
14
14
|
|
|
15
15
|
export async function handleRunSql(
|
|
16
16
|
containerName: string,
|
|
@@ -56,52 +56,12 @@ export async function handleRunSql(
|
|
|
56
56
|
// Strip quotes that terminals add when drag-and-dropping files
|
|
57
57
|
const stripQuotes = (path: string) => path.replace(/^['"]|['"]$/g, '').trim()
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const getScriptType = (engine: Engine): { type: string; lower: string } => {
|
|
66
|
-
switch (engine) {
|
|
67
|
-
// Redis-like engines use "Command" terminology
|
|
68
|
-
case Engine.Redis:
|
|
69
|
-
case Engine.Valkey:
|
|
70
|
-
return { type: 'Command', lower: 'command' }
|
|
71
|
-
|
|
72
|
-
// Document/search engines use "Script" terminology
|
|
73
|
-
// MongoDB and FerretDB use JavaScript via mongosh
|
|
74
|
-
// Qdrant, Meilisearch, and CouchDB use REST API (JSON)
|
|
75
|
-
case Engine.MongoDB:
|
|
76
|
-
case Engine.FerretDB:
|
|
77
|
-
case Engine.Qdrant:
|
|
78
|
-
case Engine.Meilisearch:
|
|
79
|
-
case Engine.CouchDB:
|
|
80
|
-
return { type: 'Script', lower: 'script' }
|
|
81
|
-
|
|
82
|
-
// SurrealDB uses SurrealQL (distinct from SQL)
|
|
83
|
-
case Engine.SurrealDB:
|
|
84
|
-
return { type: 'SurrealQL', lower: 'SurrealQL' }
|
|
85
|
-
|
|
86
|
-
// SQL engines use "SQL" terminology
|
|
87
|
-
case Engine.PostgreSQL:
|
|
88
|
-
case Engine.MySQL:
|
|
89
|
-
case Engine.MariaDB:
|
|
90
|
-
case Engine.SQLite:
|
|
91
|
-
case Engine.DuckDB:
|
|
92
|
-
case Engine.ClickHouse:
|
|
93
|
-
case Engine.CockroachDB:
|
|
94
|
-
case Engine.QuestDB:
|
|
95
|
-
return { type: 'SQL', lower: 'sql' }
|
|
96
|
-
|
|
97
|
-
default:
|
|
98
|
-
assertExhaustive(engine)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const { type: scriptType, lower: scriptTypeLower } = getScriptType(
|
|
103
|
-
config.engine,
|
|
104
|
-
)
|
|
59
|
+
// Script type terminology derived from engines.json scriptFileLabel
|
|
60
|
+
// e.g., "Run SQL file" → type: "SQL", "Run TypeQL file" → type: "TypeQL"
|
|
61
|
+
const engineConfig = await getEngineConfig(config.engine)
|
|
62
|
+
const scriptType = (engineConfig.scriptFileLabel ?? 'Script file')
|
|
63
|
+
.replace(/^Run\s+/, '')
|
|
64
|
+
.replace(/\s+file$/, '')
|
|
105
65
|
|
|
106
66
|
// Prompt for file path (empty input = go back)
|
|
107
67
|
console.log(
|
|
@@ -135,9 +95,7 @@ export async function handleRunSql(
|
|
|
135
95
|
const databaseName = database || config.database
|
|
136
96
|
|
|
137
97
|
console.log()
|
|
138
|
-
console.log(
|
|
139
|
-
uiInfo(`Running ${scriptTypeLower} file against "${databaseName}"...`),
|
|
140
|
-
)
|
|
98
|
+
console.log(uiInfo(`Running ${scriptType} file against "${databaseName}"...`))
|
|
141
99
|
console.log()
|
|
142
100
|
|
|
143
101
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function validateTypedbConnectionString(input: string): string | null {
|
|
2
|
+
const hostPortPattern = /^(?:[\w.-]+|\[[\da-fA-F:]+\]):\d+(?:\/.*)?$/
|
|
3
|
+
const schemeHostPattern = /^(?:typedb|typedb-core|https?):\/\/[^/]+/
|
|
4
|
+
if (!hostPortPattern.test(input) && !schemeHostPattern.test(input)) {
|
|
5
|
+
return 'Connection string must be host:port, [IPv6]:port, typedb://, typedb-core://, or http(s):// with a host'
|
|
6
|
+
}
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { containerManager } from '../../core/container-manager'
|
|
4
|
+
import { processManager } from '../../core/process-manager'
|
|
5
|
+
import { getEngine } from '../../engines'
|
|
6
|
+
import { generatePassword } from '../../core/credential-generator'
|
|
7
|
+
import {
|
|
8
|
+
saveCredentials,
|
|
9
|
+
listCredentials,
|
|
10
|
+
credentialsExist,
|
|
11
|
+
getDefaultUsername,
|
|
12
|
+
} from '../../core/credential-manager'
|
|
13
|
+
import {
|
|
14
|
+
assertValidUsername,
|
|
15
|
+
UnsupportedOperationError,
|
|
16
|
+
} from '../../core/error-handler'
|
|
17
|
+
import { platformService } from '../../core/platform-service'
|
|
18
|
+
import { isFileBasedEngine } from '../../types'
|
|
19
|
+
import { uiError, uiSuccess, uiWarning } from '../ui/theme'
|
|
20
|
+
|
|
21
|
+
function exitWithError(message: string, json?: boolean): never {
|
|
22
|
+
if (json) {
|
|
23
|
+
console.log(JSON.stringify({ error: message }))
|
|
24
|
+
} else {
|
|
25
|
+
console.error(uiError(message))
|
|
26
|
+
}
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const usersCommand = new Command('users').description(
|
|
31
|
+
'Manage database users and credentials',
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
usersCommand
|
|
35
|
+
.command('create')
|
|
36
|
+
.description('Create a database user')
|
|
37
|
+
.argument('<container>', 'Container name')
|
|
38
|
+
.argument('[username]', 'Username to create')
|
|
39
|
+
.option('-p, --password <password>', 'Use specific password')
|
|
40
|
+
.option('-d, --database <database>', 'Target database for grants')
|
|
41
|
+
.option('-c, --copy', 'Copy connection string to clipboard')
|
|
42
|
+
.option('-j, --json', 'Output as JSON')
|
|
43
|
+
.option('--no-save', 'Do not save credentials to disk')
|
|
44
|
+
.option('--force', 'Overwrite existing credential file')
|
|
45
|
+
.action(
|
|
46
|
+
async (
|
|
47
|
+
containerName: string,
|
|
48
|
+
username: string | undefined,
|
|
49
|
+
options: {
|
|
50
|
+
password?: string
|
|
51
|
+
database?: string
|
|
52
|
+
copy?: boolean
|
|
53
|
+
json?: boolean
|
|
54
|
+
save: boolean
|
|
55
|
+
force?: boolean
|
|
56
|
+
},
|
|
57
|
+
) => {
|
|
58
|
+
try {
|
|
59
|
+
const config = await containerManager.getConfig(containerName)
|
|
60
|
+
if (!config) {
|
|
61
|
+
exitWithError(
|
|
62
|
+
`Container "${containerName}" not found. Run "spindb list" to see available containers.`,
|
|
63
|
+
options.json,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const engineName = config.engine
|
|
68
|
+
|
|
69
|
+
// Check container is running (skip for file-based engines)
|
|
70
|
+
if (!isFileBasedEngine(engineName)) {
|
|
71
|
+
const running = await processManager.isRunning(containerName, {
|
|
72
|
+
engine: engineName,
|
|
73
|
+
})
|
|
74
|
+
if (!running) {
|
|
75
|
+
exitWithError(
|
|
76
|
+
`Container "${containerName}" is not running. Start it with: spindb start ${containerName}`,
|
|
77
|
+
options.json,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Use default username if none provided
|
|
83
|
+
const resolvedUsername = username || getDefaultUsername(config.engine)
|
|
84
|
+
|
|
85
|
+
assertValidUsername(resolvedUsername)
|
|
86
|
+
|
|
87
|
+
const engine = getEngine(engineName)
|
|
88
|
+
|
|
89
|
+
// Generate or use provided password
|
|
90
|
+
const password =
|
|
91
|
+
options.password ||
|
|
92
|
+
generatePassword({ length: 20, alphanumericOnly: true })
|
|
93
|
+
|
|
94
|
+
const database = options.database || config.database
|
|
95
|
+
|
|
96
|
+
// Check if credential file already exists
|
|
97
|
+
if (
|
|
98
|
+
options.save &&
|
|
99
|
+
!options.force &&
|
|
100
|
+
credentialsExist(containerName, engineName, resolvedUsername)
|
|
101
|
+
) {
|
|
102
|
+
exitWithError(
|
|
103
|
+
`Credential file already exists for "${resolvedUsername}" in "${containerName}". Use --force to overwrite.`,
|
|
104
|
+
options.json,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create the user in the database
|
|
109
|
+
const credentials = await engine.createUser(config, {
|
|
110
|
+
username: resolvedUsername,
|
|
111
|
+
password,
|
|
112
|
+
database,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Save credentials to disk (non-fatal — credentials are already created)
|
|
116
|
+
let credentialFile: string | undefined
|
|
117
|
+
if (options.save) {
|
|
118
|
+
try {
|
|
119
|
+
credentialFile = await saveCredentials(
|
|
120
|
+
containerName,
|
|
121
|
+
engineName,
|
|
122
|
+
credentials,
|
|
123
|
+
)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (!options.json) {
|
|
126
|
+
console.error(
|
|
127
|
+
uiWarning(
|
|
128
|
+
`Could not save credentials to disk: ${(error as Error).message}`,
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
process.exitCode = 1
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Copy to clipboard before output so JSON includes the status
|
|
137
|
+
let clipboardCopied: boolean | undefined
|
|
138
|
+
if (options.copy) {
|
|
139
|
+
const textToCopy = credentials.apiKey || credentials.connectionString
|
|
140
|
+
if (textToCopy) {
|
|
141
|
+
clipboardCopied = await platformService.copyToClipboard(textToCopy)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Output results
|
|
146
|
+
if (options.json) {
|
|
147
|
+
const result: Record<string, unknown> = {
|
|
148
|
+
username: credentials.username,
|
|
149
|
+
password: credentials.password,
|
|
150
|
+
...(credentials.database != null && {
|
|
151
|
+
database: credentials.database,
|
|
152
|
+
}),
|
|
153
|
+
connectionString: credentials.connectionString,
|
|
154
|
+
...(credentials.apiKey != null && { apiKey: credentials.apiKey }),
|
|
155
|
+
...(credentialFile != null && {
|
|
156
|
+
credentialFile,
|
|
157
|
+
}),
|
|
158
|
+
...(clipboardCopied !== undefined && { clipboardCopied }),
|
|
159
|
+
}
|
|
160
|
+
console.log(JSON.stringify(result, null, 2))
|
|
161
|
+
} else {
|
|
162
|
+
console.log()
|
|
163
|
+
console.log(uiSuccess(`Created user "${resolvedUsername}"`))
|
|
164
|
+
console.log()
|
|
165
|
+
if (credentials.apiKey) {
|
|
166
|
+
console.log(` ${chalk.gray('Key name:')} ${credentials.username}`)
|
|
167
|
+
console.log(` ${chalk.gray('API key:')} ${credentials.apiKey}`)
|
|
168
|
+
console.log(
|
|
169
|
+
` ${chalk.gray('API URL:')} ${credentials.connectionString}`,
|
|
170
|
+
)
|
|
171
|
+
} else {
|
|
172
|
+
console.log(` ${chalk.gray('Username:')} ${credentials.username}`)
|
|
173
|
+
console.log(` ${chalk.gray('Password:')} ${credentials.password}`)
|
|
174
|
+
if (credentials.database) {
|
|
175
|
+
console.log(
|
|
176
|
+
` ${chalk.gray('Database:')} ${credentials.database}`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
console.log(
|
|
180
|
+
` ${chalk.gray('URL:')} ${credentials.connectionString}`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
if (credentialFile) {
|
|
184
|
+
console.log()
|
|
185
|
+
console.log(` ${chalk.gray('Saved to:')} ${credentialFile}`)
|
|
186
|
+
}
|
|
187
|
+
console.log()
|
|
188
|
+
|
|
189
|
+
// Show clipboard status in human-readable output
|
|
190
|
+
if (clipboardCopied !== undefined) {
|
|
191
|
+
if (clipboardCopied) {
|
|
192
|
+
console.log(
|
|
193
|
+
uiSuccess(
|
|
194
|
+
credentials.apiKey
|
|
195
|
+
? 'API key copied to clipboard'
|
|
196
|
+
: 'Connection string copied to clipboard',
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
} else {
|
|
200
|
+
console.log(uiWarning('Could not copy to clipboard'))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof UnsupportedOperationError) {
|
|
206
|
+
exitWithError(
|
|
207
|
+
'User management is not supported for this engine',
|
|
208
|
+
options.json,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
exitWithError((error as Error).message, options.json)
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
usersCommand
|
|
217
|
+
.command('list')
|
|
218
|
+
.description('List saved credentials for a container')
|
|
219
|
+
.argument('<container>', 'Container name')
|
|
220
|
+
.option('-j, --json', 'Output as JSON')
|
|
221
|
+
.action(async (containerName: string, options: { json?: boolean }) => {
|
|
222
|
+
try {
|
|
223
|
+
const config = await containerManager.getConfig(containerName)
|
|
224
|
+
if (!config) {
|
|
225
|
+
exitWithError(`Container "${containerName}" not found`, options.json)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const usernames = await listCredentials(containerName, config.engine)
|
|
229
|
+
|
|
230
|
+
if (options.json) {
|
|
231
|
+
console.log(
|
|
232
|
+
JSON.stringify(
|
|
233
|
+
{
|
|
234
|
+
container: containerName,
|
|
235
|
+
engine: config.engine,
|
|
236
|
+
users: usernames,
|
|
237
|
+
},
|
|
238
|
+
null,
|
|
239
|
+
2,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
} else {
|
|
243
|
+
if (usernames.length === 0) {
|
|
244
|
+
console.log()
|
|
245
|
+
console.log(chalk.gray(`No saved credentials for "${containerName}"`))
|
|
246
|
+
console.log(
|
|
247
|
+
chalk.gray(
|
|
248
|
+
` Create one with: spindb users create ${containerName} <username>`,
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
console.log()
|
|
252
|
+
} else {
|
|
253
|
+
console.log()
|
|
254
|
+
console.log(chalk.bold(`Saved credentials for "${containerName}":`))
|
|
255
|
+
for (const user of usernames) {
|
|
256
|
+
console.log(` ${chalk.cyan(user)}`)
|
|
257
|
+
}
|
|
258
|
+
console.log()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
exitWithError((error as Error).message, options.json)
|
|
263
|
+
}
|
|
264
|
+
})
|
package/cli/constants.ts
CHANGED
|
@@ -75,6 +75,8 @@ export const ENGINE_BRAND_COLORS: Record<Engine, BrandColor> = {
|
|
|
75
75
|
[Engine.CockroachDB]: { foreground: '#FFFFFF', background: '#6933FF' }, // White on purple
|
|
76
76
|
[Engine.SurrealDB]: { foreground: '#FFFFFF', background: '#FF00A0' }, // White on pink
|
|
77
77
|
[Engine.QuestDB]: { foreground: '#000000', background: '#02FC04' }, // Black on green
|
|
78
|
+
[Engine.TypeDB]: { foreground: '#FFFFFF', background: '#7B2D8E' }, // White on purple
|
|
79
|
+
[Engine.InfluxDB]: { foreground: '#FFFFFF', background: '#9394FF' }, // White on indigo/purple
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
// ASCII fallback icons - work in any terminal
|
|
@@ -95,6 +97,8 @@ const ASCII_ICONS: Record<Engine, string> = {
|
|
|
95
97
|
[Engine.CockroachDB]: '[CR]',
|
|
96
98
|
[Engine.SurrealDB]: '[SR]',
|
|
97
99
|
[Engine.QuestDB]: '[QS]',
|
|
100
|
+
[Engine.TypeDB]: '[TB]',
|
|
101
|
+
[Engine.InfluxDB]: '[IX]',
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
// Nerd Font icons - require a patched font
|
|
@@ -116,6 +120,8 @@ const NERD_ICONS: Record<Engine, string> = {
|
|
|
116
120
|
[Engine.CockroachDB]: '\ue269', // nf-fae-cockroach
|
|
117
121
|
[Engine.SurrealDB]: '\uedfe', // nf-fa-infinity (multi-model)
|
|
118
122
|
[Engine.QuestDB]: '\ued2f', // nf-fa-gauge-high (time-series performance)
|
|
123
|
+
[Engine.TypeDB]: '\ue706', // nf-dev-database (knowledge graph)
|
|
124
|
+
[Engine.InfluxDB]: '\udb85\udf95', // nf-md-chart-line (time-series)
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
// Emoji icons - original icons, inconsistent width across terminals
|
|
@@ -136,6 +142,8 @@ const EMOJI_ICONS: Record<Engine, string> = {
|
|
|
136
142
|
[Engine.CockroachDB]: '🪳',
|
|
137
143
|
[Engine.SurrealDB]: '🌀',
|
|
138
144
|
[Engine.QuestDB]: '⏱',
|
|
145
|
+
[Engine.TypeDB]: '🤖',
|
|
146
|
+
[Engine.InfluxDB]: '📈',
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
const DEFAULT_ICONS: Record<IconMode, string> = {
|