@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 +1 -0
- package/dist/commands/auth/index.js +8 -2
- package/dist/commands/gateway/index.js +14 -4
- package/dist/daemon/autoInstall.d.ts +32 -0
- package/dist/daemon/autoInstall.js +106 -0
- package/package.json +1 -1
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
|
-
/**
|
|
60
|
-
|
|
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
|
+
}
|