@dinoxx/dinox-cli 2.8.1 → 2.9.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/README.md CHANGED
@@ -1135,6 +1135,7 @@ dino schema todo.update --format json
1135
1135
  | `DINOX_DATA_DIR` | 覆盖本地数据目录 |
1136
1136
  | `DINOX_TOKEN` | 用环境变量注入登录 token,优先级高于本地配置;可传 token 本体或 `Bearer ...` |
1137
1137
  | `DINOX_SYNC_TIMEOUT_MS` | 覆盖同步超时 |
1138
+ | `DINOX_DAEMON_AUTO_INSTALL` | 设为 `0` 时,`auth login` / `gateway add` 不再自动把 daemon 安装为登录服务 |
1138
1139
 
1139
1140
  ### 9.3 macOS / Linux 与 Windows 的多行命令差异
1140
1141
 
@@ -6,6 +6,7 @@ import { resolveConfig } from '../../config/resolve.js';
6
6
  import { getDaemonInfo } from '../../daemon/client.js';
7
7
  import { restoreSuspendedDaemonLocked, suspendDaemonForExclusiveAccessLocked, } from '../../daemon/lifecycle.js';
8
8
  import { stopDaemonLocked } from '../../daemon/index.js';
9
+ import { autoInstallDaemonService, describeDaemonServiceAutoInstall } from '../../daemon/autoInstall.js';
9
10
  import { acquireDaemonStartLock } from '../../daemon/pid.js';
10
11
  import { fetchCredentialsWithAuthorization } from '../../powersync/connector.js';
11
12
  import { withExclusivePowerSync } from '../../powersync/exclusiveRuntime.js';
@@ -244,8 +245,10 @@ async function runAuthLogin(context) {
244
245
  }
245
246
  const structuredOutput = getRequestedStructuredOutputFormat() !== null;
246
247
  const result = await performAuthLogin(rawToken, context.globals.syncTimeout);
248
+ // After the login lock is released: the service starts the daemon under the new login.
249
+ const daemonService = await autoInstallDaemonService();
247
250
  if (structuredOutput) {
248
- printSuccess(result);
251
+ printSuccess({ ...result, daemonService });
249
252
  return;
250
253
  }
251
254
  printHumanReadableLine('Login verified.');
@@ -258,6 +261,9 @@ async function runAuthLogin(context) {
258
261
  if (result.stale) {
259
262
  printHumanReadableLine('- warning: initial sync timed out; local cache may be stale');
260
263
  }
264
+ const serviceNote = describeDaemonServiceAutoInstall(daemonService);
265
+ if (serviceNote)
266
+ printHumanReadableWarning(serviceNote);
261
267
  }
262
268
  async function runAuthLogoutLocked(options) {
263
269
  const config = await loadConfig();
@@ -494,7 +500,7 @@ export const authCommandGroup = {
494
500
  output: {
495
501
  type: 'structured',
496
502
  summary: 'Login verification result',
497
- fields: ['verified', 'endpoint', 'dbPath', 'stale', 'lastSyncedAt', 'userId'],
503
+ fields: ['verified', 'endpoint', 'dbPath', 'stale', 'lastSyncedAt', 'userId', 'daemonService'],
498
504
  },
499
505
  examples: ['dino auth login --token-stdin --format json'],
500
506
  },
@@ -1,6 +1,7 @@
1
1
  import { getDaemonInfo, isDaemonRunning, sendDaemonRequest } from '../../daemon/client.js';
2
2
  import { getDaemonService } from '../../daemon/supervisor.js';
3
3
  import { ensureDaemonRunning } from '../../daemon/ensure.js';
4
+ import { autoInstallDaemonService, describeDaemonServiceAutoInstall } from '../../daemon/autoInstall.js';
4
5
  import { loadConfig } from '../../config/store.js';
5
6
  import { addGateway, adoptDetectedAgent, getGatewaysFilePath, loadGateways, removeGateway, } from '../../gateway/config.js';
6
7
  import { detectAcpAgents, inferAgentId } from '../../gateway/detect.js';
@@ -56,8 +57,17 @@ async function warnIfDaemonUnmanaged() {
56
57
  printHumanReadableWarning('warning: gateways run in a daemon owned by the shell or agent session that started it, and go offline when that session ends. Run `dino daemon install` to keep them online.');
57
58
  }
58
59
  }
