@dinoxx/dinox-cli 2.6.0 → 2.8.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.
Files changed (38) hide show
  1. package/dist/commands/gateway/index.js +1 -1
  2. package/dist/commands/schema/renderSkills.js +1 -1
  3. package/dist/commands/storage/repo/remote.d.ts +1 -1
  4. package/dist/commands/storage/repo/remote.js +2 -2
  5. package/dist/daemon/index.js +37 -6
  6. package/dist/daemon/launch.d.ts +5 -1
  7. package/dist/daemon/launch.js +14 -6
  8. package/dist/daemon/log.d.ts +2 -0
  9. package/dist/daemon/log.js +16 -3
  10. package/dist/daemon/server.js +17 -1
  11. package/dist/daemon/supervise.d.ts +29 -0
  12. package/dist/daemon/supervise.js +98 -0
  13. package/dist/daemon/supervisor.d.ts +21 -5
  14. package/dist/daemon/supervisor.js +195 -7
  15. package/dist/daemon/types.d.ts +1 -1
  16. package/dist/gateway/adapters/acp.d.ts +21 -5
  17. package/dist/gateway/adapters/acp.js +86 -7
  18. package/dist/gateway/adapters/codexImages.d.ts +4 -0
  19. package/dist/gateway/adapters/codexImages.js +34 -0
  20. package/dist/gateway/adapters/index.d.ts +5 -2
  21. package/dist/gateway/adapters/index.js +19 -10
  22. package/dist/gateway/assistantProjection.d.ts +5 -1
  23. package/dist/gateway/assistantProjection.js +48 -11
  24. package/dist/gateway/detect.js +1 -1
  25. package/dist/gateway/deviceHost.d.ts +3 -1
  26. package/dist/gateway/deviceHost.js +6 -0
  27. package/dist/gateway/fileUpload.d.ts +12 -0
  28. package/dist/gateway/fileUpload.js +154 -0
  29. package/dist/gateway/mcpEnv.d.ts +5 -0
  30. package/dist/gateway/mcpEnv.js +5 -0
  31. package/dist/gateway/mcpServer.d.ts +12 -2
  32. package/dist/gateway/mcpServer.js +41 -7
  33. package/dist/gateway/runExecutor.d.ts +9 -1
  34. package/dist/gateway/runExecutor.js +43 -2
  35. package/dist/gateway/service.d.ts +3 -0
  36. package/dist/gateway/service.js +9 -0
  37. package/dist/gateway/types.d.ts +27 -0
  38. package/package.json +1 -1
@@ -291,7 +291,7 @@ export const gatewayCommandGroup = {
291
291
  },
