mioku-service-webui 4.1.0 → 4.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/config-page-loader.ts +24 -11
- package/index.ts +11 -2
- package/package.json +1 -1
- package/routes/access-control.ts +162 -0
- package/routes/db.ts +100 -1
- package/routes/index.ts +2 -1
- package/system.ts +26 -0
package/config-page-loader.ts
CHANGED
|
@@ -27,25 +27,40 @@ export interface ConfigPageManifest {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function loadPluginConfigPage(pluginName: string): ConfigPageManifest | null {
|
|
30
|
-
// 支持两种路径:本地 plugins 目录和 npm 包
|
|
31
30
|
const possiblePaths = [
|
|
32
31
|
path.join(process.cwd(), "plugins", pluginName),
|
|
33
32
|
path.join(process.cwd(), "node_modules", `mioku-plugin-${pluginName}`),
|
|
34
33
|
];
|
|
35
34
|
|
|
35
|
+
return loadConfigPage(pluginName, possiblePaths);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function loadServiceConfigPage(serviceName: string): ConfigPageManifest | null {
|
|
39
|
+
const possiblePaths = [
|
|
40
|
+
path.join(process.cwd(), "services", serviceName),
|
|
41
|
+
path.join(process.cwd(), "node_modules", `mioku-service-${serviceName}`),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return loadConfigPage(serviceName, possiblePaths);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadConfigPage(
|
|
48
|
+
packageName: string,
|
|
49
|
+
possiblePaths: string[],
|
|
50
|
+
): ConfigPageManifest | null {
|
|
36
51
|
let configMdPath: string | null = null;
|
|
37
|
-
let
|
|
52
|
+
let packageDir: string | null = null;
|
|
38
53
|
|
|
39
54
|
for (const dir of possiblePaths) {
|
|
40
55
|
const candidate = path.join(dir, "config.md");
|
|
41
56
|
if (fs.existsSync(candidate)) {
|
|
42
57
|
configMdPath = candidate;
|
|
43
|
-
|
|
58
|
+
packageDir = dir;
|
|
44
59
|
break;
|
|
45
60
|
}
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
if (!configMdPath || !
|
|
63
|
+
if (!configMdPath || !packageDir) {
|
|
49
64
|
return null;
|
|
50
65
|
}
|
|
51
66
|
|
|
@@ -56,29 +71,27 @@ export function loadPluginConfigPage(pluginName: string): ConfigPageManifest | n
|
|
|
56
71
|
const frontmatter = parsed.data as any;
|
|
57
72
|
const fields = (frontmatter.fields || []) as ConfigField[];
|
|
58
73
|
|
|
59
|
-
// Validate fields
|
|
60
74
|
for (const field of fields) {
|
|
61
75
|
if (!field.key || !field.label || !field.type) {
|
|
62
|
-
logger.warn(`Invalid field in ${
|
|
76
|
+
logger.warn(`Invalid field in ${packageName}/config.md: missing key, label, or type`);
|
|
63
77
|
continue;
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
// Validate key format: <configName>.<jsonPath>
|
|
67
80
|
if (!field.key.includes(".")) {
|
|
68
|
-
logger.warn(`Invalid field key in ${
|
|
81
|
+
logger.warn(`Invalid field key in ${packageName}/config.md: ${field.key} (must be <configName>.<path>)`);
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
return {
|
|
73
|
-
plugin:
|
|
74
|
-
title: frontmatter.title || `${
|
|
86
|
+
plugin: packageName,
|
|
87
|
+
title: frontmatter.title || `${packageName} Configuration`,
|
|
75
88
|
description: frontmatter.description,
|
|
76
89
|
markdown: parsed.content,
|
|
77
90
|
fields,
|
|
78
91
|
hasCustomPage: true,
|
|
79
92
|
};
|
|
80
93
|
} catch (error: any) {
|
|
81
|
-
logger.error(`Failed to load config page for ${
|
|
94
|
+
logger.error(`Failed to load config page for ${packageName}: ${error.message}`);
|
|
82
95
|
return null;
|
|
83
96
|
}
|
|
84
97
|
}
|
package/index.ts
CHANGED
|
@@ -8,7 +8,12 @@ import { createNodeWebSocket } from "@hono/node-ws";
|
|
|
8
8
|
import type { MiokuService } from "mioku";
|
|
9
9
|
import type { WebUISettings } from "./types";
|
|
10
10
|
import { ensureAuthConfig, loginWithToken, requireAuth } from "./auth";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ensureWebUIDist,
|
|
13
|
+
getWebUISettings,
|
|
14
|
+
getSystemOverview,
|
|
15
|
+
getSaying,
|
|
16
|
+
} from "./system";
|
|
12
17
|
import { CHAT_CONFIG_DIR, CHAT_DATA_DIR, LOGS_DIR, WEBUI_DIST } from "./utils";
|
|
13
18
|
import { initBuiltinDatasources } from "./datasources";
|
|
14
19
|
import {
|
|
@@ -17,11 +22,13 @@ import {
|
|
|
17
22
|
createAIRoutes,
|
|
18
23
|
createDBRoutes,
|
|
19
24
|
createPluginConfigRoutes,
|
|
25
|
+
createServiceConfigRoutes,
|
|
20
26
|
createMemeRoutes,
|
|
21
27
|
createManageRoutes,
|
|
22
28
|
createStoreRoutes,
|
|
23
29
|
createDataManagementRoutes,
|
|
24
30
|
createStrangerRoutes,
|
|
31
|
+
createAccessControlRoutes,
|
|
25
32
|
} from "./routes";
|
|
26
33
|
|
|
27
34
|
export interface WebUIServiceAPI {
|
|
@@ -99,9 +106,11 @@ class WebUIRuntime {
|
|
|
99
106
|
this.app.route("/api/store", createStoreRoutes());
|
|
100
107
|
this.app.route("/api/db", createDBRoutes());
|
|
101
108
|
this.app.route("/api/plugin-config", createPluginConfigRoutes());
|
|
109
|
+
this.app.route("/api/service-config", createServiceConfigRoutes());
|
|
102
110
|
this.app.route("/api/meme", createMemeRoutes());
|
|
103
111
|
this.app.route("/api/data-management", createDataManagementRoutes());
|
|
104
112
|
this.app.route("/api/stranger", createStrangerRoutes());
|
|
113
|
+
this.app.route("/api/access-control", createAccessControlRoutes());
|
|
105
114
|
|
|
106
115
|
this.app.get(
|
|
107
116
|
"/api/ws/logs",
|
|
@@ -213,7 +222,7 @@ const runtime = new WebUIRuntime();
|
|
|
213
222
|
|
|
214
223
|
const webUIService: MiokuService = {
|
|
215
224
|
name: "webui",
|
|
216
|
-
version: "2.0
|
|
225
|
+
version: "4.2.0",
|
|
217
226
|
description: "Mioku WebUI 管理服务",
|
|
218
227
|
api: {
|
|
219
228
|
getSettings: () => getWebUISettings(),
|
package/package.json
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { logger } from "mioki";
|
|
5
|
+
import { readJsonFile, writeJsonFile, NODE_MODULES_DIR } from "../utils";
|
|
6
|
+
|
|
7
|
+
const ACCESS_CONFIG_PATH = path.resolve(
|
|
8
|
+
process.cwd(),
|
|
9
|
+
"config/boot/access-control.json",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
version: 1,
|
|
14
|
+
global: { plugins: {}, commands: {} },
|
|
15
|
+
groups: {},
|
|
16
|
+
users: {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function normalizeAccessConfig(input: any) {
|
|
20
|
+
return {
|
|
21
|
+
version: 1,
|
|
22
|
+
global: {
|
|
23
|
+
plugins: input?.global?.plugins ?? {},
|
|
24
|
+
commands: input?.global?.commands ?? {},
|
|
25
|
+
},
|
|
26
|
+
groups: input?.groups ?? {},
|
|
27
|
+
users: input?.users ?? {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type AccessItemKind = "plugin" | "command";
|
|
32
|
+
|
|
33
|
+
interface AccessItem {
|
|
34
|
+
kind: AccessItemKind;
|
|
35
|
+
plugin: string;
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
desc?: string;
|
|
39
|
+
match?: string;
|
|
40
|
+
event?: string;
|
|
41
|
+
fromHook: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readAccessCatalog(): AccessItem[] {
|
|
45
|
+
if (!fs.existsSync(NODE_MODULES_DIR)) return [];
|
|
46
|
+
const entries = fs.readdirSync(NODE_MODULES_DIR, { withFileTypes: true });
|
|
47
|
+
const items: AccessItem[] = [];
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.name.startsWith("mioku-plugin-")) continue;
|
|
51
|
+
const fullPath = path.join(NODE_MODULES_DIR, entry.name);
|
|
52
|
+
let stat: fs.Stats;
|
|
53
|
+
try {
|
|
54
|
+
stat = fs.lstatSync(fullPath);
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
|
|
59
|
+
|
|
60
|
+
const pkgPath = path.join(fullPath, "package.json");
|
|
61
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
62
|
+
let pkg: any;
|
|
63
|
+
try {
|
|
64
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const name = entry.name.replace(/^mioku-plugin-/, "");
|
|
70
|
+
const mioku = pkg.mioku || {};
|
|
71
|
+
const help = mioku.help || { title: name, description: "", commands: [] };
|
|
72
|
+
const hooks: Array<{ id: string; match?: string; event?: string; description?: string }> =
|
|
73
|
+
mioku.accessHooks || [];
|
|
74
|
+
const helpCommands: Array<any> = help.commands || [];
|
|
75
|
+
|
|
76
|
+
items.push({
|
|
77
|
+
kind: "plugin",
|
|
78
|
+
plugin: name,
|
|
79
|
+
id: name,
|
|
80
|
+
label: help.title || name,
|
|
81
|
+
desc: help.description,
|
|
82
|
+
fromHook: false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const seen = new Set<string>();
|
|
86
|
+
for (const hook of hooks) {
|
|
87
|
+
seen.add(hook.id);
|
|
88
|
+
items.push({
|
|
89
|
+
kind: "command",
|
|
90
|
+
plugin: name,
|
|
91
|
+
id: hook.id,
|
|
92
|
+
label: hook.id,
|
|
93
|
+
desc: hook.description,
|
|
94
|
+
match: hook.match,
|
|
95
|
+
event: hook.event,
|
|
96
|
+
fromHook: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const c of helpCommands) {
|
|
100
|
+
const id = String(c.cmd || "").trim();
|
|
101
|
+
if (!id || seen.has(id)) continue;
|
|
102
|
+
items.push({
|
|
103
|
+
kind: "command",
|
|
104
|
+
plugin: name,
|
|
105
|
+
id,
|
|
106
|
+
label: id,
|
|
107
|
+
desc: c.desc,
|
|
108
|
+
fromHook: false,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return items.sort((a, b) => {
|
|
114
|
+
if (a.kind !== b.kind) return a.kind === "plugin" ? -1 : 1;
|
|
115
|
+
if (a.plugin !== b.plugin) return a.plugin.localeCompare(b.plugin);
|
|
116
|
+
return a.id.localeCompare(b.id);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function filterCatalog(items: AccessItem[], query?: string): AccessItem[] {
|
|
121
|
+
if (!query) return items;
|
|
122
|
+
const q = query.trim().toLowerCase();
|
|
123
|
+
if (!q) return items;
|
|
124
|
+
return items.filter((it) => {
|
|
125
|
+
return (
|
|
126
|
+
it.id.toLowerCase().includes(q) ||
|
|
127
|
+
it.label.toLowerCase().includes(q) ||
|
|
128
|
+
it.plugin.toLowerCase().includes(q) ||
|
|
129
|
+
(it.desc || "").toLowerCase().includes(q)
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createAccessControlRoutes() {
|
|
135
|
+
const app = new Hono();
|
|
136
|
+
|
|
137
|
+
app.get("/", (c) => {
|
|
138
|
+
const data = readJsonFile(ACCESS_CONFIG_PATH, DEFAULT_CONFIG);
|
|
139
|
+
return c.json({ ok: true, data: normalizeAccessConfig(data) });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
app.put("/", async (c) => {
|
|
143
|
+
const body = await c.req.json().catch(() => null);
|
|
144
|
+
if (!body) {
|
|
145
|
+
return c.json({ ok: false, error: "INVALID_BODY" }, 400);
|
|
146
|
+
}
|
|
147
|
+
const data = normalizeAccessConfig(body);
|
|
148
|
+
writeJsonFile(ACCESS_CONFIG_PATH, data);
|
|
149
|
+
logger.info(
|
|
150
|
+
`[webui-action] access-control.update | groups=${Object.keys(data.groups).length} users=${Object.keys(data.users).length}`,
|
|
151
|
+
);
|
|
152
|
+
return c.json({ ok: true, data });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
app.get("/catalog", (c) => {
|
|
156
|
+
const q = c.req.query("q");
|
|
157
|
+
const data = filterCatalog(readAccessCatalog(), q);
|
|
158
|
+
return c.json({ ok: true, data });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return app;
|
|
162
|
+
}
|
package/routes/db.ts
CHANGED
|
@@ -14,8 +14,12 @@ import {
|
|
|
14
14
|
updateMessage,
|
|
15
15
|
} from "../database";
|
|
16
16
|
import { CHAT_DATA_DIR } from "../utils";
|
|
17
|
-
import { loadPluginConfigPage } from "../config-page-loader";
|
|
17
|
+
import { loadPluginConfigPage, loadServiceConfigPage } from "../config-page-loader";
|
|
18
18
|
import { getDatasource } from "../datasources";
|
|
19
|
+
import {
|
|
20
|
+
getServiceConfigs,
|
|
21
|
+
updateServiceConfig,
|
|
22
|
+
} from "mioku";
|
|
19
23
|
|
|
20
24
|
export function createDBRoutes() {
|
|
21
25
|
const app = new Hono();
|
|
@@ -219,3 +223,98 @@ export function createMemeRoutes() {
|
|
|
219
223
|
|
|
220
224
|
return app;
|
|
221
225
|
}
|
|
226
|
+
|
|
227
|
+
export function createServiceConfigRoutes() {
|
|
228
|
+
const app = new Hono();
|
|
229
|
+
const SERVICE_CONFIG_ROOT = path.join(process.cwd(), "config", "service");
|
|
230
|
+
|
|
231
|
+
app.get("/overview", (c) => {
|
|
232
|
+
const modulesPath = path.join(process.cwd(), "node_modules");
|
|
233
|
+
if (!fs.existsSync(modulesPath)) {
|
|
234
|
+
return c.json({ ok: true, data: [] });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const services: string[] = [];
|
|
238
|
+
const entries = fs.readdirSync(modulesPath, { withFileTypes: true });
|
|
239
|
+
for (const entry of entries) {
|
|
240
|
+
if (entry.name.startsWith("mioku-service-")) {
|
|
241
|
+
services.push(entry.name.replace(/^mioku-service-/, ""));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const overview = services
|
|
246
|
+
.map((name) => {
|
|
247
|
+
const configDir = path.join(SERVICE_CONFIG_ROOT, name);
|
|
248
|
+
const manifest = loadServiceConfigPage(name);
|
|
249
|
+
const existingConfigFiles =
|
|
250
|
+
fs.existsSync(configDir)
|
|
251
|
+
? fs
|
|
252
|
+
.readdirSync(configDir)
|
|
253
|
+
.filter((f) => f.endsWith(".json"))
|
|
254
|
+
.map((f) => f.replace(".json", ""))
|
|
255
|
+
: [];
|
|
256
|
+
const declaredConfigFiles = manifest
|
|
257
|
+
? Array.from(
|
|
258
|
+
new Set(
|
|
259
|
+
(manifest.fields || [])
|
|
260
|
+
.map((field) => String(field.key || ""))
|
|
261
|
+
.filter((key) => key.includes("."))
|
|
262
|
+
.map((key) => key.split(".")[0]),
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
: [];
|
|
266
|
+
const configFiles = Array.from(
|
|
267
|
+
new Set([...existingConfigFiles, ...declaredConfigFiles]),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
name,
|
|
272
|
+
title: manifest?.title || name,
|
|
273
|
+
description: manifest?.description,
|
|
274
|
+
hasPage: manifest?.hasCustomPage || false,
|
|
275
|
+
configFiles,
|
|
276
|
+
};
|
|
277
|
+
})
|
|
278
|
+
.filter((p) => p.hasPage);
|
|
279
|
+
|
|
280
|
+
return c.json({ ok: true, data: overview });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
app.get("/:name/page", (c) => {
|
|
284
|
+
const name = c.req.param("name");
|
|
285
|
+
const manifest = loadServiceConfigPage(name);
|
|
286
|
+
const configs = getServiceConfigs(name);
|
|
287
|
+
|
|
288
|
+
if (!manifest) {
|
|
289
|
+
return c.json({ ok: true, data: null });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return c.json({
|
|
293
|
+
ok: true,
|
|
294
|
+
data: {
|
|
295
|
+
plugin: manifest.plugin,
|
|
296
|
+
title: manifest.title,
|
|
297
|
+
description: manifest.description,
|
|
298
|
+
markdown: manifest.markdown,
|
|
299
|
+
fields: manifest.fields,
|
|
300
|
+
hasCustomPage: manifest.hasCustomPage,
|
|
301
|
+
configs,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
app.get("/:name", (c) => {
|
|
307
|
+
const name = c.req.param("name");
|
|
308
|
+
return c.json({ ok: true, data: getServiceConfigs(name) });
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
app.put("/:name/:config", async (c) => {
|
|
312
|
+
const serviceName = c.req.param("name");
|
|
313
|
+
const configName = c.req.param("config");
|
|
314
|
+
const body = await c.req.json();
|
|
315
|
+
updateServiceConfig(serviceName, configName, body);
|
|
316
|
+
return c.json({ ok: true });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return app;
|
|
320
|
+
}
|
package/routes/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { createConfigRoutes } from "./config";
|
|
2
2
|
export { createWebUISettingsRoutes } from "./webui";
|
|
3
3
|
export { createAIRoutes } from "./ai";
|
|
4
|
-
export { createDBRoutes, createPluginConfigRoutes, createMemeRoutes } from "./db";
|
|
4
|
+
export { createDBRoutes, createPluginConfigRoutes, createMemeRoutes, createServiceConfigRoutes } from "./db";
|
|
5
5
|
export { createManageRoutes } from "./manage";
|
|
6
6
|
export { createStoreRoutes } from "./store";
|
|
7
7
|
export { createDataManagementRoutes } from "./data-management";
|
|
8
8
|
export { createStrangerRoutes } from "./stranger";
|
|
9
|
+
export { createAccessControlRoutes } from "./access-control";
|
package/system.ts
CHANGED
|
@@ -322,6 +322,26 @@ function readPackageJson(dir: string): any {
|
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
const WEBUI_PLUGIN_PREFIX = "mioku-plugin-";
|
|
326
|
+
|
|
327
|
+
function appendToMiokiPlugins(cwd: string, pkgName: string): boolean {
|
|
328
|
+
if (!pkgName.startsWith(WEBUI_PLUGIN_PREFIX)) return false;
|
|
329
|
+
const shortName = pkgName.slice(WEBUI_PLUGIN_PREFIX.length);
|
|
330
|
+
|
|
331
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
332
|
+
if (!fs.existsSync(packageJsonPath)) return false;
|
|
333
|
+
|
|
334
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
335
|
+
const mioki = pkg.mioki ?? {};
|
|
336
|
+
const plugins = Array.isArray(mioki.plugins) ? [...mioki.plugins] : [];
|
|
337
|
+
if (plugins.includes(shortName)) return false;
|
|
338
|
+
|
|
339
|
+
plugins.push(shortName);
|
|
340
|
+
pkg.mioki = { ...mioki, plugins };
|
|
341
|
+
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
325
345
|
function readRootPackageJson(): any {
|
|
326
346
|
return JSON.parse(fs.readFileSync(ROOT_PACKAGE_PATH, "utf-8"));
|
|
327
347
|
}
|
|
@@ -1020,12 +1040,18 @@ export async function installManagedPackage(
|
|
|
1020
1040
|
makeManagedUpdateCacheKey(input.target, installedName),
|
|
1021
1041
|
);
|
|
1022
1042
|
|
|
1043
|
+
let enabled = false;
|
|
1044
|
+
if (input.target === "plugin") {
|
|
1045
|
+
enabled = appendToMiokiPlugins(installDir, installedName);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1023
1048
|
return {
|
|
1024
1049
|
ok: true,
|
|
1025
1050
|
name: installedName,
|
|
1026
1051
|
missingServices,
|
|
1027
1052
|
packageManager: "bun",
|
|
1028
1053
|
restartRequired: true,
|
|
1054
|
+
enabled,
|
|
1029
1055
|
installOutput: install.stdout || install.stderr,
|
|
1030
1056
|
};
|
|
1031
1057
|
}
|