mioku-service-webui 4.4.5 → 4.5.1

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/database.ts CHANGED
@@ -300,7 +300,9 @@ export function listMemeTree(): Record<string, Record<string, string[]>> {
300
300
  tree[character][emotion] = fs
301
301
  .readdirSync(emotionDir)
302
302
  .filter((name) => /\.(png|jpg|jpeg|gif|webp)$/i.test(name))
303
- .map((name) => path.join("data", "chat", "meme", character, emotion, name));
303
+ .map((name) =>
304
+ path.posix.join("data", "chat", "meme", character, emotion, name),
305
+ );
304
306
  }
305
307
  }
306
308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.4.5",
3
+ "version": "4.5.1",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "type": "module",
package/port-utils.ts CHANGED
@@ -41,9 +41,33 @@ function parsePids(raw: string): number[] {
41
41
  return Array.from(pids);
42
42
  }
43
43
 
44
+ function parseNetstatPids(raw: string, port: number): number[] {
45
+ const pids = new Set<number>();
46
+ for (const line of raw.split(/\r?\n/)) {
47
+ const cols = line.trim().split(/\s+/);
48
+ if (cols.length < 5 || cols[0].toUpperCase() !== "TCP") continue;
49
+ if (!/^LISTENING$/i.test(cols[3])) continue;
50
+ // Local address is host:port — IPv6 forms look like [::]:3339.
51
+ const localPort = cols[1].slice(cols[1].lastIndexOf(":") + 1);
52
+ if (Number(localPort) !== port) continue;
53
+ const pid = Number(cols[4]);
54
+ if (Number.isInteger(pid) && pid > 0) pids.add(pid);
55
+ }
56
+ return Array.from(pids);
57
+ }
58
+
44
59
  async function listListeningPids(
45
60
  port: number,
46
61
  ): Promise<{ pids: number[]; lsofAvailable: boolean }> {
62
+ if (process.platform === "win32") {
63
+ try {
64
+ const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "TCP"]);
65
+ return { pids: parseNetstatPids(stdout, port), lsofAvailable: true };
66
+ } catch (err: any) {
67
+ return { pids: [], lsofAvailable: err?.code !== "ENOENT" };
68
+ }
69
+ }
70
+
47
71
  try {
48
72
  const { stdout } = await execFileAsync("lsof", [
49
73
  "-nP",
@@ -84,8 +108,9 @@ export async function freePort(
84
108
  const initial = await listListeningPids(port);
85
109
 
86
110
  if (!initial.lsofAvailable) {
111
+ const missing = process.platform === "win32" ? "netstat" : "lsof";
87
112
  log.warn(
88
- `未检测到 lsof 命令,无法定位端口 ${port} 的占用进程,建议安装 lsof`,
113
+ `未检测到 ${missing} 命令,无法定位端口 ${port} 的占用进程,建议安装 ${missing}`,
89
114
  );
90
115
  return {
91
116
  port,
package/routes/ai.ts CHANGED
@@ -3,7 +3,7 @@ import { logger } from "mioki";
3
3
  import { getChatConfig, updateChatConfig } from "../system";
4
4
  import { listStickerCatalog } from "../database";
5
5
  import aiService from "mioku-service-ai";
6
- import type { AIUsageRange } from "mioku-service-ai/usage/types";
6
+ import type { AIModelRole, AIProtocol, AIUsageRange } from "mioku";
7
7
 
8
8
  const DEFAULT_MEMORY_GROUP_HISTORY_LIMIT = 300;
9
9
  const DEFAULT_MEMORY_USER_HISTORY_LIMIT = 100;
@@ -12,6 +12,17 @@ const DEFAULT_TOPIC_HISTORY_WINDOW_COUNT = 3;
12
12
  const DEFAULT_EXPRESSION_LEARN_AFTER_MESSAGES = 100;
13
13
  const DEFAULT_EXPRESSION_SAMPLE_SIZE = 8;
14
14
  const usageRanges = new Set<AIUsageRange>(["today", "7d", "30d"]);
15
+ const protocols = new Set<AIProtocol>([
16
+ "openai-chat",
17
+ "openai-response",
18
+ "anthropic",
19
+ "gemini",
20
+ ]);
21
+ const roles = new Set<AIModelRole>(["main", "working", "vision"]);
22
+
23
+ function getAIApi() {
24
+ return aiService?.api as any;
25
+ }
15
26
 
16
27
  function normalizePersonalizationConfig(input: any): any {
17
28
  const data =
@@ -122,14 +133,48 @@ function normalizePersonalizationConfig(input: any): any {
122
133
  return data;
123
134
  }
124
135
 
136
+ function sanitizeBaseForSave(body: any): any {
137
+ if (!body || typeof body !== "object") return body;
138
+ const next = { ...body };
139
+ delete next.apiUrl;
140
+ delete next.apiKey;
141
+ delete next.model;
142
+ delete next.workingModel;
143
+ delete next.multimodalWorkingModel;
144
+ delete next.isMultimodal;
145
+ return next;
146
+ }
147
+
125
148
  export function createAIRoutes() {
126
149
  const app = new Hono();
127
150
 
128
- app.get("/base", (c) =>
129
- c.json({ ok: true, data: getChatConfig("base.json") }),
130
- );
151
+ app.get("/base", (c) => {
152
+ const data = getChatConfig("base.json") || {};
153
+ const api = getAIApi();
154
+ const rolesData = api?.getRoleBindings?.() ?? {};
155
+ const models = api?.listModels?.() ?? [];
156
+ const pickModelId = (full?: string) => {
157
+ if (!full) return "";
158
+ const idx = full.indexOf("/");
159
+ return idx > 0 ? full.slice(idx + 1) : full;
160
+ };
161
+ const vision = models.find((item: any) => item.id === rolesData.vision);
162
+ return c.json({
163
+ ok: true,
164
+ data: {
165
+ ...data,
166
+ model: pickModelId(rolesData.main) || data.model || "",
167
+ workingModel: pickModelId(rolesData.working) || data.workingModel || "",
168
+ multimodalWorkingModel:
169
+ pickModelId(rolesData.vision) || data.multimodalWorkingModel || "",
170
+ isMultimodal: vision?.capabilities?.includes?.("vision") ?? true,
171
+ providersManaged: true,
172
+ },
173
+ });
174
+ });
175
+
131
176
  app.put("/base", async (c) => {
132
- const body = await c.req.json();
177
+ const body = sanitizeBaseForSave(await c.req.json());
133
178
  logger.info(`[webui-action] ai.base.update`);
134
179
  return c.json({ ok: true, data: updateChatConfig("base.json", body) });
135
180
  });
@@ -161,48 +206,230 @@ export function createAIRoutes() {
161
206
  });
162
207
  });
163
208
 
209
+ app.get("/providers", (c) => {
210
+ const api = getAIApi();
211
+ if (!api?.listProviders) {
212
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
213
+ }
214
+ const providers = api.listProviders().map((item: any) => ({
215
+ ...item,
216
+ apiKey: item.apiKey ? "***" : "",
217
+ hasApiKey: Boolean(item.apiKey),
218
+ }));
219
+ return c.json({ ok: true, data: providers });
220
+ });
221
+
222
+ app.post("/providers", async (c) => {
223
+ const api = getAIApi();
224
+ if (!api?.createProvider) {
225
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
226
+ }
227
+ const body = await c.req.json();
228
+ const protocol = String(body?.protocol || "openai-chat") as AIProtocol;
229
+ if (!protocols.has(protocol)) {
230
+ return c.json({ ok: false, error: "INVALID_PROTOCOL" }, 400);
231
+ }
232
+ logger.info(`[webui-action] ai.provider.create`, {
233
+ name: body?.name,
234
+ protocol,
235
+ });
236
+ const created = await api.createProvider({
237
+ id: body?.id,
238
+ name: body?.name,
239
+ protocol,
240
+ apiUrl: body?.apiUrl,
241
+ apiKey: body?.apiKey,
242
+ enabled: body?.enabled !== false,
243
+ headers: body?.headers,
244
+ });
245
+ return c.json({
246
+ ok: true,
247
+ data: {
248
+ ...created,
249
+ apiKey: created.apiKey ? "***" : "",
250
+ hasApiKey: Boolean(created.apiKey),
251
+ },
252
+ });
253
+ });
254
+
255
+ app.put("/providers/:id", async (c) => {
256
+ const api = getAIApi();
257
+ if (!api?.updateProvider) {
258
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
259
+ }
260
+ const id = c.req.param("id");
261
+ const body = await c.req.json();
262
+ logger.info(`[webui-action] ai.provider.update`, { id });
263
+ const patch: Record<string, unknown> = { ...body };
264
+ if (patch.apiKey === "***" || patch.apiKey === "") delete patch.apiKey;
265
+ if (patch.protocol && !protocols.has(patch.protocol as AIProtocol)) {
266
+ return c.json({ ok: false, error: "INVALID_PROTOCOL" }, 400);
267
+ }
268
+ const updated = await api.updateProvider(id, patch);
269
+ return c.json({
270
+ ok: true,
271
+ data: {
272
+ ...updated,
273
+ apiKey: updated.apiKey ? "***" : "",
274
+ hasApiKey: Boolean(updated.apiKey),
275
+ },
276
+ });
277
+ });
278
+
279
+ app.delete("/providers/:id", (c) => {
280
+ const api = getAIApi();
281
+ if (!api?.removeProvider) {
282
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
283
+ }
284
+ const id = c.req.param("id");
285
+ logger.info(`[webui-action] ai.provider.remove`, { id });
286
+ return c.json({ ok: Boolean(api.removeProvider(id)) });
287
+ });
288
+
289
+ app.post("/providers/:id/test", async (c) => {
290
+ const api = getAIApi();
291
+ if (!api?.testProvider) {
292
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
293
+ }
294
+ const id = c.req.param("id");
295
+ logger.info(`[webui-action] ai.provider.test`, { id });
296
+ const result = await api.testProvider(id);
297
+ return c.json({ ok: true, data: result });
298
+ });
299
+
300
+ app.post("/providers/:id/models/refresh", async (c) => {
301
+ const api = getAIApi();
302
+ if (!api?.refreshModels) {
303
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
304
+ }
305
+ const id = c.req.param("id");
306
+ logger.info(`[webui-action] ai.models.refresh`, { id });
307
+ const models = await api.refreshModels(id);
308
+ return c.json({ ok: true, data: models });
309
+ });
310
+
311
+ app.get("/models", (c) => {
312
+ const api = getAIApi();
313
+ if (!api?.listModels) {
314
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
315
+ }
316
+ const providerId = c.req.query("providerId") || undefined;
317
+ return c.json({ ok: true, data: api.listModels(providerId) });
318
+ });
319
+
320
+ app.post("/models", async (c) => {
321
+ const api = getAIApi();
322
+ if (!api?.registerCustomModel) {
323
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
324
+ }
325
+ const body = await c.req.json();
326
+ logger.info(`[webui-action] ai.model.create`, {
327
+ providerId: body?.providerId,
328
+ modelId: body?.modelId,
329
+ });
330
+ const model = api.registerCustomModel({
331
+ providerId: body?.providerId,
332
+ modelId: body?.modelId,
333
+ name: body?.name,
334
+ capabilities: body?.capabilities,
335
+ });
336
+ return c.json({ ok: true, data: model });
337
+ });
338
+
339
+ app.delete("/models/:id", (c) => {
340
+ const api = getAIApi();
341
+ if (!api?.removeCustomModel) {
342
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
343
+ }
344
+ const id = decodeURIComponent(c.req.param("id"));
345
+ logger.info(`[webui-action] ai.model.remove`, { id });
346
+ return c.json({ ok: Boolean(api.removeCustomModel(id)) });
347
+ });
348
+
349
+ app.get("/roles", (c) => {
350
+ const api = getAIApi();
351
+ if (!api?.getRoleBindings) {
352
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
353
+ }
354
+ return c.json({ ok: true, data: api.getRoleBindings() });
355
+ });
356
+
357
+ app.put("/roles", async (c) => {
358
+ const api = getAIApi();
359
+ if (!api?.setRoleBinding) {
360
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
361
+ }
362
+ const body = await c.req.json();
363
+ logger.info(`[webui-action] ai.roles.update`, body);
364
+ for (const role of ["main", "working", "vision"] as AIModelRole[]) {
365
+ if (!(role in (body || {}))) continue;
366
+ const value = body[role];
367
+ if (value === null || value === undefined || value === "") {
368
+ api.setRoleBinding(role, undefined);
369
+ } else {
370
+ api.setRoleBinding(role, String(value));
371
+ }
372
+ }
373
+ return c.json({ ok: true, data: api.getRoleBindings() });
374
+ });
375
+
164
376
  app.get("/instances", (c) => {
165
- const names = aiService?.api?.list?.() ?? [];
166
- return c.json({ ok: true, data: names });
377
+ const api = getAIApi();
378
+ if (api?.listInstances) {
379
+ return c.json({ ok: true, data: api.listInstances() });
380
+ }
381
+ const names = api?.list?.() ?? [];
382
+ return c.json({ ok: true, data: names.map((name: string) => ({ name })) });
167
383
  });
168
384
 
169
385
  app.post("/instances", async (c) => {
170
386
  const body = await c.req.json();
171
387
  logger.info(`[webui-action] ai.instance.create`, {
172
388
  name: body?.name,
173
- apiUrl: body?.apiUrl,
174
- modelType: body?.modelType,
389
+ providerId: body?.providerId,
390
+ modelId: body?.modelId,
175
391
  });
176
- if (!aiService?.api?.create) {
392
+ const api = getAIApi();
393
+ if (api?.createInstance && body?.providerId && body?.modelId) {
394
+ await api.createInstance({
395
+ name: body.name,
396
+ providerId: body.providerId,
397
+ modelId: body.modelId,
398
+ role: roles.has(body.role) ? body.role : undefined,
399
+ });
400
+ return c.json({ ok: true, data: api.listInstances?.() ?? api.list?.() });
401
+ }
402
+ if (!api?.create) {
177
403
  return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
178
404
  }
179
-
180
- await aiService.api.create({
405
+ await api.create({
181
406
  name: body.name,
182
407
  apiUrl: body.apiUrl,
183
408
  apiKey: body.apiKey,
184
409
  modelType: body.modelType || "text",
410
+ model: body.model,
185
411
  });
186
- return c.json({ ok: true, data: aiService.api.list() });
412
+ return c.json({ ok: true, data: api.list() });
187
413
  });
188
414
 
189
415
  app.delete("/instances/:name", (c) => {
190
416
  const name = c.req.param("name");
191
417
  logger.info(`[webui-action] ai.instance.remove`, { name });
192
- const ok = aiService?.api?.remove?.(name);
418
+ const ok = getAIApi()?.remove?.(name);
193
419
  return c.json({ ok: Boolean(ok) });
194
420
  });
195
421
 
196
422
  app.post("/default/:name", (c) => {
197
423
  const name = c.req.param("name");
198
424
  logger.info(`[webui-action] ai.instance.set-default`, { name });
199
- const ok = aiService?.api?.setDefault?.(name);
425
+ const ok = getAIApi()?.setDefault?.(name);
200
426
  return c.json({ ok: Boolean(ok) });
201
427
  });
202
428
 
203
429
  app.get("/skills", (c) => {
204
- const skills = aiService?.api?.getAllSkills?.();
205
- const tools = aiService?.api?.getAllTools?.();
430
+ const api = getAIApi();
431
+ const skills = api?.getAllSkills?.();
432
+ const tools = api?.getAllTools?.();
206
433
  return c.json({
207
434
  ok: true,
208
435
  data: {
@@ -217,7 +444,8 @@ export function createAIRoutes() {
217
444
  );
218
445
 
219
446
  app.get("/usage", (c) => {
220
- if (!aiService?.api?.getUsageSummary) {
447
+ const api = getAIApi();
448
+ if (!api?.getUsageSummary) {
221
449
  return c.json({ ok: false, error: "AI_USAGE_UNAVAILABLE" }, 503);
222
450
  }
223
451
 
@@ -236,7 +464,7 @@ export function createAIRoutes() {
236
464
 
237
465
  return c.json({
238
466
  ok: true,
239
- data: aiService.api.getUsageSummary({ range, botId }),
467
+ data: api.getUsageSummary({ range, botId }),
240
468
  });
241
469
  });
242
470
 
package/routes/db.ts CHANGED
@@ -231,7 +231,10 @@ export function createMemeRoutes() {
231
231
  const buffer = Buffer.from(await file.arrayBuffer());
232
232
  fs.writeFileSync(filePath, buffer);
233
233
 
234
- return c.json({ ok: true, path: path.relative(process.cwd(), filePath) });
234
+ return c.json({
235
+ ok: true,
236
+ path: path.relative(process.cwd(), filePath).split(path.sep).join("/"),
237
+ });
235
238
  });
236
239
 
237
240
  app.delete("/", async (c) => {
package/system.ts CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  WEBUI_DIST,
34
34
  writeJsonFile,
35
35
  } from "./utils";
36
- import { spawn } from "node:child_process";
36
+ import * as fsp from "node:fs/promises";
37
37
 
38
38
  async function fetchJson(url: string): Promise<any> {
39
39
  const res = await fetch(url, {
@@ -553,44 +553,6 @@ function getRepositoryFromPackage(pkg: any): string {
553
553
  return "";
554
554
  }
555
555
 
556
- async function commandExists(cmd: string): Promise<boolean> {
557
- const result = await runCommand("which", [cmd], process.cwd());
558
- return result.code === 0;
559
- }
560
-
561
- async function ensureUnzipCommand(): Promise<void> {
562
- if (await commandExists("unzip")) {
563
- return;
564
- }
565
-
566
- const platform = os.platform();
567
- if (platform === "darwin") {
568
- const installRes = await runCommand(
569
- "brew",
570
- ["install", "unzip"],
571
- process.cwd(),
572
- );
573
- if (installRes.code !== 0) {
574
- throw new Error(
575
- `未找到 unzip 且自动安装失败: ${installRes.stderr || installRes.stdout}。\n` +
576
- `请手动运行: brew install unzip`,
577
- );
578
- }
579
- } else {
580
- const installRes = await runCommand(
581
- "sudo",
582
- ["apt", "install", "-y", "unzip"],
583
- process.cwd(),
584
- );
585
- if (installRes.code !== 0) {
586
- throw new Error(
587
- `未找到 unzip 且自动安装失败: ${installRes.stderr || installRes.stdout}。\n` +
588
- `请手动运行: sudo apt install unzip`,
589
- );
590
- }
591
- }
592
- }
593
-
594
556
  async function getGitOriginUrl(dir: string): Promise<string> {
595
557
  const res = await runCommand("git", ["remote", "get-url", "origin"], dir);
596
558
  if (res.code !== 0) return "";
@@ -1912,58 +1874,31 @@ export async function updateWebUIDistFromRelease(): Promise<
1912
1874
  try {
1913
1875
  logger.info(`[webui] 正在下载 WebUI dist...`);
1914
1876
 
1915
- await new Promise<void>((resolve, reject) => {
1916
- const curl = spawn(
1917
- "curl",
1918
- [
1919
- "-L",
1920
- "-o",
1921
- zipPath,
1922
- "-A",
1923
- "mioku-webui-updater/1.0",
1924
- "--silent",
1925
- "--show-error",
1926
- downloadUrl,
1927
- ],
1928
- {
1929
- stdio: ["ignore", "pipe", "pipe"],
1930
- },
1931
- );
1932
-
1933
- let stderr = "";
1934
- curl.stderr.on("data", (chunk) => {
1935
- stderr += String(chunk);
1936
- });
1877
+ const res = await fetch(downloadUrl, {
1878
+ headers: { "User-Agent": "mioku-webui-updater/1.0" },
1879
+ redirect: "follow",
1880
+ });
1881
+ if (!res.ok || !res.body) {
1882
+ throw new Error(`下载失败: HTTP ${res.status}`);
1883
+ }
1937
1884
 
1938
- curl.on("close", (code) => {
1939
- if (code === 0) {
1940
- resolve();
1941
- } else {
1942
- reject(new Error(`curl 下载失败 (code ${code}): ${stderr}`));
1943
- }
1944
- });
1945
-
1946
- curl.on("error", (err) => {
1947
- reject(new Error(`curl 进程异常: ${err.message}`));
1948
- });
1949
-
1950
- // Periodically log progress by checking file size
1951
- const progressTimer = setInterval(() => {
1952
- if (fs.existsSync(zipPath)) {
1953
- const stat = fs.statSync(zipPath);
1954
- const now = Date.now();
1955
- if (now - lastLogTime >= LOG_INTERVAL_MS) {
1956
- logger.info(
1957
- `[webui] 正在下载 WebUI dist... ${(stat.size / 1024 / 1024).toFixed(1)} MB 已下载`,
1958
- );
1959
- lastLogTime = now;
1960
- }
1885
+ let downloaded = 0;
1886
+ const handle = await fsp.open(zipPath, "w");
1887
+ try {
1888
+ for await (const chunk of res.body as unknown as AsyncIterable<Uint8Array>) {
1889
+ await handle.write(chunk);
1890
+ downloaded += chunk.byteLength;
1891
+ const now = Date.now();
1892
+ if (now - lastLogTime >= LOG_INTERVAL_MS) {
1893
+ logger.info(
1894
+ `[webui] 正在下载 WebUI dist... ${(downloaded / 1024 / 1024).toFixed(1)} MB 已下载`,
1895
+ );
1896
+ lastLogTime = now;
1961
1897
  }
1962
- }, 1000);
1963
-
1964
- // Clear timer when done
1965
- curl.on("close", () => clearInterval(progressTimer));
1966
- });
1898
+ }
1899
+ } finally {
1900
+ await handle.close();
1901
+ }
1967
1902
 
1968
1903
  const zipStat = fs.statSync(zipPath);
1969
1904
  logger.info(
package/utils.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { spawn } from "node:child_process";
5
4
  import type { PackageManager, WebUISettings } from "./types";
6
5
  import {
7
6
  getServiceConfigDir,
8
7
  getServiceDataDir,
9
8
  getConfigDir,
10
9
  getDataDir,
10
+ runCommand as runMiokuCommand,
11
11
  } from "mioku";
12
12
 
13
13
  const __filename = fileURLToPath(import.meta.url);
@@ -86,34 +86,7 @@ export async function runCommand(
86
86
  args: string[],
87
87
  cwd: string,
88
88
  ): Promise<{ stdout: string; stderr: string; code: number }> {
89
- return new Promise((resolve) => {
90
- const child = spawn(command, args, {
91
- cwd,
92
- stdio: ["ignore", "pipe", "pipe"],
93
- });
94
- let stdout = "";
95
- let stderr = "";
96
-
97
- child.stdout.on("data", (chunk) => {
98
- stdout += String(chunk);
99
- });
100
-
101
- child.stderr.on("data", (chunk) => {
102
- stderr += String(chunk);
103
- });
104
-
105
- child.on("close", (code) => {
106
- resolve({ stdout, stderr, code: code ?? 1 });
107
- });
108
-
109
- child.on("error", (error) => {
110
- resolve({
111
- stdout,
112
- stderr: `${stderr}\n${error.message}`.trim(),
113
- code: 1,
114
- });
115
- });
116
- });
89
+ return runMiokuCommand(command, args, { cwd });
117
90
  }
118
91
 
119
92
  export function getInstallCommand(): { cmd: string; args: string[] } {