292
292
  {
293
293
  name: 'mcp',
294
- description: 'Serve read-only Dinox knowledge tools over stdio MCP (started by gateway agents)',
294
+ description: 'Serve Dinox tools over stdio MCP: read-only knowledge tools and send_file (started by gateway agents)',
295
295
  schema: {
296
296
  path: 'gateway.mcp',
297
297
  risk: 'read',
@@ -4,7 +4,7 @@ import { listErrorCodes } from '../../utils/errorCodes.js';
4
4
  import { listCommandSchemas } from './spec.js';
5
5
  const SECTION_ORDER = [
6
6
  { title: 'Auth', paths: ['auth.login', 'auth.logout', 'auth.status'] },
7
- { title: 'Daemon', paths: ['daemon.start', 'daemon.status', 'daemon.restart', 'daemon.stop', 'daemon.install', 'daemon.uninstall'] },
7
+ { title: 'Daemon', paths: ['daemon.start', 'daemon.status', 'daemon.restart', 'daemon.stop', 'daemon.install', 'daemon.uninstall', 'daemon.supervise'] },
8
8
  {
9
9
  title: 'Agent Gateways',
10
10
  paths: ['gateway.detect', 'gateway.add', 'gateway.list', 'gateway.remove', 'gateway.status', 'gateway.reload', 'gateway.mcp'],
@@ -28,5 +28,5 @@ export declare function defaultUploadBytes(input: {
28
28
  }): Promise<UploadObjectResult>;
29
29
  type SharpFactory = typeof import('sharp');
30
30
  export declare function loadSharp(): Promise<SharpFactory>;
31
- export declare function generateThumbnailBuffer(sharp: SharpFactory, localPath: string): Promise<Buffer>;
31
+ export declare function generateThumbnailBuffer(sharp: SharpFactory, input: string | Buffer): Promise<Buffer>;
32
32
  export {};
@@ -124,8 +124,8 @@ export async function loadSharp() {
124
124
  });
125
125
  }
126
126
  }
127
- export async function generateThumbnailBuffer(sharp, localPath) {
128
- return sharp(localPath)
127
+ export async function generateThumbnailBuffer(sharp, input) {
128
+ return sharp(input)
129
129
  .resize({ width: 400, withoutEnlargement: true })
130
130
  .webp({ quality: 50 })
131
131
  .toBuffer();
@@ -16,6 +16,7 @@ import { getPackageVersion } from '../utils/version.js';
16
16
  import { DAEMON_CHILD_ENV, daemonServiceSpec, envTokenOverridesLogin, launchDaemon } from './launch.js';
17
17
  import { getDaemonLogPath, rotateDaemonLog, writeDaemonLogLine } from './log.js';
18
18
  import { DAEMON_SERVICE_ENV, getDaemonService } from './supervisor.js';
19
+ import { superviseCurrentDaemon } from './supervise.js';
19
20
  import { normalizeAuthToken } from '../utils/authToken.js';
20
21
  const DAEMON_STOP_POLL_INTERVAL_MS = 100;
21
22
  const DAEMON_STOP_TIMEOUT_MS = 5_000;
@@ -380,11 +381,20 @@ export async function uninstallDaemonService() {
380
381
  if (!service) {
381
382
  return { uninstalled: false, service: null, definitionPath: null };
382
383
  }
383
- return withDaemonStartLock(async () => ({
384
- uninstalled: await service.uninstall(),
385
- service: service.kind,
386
- definitionPath: service.definitionPath,
387
- }));
384
+ return withDaemonStartLock(async () => {
385
+ if (service.isInstalled()) {
386
+ // Stop through the socket first: a clean exit is not restarted, and
387
+ // deleting a Windows task does not end the process it started.
388
+ const info = await getDaemonInfo();
389
+ if (info?.supervisor === service.kind)
390
+ await stopDaemonLocked();
391
+ }
392
+ return {
393
+ uninstalled: await service.uninstall(),
394
+ service: service.kind,
395
+ definitionPath: service.definitionPath,
396
+ };
397
+ });
388
398
  }
389
399
  function printDaemonStartResult(result) {
390
400
  if (result.alreadyRunning) {
@@ -432,7 +442,7 @@ function printDaemonInstallResult(result) {
432
442
  printHumanReadableLine(`Installed ${result.service} service ${result.label}`);
433
443
  printHumanReadableLine(`Definition: ${result.definitionPath}`);
434
444
  printHumanReadableLine(`Log: ${result.logPath}`);
435
- printHumanReadableLine('The daemon now starts at login and restarts after a crash.');
445
+ printHumanReadableLine(`The daemon now starts at ${result.service === 'schtasks' ? 'logon (Task Scheduler)' : 'login'} and restarts after a crash.`);
436
446
  if (result.service === 'systemd') {
437
447
  printHumanReadableLine('To keep it running after you log out, run `loginctl enable-linger`.');
438
448
  }
@@ -482,6 +492,9 @@ async function runDaemonStatus() {
482
492
  }
483
493
  printDaemonStatusResult(result);
484
494
  }
495
+ async function runDaemonSupervise(context) {
496
+ process.exitCode = await superviseCurrentDaemon(parsePort(context.options.port));
497
+ }
485
498
  async function runDaemonInstall(context) {
486
499
  const result = await installDaemonService(parsePort(context.options.port));
487
500
  if (getRequestedStructuredOutputFormat()) {
@@ -620,6 +633,24 @@ export const daemonCommandGroup = {
620
633
  },
621
634
  action: runDaemonUninstall,
622
635
  },
636
+ {
637
+ name: 'supervise',
638
+ description: 'Run the daemon in the foreground and restart it after a crash (used by the Windows logon task)',
639
+ options: [{ flags: '--port <number>', description: 'Legacy compatibility field; daemon listens on a private local socket', type: 'number', defaultValue: DEFAULT_DAEMON_PORT }],
640
+ schema: {
641
+ path: 'daemon.supervise',
642
+ risk: 'write',
643
+ auth: 'none',
644
+ globalOptions: [],
645
+ output: {
646
+ type: 'structured',
647
+ summary: 'Runs until the daemon exits cleanly or the supervisor is stopped; no structured payload',
648
+ fields: [],
649
+ },
650
+ examples: ['dino daemon supervise'],
651
+ },
652
+ action: runDaemonSupervise,
653
+ },
623
654
  ],
624
655
  };
625
656
  export function registerDaemonCommands(program) {
@@ -14,7 +14,11 @@ export type DaemonLaunch = {
14
14
  * of its own environment can have.
15
15
  */
16
16
  export declare function envTokenOverridesLogin(raw: DinoxConfig, env?: NodeJS.ProcessEnv): boolean;
17
- /** What the service definition runs: this CLI, with no secrets written to disk. */
17
+ /**
18
+ * What the service definition runs: this CLI, with no secrets written to disk.
19
+ * Task Scheduler cannot restart a crashed process, so on Windows the service
20
+ * runs the supervisor, which runs the daemon.
21
+ */
18
22
  export declare function daemonServiceSpec(service: DaemonService, port: number): DaemonServiceSpec;
19
23
  /** The installed service when this caller can use it, else null. */
20
24
  export declare function usableDaemonService(): Promise<DaemonService | null>;
@@ -6,17 +6,19 @@ import { DinoxError } from '../utils/errors.js';
6
6
  import { buildDaemonEnv } from './env.js';
7
7
  import { closeDaemonLogFd, getDaemonLogPath, openDaemonLogForChild } from './log.js';
8
8
  import { ensureDaemonDir } from './pid.js';
9
- import { DAEMON_SERVICE_ENV, getDaemonService, serviceDefinitionIsStale, } from './supervisor.js';
9
+ import { DAEMON_SERVICE_ENV, getDaemonService, } from './supervisor.js';
10
10
  /** Tells a daemon child its parent holds the start lock (or that it runs under a service). */
11
11
  export const DAEMON_CHILD_ENV = 'DINOX_INTERNAL_DAEMON_CHILD';
12
- function daemonCommandLine(port) {
12
+ function daemonCommandLine(port, mode = 'start') {
13
13
  const entrypoint = process.argv[1];
14
14
  if (!entrypoint) {
15
15
  throw new DinoxError('Cannot determine the CLI entrypoint for daemon startup', { code: 'DAEMON_START_FAILED' });
16
16
  }
17
17
  return {
18
18
  program: process.execPath,
19
- args: [...process.execArgv, entrypoint, 'daemon', 'start', '--port', String(port), '--no-detach'],
19
+ args: mode === 'supervise'
20
+ ? [...process.execArgv, entrypoint, 'daemon', 'supervise', '--port', String(port)]
21
+ : [...process.execArgv, entrypoint, 'daemon', 'start', '--port', String(port), '--no-detach'],
20
22
  };
21
23
  }
22
24
  /**
@@ -28,9 +30,13 @@ export function envTokenOverridesLogin(raw, env = process.env) {
28
30
  const envToken = readEnvAuthToken(env);
29
31
  return Boolean(envToken && envToken !== normalizeAuthToken(raw.auth?.authorization));
30
32
  }
31
- /** What the service definition runs: this CLI, with no secrets written to disk. */
33
+ /**
34
+ * What the service definition runs: this CLI, with no secrets written to disk.
35
+ * Task Scheduler cannot restart a crashed process, so on Windows the service
36
+ * runs the supervisor, which runs the daemon.
37
+ */
32
38
  export function daemonServiceSpec(service, port) {
33
- const { program, args } = daemonCommandLine(port);
39
+ const { program, args } = daemonCommandLine(port, service.kind === 'schtasks' ? 'supervise' : 'start');
34
40
  const env = buildDaemonEnv();
35
41
  delete env[DINOX_TOKEN_ENV_VAR];
36
42
  env[DAEMON_SERVICE_ENV] = service.kind;
@@ -55,6 +61,8 @@ function spawnDetached(port) {
55
61
  stdio: ['ignore', log, log],
56
62
  env: { ...buildDaemonEnv(), [DAEMON_CHILD_ENV]: '1' },
57
63
  cwd: os.homedir(),
64
+ // A detached Windows child otherwise opens its own console window.
65
+ windowsHide: true,
58
66
  });
59
67
  child.unref();
60
68
  return { service: null, abort: () => child.kill('SIGTERM') };
@@ -75,7 +83,7 @@ export async function launchDaemon(port) {
75
83
  return spawnDetached(port);
76
84
  ensureDaemonDir();
77
85
  const spec = daemonServiceSpec(service, port);
78
- if (serviceDefinitionIsStale(service, spec)) {
86
+ if (!service.isCurrent(spec)) {
79
87
  await service.install(spec);
80
88
  }
81
89
  else {
@@ -10,3 +10,5 @@ export declare function openDaemonLogForChild(): number | 'ignore';
10
10
  export declare function closeDaemonLogFd(fd: number | 'ignore'): void;
11
11
  /** One timestamped line on stderr, which a daemon child has pointed at its log. */
12
12
  export declare function writeDaemonLogLine(scope: string, message: string, details?: Record<string, unknown>): void;
13
+ /** One timestamped line appended to the log file itself, for processes with no usable stderr. */
14
+ export declare function appendDaemonLogLine(scope: string, message: string, details?: Record<string, unknown>): void;
@@ -1,4 +1,4 @@
1
- import { closeSync, copyFileSync, openSync, statSync, truncateSync } from 'node:fs';
1
+ import { appendFileSync, closeSync, copyFileSync, openSync, statSync, truncateSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ensureDaemonDir, getDaemonDir } from './pid.js';
4
4
  const MAX_DAEMON_LOG_BYTES = 5 * 1024 * 1024;
@@ -37,8 +37,21 @@ export function closeDaemonLogFd(fd) {
37
37
  if (fd !== 'ignore')
38
38
  closeSync(fd);
39
39
  }
40
+ function formatLogLine(scope, message, details) {
41
+ const suffix = details ? ` ${JSON.stringify(details)}` : '';
42
+ return `${new Date().toISOString()} [${scope}] ${message}${suffix}\n`;
43
+ }
40
44
  /** One timestamped line on stderr, which a daemon child has pointed at its log. */
41
45
  export function writeDaemonLogLine(scope, message, details) {
42
- const suffix = details ? ` ${JSON.stringify(details)}` : '';
43
- process.stderr.write(`${new Date().toISOString()} [${scope}] ${message}${suffix}\n`);
46
+ process.stderr.write(formatLogLine(scope, message, details));
47
+ }
48
+ /** One timestamped line appended to the log file itself, for processes with no usable stderr. */
49
+ export function appendDaemonLogLine(scope, message, details) {
50
+ try {
51
+ ensureDaemonDir();
52
+ appendFileSync(getDaemonLogPath(), formatLogLine(scope, message, details), { mode: 0o600 });
53
+ }
54
+ catch {
55
+ // Logging is best effort.
56
+ }
44
57
  }
@@ -1,6 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { createServer } from 'http';
3
3
  import { chmodSync, existsSync } from 'node:fs';
4
+ import path from 'node:path';
4
5
  import { DAEMON_SERVICE_ENV } from './supervisor.js';
5
6
  import { writeDaemonLogLine } from './log.js';
6
7
  import { loadConfig } from '../config/store.js';
@@ -21,7 +22,7 @@ import { readConversation } from '../gateway/conversationContext.js';
21
22
  const MAX_DAEMON_REQUEST_BYTES = 1 * 1024 * 1024;
22
23
  function daemonSupervisor() {
23
24
  const value = process.env[DAEMON_SERVICE_ENV];
24
- return value === 'launchd' || value === 'systemd' ? value : null;
25
+ return value === 'launchd' || value === 'systemd' || value === 'schtasks' ? value : null;
25
26
  }
26
27
  const DAEMON_PROCESS_VERSION = getPackageVersion();
27
28
  async function readJsonBody(req) {
@@ -183,6 +184,21 @@ export class DaemonServer {
183
184
  }
184
185
  return { id: req.id, ok: true, result: { gateways: await this.gateways.reload() } };
185
186
  }
187
+ case 'gateway.sendFile': {
188
+ if (!this.gateways) {
189
+ throw new DinoxError('Gateways need a signed-in daemon', { code: 'AUTH_REQUIRED' });
190
+ }
191
+ const { token, path: filePath, name } = (params ?? {});
192
+ if (typeof token !== 'string' || !token || typeof filePath !== 'string' || !path.isAbsolute(filePath)) {
193
+ throw new DinoxError('gateway.sendFile needs a session token and an absolute path', { code: 'USAGE_ERROR' });
194
+ }
195
+ const sent = await this.gateways.sendFile(token, {
196
+ kind: 'path',
197
+ path: filePath,
198
+ ...(typeof name === 'string' && name.trim() ? { name: name.trim() } : {}),
199
+ });
200
+ return { id: req.id, ok: true, result: sent };
201
+ }
186
202
  case 'thread.messages': {
187
203
  if (!this.userId) {
188
204
  throw new DinoxError('Reading conversations needs a signed-in daemon', { code: 'AUTH_REQUIRED' });
@@ -0,0 +1,29 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ /** First restart delay after a crash; doubles per consecutive crash. */
3
+ export declare const SUPERVISE_INITIAL_BACKOFF_MS = 1000;
4
+ export declare const SUPERVISE_MAX_BACKOFF_MS = 60000;
5
+ /** A daemon that ran this long counts as healthy, so the next crash restarts quickly again. */
6
+ export declare const SUPERVISE_HEALTHY_RUN_MS = 60000;
7
+ export type SupervisedExit = {
8
+ code: number | null;
9
+ signal: NodeJS.Signals | null;
10
+ };
11
+ export type SuperviseDependencies = {
12
+ startChild: () => ChildProcess;
13
+ sleep: (ms: number) => Promise<void>;
14
+ now: () => number;
15
+ log: (message: string, details?: Record<string, unknown>) => void;
16
+ /** Registers a handler for a stop request (signal); the loop then stops the child and returns. */
17
+ onStopRequest: (handler: () => void) => void;
18
+ };
19
+ /**
20
+ * Keeps one daemon running, the way launchd's KeepAlive.SuccessfulExit=false
21
+ * and systemd's Restart=on-failure do: restarts it after a crash with an
22
+ * exponential backoff, and returns once it exits cleanly (exit code 0 from
23
+ * `dino daemon stop`, an identity restart, or a stop for exclusive DB access)
24
+ * or when asked to stop. Used where the service manager cannot restart the
25
+ * daemon itself (Windows Task Scheduler).
26
+ */
27
+ export declare function superviseDaemon(deps: SuperviseDependencies): Promise<number>;
28
+ /** Runs the supervisor for this CLI's daemon until it exits cleanly or is signalled. */
29
+ export declare function superviseCurrentDaemon(port: number): Promise<number>;
@@ -0,0 +1,98 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { appendDaemonLogLine, closeDaemonLogFd, openDaemonLogForChild } from './log.js';
3
+ /** First restart delay after a crash; doubles per consecutive crash. */
4
+ export const SUPERVISE_INITIAL_BACKOFF_MS = 1_000;
5
+ export const SUPERVISE_MAX_BACKOFF_MS = 60_000;
6
+ /** A daemon that ran this long counts as healthy, so the next crash restarts quickly again. */
7
+ export const SUPERVISE_HEALTHY_RUN_MS = 60_000;
8
+ function waitForExit(child) {
9
+ return new Promise((resolve) => {
10
+ let settled = false;
11
+ const settle = (exit) => {
12
+ if (settled)
13
+ return;
14
+ settled = true;
15
+ resolve(exit);
16
+ };
17
+ child.once('exit', (code, signal) => settle({ code, signal }));
18
+ // A child that cannot be spawned at all counts as a crash.
19
+ child.once('error', () => settle({ code: 1, signal: null }));
20
+ });
21
+ }
22
+ /**
23
+ * Keeps one daemon running, the way launchd's KeepAlive.SuccessfulExit=false
24
+ * and systemd's Restart=on-failure do: restarts it after a crash with an
25
+ * exponential backoff, and returns once it exits cleanly (exit code 0 from
26
+ * `dino daemon stop`, an identity restart, or a stop for exclusive DB access)
27
+ * or when asked to stop. Used where the service manager cannot restart the
28
+ * daemon itself (Windows Task Scheduler).
29
+ */
30
+ export async function superviseDaemon(deps) {
31
+ let stopping = false;
32
+ let current = null;
33
+ let wakeSleep = null;
34
+ deps.onStopRequest(() => {
35
+ stopping = true;
36
+ current?.kill('SIGTERM');
37
+ wakeSleep?.();
38
+ });
39
+ let backoffMs = SUPERVISE_INITIAL_BACKOFF_MS;
40
+ while (!stopping) {
41
+ const startedAt = deps.now();
42
+ const child = deps.startChild();
43
+ current = child;
44
+ deps.log('daemon started', { pid: child.pid ?? null });
45
+ const exit = await waitForExit(child);
46
+ current = null;
47
+ if (stopping)
48
+ break;
49
+ if (exit.code === 0) {
50
+ deps.log('daemon exited cleanly; supervisor stopping');
51
+ return 0;
52
+ }
53
+ if (deps.now() - startedAt >= SUPERVISE_HEALTHY_RUN_MS)
54
+ backoffMs = SUPERVISE_INITIAL_BACKOFF_MS;
55
+ deps.log('daemon crashed; restarting', { code: exit.code, signal: exit.signal, delayMs: backoffMs });
56
+ await Promise.race([
57
+ deps.sleep(backoffMs),
58
+ new Promise((resolve) => {
59
+ wakeSleep = resolve;
60
+ }),
61
+ ]);
62
+ wakeSleep = null;
63
+ backoffMs = Math.min(backoffMs * 2, SUPERVISE_MAX_BACKOFF_MS);
64
+ }
65
+ deps.log('supervisor stopped on request');
66
+ return 0;
67
+ }
68
+ /** Runs the supervisor for this CLI's daemon until it exits cleanly or is signalled. */
69
+ export async function superviseCurrentDaemon(port) {
70
+ const entrypoint = process.argv[1];
71
+ if (!entrypoint)
72
+ throw new Error('Cannot determine the CLI entrypoint for the daemon');
73
+ // Launched hidden at logon there is no console, so write to the log file directly.
74
+ const log = (message, details) => appendDaemonLogLine('dinox-supervisor', message, details);
75
+ return superviseDaemon({
76
+ startChild: () => {
77
+ const logFd = openDaemonLogForChild();
78
+ try {
79
+ return spawn(process.execPath, [...process.execArgv, entrypoint, 'daemon', 'start', '--port', String(port), '--no-detach'],
80
+ // Same (already clean) environment, which carries the service marker.
81
+ { stdio: ['ignore', logFd, logFd], env: process.env, windowsHide: true });
82
+ }
83
+ finally {
84
+ closeDaemonLogFd(logFd);
85
+ }
86
+ },
87
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
88
+ now: () => Date.now(),
89
+ log,
90
+ onStopRequest: (handler) => {
91
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGBREAK']) {
92
+ process.on(signal, handler);
93
+ }
94
+ // Like the daemon, never stop on a hangup.
95
+ process.on('SIGHUP', () => undefined);
96
+ },
97
+ });
98
+ }
@@ -7,9 +7,11 @@
7
7
  *
8
8
  * Restart policy is "on failure" only: a clean exit (`dino daemon stop`, a
9
9
  * version/account restart, exclusive DB access for `doctor`) leaves it
10
- * stopped until the CLI starts it again through the manager.
10
+ * stopped until the CLI starts it again through the manager. launchd and
11
+ * systemd restart the daemon themselves; on Windows, Task Scheduler only
12
+ * starts `dino daemon supervise`, which restarts it (see supervise.ts).
11
13
  */
12
- export type DaemonServiceKind = 'launchd' | 'systemd';
14
+ export type DaemonServiceKind = 'launchd' | 'systemd' | 'schtasks';
13
15
  export type DaemonServiceSpec = {
14
16
  program: string;
15
17
  args: string[];
@@ -29,6 +31,8 @@ export type DaemonService = {
29
31
  readonly definitionPath: string;
30
32
  render(spec: DaemonServiceSpec): string;
31
33
  isInstalled(): boolean;
34
+ /** True when the installed definition is exactly what this CLI would write. */
35
+ isCurrent(spec: DaemonServiceSpec): boolean;
32
36
  /** Writes the definition and (re)loads it, which starts the daemon. */
33
37
  install(spec: DaemonServiceSpec): Promise<void>;
34
38
  /** Starts the installed daemon when it is not running. */
@@ -44,16 +48,28 @@ export declare const runServiceCommand: ServiceCommandRunner;
44
48
  * non-default `DINOX_DATA_DIR` gets its own service.
45
49
  */
46
50
  export declare function daemonServiceLabel(env?: NodeJS.ProcessEnv): string;
47
- /** True when the installed definition differs from what this CLI would write. */
48
- export declare function serviceDefinitionIsStale(service: DaemonService, spec: DaemonServiceSpec): boolean;
49
51
  export declare function renderLaunchdPlist(label: string, spec: DaemonServiceSpec): string;
50
52
  export declare function renderSystemdUnit(spec: DaemonServiceSpec): string;
53
+ /** Quotes one argument the way CommandLineToArgvW (and so Node) parses it back. */
54
+ export declare function quoteWindowsArg(value: string): string;
55
+ /**
56
+ * Task Scheduler runs console programs in a visible window, so the task runs
57
+ * this script with wscript, which sets the daemon environment and starts the
58
+ * supervisor with a hidden window (the launcher pattern pm2-windows-startup
59
+ * uses). The supervisor then outlives the script.
60
+ */
61
+ export declare function renderWindowsLauncher(spec: DaemonServiceSpec): string;
62
+ export declare function renderWindowsTaskXml(userId: string, wscriptPath: string, launcherPath: string): string;
51
63
  export type DaemonServiceOptions = {
52
64
  platform?: NodeJS.Platform;
53
65
  env?: NodeJS.ProcessEnv;
54
66
  homeDir?: string;
55
67
  uid?: number;
56
68
  run?: ServiceCommandRunner;
69
+ /** Windows: where the launcher script lives (the daemon directory). */
70
+ launcherDir?: string;
71
+ /** Windows: `DOMAIN\\user` the logon task runs as. */
72
+ windowsUserId?: string;
57
73
  };
58
- /** The service manager for this platform, or null where none is supported (Windows). */
74
+ /** The service manager for this platform, or null where none is supported. */
59
75
  export declare function getDaemonService(options?: DaemonServiceOptions): DaemonService | null;