@nowcrew/daemon 0.5.36 → 0.5.38
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 +69 -9
- package/dist/computer-cli.js +133 -12
- package/dist/computer-service.js +88 -23
- package/dist/daemon-installation-lease.js +86 -0
- package/dist/daemon-installation.js +38 -0
- package/dist/daemon-update-controller.js +30 -2
- package/dist/daemon-update-eligibility.js +70 -39
- package/dist/daemon-updater.js +16 -0
- package/dist/i18n.js +1 -1
- package/dist/main.js +28 -6
- package/dist/managed-service-diagnostics.js +92 -0
- package/dist/managed-service-lifecycle.js +189 -0
- package/dist/managed-service-registry.js +285 -0
- package/dist/managed-service-startup.js +86 -0
- package/dist/serve.js +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { acquireProfileSaveLock } from "./computer-profile-lock.js";
|
|
8
|
+
const AbsolutePathSchema = z.string().min(1).refine(isAbsolute, "must be an absolute path");
|
|
9
|
+
const ManagedServiceRecordSchema = z.object({
|
|
10
|
+
version: z.literal(1),
|
|
11
|
+
generation: z.string().uuid(),
|
|
12
|
+
platform: z.enum(["darwin", "linux"]),
|
|
13
|
+
serviceId: z.string().min(1).max(256),
|
|
14
|
+
profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/),
|
|
15
|
+
daemonHome: AbsolutePathSchema,
|
|
16
|
+
agentsRoot: AbsolutePathSchema,
|
|
17
|
+
nodePath: AbsolutePathSchema,
|
|
18
|
+
entryPath: AbsolutePathSchema,
|
|
19
|
+
packageRoot: AbsolutePathSchema,
|
|
20
|
+
npmPrefix: AbsolutePathSchema,
|
|
21
|
+
descriptorPath: AbsolutePathSchema,
|
|
22
|
+
descriptorSha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
23
|
+
createdAt: z.string().datetime({ offset: true }),
|
|
24
|
+
}).strict();
|
|
25
|
+
const ManagedServiceRegistrySchema = z.object({
|
|
26
|
+
version: z.literal(1),
|
|
27
|
+
services: z.array(ManagedServiceRecordSchema).max(256),
|
|
28
|
+
}).strict();
|
|
29
|
+
export class ManagedServiceRegistryError extends Error {
|
|
30
|
+
constructor(message, options) {
|
|
31
|
+
super(message, options);
|
|
32
|
+
this.name = "ManagedServiceRegistryError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class ManagedServiceConflictError extends ManagedServiceRegistryError {
|
|
36
|
+
resource;
|
|
37
|
+
owner;
|
|
38
|
+
constructor(resource, candidate, owner) {
|
|
39
|
+
super(`Managed service '${candidate.serviceId}' conflicts with '${owner.serviceId}' on ${resource}`);
|
|
40
|
+
this.name = "ManagedServiceConflictError";
|
|
41
|
+
this.resource = resource;
|
|
42
|
+
this.owner = owner;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function registryDirectory(userHome) {
|
|
46
|
+
return resolve(userHome, ".crew", "daemon", "managed-services");
|
|
47
|
+
}
|
|
48
|
+
export function managedServiceRegistryPath(userHome = homedir()) {
|
|
49
|
+
return resolve(registryDirectory(userHome), "registry.json");
|
|
50
|
+
}
|
|
51
|
+
export async function listManagedServiceDescriptorPaths(platform, userHome = homedir()) {
|
|
52
|
+
const directory = platform === "darwin"
|
|
53
|
+
? resolve(userHome, "Library", "LaunchAgents")
|
|
54
|
+
: platform === "linux"
|
|
55
|
+
? resolve(userHome, ".config", "systemd", "user")
|
|
56
|
+
: null;
|
|
57
|
+
if (directory === null)
|
|
58
|
+
return [];
|
|
59
|
+
const pattern = platform === "darwin"
|
|
60
|
+
? /^com\.nowcrew\.daemon\.[a-z0-9][a-z0-9_-]{0,47}(?:\.[0-9a-f]{12})?\.plist$/
|
|
61
|
+
: /^nowcrew-daemon-[a-z0-9][a-z0-9_-]{0,47}(?:\.[0-9a-f]{12})?\.service$/;
|
|
62
|
+
try {
|
|
63
|
+
return (await readdir(directory, { withFileTypes: true }))
|
|
64
|
+
.filter((entry) => entry.isFile() && pattern.test(entry.name))
|
|
65
|
+
.map((entry) => resolve(directory, entry.name))
|
|
66
|
+
.sort();
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === "ENOENT")
|
|
70
|
+
return [];
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function assertRegistryCoversDescriptors(services, descriptorPaths) {
|
|
75
|
+
const registered = new Set(services.map((record) => canonicalizePath(record.descriptorPath)));
|
|
76
|
+
const unregistered = descriptorPaths.map(canonicalizePath).find((path) => !registered.has(path));
|
|
77
|
+
if (unregistered !== undefined) {
|
|
78
|
+
throw new ManagedServiceRegistryError(`Unregistered NowCrew service descriptor: ${unregistered}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function normalizeManagedServiceRecord(input) {
|
|
82
|
+
const record = ManagedServiceRecordSchema.parse(input);
|
|
83
|
+
return ManagedServiceRecordSchema.parse({
|
|
84
|
+
...record,
|
|
85
|
+
daemonHome: canonicalizePath(record.daemonHome),
|
|
86
|
+
agentsRoot: canonicalizePath(record.agentsRoot),
|
|
87
|
+
nodePath: canonicalizePath(record.nodePath),
|
|
88
|
+
entryPath: canonicalizePath(record.entryPath),
|
|
89
|
+
packageRoot: canonicalizePath(record.packageRoot),
|
|
90
|
+
npmPrefix: canonicalizePath(record.npmPrefix),
|
|
91
|
+
descriptorPath: canonicalizePath(record.descriptorPath),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function canonicalizePath(path) {
|
|
95
|
+
const absolute = resolve(path);
|
|
96
|
+
try {
|
|
97
|
+
return realpathSync.native(absolute);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const code = error.code;
|
|
101
|
+
if (code === "ENOTDIR")
|
|
102
|
+
return absolute;
|
|
103
|
+
if (code !== "ENOENT")
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
const missing = [];
|
|
107
|
+
let ancestor = absolute;
|
|
108
|
+
for (;;) {
|
|
109
|
+
const parent = dirname(ancestor);
|
|
110
|
+
if (parent === ancestor)
|
|
111
|
+
return absolute;
|
|
112
|
+
missing.unshift(ancestor.slice(parent.length + (parent.endsWith("/") ? 0 : 1)));
|
|
113
|
+
ancestor = parent;
|
|
114
|
+
try {
|
|
115
|
+
return resolve(realpathSync.native(ancestor), ...missing);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const code = error.code;
|
|
119
|
+
if (code === "ENOTDIR")
|
|
120
|
+
return absolute;
|
|
121
|
+
if (code !== "ENOENT")
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function parseRegistry(path, raw) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = ManagedServiceRegistrySchema.parse(JSON.parse(raw));
|
|
129
|
+
const services = parsed.services.map(normalizeManagedServiceRecord);
|
|
130
|
+
for (let index = 0; index < services.length; index += 1) {
|
|
131
|
+
const candidate = services[index];
|
|
132
|
+
const conflict = managedServiceConflict(services.slice(0, index), candidate);
|
|
133
|
+
if (conflict !== null) {
|
|
134
|
+
throw new ManagedServiceConflictError(conflict.resource, candidate, conflict.owner);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return services;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
throw new ManagedServiceRegistryError(`Invalid managed service registry: ${path}`, { cause: error });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function serviceDescriptorSha256(descriptor) {
|
|
144
|
+
return createHash("sha256").update(descriptor).digest("hex");
|
|
145
|
+
}
|
|
146
|
+
export function managedServiceIdentityMatches(record, expected) {
|
|
147
|
+
const normalized = normalizeManagedServiceRecord(record);
|
|
148
|
+
return normalized.version === expected.version
|
|
149
|
+
&& normalized.platform === expected.platform
|
|
150
|
+
&& normalized.serviceId === expected.serviceId
|
|
151
|
+
&& normalized.profile === expected.profile
|
|
152
|
+
&& normalized.daemonHome === canonicalizePath(expected.daemonHome)
|
|
153
|
+
&& normalized.agentsRoot === canonicalizePath(expected.agentsRoot)
|
|
154
|
+
&& normalized.nodePath === canonicalizePath(expected.nodePath)
|
|
155
|
+
&& normalized.entryPath === canonicalizePath(expected.entryPath)
|
|
156
|
+
&& normalized.packageRoot === canonicalizePath(expected.packageRoot)
|
|
157
|
+
&& normalized.npmPrefix === canonicalizePath(expected.npmPrefix)
|
|
158
|
+
&& normalized.descriptorPath === canonicalizePath(expected.descriptorPath)
|
|
159
|
+
&& normalized.descriptorSha256 === expected.descriptorSha256;
|
|
160
|
+
}
|
|
161
|
+
async function readRegistryFile(path) {
|
|
162
|
+
try {
|
|
163
|
+
return parseRegistry(path, await readFile(path, "utf8"));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error.code === "ENOENT")
|
|
167
|
+
return [];
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function syncDirectory(path, platform) {
|
|
172
|
+
if (platform === "win32")
|
|
173
|
+
return;
|
|
174
|
+
const handle = await open(path, "r");
|
|
175
|
+
try {
|
|
176
|
+
await handle.sync();
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
await handle.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function writeRegistryFile(path, services) {
|
|
183
|
+
const directory = dirname(path);
|
|
184
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
185
|
+
await chmod(directory, 0o700);
|
|
186
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
187
|
+
let handle = null;
|
|
188
|
+
try {
|
|
189
|
+
handle = await open(temporary, "wx", 0o600);
|
|
190
|
+
await handle.writeFile(`${JSON.stringify({ version: 1, services }, null, 2)}\n`, "utf8");
|
|
191
|
+
await handle.sync();
|
|
192
|
+
await handle.close();
|
|
193
|
+
handle = null;
|
|
194
|
+
await rename(temporary, path);
|
|
195
|
+
await chmod(path, 0o600);
|
|
196
|
+
await syncDirectory(directory, process.platform);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await handle?.close().catch(() => undefined);
|
|
200
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function sameRecord(left, right) {
|
|
204
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
205
|
+
}
|
|
206
|
+
export function managedServiceConflict(services, candidate) {
|
|
207
|
+
for (const owner of services) {
|
|
208
|
+
if (owner.serviceId === candidate.serviceId)
|
|
209
|
+
return { resource: "serviceId", owner };
|
|
210
|
+
if (owner.daemonHome === candidate.daemonHome && owner.profile === candidate.profile) {
|
|
211
|
+
return { resource: "daemonProfile", owner };
|
|
212
|
+
}
|
|
213
|
+
if (owner.agentsRoot === candidate.agentsRoot)
|
|
214
|
+
return { resource: "agentsRoot", owner };
|
|
215
|
+
if (owner.npmPrefix === candidate.npmPrefix)
|
|
216
|
+
return { resource: "npmPrefix", owner };
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
export async function withManagedServiceRegistryLock(options, operation) {
|
|
221
|
+
const userHome = options.userHome ?? homedir();
|
|
222
|
+
const directory = registryDirectory(userHome);
|
|
223
|
+
const release = await acquireProfileSaveLock(directory, {
|
|
224
|
+
...(options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs }),
|
|
225
|
+
...(options.lockRetryMs === undefined ? {} : { retryMs: options.lockRetryMs }),
|
|
226
|
+
});
|
|
227
|
+
let operationError;
|
|
228
|
+
try {
|
|
229
|
+
const path = managedServiceRegistryPath(userHome);
|
|
230
|
+
const services = await readRegistryFile(path);
|
|
231
|
+
return await operation({
|
|
232
|
+
services,
|
|
233
|
+
replace: async (replacement) => {
|
|
234
|
+
const normalized = replacement.map(normalizeManagedServiceRecord);
|
|
235
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
236
|
+
const candidate = normalized[index];
|
|
237
|
+
const conflict = managedServiceConflict(normalized.slice(0, index), candidate);
|
|
238
|
+
if (conflict !== null) {
|
|
239
|
+
throw new ManagedServiceConflictError(conflict.resource, candidate, conflict.owner);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
await writeRegistryFile(path, normalized);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
operationError = error;
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
try {
|
|
252
|
+
await release();
|
|
253
|
+
}
|
|
254
|
+
catch (releaseError) {
|
|
255
|
+
if (operationError === undefined)
|
|
256
|
+
throw releaseError;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export async function readManagedServiceRegistry(options = {}) {
|
|
261
|
+
return readRegistryFile(managedServiceRegistryPath(options.userHome ?? homedir()));
|
|
262
|
+
}
|
|
263
|
+
export async function registerManagedService(input, options = {}) {
|
|
264
|
+
const candidate = normalizeManagedServiceRecord(input);
|
|
265
|
+
await withManagedServiceRegistryLock(options, async ({ services, replace }) => {
|
|
266
|
+
const conflict = managedServiceConflict(services, candidate);
|
|
267
|
+
if (conflict !== null)
|
|
268
|
+
throw new ManagedServiceConflictError(conflict.resource, candidate, conflict.owner);
|
|
269
|
+
await replace([...services, candidate].sort((left, right) => left.serviceId.localeCompare(right.serviceId)));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
export async function unregisterManagedService(input, options = {}) {
|
|
273
|
+
const candidate = normalizeManagedServiceRecord(input);
|
|
274
|
+
await withManagedServiceRegistryLock(options, async ({ services, replace }) => {
|
|
275
|
+
const index = services.findIndex((record) => record.serviceId === candidate.serviceId
|
|
276
|
+
&& record.daemonHome === candidate.daemonHome
|
|
277
|
+
&& record.profile === candidate.profile);
|
|
278
|
+
if (index < 0)
|
|
279
|
+
throw new ManagedServiceRegistryError(`Managed service '${candidate.serviceId}' is not registered`);
|
|
280
|
+
if (!sameRecord(services[index], candidate)) {
|
|
281
|
+
throw new ManagedServiceRegistryError(`Managed service '${candidate.serviceId}' identity changed before removal`);
|
|
282
|
+
}
|
|
283
|
+
await replace(services.filter((_, candidateIndex) => candidateIndex !== index));
|
|
284
|
+
});
|
|
285
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { buildServiceSpec, readServiceDescriptor } from "./computer-service.js";
|
|
4
|
+
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
5
|
+
import { ManagedServiceRegistryError, assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
|
|
6
|
+
export async function assertManagedServiceStartup(input) {
|
|
7
|
+
if (input.platform !== "darwin" && input.platform !== "linux")
|
|
8
|
+
return;
|
|
9
|
+
const spec = input.spec ?? buildServiceSpec({
|
|
10
|
+
platform: input.platform,
|
|
11
|
+
profile: input.profile,
|
|
12
|
+
userHome: input.userHome,
|
|
13
|
+
uid: input.uid,
|
|
14
|
+
nodePath: input.nodePath,
|
|
15
|
+
entryPath: input.entryPath,
|
|
16
|
+
profileHome: input.daemonHome,
|
|
17
|
+
});
|
|
18
|
+
const readRegistry = input.readManagedServices
|
|
19
|
+
?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
|
|
20
|
+
const listDescriptors = input.listManagedDescriptorPaths
|
|
21
|
+
?? (() => listManagedServiceDescriptorPaths(input.platform, input.userHome));
|
|
22
|
+
const readDescriptor = input.readServiceDescriptor ?? readServiceDescriptor;
|
|
23
|
+
const [services, descriptorPaths, descriptor] = await Promise.all([
|
|
24
|
+
readRegistry(),
|
|
25
|
+
listDescriptors(),
|
|
26
|
+
readDescriptor(spec),
|
|
27
|
+
]);
|
|
28
|
+
const registered = services.find((record) => record.serviceId === spec.id
|
|
29
|
+
|| (record.daemonHome === input.daemonHome && record.profile === input.profile));
|
|
30
|
+
const defaultDaemonHome = resolve(input.userHome, ".crew", "daemon");
|
|
31
|
+
if (registered === undefined
|
|
32
|
+
&& resolve(input.daemonHome) === defaultDaemonHome
|
|
33
|
+
&& descriptor === spec.descriptor) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (registered === undefined && descriptor === null) {
|
|
37
|
+
const legacySpec = buildServiceSpec({
|
|
38
|
+
platform: input.platform,
|
|
39
|
+
profile: input.profile,
|
|
40
|
+
userHome: input.userHome,
|
|
41
|
+
uid: input.uid,
|
|
42
|
+
nodePath: input.nodePath,
|
|
43
|
+
entryPath: input.entryPath,
|
|
44
|
+
profileHome: input.daemonHome,
|
|
45
|
+
legacyServiceId: true,
|
|
46
|
+
});
|
|
47
|
+
const legacyDescriptor = await readDescriptor(legacySpec);
|
|
48
|
+
if (legacyDescriptor === legacySpec.descriptor)
|
|
49
|
+
return;
|
|
50
|
+
if (descriptorPaths.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (registered === undefined) {
|
|
54
|
+
throw new ManagedServiceRegistryError(`Managed service '${spec.id}' is installed but not registered`);
|
|
55
|
+
}
|
|
56
|
+
assertRegistryCoversDescriptors(services, descriptorPaths);
|
|
57
|
+
const installation = daemonGlobalInstallation(input.entryPath, input.platform);
|
|
58
|
+
if (installation === null || descriptor === null || spec.descriptorPath === null || spec.descriptor === null
|
|
59
|
+
|| descriptor !== spec.descriptor
|
|
60
|
+
|| !managedServiceIdentityMatches(registered, {
|
|
61
|
+
version: 1,
|
|
62
|
+
platform: input.platform,
|
|
63
|
+
serviceId: spec.id,
|
|
64
|
+
profile: input.profile,
|
|
65
|
+
daemonHome: input.daemonHome,
|
|
66
|
+
agentsRoot: input.agentsRoot,
|
|
67
|
+
nodePath: input.nodePath,
|
|
68
|
+
entryPath: input.entryPath,
|
|
69
|
+
packageRoot: installation.packageRoot,
|
|
70
|
+
npmPrefix: installation.npmPrefix,
|
|
71
|
+
descriptorPath: spec.descriptorPath,
|
|
72
|
+
descriptorSha256: serviceDescriptorSha256(descriptor),
|
|
73
|
+
})) {
|
|
74
|
+
throw new ManagedServiceRegistryError(`Managed service '${spec.id}' identity does not match its runtime`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function defaultManagedServiceStartupInput(input) {
|
|
78
|
+
return {
|
|
79
|
+
...input,
|
|
80
|
+
platform: process.platform,
|
|
81
|
+
userHome: homedir(),
|
|
82
|
+
uid: process.getuid?.(),
|
|
83
|
+
nodePath: process.execPath,
|
|
84
|
+
entryPath: process.argv[1] ?? "",
|
|
85
|
+
};
|
|
86
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -1120,6 +1120,7 @@ export function serve(config, opts = {}) {
|
|
|
1120
1120
|
const webSocketClosed = closeWebSocketWithinDeadline(ws, deadline.signal);
|
|
1121
1121
|
const pending = [
|
|
1122
1122
|
webSocketClosed,
|
|
1123
|
+
updateController.drain(),
|
|
1123
1124
|
executionFrameQueue,
|
|
1124
1125
|
...executionRuns.values(),
|
|
1125
1126
|
...[...legacyRuns.values()].map((run) => run.done),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
21
|
+
"@nowcrew/cli": "^0.4.13",
|
|
21
22
|
"cross-spawn": "^7.0.6",
|
|
22
23
|
"ws": "^8",
|
|
23
|
-
"zod": "^3.23.0"
|
|
24
|
-
"@nowcrew/cli": "^0.4.13"
|
|
24
|
+
"zod": "^3.23.0"
|
|
25
25
|
},
|
|
26
26
|
"optionalDependencies": {
|
|
27
27
|
"koffi": "^2.9.0"
|