goke 6.13.0 → 6.14.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.js +91 -0
- package/dist/daemon.d.ts +14 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +21 -2
- package/package.json +1 -1
- package/src/__test__/daemon.test.ts +94 -0
- package/src/daemon.ts +29 -2
|
@@ -282,6 +282,97 @@ process.exit(0)
|
|
|
282
282
|
// Should return false because heartbeat is stale (even though PID is alive)
|
|
283
283
|
expect(await ctx.isRunning()).toBe(false);
|
|
284
284
|
});
|
|
285
|
+
test('start with attach: true pipes stdout/stderr and waits for exit', async () => {
|
|
286
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-attach.mjs');
|
|
287
|
+
// This helper writes to stdout/stderr and exits after a short delay.
|
|
288
|
+
// The PID file setup mirrors the standard daemon helper.
|
|
289
|
+
fs.writeFileSync(helperScript, `
|
|
290
|
+
import fs from 'node:fs'
|
|
291
|
+
import path from 'node:path'
|
|
292
|
+
import os from 'node:os'
|
|
293
|
+
import crypto from 'node:crypto'
|
|
294
|
+
|
|
295
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
296
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
297
|
+
const cmdName = process.env.TEST_CMD_NAME || 'attach-test'
|
|
298
|
+
const safeName = cliName + '--' + cmdName
|
|
299
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
300
|
+
|
|
301
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
302
|
+
|
|
303
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
304
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
305
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
306
|
+
|
|
307
|
+
// Write to stdout and stderr so the parent can see it
|
|
308
|
+
console.log('DAEMON_STDOUT_MESSAGE')
|
|
309
|
+
console.error('DAEMON_STDERR_MESSAGE')
|
|
310
|
+
|
|
311
|
+
// Exit after a short delay
|
|
312
|
+
setTimeout(() => {
|
|
313
|
+
try {
|
|
314
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
315
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
316
|
+
} catch {}
|
|
317
|
+
process.exit(0)
|
|
318
|
+
}, 500)
|
|
319
|
+
`);
|
|
320
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'attach-test'));
|
|
321
|
+
const { DaemonContext } = await import('../daemon.js');
|
|
322
|
+
const ctx = new DaemonContext('test-daemon-cli', 'attach-test', [
|
|
323
|
+
process.execPath, helperScript, 'attach-test',
|
|
324
|
+
]);
|
|
325
|
+
// attach: true should wait for the daemon to finish
|
|
326
|
+
await ctx.start({ attach: true, timeoutMs: 10_000 });
|
|
327
|
+
// After start resolves, the daemon should have exited and cleaned up its PID file
|
|
328
|
+
expect(await ctx.isRunning()).toBe(false);
|
|
329
|
+
try {
|
|
330
|
+
fs.unlinkSync(helperScript);
|
|
331
|
+
}
|
|
332
|
+
catch { }
|
|
333
|
+
}, 15_000);
|
|
334
|
+
test('start with attach: true throws on non-zero exit', async () => {
|
|
335
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-attach-fail.mjs');
|
|
336
|
+
fs.writeFileSync(helperScript, `
|
|
337
|
+
import fs from 'node:fs'
|
|
338
|
+
import path from 'node:path'
|
|
339
|
+
import os from 'node:os'
|
|
340
|
+
import crypto from 'node:crypto'
|
|
341
|
+
|
|
342
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
343
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
344
|
+
const cmdName = process.env.TEST_CMD_NAME || 'attach-fail'
|
|
345
|
+
const safeName = cliName + '--' + cmdName
|
|
346
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
347
|
+
|
|
348
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
349
|
+
|
|
350
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
351
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
352
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
353
|
+
|
|
354
|
+
console.error('Something went wrong')
|
|
355
|
+
|
|
356
|
+
setTimeout(() => {
|
|
357
|
+
try {
|
|
358
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
359
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
360
|
+
} catch {}
|
|
361
|
+
process.exit(1)
|
|
362
|
+
}, 500)
|
|
363
|
+
`);
|
|
364
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'attach-fail'));
|
|
365
|
+
const { DaemonContext } = await import('../daemon.js');
|
|
366
|
+
const ctx = new DaemonContext('test-daemon-cli', 'attach-fail', [
|
|
367
|
+
process.execPath, helperScript, 'attach-fail',
|
|
368
|
+
]);
|
|
369
|
+
await expect(ctx.start({ attach: true, timeoutMs: 10_000 }))
|
|
370
|
+
.rejects.toThrow('exited with code 1');
|
|
371
|
+
try {
|
|
372
|
+
fs.unlinkSync(helperScript);
|
|
373
|
+
}
|
|
374
|
+
catch { }
|
|
375
|
+
}, 15_000);
|
|
285
376
|
test('daemon context has correct command name from parsed cli', async () => {
|
|
286
377
|
const { default: goke } = await import('../index.js');
|
|
287
378
|
const cli = goke('my-app');
|
package/dist/daemon.d.ts
CHANGED
|
@@ -29,6 +29,15 @@ interface DaemonStartOptions {
|
|
|
29
29
|
timeoutMs?: number;
|
|
30
30
|
/** Extra environment variables passed to the daemon process. */
|
|
31
31
|
env?: Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* When true, pipe daemon stdout/stderr to the parent process and wait
|
|
34
|
+
* for the daemon to exit before resolving. This lets interactive users
|
|
35
|
+
* see real-time logs and error messages from the daemon.
|
|
36
|
+
*
|
|
37
|
+
* When false (default), the daemon runs fully detached with no stdio
|
|
38
|
+
* and start() returns as soon as the PID file is confirmed.
|
|
39
|
+
*/
|
|
40
|
+
attach?: boolean;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* Daemon context available on every command's execution context.
|
|
@@ -57,6 +66,11 @@ declare class DaemonContext {
|
|
|
57
66
|
/**
|
|
58
67
|
* Spawn the current command as a detached background daemon process.
|
|
59
68
|
* Kills any existing daemon for this command first.
|
|
69
|
+
*
|
|
70
|
+
* When `attach: true`, pipes daemon stdout/stderr to the parent and
|
|
71
|
+
* waits for the daemon to exit. Throws if the daemon exits with a
|
|
72
|
+
* non-zero code. This is useful for interactive login flows where the
|
|
73
|
+
* user wants to see real-time logs and error messages.
|
|
60
74
|
*/
|
|
61
75
|
start(options?: DaemonStartOptions): Promise<void>;
|
|
62
76
|
/**
|
package/dist/daemon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA6IH,UAAU,kBAAkB;IAC1B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA6IH,UAAU,kBAAkB;IAC1B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;;;;GAUG;AACH,cAAM,aAAa;;IACjB,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;gBAYxB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAc1C;;;;;;OAMG;IACH,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa;IAkF9C;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDxD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAcpC;AAED;;;GAGG;AACH,iBAAS,mBAAmB,CAC1B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACvC,aAAa,CAEf;AAED,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,CAAA"}
|
package/dist/daemon.js
CHANGED
|
@@ -259,9 +259,15 @@ class DaemonContext {
|
|
|
259
259
|
/**
|
|
260
260
|
* Spawn the current command as a detached background daemon process.
|
|
261
261
|
* Kills any existing daemon for this command first.
|
|
262
|
+
*
|
|
263
|
+
* When `attach: true`, pipes daemon stdout/stderr to the parent and
|
|
264
|
+
* waits for the daemon to exit. Throws if the daemon exits with a
|
|
265
|
+
* non-zero code. This is useful for interactive login flows where the
|
|
266
|
+
* user wants to see real-time logs and error messages.
|
|
262
267
|
*/
|
|
263
268
|
async start(options) {
|
|
264
269
|
const timeoutMs = options?.timeoutMs ?? 10 * 60 * 1000;
|
|
270
|
+
const attach = options?.attach ?? false;
|
|
265
271
|
// Kill existing daemon if running
|
|
266
272
|
await this.stop();
|
|
267
273
|
const env = {
|
|
@@ -276,7 +282,7 @@ class DaemonContext {
|
|
|
276
282
|
const args = this.#argv.slice(1);
|
|
277
283
|
const child = spawn(execPath, args, {
|
|
278
284
|
detached: true,
|
|
279
|
-
stdio: 'ignore',
|
|
285
|
+
stdio: attach ? ['ignore', 'inherit', 'inherit'] : 'ignore',
|
|
280
286
|
env,
|
|
281
287
|
});
|
|
282
288
|
child.unref();
|
|
@@ -286,7 +292,20 @@ class DaemonContext {
|
|
|
286
292
|
await new Promise((r) => setTimeout(r, 100));
|
|
287
293
|
const pidData = readPidFile(this.#pidFile);
|
|
288
294
|
if (pidData && isProcessAlive(pidData.pid)) {
|
|
289
|
-
|
|
295
|
+
if (!attach)
|
|
296
|
+
return;
|
|
297
|
+
// Attached mode: wait for the daemon process to exit
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
child.on('close', (code) => {
|
|
300
|
+
if (code && code !== 0) {
|
|
301
|
+
reject(new Error(`Daemon "${this.#cliName} ${this.#commandName}" exited with code ${code}`));
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
resolve();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
child.on('error', reject);
|
|
308
|
+
});
|
|
290
309
|
}
|
|
291
310
|
}
|
|
292
311
|
throw new Error(`Failed to start daemon for "${this.#cliName} ${this.#commandName}"`);
|
package/package.json
CHANGED
|
@@ -318,6 +318,100 @@ process.exit(0)
|
|
|
318
318
|
expect(await ctx.isRunning()).toBe(false)
|
|
319
319
|
})
|
|
320
320
|
|
|
321
|
+
test('start with attach: true pipes stdout/stderr and waits for exit', async () => {
|
|
322
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-attach.mjs')
|
|
323
|
+
// This helper writes to stdout/stderr and exits after a short delay.
|
|
324
|
+
// The PID file setup mirrors the standard daemon helper.
|
|
325
|
+
fs.writeFileSync(helperScript, `
|
|
326
|
+
import fs from 'node:fs'
|
|
327
|
+
import path from 'node:path'
|
|
328
|
+
import os from 'node:os'
|
|
329
|
+
import crypto from 'node:crypto'
|
|
330
|
+
|
|
331
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
332
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
333
|
+
const cmdName = process.env.TEST_CMD_NAME || 'attach-test'
|
|
334
|
+
const safeName = cliName + '--' + cmdName
|
|
335
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
336
|
+
|
|
337
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
338
|
+
|
|
339
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
340
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
341
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
342
|
+
|
|
343
|
+
// Write to stdout and stderr so the parent can see it
|
|
344
|
+
console.log('DAEMON_STDOUT_MESSAGE')
|
|
345
|
+
console.error('DAEMON_STDERR_MESSAGE')
|
|
346
|
+
|
|
347
|
+
// Exit after a short delay
|
|
348
|
+
setTimeout(() => {
|
|
349
|
+
try {
|
|
350
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
351
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
352
|
+
} catch {}
|
|
353
|
+
process.exit(0)
|
|
354
|
+
}, 500)
|
|
355
|
+
`)
|
|
356
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'attach-test'))
|
|
357
|
+
|
|
358
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
359
|
+
const ctx = new DaemonContext('test-daemon-cli', 'attach-test', [
|
|
360
|
+
process.execPath, helperScript, 'attach-test',
|
|
361
|
+
])
|
|
362
|
+
|
|
363
|
+
// attach: true should wait for the daemon to finish
|
|
364
|
+
await ctx.start({ attach: true, timeoutMs: 10_000 })
|
|
365
|
+
|
|
366
|
+
// After start resolves, the daemon should have exited and cleaned up its PID file
|
|
367
|
+
expect(await ctx.isRunning()).toBe(false)
|
|
368
|
+
|
|
369
|
+
try { fs.unlinkSync(helperScript) } catch {}
|
|
370
|
+
}, 15_000)
|
|
371
|
+
|
|
372
|
+
test('start with attach: true throws on non-zero exit', async () => {
|
|
373
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-attach-fail.mjs')
|
|
374
|
+
fs.writeFileSync(helperScript, `
|
|
375
|
+
import fs from 'node:fs'
|
|
376
|
+
import path from 'node:path'
|
|
377
|
+
import os from 'node:os'
|
|
378
|
+
import crypto from 'node:crypto'
|
|
379
|
+
|
|
380
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
381
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
382
|
+
const cmdName = process.env.TEST_CMD_NAME || 'attach-fail'
|
|
383
|
+
const safeName = cliName + '--' + cmdName
|
|
384
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
385
|
+
|
|
386
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
387
|
+
|
|
388
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
389
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
390
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
391
|
+
|
|
392
|
+
console.error('Something went wrong')
|
|
393
|
+
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
try {
|
|
396
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
397
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
398
|
+
} catch {}
|
|
399
|
+
process.exit(1)
|
|
400
|
+
}, 500)
|
|
401
|
+
`)
|
|
402
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'attach-fail'))
|
|
403
|
+
|
|
404
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
405
|
+
const ctx = new DaemonContext('test-daemon-cli', 'attach-fail', [
|
|
406
|
+
process.execPath, helperScript, 'attach-fail',
|
|
407
|
+
])
|
|
408
|
+
|
|
409
|
+
await expect(ctx.start({ attach: true, timeoutMs: 10_000 }))
|
|
410
|
+
.rejects.toThrow('exited with code 1')
|
|
411
|
+
|
|
412
|
+
try { fs.unlinkSync(helperScript) } catch {}
|
|
413
|
+
}, 15_000)
|
|
414
|
+
|
|
321
415
|
test('daemon context has correct command name from parsed cli', async () => {
|
|
322
416
|
const { default: goke } = await import('../index.js')
|
|
323
417
|
const cli = goke('my-app')
|
package/src/daemon.ts
CHANGED
|
@@ -169,6 +169,15 @@ interface DaemonStartOptions {
|
|
|
169
169
|
timeoutMs?: number
|
|
170
170
|
/** Extra environment variables passed to the daemon process. */
|
|
171
171
|
env?: Record<string, string>
|
|
172
|
+
/**
|
|
173
|
+
* When true, pipe daemon stdout/stderr to the parent process and wait
|
|
174
|
+
* for the daemon to exit before resolving. This lets interactive users
|
|
175
|
+
* see real-time logs and error messages from the daemon.
|
|
176
|
+
*
|
|
177
|
+
* When false (default), the daemon runs fully detached with no stdio
|
|
178
|
+
* and start() returns as soon as the PID file is confirmed.
|
|
179
|
+
*/
|
|
180
|
+
attach?: boolean
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
/**
|
|
@@ -305,9 +314,15 @@ class DaemonContext {
|
|
|
305
314
|
/**
|
|
306
315
|
* Spawn the current command as a detached background daemon process.
|
|
307
316
|
* Kills any existing daemon for this command first.
|
|
317
|
+
*
|
|
318
|
+
* When `attach: true`, pipes daemon stdout/stderr to the parent and
|
|
319
|
+
* waits for the daemon to exit. Throws if the daemon exits with a
|
|
320
|
+
* non-zero code. This is useful for interactive login flows where the
|
|
321
|
+
* user wants to see real-time logs and error messages.
|
|
308
322
|
*/
|
|
309
323
|
async start(options?: DaemonStartOptions): Promise<void> {
|
|
310
324
|
const timeoutMs = options?.timeoutMs ?? 10 * 60 * 1000
|
|
325
|
+
const attach = options?.attach ?? false
|
|
311
326
|
|
|
312
327
|
// Kill existing daemon if running
|
|
313
328
|
await this.stop()
|
|
@@ -326,7 +341,7 @@ class DaemonContext {
|
|
|
326
341
|
|
|
327
342
|
const child = spawn(execPath, args, {
|
|
328
343
|
detached: true,
|
|
329
|
-
stdio: 'ignore',
|
|
344
|
+
stdio: attach ? ['ignore', 'inherit', 'inherit'] : 'ignore',
|
|
330
345
|
env,
|
|
331
346
|
})
|
|
332
347
|
|
|
@@ -338,7 +353,19 @@ class DaemonContext {
|
|
|
338
353
|
await new Promise((r) => setTimeout(r, 100))
|
|
339
354
|
const pidData = readPidFile(this.#pidFile)
|
|
340
355
|
if (pidData && isProcessAlive(pidData.pid)) {
|
|
341
|
-
return
|
|
356
|
+
if (!attach) return
|
|
357
|
+
|
|
358
|
+
// Attached mode: wait for the daemon process to exit
|
|
359
|
+
return new Promise<void>((resolve, reject) => {
|
|
360
|
+
child.on('close', (code) => {
|
|
361
|
+
if (code && code !== 0) {
|
|
362
|
+
reject(new Error(`Daemon "${this.#cliName} ${this.#commandName}" exited with code ${code}`))
|
|
363
|
+
} else {
|
|
364
|
+
resolve()
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
child.on('error', reject)
|
|
368
|
+
})
|
|
342
369
|
}
|
|
343
370
|
}
|
|
344
371
|
|