@dinoxx/dinox-cli 2.2.0 → 2.4.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 +17 -3
- package/dist/cli.js +6 -0
- package/dist/commands/domain/options.d.ts +10 -0
- package/dist/commands/domain/options.js +75 -0
- package/dist/commands/domain/run.d.ts +24 -0
- package/dist/commands/domain/run.js +137 -0
- package/dist/commands/gateway/index.d.ts +4 -0
- package/dist/commands/gateway/index.js +300 -0
- package/dist/commands/notes/actions.js +47 -18
- package/dist/commands/notes/createFromTemplate.d.ts +35 -0
- package/dist/commands/notes/createFromTemplate.js +58 -0
- package/dist/commands/notes/descriptors.js +9 -4
- package/dist/commands/notes/repo/read.js +4 -23
- package/dist/commands/notes/repo/write.d.ts +2 -0
- package/dist/commands/notes/repo/write.js +1 -1
- package/dist/commands/properties/index.d.ts +16 -0
- package/dist/commands/properties/index.js +640 -0
- package/dist/commands/properties/repo.d.ts +150 -0
- package/dist/commands/properties/repo.js +272 -0
- package/dist/commands/savedViews/fields.d.ts +28 -0
- package/dist/commands/savedViews/fields.js +87 -0
- package/dist/commands/savedViews/index.js +203 -164
- package/dist/commands/savedViews/model.d.ts +27 -69
- package/dist/commands/savedViews/model.js +63 -279
- package/dist/commands/savedViews/repo.d.ts +64 -62
- package/dist/commands/savedViews/repo.js +309 -428
- package/dist/commands/schema/renderSkills.js +33 -5
- package/dist/commands/schema/spec.js +6 -0
- package/dist/commands/templates/index.d.ts +4 -0
- package/dist/commands/templates/index.js +290 -0
- package/dist/commands/templates/model.d.ts +12 -34
- package/dist/commands/templates/model.js +39 -83
- package/dist/commands/templates/repo.d.ts +53 -0
- package/dist/commands/templates/repo.js +78 -0
- package/dist/config/paths.d.ts +1 -0
- package/dist/config/paths.js +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.js +31 -0
- package/dist/gateway/adapters/acp.d.ts +42 -0
- package/dist/gateway/adapters/acp.js +285 -0
- package/dist/gateway/adapters/echo.d.ts +12 -0
- package/dist/gateway/adapters/echo.js +51 -0
- package/dist/gateway/adapters/index.d.ts +9 -0
- package/dist/gateway/adapters/index.js +30 -0
- package/dist/gateway/assistantProjection.d.ts +32 -0
- package/dist/gateway/assistantProjection.js +193 -0
- package/dist/gateway/config.d.ts +50 -0
- package/dist/gateway/config.js +253 -0
- package/dist/gateway/detect.d.ts +40 -0
- package/dist/gateway/detect.js +145 -0
- package/dist/gateway/deviceHost.d.ts +82 -0
- package/dist/gateway/deviceHost.js +530 -0
- package/dist/gateway/mcpServer.d.ts +4 -0
- package/dist/gateway/mcpServer.js +118 -0
- package/dist/gateway/protocol.d.ts +29 -0
- package/dist/gateway/protocol.js +127 -0
- package/dist/gateway/runExecutor.d.ts +67 -0
- package/dist/gateway/runExecutor.js +316 -0
- package/dist/gateway/service.d.ts +26 -0
- package/dist/gateway/service.js +82 -0
- package/dist/gateway/streamPublisher.d.ts +33 -0
- package/dist/gateway/streamPublisher.js +93 -0
- package/dist/gateway/threadStore.d.ts +157 -0
- package/dist/gateway/threadStore.js +430 -0
- package/dist/gateway/types.d.ts +117 -0
- package/dist/gateway/types.js +9 -0
- package/dist/skills/registry.js +7 -0
- package/dist/utils/errorCodes.d.ts +212 -0
- package/dist/utils/errorCodes.js +53 -0
- package/dist/vendor/electron/domain/note-templates.d.ts +3 -0
- package/dist/vendor/electron/domain/note-templates.js +232 -0
- package/dist/vendor/electron/domain/properties.d.ts +10 -0
- package/dist/vendor/electron/domain/properties.js +1549 -0
- package/dist/vendor/electron/domain/saved-view-analytics.d.ts +45 -0
- package/dist/vendor/electron/domain/saved-view-analytics.js +365 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.d.ts +21 -0
- package/dist/vendor/electron/domain/saved-view-relative-date.js +153 -0
- package/dist/vendor/electron/domain/saved-views.d.ts +61 -0
- package/dist/vendor/electron/domain/saved-views.js +1120 -0
- package/dist/vendor/electron/rpc.d.ts +22 -0
- package/dist/vendor/electron/rpc.js +27 -0
- package/dist/vendor/electron/shared/ai-errors.d.ts +11 -0
- package/dist/vendor/electron/shared/ai-errors.js +107 -0
- package/dist/vendor/electron/shared/external-types.d.ts +20 -0
- package/dist/vendor/electron/shared/external-types.js +1 -0
- package/dist/vendor/electron/shared/index.d.ts +9 -0
- package/dist/vendor/electron/shared/index.js +7 -0
- package/dist/vendor/electron/shared/note-properties.d.ts +328 -0
- package/dist/vendor/electron/shared/note-properties.js +1027 -0
- package/dist/vendor/electron/shared/note-templates.d.ts +82 -0
- package/dist/vendor/electron/shared/note-templates.js +84 -0
- package/dist/vendor/electron/shared/powersync-read-models.d.ts +1716 -0
- package/dist/vendor/electron/shared/powersync-read-models.js +1 -0
- package/dist/vendor/electron/shared/remote-device-agent.d.ts +373 -0
- package/dist/vendor/electron/shared/remote-device-agent.js +191 -0
- package/dist/vendor/electron/shared/saved-views.d.ts +394 -0
- package/dist/vendor/electron/shared/saved-views.js +981 -0
- package/dist/vendor/electron/sql-utils.d.ts +5 -0
- package/dist/vendor/electron/sql-utils.js +32 -0
- package/package.json +10 -4
- package/dist/commands/savedViews/semantics.d.ts +0 -46
- package/dist/commands/savedViews/semantics.js +0 -738
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Dinox CLI 适合这几类场景:
|
|
|
52
52
|
|
|
53
53
|
安装前请先确认:
|
|
54
54
|
|
|
55
|
-
- 推荐使用 Node.js 24 LTS;支持 Node.js
|
|
55
|
+
- 推荐使用 Node.js 24 LTS;支持 Node.js 22、23、24、25(不支持 Node.js 21 及以下或 26+)
|
|
56
56
|
- 已安装 npm
|
|
57
57
|
- 你的系统可以正常执行全局 npm 命令
|
|
58
58
|
|
|
@@ -937,13 +937,27 @@ daemon 通过用户数据目录下的私有 Unix domain socket 通信,Windows
|
|
|
937
937
|
|
|
938
938
|
需要在当前进程打开 PowerSync SQLite 的命令(例如写命令、`sync`、在线 `auth status`、`--offline` 或 `--require-sync` 读取)会先取得跨进程生命周期锁。如果同一用户数据库正由健康 daemon 持有,CLI 会暂时停止该实例;只有在命令结束、数据库关闭且 owner lease 成功释放后,才会按原端口恢复它。使用其他用户数据库的 daemon 不会被停止;如果关闭或释放失败,CLI 会保留 daemon 为停止状态并在 stderr 报告警告,避免两个运行时同时持有同一 SQLite 数据库。
|
|
939
939
|
|
|
940
|
-
### 6.15 `
|
|
940
|
+
### 6.15 `gateway`:把本机 Agent 挂成远程设备
|
|
941
|
+
|
|
942
|
+
```bash
|
|
943
|
+
dino gateway detect
|
|
944
|
+
dino gateway detect --add
|
|
945
|
+
dino gateway add Hermes --command hermes --arg acp
|
|
946
|
+
dino gateway list
|
|
947
|
+
dino gateway status
|
|
948
|
+
dino gateway reload
|
|
949
|
+
dino gateway remove Hermes
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
`detect` 会在 `PATH` 上查找 ACP agent,并用真实握手确认能否启动。`--add` 把已确认的 agent 写入当前账号的 gateway 配置,并由 daemon 托管。`gateway list` 只显示环境变量名,不显示值。`dino gateway mcp` 由这些 agent 拉起,对外提供只读的 Dinox 知识工具。
|
|
953
|
+
|
|
954
|
+
### 6.16 `info`:查看版本与技能仓库地址
|
|
941
955
|
|
|
942
956
|
```bash
|
|
943
957
|
dino info
|
|
944
958
|
```
|
|
945
959
|
|
|
946
|
-
### 6.
|
|
960
|
+
### 6.17 `update`:升级 CLI
|
|
947
961
|
|
|
948
962
|
```bash
|
|
949
963
|
dino update
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { registerAuthCommands } from './commands/auth/index.js';
|
|
|
3
3
|
import { registerBoxesCommand } from './commands/boxes/index.js';
|
|
4
4
|
import { registerConfigCommands } from './commands/config/index.js';
|
|
5
5
|
import { registerDaemonCommands } from './daemon/index.js';
|
|
6
|
+
import { registerGatewayCommands } from './commands/gateway/index.js';
|
|
6
7
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
7
8
|
import { registerGraphCommands } from './commands/graph/index.js';
|
|
8
9
|
import { registerInfoCommand } from './commands/info/index.js';
|
|
@@ -10,12 +11,14 @@ import { registerMetaCommands } from './commands/meta/index.js';
|
|
|
10
11
|
import { registerNoteCommands } from './commands/notes/index.js';
|
|
11
12
|
import { registerPromptCommands } from './commands/prompt/index.js';
|
|
12
13
|
import { registerSchemaCommands } from './commands/schema/index.js';
|
|
14
|
+
import { registerPropertyCommands } from './commands/properties/index.js';
|
|
13
15
|
import { registerSavedViewCommands } from './commands/savedViews/index.js';
|
|
14
16
|
import { registerSkillsCommands } from './commands/skills/index.js';
|
|
15
17
|
import { registerSyncCommand } from './commands/sync.js';
|
|
16
18
|
import { registerStorageCommands } from './commands/storage/index.js';
|
|
17
19
|
import { registerUpdateCommand } from './commands/update.js';
|
|
18
20
|
import { registerTagsCommand } from './commands/tags/index.js';
|
|
21
|
+
import { registerTemplateCommands } from './commands/templates/index.js';
|
|
19
22
|
import { registerTodoCommands } from './commands/todo/index.js';
|
|
20
23
|
import { getPackageVersion } from './utils/version.js';
|
|
21
24
|
import { buildStructuredErrorPayload, DinoxError } from './utils/errors.js';
|
|
@@ -77,6 +80,7 @@ export function createCli() {
|
|
|
77
80
|
registerBoxesCommand(program);
|
|
78
81
|
registerConfigCommands(program);
|
|
79
82
|
registerDaemonCommands(program);
|
|
83
|
+
registerGatewayCommands(program);
|
|
80
84
|
registerDoctorCommand(program);
|
|
81
85
|
registerGraphCommands(program);
|
|
82
86
|
registerInfoCommand(program);
|
|
@@ -85,6 +89,8 @@ export function createCli() {
|
|
|
85
89
|
registerUpdateCommand(program);
|
|
86
90
|
registerPromptCommands(program);
|
|
87
91
|
registerSavedViewCommands(program);
|
|
92
|
+
registerPropertyCommands(program);
|
|
93
|
+
registerTemplateCommands(program);
|
|
88
94
|
registerSchemaCommands(program);
|
|
89
95
|
registerSkillsCommands(program);
|
|
90
96
|
registerStorageCommands(program);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function parseIntegerOption(value: string): number;
|
|
2
|
+
export declare function parsePositiveIntegerOption(value: string): number;
|
|
3
|
+
export declare function parseNonNegativeIntegerOption(value: string): number;
|
|
4
|
+
/** Commander collector for repeatable options. */
|
|
5
|
+
export declare function collectOption(value: string, previous: unknown): string[];
|
|
6
|
+
export declare function parseTimeZoneOption(value: string): string;
|
|
7
|
+
/** Reads an inline JSON value or `@file` and parses it. */
|
|
8
|
+
export declare function readJsonOption(value: unknown, label: string, schemaPath: string): Promise<unknown>;
|
|
9
|
+
export declare function requiredArgument(value: string | undefined, label: string): string;
|
|
10
|
+
export declare function assertExclusive(conflicts: Array<[boolean, string]>): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Option parsers shared by the commands backed by the vendored desktop domain
|
|
2
|
+
// (view, prop, template).
|
|
3
|
+
import { readStringOrFile } from '../../utils/argValue.js';
|
|
4
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
5
|
+
export function parseIntegerOption(value) {
|
|
6
|
+
const normalized = value.trim();
|
|
7
|
+
if (!/^-?\d+$/.test(normalized)) {
|
|
8
|
+
throw new DinoxError('Expected an integer', { code: 'USAGE_ERROR' });
|
|
9
|
+
}
|
|
10
|
+
const parsed = Number(normalized);
|
|
11
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
12
|
+
throw new DinoxError('Expected a safe integer', { code: 'USAGE_ERROR' });
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
export function parsePositiveIntegerOption(value) {
|
|
17
|
+
const parsed = parseIntegerOption(value);
|
|
18
|
+
if (parsed <= 0)
|
|
19
|
+
throw new DinoxError('Expected a positive integer', { code: 'USAGE_ERROR' });
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
export function parseNonNegativeIntegerOption(value) {
|
|
23
|
+
const parsed = parseIntegerOption(value);
|
|
24
|
+
if (parsed < 0)
|
|
25
|
+
throw new DinoxError('Expected a non-negative integer', { code: 'USAGE_ERROR' });
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
/** Commander collector for repeatable options. */
|
|
29
|
+
export function collectOption(value, previous) {
|
|
30
|
+
return [...(Array.isArray(previous) ? previous : []), value];
|
|
31
|
+
}
|
|
32
|
+
export function parseTimeZoneOption(value) {
|
|
33
|
+
const normalized = value.trim();
|
|
34
|
+
try {
|
|
35
|
+
new Intl.DateTimeFormat('en-US', { timeZone: normalized });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new DinoxError(`Invalid IANA time zone: ${value}`, { code: 'SAVED_VIEW_INVALID_TIME_ZONE' });
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
/** Reads an inline JSON value or `@file` and parses it. */
|
|
43
|
+
export async function readJsonOption(value, label, schemaPath) {
|
|
44
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
45
|
+
throw new DinoxError(`${label} requires inline JSON or @file`, { code: 'USAGE_ERROR' });
|
|
46
|
+
}
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = await readStringOrFile(value);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new DinoxError(`Failed to read ${label}`, { code: 'USAGE_ERROR', cause: error });
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(raw);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new DinoxError(`${label} must be valid JSON`, {
|
|
59
|
+
code: 'USAGE_ERROR',
|
|
60
|
+
cause: error,
|
|
61
|
+
suggestion: `Pass inline JSON or use @file. See \`dino schema ${schemaPath} --format json\`.`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function requiredArgument(value, label) {
|
|
66
|
+
const normalized = value?.trim();
|
|
67
|
+
if (!normalized)
|
|
68
|
+
throw new DinoxError(`${label} is required`, { code: 'USAGE_ERROR' });
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
export function assertExclusive(conflicts) {
|
|
72
|
+
const conflict = conflicts.find(([active]) => active);
|
|
73
|
+
if (conflict)
|
|
74
|
+
throw new DinoxError(conflict[1], { code: 'USAGE_ERROR' });
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type z } from 'zod';
|
|
3
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
4
|
+
export type DomainCommandResult<T> = {
|
|
5
|
+
result: T;
|
|
6
|
+
affectedTables: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Converts a desktop domain failure into a DinoxError; DinoxErrors pass through unchanged. */
|
|
9
|
+
export declare function toDinoxError(error: unknown, label?: string): DinoxError;
|
|
10
|
+
export declare function parseDomainInput<T>(definition: {
|
|
11
|
+
inputSchema: z.ZodType<T>;
|
|
12
|
+
}, input: unknown, label: string): T;
|
|
13
|
+
export declare function runDomainCommand<T>(db: AbstractPowerSyncDatabase, name: string, input: unknown): Promise<DomainCommandResult<T>>;
|
|
14
|
+
export declare function fetchDomainReadModel<T>(db: AbstractPowerSyncDatabase, name: string, input: unknown): Promise<T[]>;
|
|
15
|
+
/** Runs `run` in one read transaction (a consistent snapshot) when the database offers it. */
|
|
16
|
+
export declare function withReadTransaction<T>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<T>): Promise<T>;
|
|
17
|
+
/** Runs `run` in one write transaction; vendored commands called inside reuse it. */
|
|
18
|
+
export declare function withNestedWriteTransaction<T>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<T>): Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Executes `run` and then `inspect` inside one write transaction that is always rolled
|
|
21
|
+
* back, so a preview reflects exactly what the real write would do (including desktop
|
|
22
|
+
* validation and derived values) without persisting or queueing an upload.
|
|
23
|
+
*/
|
|
24
|
+
export declare function previewInRolledBackTransaction<TResult, TPreview>(db: AbstractPowerSyncDatabase, run: (tx: AbstractPowerSyncDatabase) => Promise<TResult>, inspect: (tx: AbstractPowerSyncDatabase, result: TResult) => Promise<TPreview>): Promise<TPreview>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Runs the vendored desktop domain (src/vendor/electron) against the CLI's PowerSync
|
|
2
|
+
// database: validates inputs with the desktop zod schemas, maps desktop error strings onto
|
|
3
|
+
// registered DinoxError codes, and previews writes by executing them inside a transaction
|
|
4
|
+
// that is always rolled back.
|
|
5
|
+
import { ZodError } from 'zod';
|
|
6
|
+
import { withPowerSyncWriteTransaction } from '../../powersync/runtime.js';
|
|
7
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
8
|
+
import { isRegisteredErrorCode } from '../../utils/errorCodes.js';
|
|
9
|
+
import { createNoteTemplateCommands, createNoteTemplateReadModels } from '../../vendor/electron/domain/note-templates.js';
|
|
10
|
+
import { createPropertyCommands, createPropertyReadModels } from '../../vendor/electron/domain/properties.js';
|
|
11
|
+
import { createSavedViewCommands, createSavedViewReadModels } from '../../vendor/electron/domain/saved-views.js';
|
|
12
|
+
const COMMANDS = new Map([...createPropertyCommands(), ...createSavedViewCommands(), ...createNoteTemplateCommands()]
|
|
13
|
+
.map((definition) => [definition.name, definition]));
|
|
14
|
+
const READ_MODELS = new Map([...createPropertyReadModels(), ...createSavedViewReadModels(), ...createNoteTemplateReadModels()]
|
|
15
|
+
.map((definition) => [definition.name, definition]));
|
|
16
|
+
const DOMAIN_CODE_PATTERN = /^[A-Z][A-Z0-9_]+$/;
|
|
17
|
+
function zodIssues(error) {
|
|
18
|
+
return error.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message }));
|
|
19
|
+
}
|
|
20
|
+
/** Converts a desktop domain failure into a DinoxError; DinoxErrors pass through unchanged. */
|
|
21
|
+
export function toDinoxError(error, label = 'Dinox domain operation') {
|
|
22
|
+
if (error instanceof DinoxError)
|
|
23
|
+
return error;
|
|
24
|
+
if (error instanceof ZodError) {
|
|
25
|
+
return new DinoxError(`${label} input is invalid`, {
|
|
26
|
+
code: 'INVALID_ARGUMENT',
|
|
27
|
+
details: { issues: zodIssues(error) },
|
|
28
|
+
recoverable: false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
if (DOMAIN_CODE_PATTERN.test(message) && isRegisteredErrorCode(message)) {
|
|
33
|
+
return new DinoxError(message, { code: message, cause: error, recoverable: false });
|
|
34
|
+
}
|
|
35
|
+
if (/^Note not found: /.test(message)) {
|
|
36
|
+
return new DinoxError(message, { code: 'NOTE_NOT_FOUND', cause: error });
|
|
37
|
+
}
|
|
38
|
+
if (/^Invalid note template .* input$/.test(message)) {
|
|
39
|
+
return new DinoxError(message, { code: 'INVALID_ARGUMENT', cause: error, recoverable: false });
|
|
40
|
+
}
|
|
41
|
+
return new DinoxError(message || `${label} failed`, { code: 'COMMAND_FAILED', cause: error });
|
|
42
|
+
}
|
|
43
|
+
function requireCommand(name) {
|
|
44
|
+
const definition = COMMANDS.get(name);
|
|
45
|
+
if (!definition)
|
|
46
|
+
throw new DinoxError(`Unknown domain command: ${name}`, { code: 'INTERNAL_ERROR' });
|
|
47
|
+
return definition;
|
|
48
|
+
}
|
|
49
|
+
function requireReadModel(name) {
|
|
50
|
+
const definition = READ_MODELS.get(name);
|
|
51
|
+
if (!definition)
|
|
52
|
+
throw new DinoxError(`Unknown domain read model: ${name}`, { code: 'INTERNAL_ERROR' });
|
|
53
|
+
return definition;
|
|
54
|
+
}
|
|
55
|
+
export function parseDomainInput(definition, input, label) {
|
|
56
|
+
const parsed = definition.inputSchema.safeParse(input);
|
|
57
|
+
if (!parsed.success)
|
|
58
|
+
throw toDinoxError(parsed.error, label);
|
|
59
|
+
return parsed.data;
|
|
60
|
+
}
|
|
61
|
+
export async function runDomainCommand(db, name, input) {
|
|
62
|
+
const definition = requireCommand(name);
|
|
63
|
+
const parsed = parseDomainInput(definition, input, name);
|
|
64
|
+
try {
|
|
65
|
+
return await definition.run(db, parsed);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw toDinoxError(error, name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function fetchDomainReadModel(db, name, input) {
|
|
72
|
+
const definition = requireReadModel(name);
|
|
73
|
+
const parsed = parseDomainInput(definition, input, name);
|
|
74
|
+
try {
|
|
75
|
+
return await definition.fetch(db, parsed);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw toDinoxError(error, name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Exposes an open transaction as a database whose nested write/read transactions reuse it,
|
|
83
|
+
* so vendored commands that open their own transaction run inside the caller's.
|
|
84
|
+
*/
|
|
85
|
+
function nestedDatabase(tx) {
|
|
86
|
+
const proxy = new Proxy(tx, {
|
|
87
|
+
get(target, property, receiver) {
|
|
88
|
+
if (property === 'writeTransaction' || property === 'readTransaction') {
|
|
89
|
+
return async (run) => run(proxy);
|
|
90
|
+
}
|
|
91
|
+
const value = Reflect.get(target, property, receiver);
|
|
92
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
return proxy;
|
|
96
|
+
}
|
|
97
|
+
/** Runs `run` in one read transaction (a consistent snapshot) when the database offers it. */
|
|
98
|
+
export async function withReadTransaction(db, run) {
|
|
99
|
+
const candidate = db;
|
|
100
|
+
if (typeof candidate.readTransaction !== 'function')
|
|
101
|
+
return run(db);
|
|
102
|
+
return candidate.readTransaction.call(db, (tx) => run(nestedDatabase(tx)));
|
|
103
|
+
}
|
|
104
|
+
/** Runs `run` in one write transaction; vendored commands called inside reuse it. */
|
|
105
|
+
export async function withNestedWriteTransaction(db, run) {
|
|
106
|
+
try {
|
|
107
|
+
return await withPowerSyncWriteTransaction(db, (tx) => run(nestedDatabase(tx)));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw toDinoxError(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
class DryRunRollback extends Error {
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Executes `run` and then `inspect` inside one write transaction that is always rolled
|
|
117
|
+
* back, so a preview reflects exactly what the real write would do (including desktop
|
|
118
|
+
* validation and derived values) without persisting or queueing an upload.
|
|
119
|
+
*/
|
|
120
|
+
export async function previewInRolledBackTransaction(db, run, inspect) {
|
|
121
|
+
let preview = null;
|
|
122
|
+
try {
|
|
123
|
+
await withPowerSyncWriteTransaction(db, async (tx) => {
|
|
124
|
+
const nested = nestedDatabase(tx);
|
|
125
|
+
const result = await run(nested);
|
|
126
|
+
preview = { value: await inspect(nested, result) };
|
|
127
|
+
throw new DryRunRollback('dry-run rollback');
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!(error instanceof DryRunRollback))
|
|
132
|
+
throw toDinoxError(error);
|
|
133
|
+
}
|
|
134
|
+
if (!preview)
|
|
135
|
+
throw new DinoxError('Dry-run preview did not complete', { code: 'INTERNAL_ERROR' });
|
|
136
|
+
return preview.value;
|
|
137
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { isDaemonRunning, sendDaemonRequest } from '../../daemon/client.js';
|
|
2
|
+
import { ensureDaemonRunning } from '../../daemon/ensure.js';
|
|
3
|
+
import { loadConfig } from '../../config/store.js';
|
|
4
|
+
import { addGateway, adoptDetectedAgent, getGatewaysFilePath, loadGateways, removeGateway, } from '../../gateway/config.js';
|
|
5
|
+
import { detectAcpAgents, inferAgentId } from '../../gateway/detect.js';
|
|
6
|
+
import { serveDinoxMcpOverStdio } from '../../gateway/mcpServer.js';
|
|
7
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
8
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess } from '../../utils/output.js';
|
|
9
|
+
import { registerCommandGroup } from '../descriptor/register.js';
|
|
10
|
+
import { FORMAT_OPTIONS } from '../schema/globalOptions.js';
|
|
11
|
+
const GATEWAY_KINDS = ['acp', 'echo'];
|
|
12
|
+
function collect(value, previous) {
|
|
13
|
+
return [...(Array.isArray(previous) ? previous : []), value];
|
|
14
|
+
}
|
|
15
|
+
function parseEnv(entries) {
|
|
16
|
+
const env = {};
|
|
17
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
18
|
+
const separator = entry.indexOf('=');
|
|
19
|
+
if (separator <= 0) {
|
|
20
|
+
throw new DinoxError(`--env expects KEY=VALUE, got "${entry}"`, { code: 'INVALID_ARGUMENT' });
|
|
21
|
+
}
|
|
22
|
+
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
23
|
+
}
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
/** Gateways belong to the logged-in account. */
|
|
27
|
+
async function currentUserId() {
|
|
28
|
+
return (await loadConfig()).auth?.userId ?? '';
|
|
29
|
+
}
|
|
30
|
+
/** Public view: never echoes env values (they may hold API keys). */
|
|
31
|
+
function describeGateway(gateway) {
|
|
32
|
+
return {
|
|
33
|
+
id: gateway.id,
|
|
34
|
+
name: gateway.name,
|
|
35
|
+
kind: gateway.kind,
|
|
36
|
+
agentId: gateway.agentId ?? null,
|
|
37
|
+
deviceId: gateway.deviceId,
|
|
38
|
+
command: gateway.command ?? null,
|
|
39
|
+
args: gateway.args ?? [],
|
|
40
|
+
cwd: gateway.cwd ?? null,
|
|
41
|
+
envKeys: Object.keys(gateway.env ?? {}),
|
|
42
|
+
enabled: gateway.enabled,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Applies the account's gateways to the running daemon, starting it when needed. */
|
|
46
|
+
async function applyToDaemon() {
|
|
47
|
+
if (!(await isDaemonRunning())) {
|
|
48
|
+
await ensureDaemonRunning();
|
|
49
|
+
return (await sendDaemonRequest('gateway.status')).gateways;
|
|
50
|
+
}
|
|
51
|
+
return (await sendDaemonRequest('gateway.reload', undefined, { timeoutMs: 30_000 })).gateways;
|
|
52
|
+
}
|
|
53
|
+
function printStatuses(statuses) {
|
|
54
|
+
if (statuses.length === 0) {
|
|
55
|
+
printHumanReadableLine('No gateways are running.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const status of statuses) {
|
|
59
|
+
const busy = status.busyThreadIds.length ? ` · ${status.busyThreadIds.length} running` : '';
|
|
60
|
+
const error = status.lastError ? ` · ${status.lastError}` : '';
|
|
61
|
+
printHumanReadableLine(`${status.name} (${status.kind}, ${status.deviceId}): ${status.state}${busy}${error}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function runAdd(context) {
|
|
65
|
+
const kind = context.options.kind ?? 'acp';
|
|
66
|
+
if (!GATEWAY_KINDS.includes(kind)) {
|
|
67
|
+
throw new DinoxError(`--kind must be one of ${GATEWAY_KINDS.join(', ')}`, { code: 'INVALID_ARGUMENT' });
|
|
68
|
+
}
|
|
69
|
+
const command = context.options.command;
|
|
70
|
+
const args = context.options.arg;
|
|
71
|
+
const gateway = await addGateway(await currentUserId(), {
|
|
72
|
+
name: context.args.name ?? '',
|
|
73
|
+
kind,
|
|
74
|
+
// A manual launch of a known agent counts as that agent, so detect never hosts it twice.
|
|
75
|
+
agentId: kind === 'acp' ? inferAgentId(command, args) : undefined,
|
|
76
|
+
command,
|
|
77
|
+
args,
|
|
78
|
+
cwd: context.options.cwd,
|
|
79
|
+
env: parseEnv(context.options.env),
|
|
80
|
+
});
|
|
81
|
+
const statuses = await applyToDaemon();
|
|
82
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
83
|
+
printSuccess({ gateway: describeGateway(gateway), status: statuses.find((item) => item.gatewayId === gateway.id) ?? null });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
printHumanReadableLine(`Added gateway "${gateway.name}" as device ${gateway.deviceId}.`);
|
|
87
|
+
printStatuses(statuses);
|
|
88
|
+
}
|
|
89
|
+
async function runDetect(context) {
|
|
90
|
+
const detected = await detectAcpAgents({ probe: true });
|
|
91
|
+
const added = [];
|
|
92
|
+
const updated = [];
|
|
93
|
+
if (context.options.add) {
|
|
94
|
+
const userId = await currentUserId();
|
|
95
|
+
for (const agent of detected) {
|
|
96
|
+
if (!agent.verified)
|
|
97
|
+
continue;
|
|
98
|
+
const result = await adoptDetectedAgent(userId, agent);
|
|
99
|
+
if (result.change === 'added')
|
|
100
|
+
added.push(result.gateway);
|
|
101
|
+
if (result.change === 'updated')
|
|
102
|
+
updated.push(result.gateway);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const statuses = added.length || updated.length ? await applyToDaemon() : [];
|
|
106
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
107
|
+
printSuccess({
|
|
108
|
+
agents: detected,
|
|
109
|
+
added: added.map(describeGateway),
|
|
110
|
+
updated: updated.map(describeGateway),
|
|
111
|
+
gateways: statuses,
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (detected.length === 0) {
|
|
116
|
+
printHumanReadableLine('No ACP agents found on PATH.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const agent of detected) {
|
|
120
|
+
const launch = [agent.command, ...agent.args].join(' ');
|
|
121
|
+
const label = agent.verified
|
|
122
|
+
? `ready${agent.agentVersion ? ` (${agent.agentName ?? agent.name} ${agent.agentVersion})` : ''}`
|
|
123
|
+
: agent.installHint
|
|
124
|
+
? `installed, needs its ACP adapter: ${agent.installHint}`
|
|
125
|
+
: `not responding: ${agent.error ?? 'unknown'}`;
|
|
126
|
+
printHumanReadableLine(`${agent.name}: ${label} · ${launch}`);
|
|
127
|
+
}
|
|
128
|
+
for (const gateway of added)
|
|
129
|
+
printHumanReadableLine(`Added gateway "${gateway.name}" (${gateway.deviceId}).`);
|
|
130
|
+
for (const gateway of updated) {
|
|
131
|
+
printHumanReadableLine(`Updated gateway "${gateway.name}": ${[gateway.command, ...(gateway.args ?? [])].join(' ')}`);
|
|
132
|
+
}
|
|
133
|
+
if (!context.options.add && detected.some((agent) => agent.verified)) {
|
|
134
|
+
printHumanReadableLine('Run `dino gateway detect --add` to host the ready agents.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function runList() {
|
|
138
|
+
const userId = await currentUserId();
|
|
139
|
+
const gateways = (await loadGateways(userId)).map(describeGateway);
|
|
140
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
141
|
+
printSuccess({ gateways, file: getGatewaysFilePath(userId) });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (gateways.length === 0) {
|
|
145
|
+
printHumanReadableLine('No gateways configured. Add one with `dino gateway add <name> --command <agent>`.');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const gateway of gateways) {
|
|
149
|
+
const command = gateway.command ? ` · ${[gateway.command, ...gateway.args].join(' ')}` : '';
|
|
150
|
+
printHumanReadableLine(`${gateway.name} (${gateway.kind}, ${gateway.deviceId})${command}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function runRemove(context) {
|
|
154
|
+
const gateway = await removeGateway(await currentUserId(), context.args.gateway ?? '');
|
|
155
|
+
const statuses = (await isDaemonRunning()) ? await applyToDaemon() : [];
|
|
156
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
157
|
+
printSuccess({ removed: describeGateway(gateway), gateways: statuses });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
printHumanReadableLine(`Removed gateway "${gateway.name}".`);
|
|
161
|
+
}
|
|
162
|
+
async function runStatus() {
|
|
163
|
+
await ensureDaemonRunning();
|
|
164
|
+
const { gateways } = (await sendDaemonRequest('gateway.status'));
|
|
165
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
166
|
+
printSuccess({ gateways });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
printStatuses(gateways);
|
|
170
|
+
}
|
|
171
|
+
async function runReload() {
|
|
172
|
+
const gateways = await applyToDaemon();
|
|
173
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
174
|
+
printSuccess({ gateways });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
printStatuses(gateways);
|
|
178
|
+
}
|
|
179
|
+
const STATUS_FIELDS = ['gatewayId', 'name', 'deviceId', 'kind', 'state', 'connectedAt', 'lastError', 'busyThreadIds', 'agentName'];
|
|
180
|
+
export const gatewayCommandGroup = {
|
|
181
|
+
name: 'gateway',
|
|
182
|
+
description: 'Host external agents (Hermes, OpenClaw, pi, Claude Code, ...) as Dinox remote devices',
|
|
183
|
+
commands: [
|
|
184
|
+
{
|
|
185
|
+
name: 'add',
|
|
186
|
+
description: 'Add an agent gateway and start hosting it in the daemon',
|
|
187
|
+
arguments: [{ syntax: '<name>', description: 'Display name shown in the Dinox app' }],
|
|
188
|
+
options: [
|
|
189
|
+
{ flags: '--kind <kind>', description: 'Agent runtime protocol', type: 'string', enum: GATEWAY_KINDS, defaultValue: 'acp' },
|
|
190
|
+
{ flags: '--command <command>', description: 'ACP agent executable (e.g. hermes, openclaw, claude-code-acp)', type: 'string' },
|
|
191
|
+
{ flags: '--arg <value>', description: 'Argument passed to the agent (repeatable, e.g. --arg acp)', type: 'string[]', parser: collect },
|
|
192
|
+
{ flags: '--cwd <dir>', description: 'Working directory for the agent sessions', type: 'string' },
|
|
193
|
+
{ flags: '--env <KEY=VALUE>', description: 'Environment for the agent process, stored locally (repeatable)', type: 'string[]', parser: collect },
|
|
194
|
+
],
|
|
195
|
+
schema: {
|
|
196
|
+
path: 'gateway.add',
|
|
197
|
+
risk: 'write',
|
|
198
|
+
auth: 'required',
|
|
199
|
+
globalOptions: FORMAT_OPTIONS,
|
|
200
|
+
output: {
|
|
201
|
+
type: 'structured',
|
|
202
|
+
summary: 'The added gateway and its live device status',
|
|
203
|
+
fields: ['gateway', 'status'],
|
|
204
|
+
},
|
|
205
|
+
examples: [
|
|
206
|
+
'dino gateway add Hermes --command hermes --arg acp --format json',
|
|
207
|
+
'dino gateway add Echo --kind echo --format json',
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
action: runAdd,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'detect',
|
|
214
|
+
description: 'Find ACP agents installed on this machine (verified with a real handshake)',
|
|
215
|
+
options: [{ flags: '--add', description: 'Add every verified agent that is not configured yet', type: 'boolean' }],
|
|
216
|
+
schema: {
|
|
217
|
+
path: 'gateway.detect',
|
|
218
|
+
risk: 'write',
|
|
219
|
+
auth: 'none',
|
|
220
|
+
globalOptions: FORMAT_OPTIONS,
|
|
221
|
+
output: {
|
|
222
|
+
type: 'structured',
|
|
223
|
+
summary: 'Detected agents, gateways added, and their live statuses',
|
|
224
|
+
fields: ['agents', 'added', 'gateways'],
|
|
225
|
+
},
|
|
226
|
+
examples: ['dino gateway detect --format json', 'dino gateway detect --add --format json'],
|
|
227
|
+
},
|
|
228
|
+
action: runDetect,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'list',
|
|
232
|
+
description: 'List configured agent gateways (environment values are never shown)',
|
|
233
|
+
schema: {
|
|
234
|
+
path: 'gateway.list',
|
|
235
|
+
risk: 'read',
|
|
236
|
+
auth: 'none',
|
|
237
|
+
globalOptions: FORMAT_OPTIONS,
|
|
238
|
+
output: { type: 'structured', summary: 'Configured gateways', fields: ['gateways', 'file'] },
|
|
239
|
+
examples: ['dino gateway list --format json'],
|
|
240
|
+
},
|
|
241
|
+
action: runList,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'remove',
|
|
245
|
+
description: 'Remove an agent gateway and stop hosting it',
|
|
246
|
+
arguments: [{ syntax: '<gateway>', description: 'Gateway name, id, or device id' }],
|
|
247
|
+
schema: {
|
|
248
|
+
path: 'gateway.remove',
|
|
249
|
+
risk: 'write',
|
|
250
|
+
auth: 'none',
|
|
251
|
+
globalOptions: FORMAT_OPTIONS,
|
|
252
|
+
output: { type: 'structured', summary: 'The removed gateway and remaining statuses', fields: ['removed', 'gateways'] },
|
|
253
|
+
examples: ['dino gateway remove Hermes --format json'],
|
|
254
|
+
},
|
|
255
|
+
action: runRemove,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'status',
|
|
259
|
+
description: 'Show live connection status of each hosted gateway',
|
|
260
|
+
schema: {
|
|
261
|
+
path: 'gateway.status',
|
|
262
|
+
risk: 'read',
|
|
263
|
+
auth: 'required',
|
|
264
|
+
globalOptions: FORMAT_OPTIONS,
|
|
265
|
+
output: { type: 'structured', summary: 'Live gateway device statuses', fields: STATUS_FIELDS },
|
|
266
|
+
examples: ['dino gateway status --format json'],
|
|
267
|
+
},
|
|
268
|
+
action: runStatus,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'mcp',
|
|
272
|
+
description: 'Serve read-only Dinox knowledge tools over stdio MCP (started by gateway agents)',
|
|
273
|
+
schema: {
|
|
274
|
+
path: 'gateway.mcp',
|
|
275
|
+
risk: 'read',
|
|
276
|
+
auth: 'required',
|
|
277
|
+
globalOptions: [],
|
|
278
|
+
output: { type: 'structured', summary: 'MCP JSON-RPC over stdio until stdin closes', fields: [] },
|
|
279
|
+
examples: ['dino gateway mcp'],
|
|
280
|
+
},
|
|
281
|
+
action: serveDinoxMcpOverStdio,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'reload',
|
|
285
|
+
description: 'Re-read gateway configuration in the running daemon',
|
|
286
|
+
schema: {
|
|
287
|
+
path: 'gateway.reload',
|
|
288
|
+
risk: 'write',
|
|
289
|
+
auth: 'required',
|
|
290
|
+
globalOptions: FORMAT_OPTIONS,
|
|
291
|
+
output: { type: 'structured', summary: 'Live gateway device statuses after reload', fields: STATUS_FIELDS },
|
|
292
|
+
examples: ['dino gateway reload --format json'],
|
|
293
|
+
},
|
|
294
|
+
action: runReload,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
export function registerGatewayCommands(program) {
|
|
299
|
+
registerCommandGroup(program, gatewayCommandGroup);
|
|
300
|
+
}
|