kimaki 0.4.43 → 0.4.45
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/dist/channel-management.js +6 -15
- package/dist/cli.js +210 -32
- package/dist/commands/merge-worktree.js +152 -0
- package/dist/commands/permissions.js +21 -5
- package/dist/commands/queue.js +5 -1
- package/dist/commands/resume.js +8 -16
- package/dist/commands/session.js +18 -42
- package/dist/commands/user-command.js +8 -17
- package/dist/commands/verbosity.js +53 -0
- package/dist/commands/worktree-settings.js +88 -0
- package/dist/commands/worktree.js +146 -50
- package/dist/database.js +85 -0
- package/dist/discord-bot.js +97 -55
- package/dist/discord-utils.js +51 -13
- package/dist/discord-utils.test.js +20 -0
- package/dist/escape-backticks.test.js +14 -3
- package/dist/interaction-handler.js +15 -0
- package/dist/session-handler.js +549 -412
- package/dist/system-message.js +25 -1
- package/dist/worktree-utils.js +50 -0
- package/package.json +1 -1
- package/src/__snapshots__/first-session-no-info.md +1344 -0
- package/src/__snapshots__/first-session-with-info.md +1350 -0
- package/src/__snapshots__/session-1.md +1344 -0
- package/src/__snapshots__/session-2.md +291 -0
- package/src/__snapshots__/session-3.md +20324 -0
- package/src/__snapshots__/session-with-tools.md +1344 -0
- package/src/channel-management.ts +6 -17
- package/src/cli.ts +250 -35
- package/src/commands/merge-worktree.ts +186 -0
- package/src/commands/permissions.ts +31 -5
- package/src/commands/queue.ts +5 -1
- package/src/commands/resume.ts +8 -18
- package/src/commands/session.ts +18 -44
- package/src/commands/user-command.ts +8 -19
- package/src/commands/verbosity.ts +71 -0
- package/src/commands/worktree-settings.ts +122 -0
- package/src/commands/worktree.ts +174 -55
- package/src/database.ts +108 -0
- package/src/discord-bot.ts +119 -63
- package/src/discord-utils.test.ts +23 -0
- package/src/discord-utils.ts +52 -13
- package/src/escape-backticks.test.ts +14 -3
- package/src/interaction-handler.ts +22 -0
- package/src/session-handler.ts +681 -436
- package/src/system-message.ts +37 -0
- package/src/worktree-utils.ts +78 -0
package/src/commands/worktree.ts
CHANGED
|
@@ -9,11 +9,14 @@ import {
|
|
|
9
9
|
createPendingWorktree,
|
|
10
10
|
setWorktreeReady,
|
|
11
11
|
setWorktreeError,
|
|
12
|
+
getChannelDirectory,
|
|
13
|
+
getThreadWorktree,
|
|
12
14
|
} from '../database.js'
|
|
13
15
|
import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js'
|
|
14
16
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
|
|
15
|
-
import { extractTagsArrays } from '../xml.js'
|
|
16
17
|
import { createLogger } from '../logger.js'
|
|
18
|
+
import { createWorktreeWithSubmodules } from '../worktree-utils.js'
|
|
19
|
+
import { WORKTREE_PREFIX } from './merge-worktree.js'
|
|
17
20
|
import * as errore from 'errore'
|
|
18
21
|
|
|
19
22
|
const logger = createLogger('WORKTREE')
|
|
@@ -26,51 +29,64 @@ class WorktreeError extends Error {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
|
-
* Format worktree name: lowercase, spaces to dashes, remove special chars, add kimaki- prefix.
|
|
30
|
-
* "My Feature" → "kimaki-my-feature"
|
|
32
|
+
* Format worktree name: lowercase, spaces to dashes, remove special chars, add opencode/kimaki- prefix.
|
|
33
|
+
* "My Feature" → "opencode/kimaki-my-feature"
|
|
34
|
+
* Returns empty string if no valid name can be extracted.
|
|
31
35
|
*/
|
|
32
|
-
function formatWorktreeName(name: string): string {
|
|
36
|
+
export function formatWorktreeName(name: string): string {
|
|
33
37
|
const formatted = name
|
|
34
38
|
.toLowerCase()
|
|
35
39
|
.trim()
|
|
36
40
|
.replace(/\s+/g, '-')
|
|
37
41
|
.replace(/[^a-z0-9-]/g, '')
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
if (!formatted) {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
return `opencode/kimaki-${formatted}`
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
/**
|
|
43
|
-
*
|
|
50
|
+
* Derive worktree name from thread name.
|
|
51
|
+
* Handles existing "⬦ worktree: opencode/kimaki-name" format or uses thread name directly.
|
|
52
|
+
*/
|
|
53
|
+
function deriveWorktreeNameFromThread(threadName: string): string {
|
|
54
|
+
// Handle existing "⬦ worktree: opencode/kimaki-name" format
|
|
55
|
+
const worktreeMatch = threadName.match(/worktree:\s*(.+)$/i)
|
|
56
|
+
const extractedName = worktreeMatch?.[1]?.trim()
|
|
57
|
+
if (extractedName) {
|
|
58
|
+
// If already has opencode/kimaki- prefix, return as is
|
|
59
|
+
if (extractedName.startsWith('opencode/kimaki-')) {
|
|
60
|
+
return extractedName
|
|
61
|
+
}
|
|
62
|
+
return formatWorktreeName(extractedName)
|
|
63
|
+
}
|
|
64
|
+
// Use thread name directly
|
|
65
|
+
return formatWorktreeName(threadName)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get project directory from database.
|
|
44
70
|
*/
|
|
45
71
|
function getProjectDirectoryFromChannel(
|
|
46
72
|
channel: TextChannel,
|
|
47
73
|
appId: string,
|
|
48
74
|
): string | WorktreeError {
|
|
49
|
-
|
|
50
|
-
return new WorktreeError('This channel has no topic configured')
|
|
51
|
-
}
|
|
75
|
+
const channelConfig = getChannelDirectory(channel.id)
|
|
52
76
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
tags: ['kimaki.directory', 'kimaki.app'],
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
const projectDirectory = extracted['kimaki.directory']?.[0]?.trim()
|
|
59
|
-
const channelAppId = extracted['kimaki.app']?.[0]?.trim()
|
|
60
|
-
|
|
61
|
-
if (channelAppId && channelAppId !== appId) {
|
|
62
|
-
return new WorktreeError('This channel is not configured for this bot')
|
|
77
|
+
if (!channelConfig) {
|
|
78
|
+
return new WorktreeError('This channel is not configured with a project directory')
|
|
63
79
|
}
|
|
64
80
|
|
|
65
|
-
if (
|
|
66
|
-
return new WorktreeError('This channel is not configured
|
|
81
|
+
if (channelConfig.appId && channelConfig.appId !== appId) {
|
|
82
|
+
return new WorktreeError('This channel is not configured for this bot')
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
if (!fs.existsSync(
|
|
70
|
-
return new WorktreeError(`Directory does not exist: ${
|
|
85
|
+
if (!fs.existsSync(channelConfig.directory)) {
|
|
86
|
+
return new WorktreeError(`Directory does not exist: ${channelConfig.directory}`)
|
|
71
87
|
}
|
|
72
88
|
|
|
73
|
-
return
|
|
89
|
+
return channelConfig.directory
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
/**
|
|
@@ -89,33 +105,17 @@ async function createWorktreeInBackground({
|
|
|
89
105
|
projectDirectory: string
|
|
90
106
|
clientV2: ReturnType<typeof getOpencodeClientV2> & {}
|
|
91
107
|
}): Promise<void> {
|
|
92
|
-
// Create worktree using SDK v2
|
|
108
|
+
// Create worktree using SDK v2 and init submodules
|
|
93
109
|
logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}`)
|
|
94
|
-
const worktreeResult = await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
worktreeCreateInput: {
|
|
99
|
-
name: worktreeName,
|
|
100
|
-
},
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
if (response.error) {
|
|
104
|
-
throw new Error(`SDK error: ${JSON.stringify(response.error)}`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!response.data) {
|
|
108
|
-
throw new Error('No worktree data returned from SDK')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return response.data
|
|
112
|
-
},
|
|
113
|
-
catch: (e) => new WorktreeError('Failed to create worktree', { cause: e }),
|
|
110
|
+
const worktreeResult = await createWorktreeWithSubmodules({
|
|
111
|
+
clientV2,
|
|
112
|
+
directory: projectDirectory,
|
|
113
|
+
name: worktreeName,
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
if (
|
|
116
|
+
if (worktreeResult instanceof Error) {
|
|
117
117
|
const errorMsg = worktreeResult.message
|
|
118
|
-
logger.error('[NEW-WORKTREE] Error:', worktreeResult
|
|
118
|
+
logger.error('[NEW-WORKTREE] Error:', worktreeResult)
|
|
119
119
|
setWorktreeError({ threadId: thread.id, errorMessage: errorMsg })
|
|
120
120
|
await starterMessage.edit(`🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`)
|
|
121
121
|
return
|
|
@@ -136,18 +136,38 @@ export async function handleNewWorktreeCommand({
|
|
|
136
136
|
}: CommandContext): Promise<void> {
|
|
137
137
|
await command.deferReply({ ephemeral: false })
|
|
138
138
|
|
|
139
|
-
const
|
|
140
|
-
|
|
139
|
+
const channel = command.channel
|
|
140
|
+
if (!channel) {
|
|
141
|
+
await command.editReply('Cannot determine channel')
|
|
142
|
+
return
|
|
143
|
+
}
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
const isThread =
|
|
146
|
+
channel.type === ChannelType.PublicThread || channel.type === ChannelType.PrivateThread
|
|
147
|
+
|
|
148
|
+
// Handle command in existing thread - attach worktree to this thread
|
|
149
|
+
if (isThread) {
|
|
150
|
+
await handleWorktreeInThread({ command, appId, thread: channel as ThreadChannel })
|
|
144
151
|
return
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
// Handle command in text channel - create new thread with worktree (existing behavior)
|
|
155
|
+
if (channel.type !== ChannelType.GuildText) {
|
|
156
|
+
await command.editReply('This command can only be used in text channels or threads')
|
|
157
|
+
return
|
|
158
|
+
}
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
const rawName = command.options.getString('name')
|
|
161
|
+
if (!rawName) {
|
|
162
|
+
await command.editReply(
|
|
163
|
+
'Name is required when creating a worktree from a text channel. Use `/new-worktree name:my-feature`',
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const worktreeName = formatWorktreeName(rawName)
|
|
169
|
+
if (!worktreeName) {
|
|
170
|
+
await command.editReply('Invalid worktree name. Please use letters, numbers, and spaces.')
|
|
151
171
|
return
|
|
152
172
|
}
|
|
153
173
|
|
|
@@ -203,7 +223,7 @@ export async function handleNewWorktreeCommand({
|
|
|
203
223
|
})
|
|
204
224
|
|
|
205
225
|
const thread = await starterMessage.startThread({
|
|
206
|
-
name:
|
|
226
|
+
name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
|
|
207
227
|
autoArchiveDuration: 1440,
|
|
208
228
|
reason: 'Worktree session',
|
|
209
229
|
})
|
|
@@ -241,3 +261,102 @@ export async function handleNewWorktreeCommand({
|
|
|
241
261
|
logger.error('[NEW-WORKTREE] Background error:', e)
|
|
242
262
|
})
|
|
243
263
|
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Handle /new-worktree when called inside an existing thread.
|
|
267
|
+
* Attaches a worktree to the current thread, using thread name if no name provided.
|
|
268
|
+
*/
|
|
269
|
+
async function handleWorktreeInThread({
|
|
270
|
+
command,
|
|
271
|
+
appId,
|
|
272
|
+
thread,
|
|
273
|
+
}: CommandContext & { thread: ThreadChannel }): Promise<void> {
|
|
274
|
+
// Error if thread already has a worktree
|
|
275
|
+
if (getThreadWorktree(thread.id)) {
|
|
276
|
+
await command.editReply('This thread already has a worktree attached.')
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Get worktree name from parameter or derive from thread name
|
|
281
|
+
const rawName = command.options.getString('name')
|
|
282
|
+
const worktreeName = rawName ? formatWorktreeName(rawName) : deriveWorktreeNameFromThread(thread.name)
|
|
283
|
+
|
|
284
|
+
if (!worktreeName) {
|
|
285
|
+
await command.editReply('Invalid worktree name. Please provide a name or rename the thread.')
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Get parent channel for project directory
|
|
290
|
+
const parent = thread.parent
|
|
291
|
+
if (!parent || parent.type !== ChannelType.GuildText) {
|
|
292
|
+
await command.editReply('Cannot determine parent channel')
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const projectDirectory = getProjectDirectoryFromChannel(parent as TextChannel, appId)
|
|
297
|
+
if (errore.isError(projectDirectory)) {
|
|
298
|
+
await command.editReply(projectDirectory.message)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Initialize opencode
|
|
303
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
304
|
+
if (errore.isError(getClient)) {
|
|
305
|
+
await command.editReply(`Failed to initialize OpenCode: ${getClient.message}`)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const clientV2 = getOpencodeClientV2(projectDirectory)
|
|
310
|
+
if (!clientV2) {
|
|
311
|
+
await command.editReply('Failed to get OpenCode client')
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check if worktree with this name already exists
|
|
316
|
+
const listResult = await errore.tryAsync({
|
|
317
|
+
try: async () => {
|
|
318
|
+
const response = await clientV2.worktree.list({ directory: projectDirectory })
|
|
319
|
+
return response.data || []
|
|
320
|
+
},
|
|
321
|
+
catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
if (errore.isError(listResult)) {
|
|
325
|
+
await command.editReply(listResult.message)
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const existingWorktreePath = listResult.find((dir) => dir.endsWith(`/${worktreeName}`))
|
|
330
|
+
if (existingWorktreePath) {
|
|
331
|
+
await command.editReply(
|
|
332
|
+
`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``,
|
|
333
|
+
)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Store pending worktree in database for this existing thread
|
|
338
|
+
createPendingWorktree({
|
|
339
|
+
threadId: thread.id,
|
|
340
|
+
worktreeName,
|
|
341
|
+
projectDirectory,
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// Send status message in thread
|
|
345
|
+
const statusMessage = await thread.send({
|
|
346
|
+
content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`,
|
|
347
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`)
|
|
351
|
+
|
|
352
|
+
// Create worktree in background
|
|
353
|
+
createWorktreeInBackground({
|
|
354
|
+
thread,
|
|
355
|
+
starterMessage: statusMessage,
|
|
356
|
+
worktreeName,
|
|
357
|
+
projectDirectory,
|
|
358
|
+
clientV2,
|
|
359
|
+
}).catch((e) => {
|
|
360
|
+
logger.error('[NEW-WORKTREE] Background error:', e)
|
|
361
|
+
})
|
|
362
|
+
}
|
package/src/database.ts
CHANGED
|
@@ -105,6 +105,8 @@ export function getDatabase(): Database.Database {
|
|
|
105
105
|
`)
|
|
106
106
|
|
|
107
107
|
runModelMigrations(db)
|
|
108
|
+
runWorktreeSettingsMigrations(db)
|
|
109
|
+
runVerbosityMigrations(db)
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
return db
|
|
@@ -338,6 +340,112 @@ export function deleteThreadWorktree(threadId: string): void {
|
|
|
338
340
|
db.prepare('DELETE FROM thread_worktrees WHERE thread_id = ?').run(threadId)
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Run migrations for channel worktree settings table.
|
|
345
|
+
* Called on startup. Allows per-channel opt-in for automatic worktree creation.
|
|
346
|
+
*/
|
|
347
|
+
export function runWorktreeSettingsMigrations(database?: Database.Database): void {
|
|
348
|
+
const targetDb = database || getDatabase()
|
|
349
|
+
|
|
350
|
+
targetDb.exec(`
|
|
351
|
+
CREATE TABLE IF NOT EXISTS channel_worktrees (
|
|
352
|
+
channel_id TEXT PRIMARY KEY,
|
|
353
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
354
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
355
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
356
|
+
)
|
|
357
|
+
`)
|
|
358
|
+
|
|
359
|
+
dbLogger.log('Channel worktree settings migrations complete')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Verbosity levels for controlling output detail
|
|
363
|
+
export type VerbosityLevel = 'tools-and-text' | 'text-only'
|
|
364
|
+
|
|
365
|
+
export function runVerbosityMigrations(database?: Database.Database): void {
|
|
366
|
+
const targetDb = database || getDatabase()
|
|
367
|
+
|
|
368
|
+
targetDb.exec(`
|
|
369
|
+
CREATE TABLE IF NOT EXISTS channel_verbosity (
|
|
370
|
+
channel_id TEXT PRIMARY KEY,
|
|
371
|
+
verbosity TEXT NOT NULL DEFAULT 'tools-and-text',
|
|
372
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
373
|
+
)
|
|
374
|
+
`)
|
|
375
|
+
|
|
376
|
+
dbLogger.log('Channel verbosity settings migrations complete')
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get the verbosity setting for a channel.
|
|
381
|
+
* @returns 'tools-and-text' (default) or 'text-only'
|
|
382
|
+
*/
|
|
383
|
+
export function getChannelVerbosity(channelId: string): VerbosityLevel {
|
|
384
|
+
const db = getDatabase()
|
|
385
|
+
const row = db
|
|
386
|
+
.prepare('SELECT verbosity FROM channel_verbosity WHERE channel_id = ?')
|
|
387
|
+
.get(channelId) as { verbosity: string } | undefined
|
|
388
|
+
return (row?.verbosity as VerbosityLevel) || 'tools-and-text'
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Set the verbosity setting for a channel.
|
|
393
|
+
*/
|
|
394
|
+
export function setChannelVerbosity(channelId: string, verbosity: VerbosityLevel): void {
|
|
395
|
+
const db = getDatabase()
|
|
396
|
+
db.prepare(
|
|
397
|
+
`INSERT INTO channel_verbosity (channel_id, verbosity, updated_at)
|
|
398
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
399
|
+
ON CONFLICT(channel_id) DO UPDATE SET verbosity = ?, updated_at = CURRENT_TIMESTAMP`,
|
|
400
|
+
).run(channelId, verbosity, verbosity)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Check if automatic worktree creation is enabled for a channel.
|
|
405
|
+
*/
|
|
406
|
+
export function getChannelWorktreesEnabled(channelId: string): boolean {
|
|
407
|
+
const db = getDatabase()
|
|
408
|
+
const row = db
|
|
409
|
+
.prepare('SELECT enabled FROM channel_worktrees WHERE channel_id = ?')
|
|
410
|
+
.get(channelId) as { enabled: number } | undefined
|
|
411
|
+
return row?.enabled === 1
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Enable or disable automatic worktree creation for a channel.
|
|
416
|
+
*/
|
|
417
|
+
export function setChannelWorktreesEnabled(channelId: string, enabled: boolean): void {
|
|
418
|
+
const db = getDatabase()
|
|
419
|
+
db.prepare(
|
|
420
|
+
`INSERT INTO channel_worktrees (channel_id, enabled, updated_at)
|
|
421
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
422
|
+
ON CONFLICT(channel_id) DO UPDATE SET enabled = ?, updated_at = CURRENT_TIMESTAMP`,
|
|
423
|
+
).run(channelId, enabled ? 1 : 0, enabled ? 1 : 0)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get the directory and app_id for a channel from the database.
|
|
428
|
+
* This is the single source of truth for channel-project mappings.
|
|
429
|
+
*/
|
|
430
|
+
export function getChannelDirectory(channelId: string): {
|
|
431
|
+
directory: string
|
|
432
|
+
appId: string | null
|
|
433
|
+
} | undefined {
|
|
434
|
+
const db = getDatabase()
|
|
435
|
+
const row = db
|
|
436
|
+
.prepare('SELECT directory, app_id FROM channel_directories WHERE channel_id = ?')
|
|
437
|
+
.get(channelId) as { directory: string; app_id: string | null } | undefined
|
|
438
|
+
|
|
439
|
+
if (!row) {
|
|
440
|
+
return undefined
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
directory: row.directory,
|
|
445
|
+
appId: row.app_id,
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
341
449
|
export function closeDatabase(): void {
|
|
342
450
|
if (db) {
|
|
343
451
|
db.close()
|