goke 6.12.3 → 6.13.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/dist/__test__/daemon.test.d.ts +11 -0
- package/dist/__test__/daemon.test.d.ts.map +1 -0
- package/dist/__test__/daemon.test.js +296 -0
- package/dist/__test__/index.test.js +18 -2
- package/dist/daemon-browser.d.ts +19 -0
- package/dist/daemon-browser.d.ts.map +1 -0
- package/dist/daemon-browser.js +25 -0
- package/dist/daemon.d.ts +80 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +337 -0
- package/dist/goke.d.ts +3 -0
- package/dist/goke.d.ts.map +1 -1
- package/dist/goke.js +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/__test__/daemon.test.ts +335 -0
- package/src/__test__/index.test.ts +22 -2
- package/src/daemon-browser.ts +34 -0
- package/src/daemon.ts +404 -0
- package/src/goke.ts +15 -1
- package/src/index.ts +1 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the daemon background process support.
|
|
3
|
+
*
|
|
4
|
+
* Tests the DaemonContext lifecycle: PID file management, isDaemon detection,
|
|
5
|
+
* start/stop/isRunning, forCommand, heartbeat, and instance ID safety.
|
|
6
|
+
*
|
|
7
|
+
* Server-mode tests (isDaemon=true) run in child processes to avoid
|
|
8
|
+
* scheduling process.exit() timers inside the vitest runner.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test, afterEach } from 'vitest'
|
|
12
|
+
import { execFile } from 'node:child_process'
|
|
13
|
+
import { promisify } from 'node:util'
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import os from 'node:os'
|
|
17
|
+
|
|
18
|
+
const execFileAsync = promisify(execFile)
|
|
19
|
+
|
|
20
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
21
|
+
|
|
22
|
+
function pidFilePath(cliName: string, commandName: string): string {
|
|
23
|
+
const safeName = `${cliName}--${commandName}`
|
|
24
|
+
.replace(/\s+/g, '-')
|
|
25
|
+
.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
26
|
+
return path.join(DAEMON_DIR, `${safeName}.pid.json`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readPidFile(filePath: string): { pid: number; id: string; startedAt: number; heartbeatAt: number } | null {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isProcessAlive(pid: number): boolean {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(pid, 0)
|
|
40
|
+
return true
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function killIfAlive(pid: number): Promise<void> {
|
|
47
|
+
if (isProcessAlive(pid)) {
|
|
48
|
+
try { process.kill(pid, 'SIGKILL') } catch {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Track PIDs to clean up after tests
|
|
53
|
+
const spawnedPids: number[] = []
|
|
54
|
+
const testPidFiles: string[] = []
|
|
55
|
+
|
|
56
|
+
afterEach(async () => {
|
|
57
|
+
for (const pid of spawnedPids) {
|
|
58
|
+
await killIfAlive(pid)
|
|
59
|
+
}
|
|
60
|
+
spawnedPids.length = 0
|
|
61
|
+
|
|
62
|
+
for (const f of testPidFiles) {
|
|
63
|
+
try { fs.unlinkSync(f) } catch {}
|
|
64
|
+
}
|
|
65
|
+
testPidFiles.length = 0
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Helper script that simulates a daemon process: writes PID file with
|
|
69
|
+
// instance ID and heartbeat, stays alive until SIGTERM or timeout.
|
|
70
|
+
function writeDaemonHelper(scriptPath: string): void {
|
|
71
|
+
fs.writeFileSync(scriptPath, `
|
|
72
|
+
import fs from 'node:fs'
|
|
73
|
+
import path from 'node:path'
|
|
74
|
+
import os from 'node:os'
|
|
75
|
+
import crypto from 'node:crypto'
|
|
76
|
+
|
|
77
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
78
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
79
|
+
const cmdName = process.env.TEST_CMD_NAME || 'bg'
|
|
80
|
+
const safeName = cliName + '--' + cmdName
|
|
81
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
82
|
+
|
|
83
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
84
|
+
|
|
85
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
86
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
87
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
88
|
+
|
|
89
|
+
const hb = setInterval(() => {
|
|
90
|
+
try {
|
|
91
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
92
|
+
if (current.id === instanceId) {
|
|
93
|
+
current.heartbeatAt = Date.now()
|
|
94
|
+
fs.writeFileSync(pidFile, JSON.stringify(current), { mode: 0o600 })
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
}, 2000)
|
|
98
|
+
hb.unref()
|
|
99
|
+
|
|
100
|
+
const timeoutMs = Number(process.env.GOKE_DAEMON_TIMEOUT) || 60000
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
try {
|
|
103
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
104
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
105
|
+
} catch {}
|
|
106
|
+
process.exit(0)
|
|
107
|
+
}, timeoutMs)
|
|
108
|
+
|
|
109
|
+
process.on('SIGTERM', () => {
|
|
110
|
+
clearTimeout(timer)
|
|
111
|
+
clearInterval(hb)
|
|
112
|
+
try {
|
|
113
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
114
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
115
|
+
} catch {}
|
|
116
|
+
process.exit(0)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
process.on('exit', () => {
|
|
120
|
+
try {
|
|
121
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
122
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
123
|
+
} catch {}
|
|
124
|
+
})
|
|
125
|
+
`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
describe('DaemonContext', () => {
|
|
129
|
+
test('isDaemon is false by default (client mode)', async () => {
|
|
130
|
+
const { default: goke } = await import('../index.js')
|
|
131
|
+
const cli = goke('test-cli')
|
|
132
|
+
let isDaemon: boolean | undefined
|
|
133
|
+
|
|
134
|
+
cli.command('run', 'test').action((opts, ctx) => {
|
|
135
|
+
isDaemon = ctx.daemon.isDaemon
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
await cli.parse(['node', 'test', 'run'], { run: true })
|
|
139
|
+
expect(isDaemon).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('isDaemon is true when GOKE_DAEMON=1 (tested in child process)', async () => {
|
|
143
|
+
// Run a small script in a child process that creates a DaemonContext
|
|
144
|
+
// with the env var set and prints isDaemon. This avoids scheduling
|
|
145
|
+
// process.exit() timers inside the vitest process.
|
|
146
|
+
// Uses the compiled dist/ so plain Node can import it (no tsx needed).
|
|
147
|
+
const scriptPath = path.join(os.tmpdir(), 'goke-daemon-is-daemon-test.mjs')
|
|
148
|
+
const distDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', 'dist')
|
|
149
|
+
fs.writeFileSync(scriptPath, `
|
|
150
|
+
import { DaemonContext } from '${distDir}/daemon.js'
|
|
151
|
+
const ctx = new DaemonContext('test-is-daemon', 'cmd', ['node', 'test'])
|
|
152
|
+
console.log(ctx.isDaemon ? 'SERVER' : 'CLIENT')
|
|
153
|
+
// Exit immediately to not leave the daemon alive
|
|
154
|
+
process.exit(0)
|
|
155
|
+
`)
|
|
156
|
+
testPidFiles.push(pidFilePath('test-is-daemon', 'cmd'))
|
|
157
|
+
|
|
158
|
+
const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
|
|
159
|
+
env: { ...process.env, GOKE_DAEMON: '1', GOKE_DAEMON_TIMEOUT: '1000' },
|
|
160
|
+
})
|
|
161
|
+
expect(stdout.trim()).toBe('SERVER')
|
|
162
|
+
}, 10_000)
|
|
163
|
+
|
|
164
|
+
test('isRunning returns false when no daemon is running', async () => {
|
|
165
|
+
const { default: goke } = await import('../index.js')
|
|
166
|
+
const cli = goke('test-cli')
|
|
167
|
+
let running: boolean | undefined
|
|
168
|
+
|
|
169
|
+
cli.command('check', 'test').action(async (opts, ctx) => {
|
|
170
|
+
running = await ctx.daemon.isRunning()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
await cli.parse(['node', 'test', 'check'], { run: true })
|
|
174
|
+
expect(running).toBe(false)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('start spawns a detached daemon, isRunning returns true, stop kills it', async () => {
|
|
178
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-start.mjs')
|
|
179
|
+
writeDaemonHelper(helperScript)
|
|
180
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'bg'))
|
|
181
|
+
|
|
182
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
183
|
+
const ctx = new DaemonContext('test-daemon-cli', 'bg', [process.execPath, helperScript, 'bg'])
|
|
184
|
+
|
|
185
|
+
expect(ctx.isDaemon).toBe(false)
|
|
186
|
+
expect(await ctx.isRunning()).toBe(false)
|
|
187
|
+
|
|
188
|
+
await ctx.start({ timeoutMs: 30_000 })
|
|
189
|
+
expect(await ctx.isRunning()).toBe(true)
|
|
190
|
+
|
|
191
|
+
const pidData = readPidFile(pidFilePath('test-daemon-cli', 'bg'))
|
|
192
|
+
expect(pidData).not.toBeNull()
|
|
193
|
+
expect(pidData!.id).toBeTruthy()
|
|
194
|
+
spawnedPids.push(pidData!.pid)
|
|
195
|
+
|
|
196
|
+
await ctx.stop()
|
|
197
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
198
|
+
expect(await ctx.isRunning()).toBe(false)
|
|
199
|
+
|
|
200
|
+
try { fs.unlinkSync(helperScript) } catch {}
|
|
201
|
+
}, 15_000)
|
|
202
|
+
|
|
203
|
+
test('start kills existing daemon before spawning new one', async () => {
|
|
204
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-replace.mjs')
|
|
205
|
+
writeDaemonHelper(helperScript)
|
|
206
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'bg'))
|
|
207
|
+
|
|
208
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
209
|
+
const ctx = new DaemonContext('test-daemon-cli', 'bg', [process.execPath, helperScript, 'bg'])
|
|
210
|
+
|
|
211
|
+
await ctx.start({ timeoutMs: 30_000 })
|
|
212
|
+
const firstPid = readPidFile(pidFilePath('test-daemon-cli', 'bg'))
|
|
213
|
+
expect(firstPid).not.toBeNull()
|
|
214
|
+
spawnedPids.push(firstPid!.pid)
|
|
215
|
+
const firstId = firstPid!.id
|
|
216
|
+
|
|
217
|
+
await ctx.start({ timeoutMs: 30_000 })
|
|
218
|
+
const secondPid = readPidFile(pidFilePath('test-daemon-cli', 'bg'))
|
|
219
|
+
expect(secondPid).not.toBeNull()
|
|
220
|
+
spawnedPids.push(secondPid!.pid)
|
|
221
|
+
|
|
222
|
+
// PIDs and instance IDs should differ
|
|
223
|
+
expect(secondPid!.pid).not.toBe(firstPid!.pid)
|
|
224
|
+
expect(secondPid!.id).not.toBe(firstId)
|
|
225
|
+
|
|
226
|
+
expect(isProcessAlive(firstPid!.pid)).toBe(false)
|
|
227
|
+
expect(isProcessAlive(secondPid!.pid)).toBe(true)
|
|
228
|
+
|
|
229
|
+
await ctx.stop()
|
|
230
|
+
try { fs.unlinkSync(helperScript) } catch {}
|
|
231
|
+
}, 15_000)
|
|
232
|
+
|
|
233
|
+
test('stop is idempotent when no daemon running', async () => {
|
|
234
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
235
|
+
const ctx = new DaemonContext('nonexistent-cli', 'nope', ['node', 'nope'])
|
|
236
|
+
await ctx.stop()
|
|
237
|
+
await ctx.stop()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('forCommand returns context for a different command', async () => {
|
|
241
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
242
|
+
const loginCtx = new DaemonContext('myapp', 'login', ['node', 'myapp', 'login'])
|
|
243
|
+
const meCtx = loginCtx.forCommand('me')
|
|
244
|
+
|
|
245
|
+
// They should reference different PID files
|
|
246
|
+
expect(await loginCtx.isRunning()).toBe(false)
|
|
247
|
+
expect(await meCtx.isRunning()).toBe(false)
|
|
248
|
+
|
|
249
|
+
// forCommand context is always client mode
|
|
250
|
+
expect(meCtx.isDaemon).toBe(false)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('forCommand can check and stop another commands daemon', async () => {
|
|
254
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-forcommand.mjs')
|
|
255
|
+
writeDaemonHelper(helperScript)
|
|
256
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'bg'))
|
|
257
|
+
|
|
258
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
259
|
+
|
|
260
|
+
// Start daemon for "bg" command
|
|
261
|
+
const bgCtx = new DaemonContext('test-daemon-cli', 'bg', [process.execPath, helperScript, 'bg'])
|
|
262
|
+
await bgCtx.start({ timeoutMs: 30_000 })
|
|
263
|
+
|
|
264
|
+
const pidData = readPidFile(pidFilePath('test-daemon-cli', 'bg'))
|
|
265
|
+
if (pidData) spawnedPids.push(pidData.pid)
|
|
266
|
+
|
|
267
|
+
// Create a context for "me" command and use forCommand to check "bg"
|
|
268
|
+
const meCtx = new DaemonContext('test-daemon-cli', 'me', ['node', 'test', 'me'])
|
|
269
|
+
const bgFromMe = meCtx.forCommand('bg')
|
|
270
|
+
|
|
271
|
+
expect(await bgFromMe.isRunning()).toBe(true)
|
|
272
|
+
|
|
273
|
+
await bgFromMe.stop()
|
|
274
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
275
|
+
expect(await bgFromMe.isRunning()).toBe(false)
|
|
276
|
+
|
|
277
|
+
try { fs.unlinkSync(helperScript) } catch {}
|
|
278
|
+
}, 15_000)
|
|
279
|
+
|
|
280
|
+
test('stale PID file is cleaned up by isRunning', async () => {
|
|
281
|
+
const pidFile = pidFilePath('stale-test', 'cmd')
|
|
282
|
+
testPidFiles.push(pidFile)
|
|
283
|
+
|
|
284
|
+
// Write a PID file with a PID that doesn't exist
|
|
285
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
286
|
+
fs.writeFileSync(pidFile, JSON.stringify({
|
|
287
|
+
pid: 999999999,
|
|
288
|
+
id: 'stale-instance',
|
|
289
|
+
startedAt: Date.now(),
|
|
290
|
+
heartbeatAt: Date.now(),
|
|
291
|
+
}))
|
|
292
|
+
|
|
293
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
294
|
+
const ctx = new DaemonContext('stale-test', 'cmd', ['node', 'test'])
|
|
295
|
+
|
|
296
|
+
expect(await ctx.isRunning()).toBe(false)
|
|
297
|
+
// PID file should have been cleaned up
|
|
298
|
+
expect(fs.existsSync(pidFile)).toBe(false)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('PID file with stale heartbeat is treated as not running', async () => {
|
|
302
|
+
const pidFile = pidFilePath('heartbeat-test', 'cmd')
|
|
303
|
+
testPidFiles.push(pidFile)
|
|
304
|
+
|
|
305
|
+
// Write a PID file with current process PID but very old heartbeat
|
|
306
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
307
|
+
fs.writeFileSync(pidFile, JSON.stringify({
|
|
308
|
+
pid: process.pid, // alive PID
|
|
309
|
+
id: 'old-heartbeat',
|
|
310
|
+
startedAt: Date.now() - 60000,
|
|
311
|
+
heartbeatAt: Date.now() - 60000, // 60s old, well past the 15s threshold
|
|
312
|
+
}))
|
|
313
|
+
|
|
314
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
315
|
+
const ctx = new DaemonContext('heartbeat-test', 'cmd', ['node', 'test'])
|
|
316
|
+
|
|
317
|
+
// Should return false because heartbeat is stale (even though PID is alive)
|
|
318
|
+
expect(await ctx.isRunning()).toBe(false)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('daemon context has correct command name from parsed cli', async () => {
|
|
322
|
+
const { default: goke } = await import('../index.js')
|
|
323
|
+
const cli = goke('my-app')
|
|
324
|
+
let capturedDaemon: any
|
|
325
|
+
|
|
326
|
+
cli.command('auth login', 'Login').action((opts, ctx) => {
|
|
327
|
+
capturedDaemon = ctx.daemon
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
await cli.parse(['node', 'my-app', 'auth', 'login'], { run: true })
|
|
331
|
+
|
|
332
|
+
expect(capturedDaemon).toBeDefined()
|
|
333
|
+
expect(capturedDaemon.isDaemon).toBe(false)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
@@ -140,7 +140,7 @@ describe('error formatting', () => {
|
|
|
140
140
|
expect(stripStackTrace(stderr.text)).toMatchInlineSnapshot(`"error: connection refused"`)
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
-
test('
|
|
143
|
+
test('GokeError (validation) omits stack trace', async () => {
|
|
144
144
|
const stderr = createTestOutputStream()
|
|
145
145
|
const cli = goke('mycli', { stderr, exit: () => {} })
|
|
146
146
|
|
|
@@ -152,10 +152,30 @@ describe('error formatting', () => {
|
|
|
152
152
|
await cli.parse('node bin build --unknown'.split(' '))
|
|
153
153
|
} catch {}
|
|
154
154
|
|
|
155
|
-
// Verify that stderr contains "error:" prefix and a stack trace with "at" lines
|
|
156
155
|
const text = stderr.text
|
|
157
156
|
expect(text).toContain('error:')
|
|
158
157
|
expect(text).toContain('Unknown option `--unknown`')
|
|
158
|
+
// GokeError is a user-facing error; stack trace should be suppressed
|
|
159
|
+
expect(text).not.toMatch(/at /)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('unexpected error still includes stack trace', async () => {
|
|
163
|
+
const stderr = createTestOutputStream()
|
|
164
|
+
const cli = goke('mycli', { stderr, exit: () => {} })
|
|
165
|
+
|
|
166
|
+
cli
|
|
167
|
+
.command('deploy', 'Deploy app')
|
|
168
|
+
.action(async () => {
|
|
169
|
+
throw new Error('unexpected crash')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await cli.parse('node bin deploy'.split(' '))
|
|
173
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
174
|
+
|
|
175
|
+
const text = stderr.text
|
|
176
|
+
expect(text).toContain('error:')
|
|
177
|
+
expect(text).toContain('unexpected crash')
|
|
178
|
+
// Non-GokeError should still show the stack trace
|
|
159
179
|
expect(text).toMatch(/at /)
|
|
160
180
|
})
|
|
161
181
|
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe daemon stub.
|
|
3
|
+
*
|
|
4
|
+
* Provides the same DaemonContext interface as daemon.ts but without
|
|
5
|
+
* Node.js dependencies. start() throws; everything else is a no-op.
|
|
6
|
+
* Used via the #daemon conditional import in browser/edge runtimes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class DaemonContext {
|
|
10
|
+
readonly isDaemon = false as const
|
|
11
|
+
|
|
12
|
+
constructor() {}
|
|
13
|
+
|
|
14
|
+
forCommand(_commandName: string): DaemonContext {
|
|
15
|
+
return new DaemonContext()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async start(): Promise<void> {
|
|
19
|
+
throw new Error('ctx.daemon.start() is only available in Node.js runtimes.')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async stop(): Promise<void> {}
|
|
23
|
+
|
|
24
|
+
async isRunning(): Promise<boolean> {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createDaemonContext(): DaemonContext {
|
|
30
|
+
return new DaemonContext()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { DaemonContext, createDaemonContext }
|
|
34
|
+
export type { DaemonStartOptions } from './daemon.js'
|