59
- /** Applies the account's gateways to the running daemon, starting it when needed. */
60
- async function applyToDaemon() {
60
+ /**
61
+ * Applies the account's gateways to the running daemon, starting it when needed.
62
+ * `keepOnline` first hands the daemon to the login service (once), so a gateway
63
+ * added from an agent session does not go offline when that session ends.
64
+ */
65
+ async function applyToDaemon(options = {}) {
66
+ if (options.keepOnline) {
67
+ const note = describeDaemonServiceAutoInstall(await autoInstallDaemonService());
68
+ if (note)
69
+ printHumanReadableWarning(note);
70
+ }
61
71
  let gateways;
62
72
  if (!(await isDaemonRunning())) {
63
73
  await ensureDaemonRunning();
@@ -98,7 +108,7 @@ async function runAdd(context) {
98
108
  cwd: context.options.cwd,
99
109
  env: parseEnv(context.options.env),
100
110
  });
101
- const statuses = await applyToDaemon();
111
+ const statuses = await applyToDaemon({ keepOnline: true });
102
112
  if (getRequestedStructuredOutputFormat()) {
103
113
  printSuccess({ gateway: describeGateway(gateway), status: statuses.find((item) => item.gatewayId === gateway.id) ?? null });
104
114
  return;
@@ -122,7 +132,7 @@ async function runDetect(context) {
122
132
  updated.push(result.gateway);
123
133
  }
124
134
  }
125
- const statuses = added.length || updated.length ? await applyToDaemon() : [];
135
+ const statuses = added.length || updated.length ? await applyToDaemon({ keepOnline: true }) : [];
126
136
  if (getRequestedStructuredOutputFormat()) {
127
137
  printSuccess({
128
138
  agents: detected,
@@ -0,0 +1,32 @@
1
+ import type { DinoxConfig } from '../config/types.js';
2
+ import { type DaemonService, type DaemonServiceKind } from './supervisor.js';
3
+ /** `0` / `false` / `off` / `no` keeps the CLI from installing the daemon service on its own. */
4
+ export declare const DAEMON_AUTO_INSTALL_ENV = "DINOX_DAEMON_AUTO_INSTALL";
5
+ export type DaemonServiceAutoInstallSkipReason = 'disabled' | 'ci' | 'custom-data-dir' | 'unsupported' | 'no-login' | 'env-token';
6
+ export type DaemonServiceAutoInstallResult = {
7
+ status: 'installed' | 'already-installed' | 'skipped' | 'failed';
8
+ service: DaemonServiceKind | null;
9
+ reason: DaemonServiceAutoInstallSkipReason | null;
10
+ error: string | null;
11
+ };
12
+ export type DaemonServiceAutoInstallDependencies = {
13
+ env: NodeJS.ProcessEnv;
14
+ getService: () => DaemonService | null;
15
+ loadConfig: () => Promise<DinoxConfig>;
16
+ install: () => Promise<unknown>;
17
+ /** Removes whatever a failed install left behind. */
18
+ rollback: () => Promise<unknown>;
19
+ };
20
+ /**
21
+ * Makes the daemon a login service without the user having to know it exists.
22
+ * Without one it belongs to whichever shell or agent session first started it
23
+ * and dies with that session. Called after `auth login` and when gateways are
24
+ * added; never throws, because neither of those should fail over it.
25
+ *
26
+ * Skipped where installing something that runs at login would surprise:
27
+ * an opt-out, CI, and custom data directories (tests, sandboxes, second
28
+ * profiles), which still get it from an explicit `dino daemon install`.
29
+ */
30
+ export declare function autoInstallDaemonService(dependencies?: DaemonServiceAutoInstallDependencies): Promise<DaemonServiceAutoInstallResult>;
31
+ /** One stderr line for a human caller; nothing when there is nothing to say. */
32
+ export declare function describeDaemonServiceAutoInstall(result: DaemonServiceAutoInstallResult): string | null;
@@ -0,0 +1,106 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { loadConfig } from '../config/store.js';
3
+ import { normalizeAuthToken } from '../utils/authToken.js';
4
+ import { DinoxError } from '../utils/errors.js';
5
+ import { installDaemonService, uninstallDaemonService } from './index.js';
6
+ import { envTokenOverridesLogin } from './launch.js';
7
+ import { getDaemonService } from './supervisor.js';
8
+ /** `0` / `false` / `off` / `no` keeps the CLI from installing the daemon service on its own. */
9
+ export const DAEMON_AUTO_INSTALL_ENV = 'DINOX_DAEMON_AUTO_INSTALL';
10
+ /**
11
+ * systemd --user is missing in WSL without systemd, most containers, and
12
+ * other non-systemd distros; `/run/systemd/system` is how sd_booted() tells.
13
+ */
14
+ function usableService() {
15
+ const service = getDaemonService();
16
+ if (service?.kind === 'systemd' && !existsSync('/run/systemd/system'))
17
+ return null;
18
+ return service;
19
+ }
20
+ const defaultDependencies = {
21
+ env: process.env,
22
+ getService: usableService,
23
+ loadConfig,
24
+ install: () => installDaemonService(),
25
+ rollback: () => uninstallDaemonService(),
26
+ };
27
+ function isFalsy(value) {
28
+ return ['0', 'false', 'off', 'no'].includes((value ?? '').trim().toLowerCase());
29
+ }
30
+ function isSet(value) {
31
+ return Boolean(value?.trim()) && !isFalsy(value);
32
+ }
33
+ function skipped(service, reason) {
34
+ return { status: 'skipped', service, reason, error: null };
35
+ }
36
+ /**
37
+ * Makes the daemon a login service without the user having to know it exists.
38
+ * Without one it belongs to whichever shell or agent session first started it
39
+ * and dies with that session. Called after `auth login` and when gateways are
40
+ * added; never throws, because neither of those should fail over it.
41
+ *
42
+ * Skipped where installing something that runs at login would surprise:
43
+ * an opt-out, CI, and custom data directories (tests, sandboxes, second
44
+ * profiles), which still get it from an explicit `dino daemon install`.
45
+ */
46
+ export async function autoInstallDaemonService(dependencies = defaultDependencies) {
47
+ const { env } = dependencies;
48
+ if (isFalsy(env[DAEMON_AUTO_INSTALL_ENV]))
49
+ return skipped(null, 'disabled');
50
+ if (isSet(env.CI))
51
+ return skipped(null, 'ci');
52
+ if (env.DINOX_DATA_DIR)
53
+ return skipped(null, 'custom-data-dir');
54
+ const service = dependencies.getService();
55
+ if (!service)
56
+ return skipped(null, 'unsupported');
57
+ if (service.isInstalled()) {
58
+ return { status: 'already-installed', service: service.kind, reason: null, error: null };
59
+ }
60
+ let raw;
61
+ try {
62
+ raw = await dependencies.loadConfig();
63
+ }
64
+ catch (error) {
65
+ return { status: 'failed', service: service.kind, reason: null, error: errorMessage(error) };
66
+ }
67
+ if (!normalizeAuthToken(raw.auth?.authorization))
68
+ return skipped(service.kind, 'no-login');
69
+ if (envTokenOverridesLogin(raw, env))
70
+ return skipped(service.kind, 'env-token');
71
+ try {
72
+ await dependencies.install();
73
+ return { status: 'installed', service: service.kind, reason: null, error: null };
74
+ }
75
+ catch (error) {
76
+ // Every service writes its definition before asking the manager to load it,
77
+ // and the CLI starts the daemon through any definition it finds. Left in
78
+ // place after a failure (no GUI session over SSH, no user bus, a policy
79
+ // blocking Task Scheduler), it would break the on-demand start that worked
80
+ // before, so restore the state from before this attempt.
81
+ let message = errorMessage(error);
82
+ try {
83
+ await dependencies.rollback();
84
+ }
85
+ catch (rollbackError) {
86
+ message += `; cleanup also failed (${errorMessage(rollbackError)}), run \`dino daemon uninstall\``;
87
+ }
88
+ if (error instanceof DinoxError && error.code === 'DAEMON_SERVICE_UNSUPPORTED') {
89
+ return { status: 'skipped', service: service.kind, reason: 'unsupported', error: message };
90
+ }
91
+ return { status: 'failed', service: service.kind, reason: null, error: message };
92
+ }
93
+ }
94
+ function errorMessage(error) {
95
+ return error instanceof Error ? error.message : String(error);
96
+ }
97
+ /** One stderr line for a human caller; nothing when there is nothing to say. */
98
+ export function describeDaemonServiceAutoInstall(result) {
99
+ if (result.status === 'installed') {
100
+ return `Background service installed (${result.service}): the daemon now starts at login and restarts after a crash. Remove it with \`dino daemon uninstall\`.`;
101
+ }
102
+ if (result.status === 'failed') {
103
+ return `warning: could not install the background service (${result.error}). The daemon still starts on demand; run \`dino daemon install\` to retry.`;
104
+ }
105
+ return null;
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dinoxx/dinox-cli",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "Dinox CLI",
5
5
  "main": "dist/dinox.js",
6
6
  "scripts": {