@clawdbot/zalouser 2026.1.16
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 +221 -0
- package/index.ts +28 -0
- package/package.json +14 -0
- package/src/accounts.ts +121 -0
- package/src/channel.test.ts +18 -0
- package/src/channel.ts +523 -0
- package/src/core-bridge.ts +171 -0
- package/src/monitor.ts +372 -0
- package/src/onboarding.ts +312 -0
- package/src/send.ts +150 -0
- package/src/tool.ts +156 -0
- package/src/types.ts +109 -0
- package/src/zca.ts +183 -0
package/src/send.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { runZca } from "./zca.js";
|
|
2
|
+
|
|
3
|
+
export type ZalouserSendOptions = {
|
|
4
|
+
profile?: string;
|
|
5
|
+
mediaUrl?: string;
|
|
6
|
+
caption?: string;
|
|
7
|
+
isGroup?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ZalouserSendResult = {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
messageId?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function sendMessageZalouser(
|
|
17
|
+
threadId: string,
|
|
18
|
+
text: string,
|
|
19
|
+
options: ZalouserSendOptions = {},
|
|
20
|
+
): Promise<ZalouserSendResult> {
|
|
21
|
+
const profile = options.profile || process.env.ZCA_PROFILE || "default";
|
|
22
|
+
|
|
23
|
+
if (!threadId?.trim()) {
|
|
24
|
+
return { ok: false, error: "No threadId provided" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Handle media sending
|
|
28
|
+
if (options.mediaUrl) {
|
|
29
|
+
return sendMediaZalouser(threadId, options.mediaUrl, {
|
|
30
|
+
...options,
|
|
31
|
+
caption: text || options.caption,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Send text message
|
|
36
|
+
const args = ["msg", "send", threadId.trim(), text.slice(0, 2000)];
|
|
37
|
+
if (options.isGroup) args.push("-g");
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const result = await runZca(args, { profile });
|
|
41
|
+
|
|
42
|
+
if (result.ok) {
|
|
43
|
+
return { ok: true, messageId: extractMessageId(result.stdout) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { ok: false, error: result.stderr || "Failed to send message" };
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function sendMediaZalouser(
|
|
53
|
+
threadId: string,
|
|
54
|
+
mediaUrl: string,
|
|
55
|
+
options: ZalouserSendOptions = {},
|
|
56
|
+
): Promise<ZalouserSendResult> {
|
|
57
|
+
const profile = options.profile || process.env.ZCA_PROFILE || "default";
|
|
58
|
+
|
|
59
|
+
if (!threadId?.trim()) {
|
|
60
|
+
return { ok: false, error: "No threadId provided" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!mediaUrl?.trim()) {
|
|
64
|
+
return { ok: false, error: "No media URL provided" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Determine media type from URL
|
|
68
|
+
const lowerUrl = mediaUrl.toLowerCase();
|
|
69
|
+
let command: string;
|
|
70
|
+
if (lowerUrl.match(/\.(mp4|mov|avi|webm)$/)) {
|
|
71
|
+
command = "video";
|
|
72
|
+
} else if (lowerUrl.match(/\.(mp3|wav|ogg|m4a)$/)) {
|
|
73
|
+
command = "voice";
|
|
74
|
+
} else {
|
|
75
|
+
command = "image";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const args = ["msg", command, threadId.trim(), "-u", mediaUrl.trim()];
|
|
79
|
+
if (options.caption) {
|
|
80
|
+
args.push("-m", options.caption.slice(0, 2000));
|
|
81
|
+
}
|
|
82
|
+
if (options.isGroup) args.push("-g");
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = await runZca(args, { profile });
|
|
86
|
+
|
|
87
|
+
if (result.ok) {
|
|
88
|
+
return { ok: true, messageId: extractMessageId(result.stdout) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ok: false, error: result.stderr || `Failed to send ${command}` };
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function sendImageZalouser(
|
|
98
|
+
threadId: string,
|
|
99
|
+
imageUrl: string,
|
|
100
|
+
options: ZalouserSendOptions = {},
|
|
101
|
+
): Promise<ZalouserSendResult> {
|
|
102
|
+
const profile = options.profile || process.env.ZCA_PROFILE || "default";
|
|
103
|
+
const args = ["msg", "image", threadId.trim(), "-u", imageUrl.trim()];
|
|
104
|
+
if (options.caption) {
|
|
105
|
+
args.push("-m", options.caption.slice(0, 2000));
|
|
106
|
+
}
|
|
107
|
+
if (options.isGroup) args.push("-g");
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await runZca(args, { profile });
|
|
111
|
+
if (result.ok) {
|
|
112
|
+
return { ok: true, messageId: extractMessageId(result.stdout) };
|
|
113
|
+
}
|
|
114
|
+
return { ok: false, error: result.stderr || "Failed to send image" };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function sendLinkZalouser(
|
|
121
|
+
threadId: string,
|
|
122
|
+
url: string,
|
|
123
|
+
options: ZalouserSendOptions = {},
|
|
124
|
+
): Promise<ZalouserSendResult> {
|
|
125
|
+
const profile = options.profile || process.env.ZCA_PROFILE || "default";
|
|
126
|
+
const args = ["msg", "link", threadId.trim(), url.trim()];
|
|
127
|
+
if (options.isGroup) args.push("-g");
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const result = await runZca(args, { profile });
|
|
131
|
+
if (result.ok) {
|
|
132
|
+
return { ok: true, messageId: extractMessageId(result.stdout) };
|
|
133
|
+
}
|
|
134
|
+
return { ok: false, error: result.stderr || "Failed to send link" };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function extractMessageId(stdout: string): string | undefined {
|
|
141
|
+
// Try to extract message ID from output
|
|
142
|
+
const match = stdout.match(/message[_\s]?id[:\s]+(\S+)/i);
|
|
143
|
+
if (match) return match[1];
|
|
144
|
+
// Return first word if it looks like an ID
|
|
145
|
+
const firstWord = stdout.trim().split(/\s+/)[0];
|
|
146
|
+
if (firstWord && /^[a-zA-Z0-9_-]+$/.test(firstWord)) {
|
|
147
|
+
return firstWord;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
import { runZca, parseJsonOutput } from "./zca.js";
|
|
4
|
+
|
|
5
|
+
const ACTIONS = ["send", "image", "link", "friends", "groups", "me", "status"] as const;
|
|
6
|
+
|
|
7
|
+
function stringEnum<T extends readonly string[]>(
|
|
8
|
+
values: T,
|
|
9
|
+
options: { description?: string } = {},
|
|
10
|
+
) {
|
|
11
|
+
return Type.Unsafe<T[number]>({
|
|
12
|
+
type: "string",
|
|
13
|
+
enum: [...values],
|
|
14
|
+
...options,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Tool schema - avoiding Type.Union per tool schema guardrails
|
|
19
|
+
export const ZalouserToolSchema = Type.Object({
|
|
20
|
+
action: stringEnum(ACTIONS, { description: `Action to perform: ${ACTIONS.join(", ")}` }),
|
|
21
|
+
threadId: Type.Optional(
|
|
22
|
+
Type.String({ description: "Thread ID for messaging" }),
|
|
23
|
+
),
|
|
24
|
+
message: Type.Optional(Type.String({ description: "Message text" })),
|
|
25
|
+
isGroup: Type.Optional(Type.Boolean({ description: "Is group chat" })),
|
|
26
|
+
profile: Type.Optional(Type.String({ description: "Profile name" })),
|
|
27
|
+
query: Type.Optional(Type.String({ description: "Search query" })),
|
|
28
|
+
url: Type.Optional(Type.String({ description: "URL for media/link" })),
|
|
29
|
+
}, { additionalProperties: false });
|
|
30
|
+
|
|
31
|
+
type ToolParams = {
|
|
32
|
+
action: (typeof ACTIONS)[number];
|
|
33
|
+
threadId?: string;
|
|
34
|
+
message?: string;
|
|
35
|
+
isGroup?: boolean;
|
|
36
|
+
profile?: string;
|
|
37
|
+
query?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type ToolResult = {
|
|
42
|
+
content: Array<{ type: string; text: string }>;
|
|
43
|
+
details: unknown;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function json(payload: unknown): ToolResult {
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
49
|
+
details: payload,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function executeZalouserTool(
|
|
54
|
+
_toolCallId: string,
|
|
55
|
+
params: ToolParams,
|
|
56
|
+
): Promise<ToolResult> {
|
|
57
|
+
try {
|
|
58
|
+
switch (params.action) {
|
|
59
|
+
case "send": {
|
|
60
|
+
if (!params.threadId || !params.message) {
|
|
61
|
+
throw new Error("threadId and message required for send action");
|
|
62
|
+
}
|
|
63
|
+
const args = ["msg", "send", params.threadId, params.message];
|
|
64
|
+
if (params.isGroup) args.push("-g");
|
|
65
|
+
const result = await runZca(args, { profile: params.profile });
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
throw new Error(result.stderr || "Failed to send message");
|
|
68
|
+
}
|
|
69
|
+
return json({ success: true, output: result.stdout });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case "image": {
|
|
73
|
+
if (!params.threadId) {
|
|
74
|
+
throw new Error("threadId required for image action");
|
|
75
|
+
}
|
|
76
|
+
if (!params.url) {
|
|
77
|
+
throw new Error("url required for image action");
|
|
78
|
+
}
|
|
79
|
+
const args = ["msg", "image", params.threadId, "-u", params.url];
|
|
80
|
+
if (params.message) args.push("-m", params.message);
|
|
81
|
+
if (params.isGroup) args.push("-g");
|
|
82
|
+
const result = await runZca(args, { profile: params.profile });
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
throw new Error(result.stderr || "Failed to send image");
|
|
85
|
+
}
|
|
86
|
+
return json({ success: true, output: result.stdout });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case "link": {
|
|
90
|
+
if (!params.threadId || !params.url) {
|
|
91
|
+
throw new Error("threadId and url required for link action");
|
|
92
|
+
}
|
|
93
|
+
const args = ["msg", "link", params.threadId, params.url];
|
|
94
|
+
if (params.isGroup) args.push("-g");
|
|
95
|
+
const result = await runZca(args, { profile: params.profile });
|
|
96
|
+
if (!result.ok) {
|
|
97
|
+
throw new Error(result.stderr || "Failed to send link");
|
|
98
|
+
}
|
|
99
|
+
return json({ success: true, output: result.stdout });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case "friends": {
|
|
103
|
+
const args = params.query
|
|
104
|
+
? ["friend", "find", params.query]
|
|
105
|
+
: ["friend", "list", "-j"];
|
|
106
|
+
const result = await runZca(args, { profile: params.profile });
|
|
107
|
+
if (!result.ok) {
|
|
108
|
+
throw new Error(result.stderr || "Failed to get friends");
|
|
109
|
+
}
|
|
110
|
+
const parsed = parseJsonOutput(result.stdout);
|
|
111
|
+
return json(parsed ?? { raw: result.stdout });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
case "groups": {
|
|
115
|
+
const result = await runZca(["group", "list", "-j"], {
|
|
116
|
+
profile: params.profile,
|
|
117
|
+
});
|
|
118
|
+
if (!result.ok) {
|
|
119
|
+
throw new Error(result.stderr || "Failed to get groups");
|
|
120
|
+
}
|
|
121
|
+
const parsed = parseJsonOutput(result.stdout);
|
|
122
|
+
return json(parsed ?? { raw: result.stdout });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "me": {
|
|
126
|
+
const result = await runZca(["me", "info", "-j"], {
|
|
127
|
+
profile: params.profile,
|
|
128
|
+
});
|
|
129
|
+
if (!result.ok) {
|
|
130
|
+
throw new Error(result.stderr || "Failed to get profile");
|
|
131
|
+
}
|
|
132
|
+
const parsed = parseJsonOutput(result.stdout);
|
|
133
|
+
return json(parsed ?? { raw: result.stdout });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case "status": {
|
|
137
|
+
const result = await runZca(["auth", "status"], {
|
|
138
|
+
profile: params.profile,
|
|
139
|
+
});
|
|
140
|
+
return json({
|
|
141
|
+
authenticated: result.ok,
|
|
142
|
+
output: result.stdout || result.stderr,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
default:
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Unknown action: ${params.action}. Valid actions: send, image, link, friends, groups, me, status`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return json({
|
|
153
|
+
error: err instanceof Error ? err.message : String(err),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// zca-cli wrapper types
|
|
2
|
+
export type ZcaRunOptions = {
|
|
3
|
+
profile?: string;
|
|
4
|
+
cwd?: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ZcaResult = {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
exitCode: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ZcaProfile = {
|
|
16
|
+
name: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
isDefault?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ZcaFriend = {
|
|
22
|
+
userId: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
avatar?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ZcaGroup = {
|
|
28
|
+
groupId: string;
|
|
29
|
+
name: string;
|
|
30
|
+
memberCount?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ZcaMessage = {
|
|
34
|
+
threadId: string;
|
|
35
|
+
msgId?: string;
|
|
36
|
+
cliMsgId?: string;
|
|
37
|
+
type: number;
|
|
38
|
+
content: string;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
metadata?: {
|
|
41
|
+
isGroup: boolean;
|
|
42
|
+
threadName?: string;
|
|
43
|
+
senderName?: string;
|
|
44
|
+
fromId?: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type ZcaUserInfo = {
|
|
49
|
+
userId: string;
|
|
50
|
+
displayName: string;
|
|
51
|
+
avatar?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type CommonOptions = {
|
|
55
|
+
profile?: string;
|
|
56
|
+
json?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type SendOptions = CommonOptions & {
|
|
60
|
+
group?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type ListenOptions = CommonOptions & {
|
|
64
|
+
raw?: boolean;
|
|
65
|
+
keepAlive?: boolean;
|
|
66
|
+
webhook?: string;
|
|
67
|
+
echo?: boolean;
|
|
68
|
+
prefix?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Channel plugin config types
|
|
72
|
+
export const DEFAULT_ACCOUNT_ID = "default";
|
|
73
|
+
|
|
74
|
+
export type ZalouserAccountConfig = {
|
|
75
|
+
enabled?: boolean;
|
|
76
|
+
name?: string;
|
|
77
|
+
profile?: string;
|
|
78
|
+
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
79
|
+
allowFrom?: Array<string | number>;
|
|
80
|
+
messagePrefix?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type ZalouserConfig = {
|
|
84
|
+
enabled?: boolean;
|
|
85
|
+
name?: string;
|
|
86
|
+
profile?: string;
|
|
87
|
+
defaultAccount?: string;
|
|
88
|
+
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
89
|
+
allowFrom?: Array<string | number>;
|
|
90
|
+
messagePrefix?: string;
|
|
91
|
+
accounts?: Record<string, ZalouserAccountConfig>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type CoreConfig = {
|
|
95
|
+
channels?: {
|
|
96
|
+
zalouser?: ZalouserConfig;
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
};
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type ResolvedZalouserAccount = {
|
|
103
|
+
accountId: string;
|
|
104
|
+
name?: string;
|
|
105
|
+
enabled: boolean;
|
|
106
|
+
profile: string;
|
|
107
|
+
authenticated: boolean;
|
|
108
|
+
config: ZalouserAccountConfig;
|
|
109
|
+
};
|
package/src/zca.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { spawn, type SpawnOptions } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import type { ZcaResult, ZcaRunOptions } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const ZCA_BINARY = "zca";
|
|
6
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
7
|
+
|
|
8
|
+
function buildArgs(args: string[], options?: ZcaRunOptions): string[] {
|
|
9
|
+
const result: string[] = [];
|
|
10
|
+
// Profile flag comes first (before subcommand)
|
|
11
|
+
const profile = options?.profile || process.env.ZCA_PROFILE;
|
|
12
|
+
if (profile) {
|
|
13
|
+
result.push("--profile", profile);
|
|
14
|
+
}
|
|
15
|
+
result.push(...args);
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runZca(
|
|
20
|
+
args: string[],
|
|
21
|
+
options?: ZcaRunOptions,
|
|
22
|
+
): Promise<ZcaResult> {
|
|
23
|
+
const fullArgs = buildArgs(args, options);
|
|
24
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const spawnOpts: SpawnOptions = {
|
|
28
|
+
cwd: options?.cwd,
|
|
29
|
+
env: { ...process.env },
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const proc = spawn(ZCA_BINARY, fullArgs, spawnOpts);
|
|
34
|
+
let stdout = "";
|
|
35
|
+
let stderr = "";
|
|
36
|
+
let timedOut = false;
|
|
37
|
+
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
timedOut = true;
|
|
40
|
+
proc.kill("SIGTERM");
|
|
41
|
+
}, timeout);
|
|
42
|
+
|
|
43
|
+
proc.stdout?.on("data", (data: Buffer) => {
|
|
44
|
+
stdout += data.toString();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
48
|
+
stderr += data.toString();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
proc.on("close", (code) => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
if (timedOut) {
|
|
54
|
+
resolve({
|
|
55
|
+
ok: false,
|
|
56
|
+
stdout,
|
|
57
|
+
stderr: stderr || "Command timed out",
|
|
58
|
+
exitCode: code ?? 124,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
resolve({
|
|
63
|
+
ok: code === 0,
|
|
64
|
+
stdout: stdout.trim(),
|
|
65
|
+
stderr: stderr.trim(),
|
|
66
|
+
exitCode: code ?? 1,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
proc.on("error", (err) => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
resolve({
|
|
73
|
+
ok: false,
|
|
74
|
+
stdout: "",
|
|
75
|
+
stderr: err.message,
|
|
76
|
+
exitCode: 1,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function runZcaInteractive(
|
|
83
|
+
args: string[],
|
|
84
|
+
options?: ZcaRunOptions,
|
|
85
|
+
): Promise<ZcaResult> {
|
|
86
|
+
const fullArgs = buildArgs(args, options);
|
|
87
|
+
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const spawnOpts: SpawnOptions = {
|
|
90
|
+
cwd: options?.cwd,
|
|
91
|
+
env: { ...process.env },
|
|
92
|
+
stdio: "inherit",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const proc = spawn(ZCA_BINARY, fullArgs, spawnOpts);
|
|
96
|
+
|
|
97
|
+
proc.on("close", (code) => {
|
|
98
|
+
resolve({
|
|
99
|
+
ok: code === 0,
|
|
100
|
+
stdout: "",
|
|
101
|
+
stderr: "",
|
|
102
|
+
exitCode: code ?? 1,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
proc.on("error", (err) => {
|
|
107
|
+
resolve({
|
|
108
|
+
ok: false,
|
|
109
|
+
stdout: "",
|
|
110
|
+
stderr: err.message,
|
|
111
|
+
exitCode: 1,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parseJsonOutput<T>(stdout: string): T | null {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(stdout) as T;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function checkZcaInstalled(): Promise<boolean> {
|
|
126
|
+
const result = await runZca(["--version"], { timeout: 5000 });
|
|
127
|
+
return result.ok;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type ZcaStreamingOptions = ZcaRunOptions & {
|
|
131
|
+
onData?: (data: string) => void;
|
|
132
|
+
onError?: (err: Error) => void;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export function runZcaStreaming(
|
|
136
|
+
args: string[],
|
|
137
|
+
options?: ZcaStreamingOptions,
|
|
138
|
+
): { proc: ReturnType<typeof spawn>; promise: Promise<ZcaResult> } {
|
|
139
|
+
const fullArgs = buildArgs(args, options);
|
|
140
|
+
|
|
141
|
+
const spawnOpts: SpawnOptions = {
|
|
142
|
+
cwd: options?.cwd,
|
|
143
|
+
env: { ...process.env },
|
|
144
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const proc = spawn(ZCA_BINARY, fullArgs, spawnOpts);
|
|
148
|
+
let stdout = "";
|
|
149
|
+
let stderr = "";
|
|
150
|
+
|
|
151
|
+
proc.stdout?.on("data", (data: Buffer) => {
|
|
152
|
+
const text = data.toString();
|
|
153
|
+
stdout += text;
|
|
154
|
+
options?.onData?.(text);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
proc.stderr?.on("data", (data: Buffer) => {
|
|
158
|
+
stderr += data.toString();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const promise = new Promise<ZcaResult>((resolve) => {
|
|
162
|
+
proc.on("close", (code) => {
|
|
163
|
+
resolve({
|
|
164
|
+
ok: code === 0,
|
|
165
|
+
stdout: stdout.trim(),
|
|
166
|
+
stderr: stderr.trim(),
|
|
167
|
+
exitCode: code ?? 1,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
proc.on("error", (err) => {
|
|
172
|
+
options?.onError?.(err);
|
|
173
|
+
resolve({
|
|
174
|
+
ok: false,
|
|
175
|
+
stdout: "",
|
|
176
|
+
stderr: err.message,
|
|
177
|
+
exitCode: 1,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return { proc, promise };
|
|
183
|
+
}
|