@mushinjs/cli 0.0.0-stage → 0.2.0-next.20261112165327
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/LICENSE +21 -0
- package/bin/mushin.js +13 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +127 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/start.d.ts +27 -0
- package/dist/start.js +215 -0
- package/dist/start.js.map +1 -0
- package/dist/test.d.ts +13 -0
- package/dist/test.js +69 -0
- package/dist/test.js.map +1 -0
- package/package.json +63 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Setobox
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/mushin.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A published install runs the compiled dist/. Inside the repository there is no dist/ until a build,
|
|
3
|
+
// and the TypeScript sources run through tsx, so a change shows up without building.
|
|
4
|
+
import { existsSync } from 'node:fs'
|
|
5
|
+
|
|
6
|
+
const source = new URL('../src/cli.ts', import.meta.url)
|
|
7
|
+
if (existsSync(source)) {
|
|
8
|
+
const { register } = await import('tsx/esm/api')
|
|
9
|
+
register()
|
|
10
|
+
await import(source.href)
|
|
11
|
+
} else {
|
|
12
|
+
await import('../dist/cli.js')
|
|
13
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { start } from "./start.js";
|
|
2
|
+
import { testCommand } from "./test.js";
|
|
3
|
+
import { join, relative, resolve } from "node:path";
|
|
4
|
+
import { readJson } from "@mushinjs/core";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { resetAdminToken } from "@mushinjs/plugin-console";
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
const usage = `Usage: mushin <command> [options]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
start Start Mushin.js (default)
|
|
13
|
+
token reset Create a new admin token for the dashboard
|
|
14
|
+
test Run the saved test cases of every flow, offline; exits with 1 when one fails
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
-d, --dir <path> Instance directory (default: the current directory, or ./instance if it holds one)
|
|
18
|
+
-p, --port <port> Override the server port
|
|
19
|
+
--flow <id|name> test: only this flow
|
|
20
|
+
--json test: print the results as JSON
|
|
21
|
+
-h, --help Show this help`;
|
|
22
|
+
const { values, positionals } = parseArgs({
|
|
23
|
+
allowPositionals: true,
|
|
24
|
+
options: {
|
|
25
|
+
dir: {
|
|
26
|
+
type: "string",
|
|
27
|
+
short: "d"
|
|
28
|
+
},
|
|
29
|
+
port: {
|
|
30
|
+
type: "string",
|
|
31
|
+
short: "p"
|
|
32
|
+
},
|
|
33
|
+
help: {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
short: "h"
|
|
36
|
+
},
|
|
37
|
+
flow: { type: "string" },
|
|
38
|
+
json: { type: "boolean" }
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const CONFIG = "mushin.config.json";
|
|
42
|
+
/** The instance directory: `--dir`, else the current directory, else `./instance` when that is where an instance lives. */
|
|
43
|
+
function instanceDir() {
|
|
44
|
+
if (values.dir) return resolve(values.dir);
|
|
45
|
+
if (!existsSync(CONFIG) && existsSync(join("instance", CONFIG))) return resolve("instance");
|
|
46
|
+
return resolve(".");
|
|
47
|
+
}
|
|
48
|
+
/** How to point the CLI at this instance from the current directory, for messages. */
|
|
49
|
+
function dirFlag(dir) {
|
|
50
|
+
const path = relative(process.cwd(), dir) || ".";
|
|
51
|
+
return path === "." ? "" : ` --dir ${path.includes(" ") ? JSON.stringify(path) : path}`;
|
|
52
|
+
}
|
|
53
|
+
function signInLink(url, token) {
|
|
54
|
+
return `${url}/login#token=${encodeURIComponent(token)}`;
|
|
55
|
+
}
|
|
56
|
+
function printSignIn(info, dir) {
|
|
57
|
+
const lines = ["", ` Mushin.js dashboard: ${info.url}`];
|
|
58
|
+
if (info.fromEnv) lines.push(" Sign in with the token from MUSHIN_ADMIN_TOKEN.");
|
|
59
|
+
else lines.push(` Sign in: ${signInLink(info.url, info.token)}`, ` Admin token: ${info.token}`, ` New token: mushin token reset${dirFlag(dir)}`);
|
|
60
|
+
console.log(lines.join("\n") + "\n");
|
|
61
|
+
}
|
|
62
|
+
const [command = "start", sub] = positionals;
|
|
63
|
+
const baseDir = instanceDir();
|
|
64
|
+
if (values.help) console.log(usage);
|
|
65
|
+
else if (command === "start") {
|
|
66
|
+
let signedIn = false;
|
|
67
|
+
const instance = await start({
|
|
68
|
+
baseDir,
|
|
69
|
+
port: values.port ? Number(values.port) : void 0,
|
|
70
|
+
onSignIn: (info) => {
|
|
71
|
+
signedIn = true;
|
|
72
|
+
printSignIn(info, baseDir);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const watchdog = setInterval(() => {
|
|
76
|
+
if (signedIn) return clearInterval(watchdog);
|
|
77
|
+
const server = instance.ctx.manager.entries.find((entry) => entry.package === "@cordisjs/plugin-server");
|
|
78
|
+
const { status, error } = server ? instance.ctx.manager.status(server.id) : {
|
|
79
|
+
status: "failed",
|
|
80
|
+
error: "no server entry in mushin.config.json"
|
|
81
|
+
};
|
|
82
|
+
if (status !== "failed") return;
|
|
83
|
+
clearInterval(watchdog);
|
|
84
|
+
const port = values.port ?? server?.config?.port ?? 5160;
|
|
85
|
+
console.error(`\n Mushin.js could not start its web server: ${error}`);
|
|
86
|
+
if (/open ports|EADDRINUSE/i.test(String(error))) console.error(` Port ${port} is in use. Is Mushin.js already running? Stop it, or start with --port <another port>.\n`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}, 200);
|
|
89
|
+
let stopping = false;
|
|
90
|
+
const shutdown = async () => {
|
|
91
|
+
if (stopping) return process.exit(1);
|
|
92
|
+
stopping = true;
|
|
93
|
+
await instance.stop();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
};
|
|
96
|
+
process.on("SIGINT", shutdown);
|
|
97
|
+
process.on("SIGTERM", shutdown);
|
|
98
|
+
} else if (command === "test") {
|
|
99
|
+
if (!existsSync(join(baseDir, CONFIG))) {
|
|
100
|
+
console.error(`No Mushin.js instance in ${baseDir}. Pass the instance folder with --dir.`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
process.exit(await testCommand({
|
|
104
|
+
baseDir,
|
|
105
|
+
flow: values.flow,
|
|
106
|
+
json: values.json
|
|
107
|
+
}));
|
|
108
|
+
} else if (command === "token" && sub === "reset") {
|
|
109
|
+
const config = await readJson(join(baseDir, CONFIG));
|
|
110
|
+
if (!config) {
|
|
111
|
+
console.error(`No Mushin.js instance in ${baseDir}. Pass the instance folder with --dir, e.g. "mushin token reset --dir instance".`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const storage = config.plugins.find((entry) => entry.package === "@mushinjs/plugin-storage");
|
|
115
|
+
const server = config.plugins.find((entry) => entry.package === "@cordisjs/plugin-server");
|
|
116
|
+
const token = await resetAdminToken(resolve(baseDir, storage?.config?.dir ?? "data"));
|
|
117
|
+
const url = `http://${server?.config?.host === "0.0.0.0" || !server?.config?.host ? "127.0.0.1" : server.config.host}:${server?.config?.port ?? 5160}`;
|
|
118
|
+
console.log(`New admin token: ${token}`);
|
|
119
|
+
console.log(`Sign in: ${signInLink(url, token)}`);
|
|
120
|
+
console.log("A running Mushin.js accepts the new token right away; the old one stops working.");
|
|
121
|
+
} else {
|
|
122
|
+
console.error(usage);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
export {};
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\r\nimport { join, relative, resolve } from 'node:path'\r\nimport { parseArgs } from 'node:util'\r\nimport { type InstanceConfig, readJson } from '@mushinjs/core'\r\nimport { resetAdminToken, type SignInInfo } from '@mushinjs/plugin-console'\r\nimport { start } from './start'\r\nimport { testCommand } from './test'\r\n\r\nconst usage = `Usage: mushin <command> [options]\r\n\r\nCommands:\r\n start Start Mushin.js (default)\r\n token reset Create a new admin token for the dashboard\r\n test Run the saved test cases of every flow, offline; exits with 1 when one fails\r\n\r\nOptions:\r\n -d, --dir <path> Instance directory (default: the current directory, or ./instance if it holds one)\r\n -p, --port <port> Override the server port\r\n --flow <id|name> test: only this flow\r\n --json test: print the results as JSON\r\n -h, --help Show this help`\r\n\r\nconst { values, positionals } = parseArgs({\r\n allowPositionals: true,\r\n options: {\r\n dir: { type: 'string', short: 'd' },\r\n port: { type: 'string', short: 'p' },\r\n help: { type: 'boolean', short: 'h' },\r\n flow: { type: 'string' },\r\n json: { type: 'boolean' },\r\n },\r\n})\r\n\r\nconst CONFIG = 'mushin.config.json'\r\n\r\n/** The instance directory: `--dir`, else the current directory, else `./instance` when that is where an instance lives. */\r\nfunction instanceDir() {\r\n if (values.dir) return resolve(values.dir)\r\n if (!existsSync(CONFIG) && existsSync(join('instance', CONFIG))) return resolve('instance')\r\n return resolve('.')\r\n}\r\n\r\n/** How to point the CLI at this instance from the current directory, for messages. */\r\nfunction dirFlag(dir: string) {\r\n const path = relative(process.cwd(), dir) || '.'\r\n return path === '.' ? '' : ` --dir ${path.includes(' ') ? JSON.stringify(path) : path}`\r\n}\r\n\r\nfunction signInLink(url: string, token: string) {\r\n return `${url}/login#token=${encodeURIComponent(token)}`\r\n}\r\n\r\nfunction printSignIn(info: SignInInfo, dir: string) {\r\n const lines = ['', ` Mushin.js dashboard: ${info.url}`]\r\n if (info.fromEnv) {\r\n lines.push(' Sign in with the token from MUSHIN_ADMIN_TOKEN.')\r\n } else {\r\n lines.push(\r\n ` Sign in: ${signInLink(info.url, info.token)}`,\r\n ` Admin token: ${info.token}`,\r\n ` New token: mushin token reset${dirFlag(dir)}`,\r\n )\r\n }\r\n console.log(lines.join('\\n') + '\\n')\r\n}\r\n\r\nconst [command = 'start', sub] = positionals\r\nconst baseDir = instanceDir()\r\n\r\nif (values.help) {\r\n console.log(usage)\r\n} else if (command === 'start') {\r\n let signedIn = false\r\n const instance = await start({\r\n baseDir,\r\n port: values.port ? Number(values.port) : undefined,\r\n onSignIn: (info) => {\r\n signedIn = true\r\n printSignIn(info, baseDir)\r\n },\r\n })\r\n // Without a server nothing keeps the process alive, so it would exit silently; say why instead.\r\n const watchdog = setInterval(() => {\r\n if (signedIn) return clearInterval(watchdog)\r\n const server = instance.ctx.manager.entries.find((entry) => entry.package === '@cordisjs/plugin-server')\r\n const { status, error } = server ? instance.ctx.manager.status(server.id) : { status: 'failed', error: 'no server entry in mushin.config.json' }\r\n if (status !== 'failed') return\r\n clearInterval(watchdog)\r\n const port = values.port ?? server?.config?.port ?? 5160\r\n console.error(`\\n Mushin.js could not start its web server: ${error}`)\r\n if (/open ports|EADDRINUSE/i.test(String(error))) {\r\n console.error(` Port ${port} is in use. Is Mushin.js already running? Stop it, or start with --port <another port>.\\n`)\r\n }\r\n process.exit(1)\r\n }, 200)\r\n let stopping = false\r\n const shutdown = async () => {\r\n if (stopping) return process.exit(1)\r\n stopping = true\r\n await instance.stop()\r\n process.exit(0)\r\n }\r\n process.on('SIGINT', shutdown)\r\n process.on('SIGTERM', shutdown)\r\n} else if (command === 'test') {\r\n if (!existsSync(join(baseDir, CONFIG))) {\r\n console.error(`No Mushin.js instance in ${baseDir}. Pass the instance folder with --dir.`)\r\n process.exit(1)\r\n }\r\n process.exit(await testCommand({ baseDir, flow: values.flow, json: values.json }))\r\n} else if (command === 'token' && sub === 'reset') {\r\n const config = await readJson<InstanceConfig>(join(baseDir, CONFIG))\r\n if (!config) {\r\n console.error(`No Mushin.js instance in ${baseDir}. Pass the instance folder with --dir, e.g. \"mushin token reset --dir instance\".`)\r\n process.exit(1)\r\n }\r\n const storage = config.plugins.find((entry) => entry.package === '@mushinjs/plugin-storage')\r\n const server = config.plugins.find((entry) => entry.package === '@cordisjs/plugin-server')\r\n const token = await resetAdminToken(resolve(baseDir, storage?.config?.dir ?? 'data'))\r\n const host = server?.config?.host === '0.0.0.0' || !server?.config?.host ? '127.0.0.1' : server.config.host\r\n const url = `http://${host}:${server?.config?.port ?? 5160}`\r\n console.log(`New admin token: ${token}`)\r\n console.log(`Sign in: ${signInLink(url, token)}`)\r\n console.log('A running Mushin.js accepts the new token right away; the old one stops working.')\r\n} else {\r\n console.error(usage)\r\n process.exit(1)\r\n}\r\n"],"mappings":";;;;;;;;AAQA,MAAM,QAAQ;;;;;;;;;;;;;AAcd,MAAM,EAAE,QAAQ,gBAAgB,UAAU;CACxC,kBAAkB;CAClB,SAAS;EACP,KAAK;GAAE,MAAM;GAAU,OAAO;EAAI;EAClC,MAAM;GAAE,MAAM;GAAU,OAAO;EAAI;EACnC,MAAM;GAAE,MAAM;GAAW,OAAO;EAAI;EACpC,MAAM,EAAE,MAAM,SAAS;EACvB,MAAM,EAAE,MAAM,UAAU;CAC1B;AACF,CAAC;AAED,MAAM,SAAS;;AAGf,SAAS,cAAc;CACrB,IAAI,OAAO,KAAK,OAAO,QAAQ,OAAO,GAAG;CACzC,IAAI,CAAC,WAAW,MAAM,KAAK,WAAW,KAAK,YAAY,MAAM,CAAC,GAAG,OAAO,QAAQ,UAAU;CAC1F,OAAO,QAAQ,GAAG;AACpB;;AAGA,SAAS,QAAQ,KAAa;CAC5B,MAAM,OAAO,SAAS,QAAQ,IAAI,GAAG,GAAG,KAAK;CAC7C,OAAO,SAAS,MAAM,KAAK,UAAU,KAAK,SAAS,GAAG,IAAI,KAAK,UAAU,IAAI,IAAI;AACnF;AAEA,SAAS,WAAW,KAAa,OAAe;CAC9C,OAAO,GAAG,IAAI,eAAe,mBAAmB,KAAK;AACvD;AAEA,SAAS,YAAY,MAAkB,KAAa;CAClD,MAAM,QAAQ,CAAC,IAAI,0BAA0B,KAAK,KAAK;CACvD,IAAI,KAAK,SACP,MAAM,KAAK,mDAAmD;MAE9D,MAAM,KACJ,kBAAkB,WAAW,KAAK,KAAK,KAAK,KAAK,KACjD,kBAAkB,KAAK,SACvB,oCAAoC,QAAQ,GAAG,GACjD;CAEF,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI;AACrC;AAEA,MAAM,CAAC,UAAU,SAAS,OAAO;AACjC,MAAM,UAAU,YAAY;AAE5B,IAAI,OAAO,MACT,QAAQ,IAAI,KAAK;KACZ,IAAI,YAAY,SAAS;CAC9B,IAAI,WAAW;CACf,MAAM,WAAW,MAAM,MAAM;EAC3B;EACA,MAAM,OAAO,OAAO,OAAO,OAAO,IAAI,IAAI,KAAA;EAC1C,WAAW,SAAS;GAClB,WAAW;GACX,YAAY,MAAM,OAAO;EAC3B;CACF,CAAC;CAED,MAAM,WAAW,kBAAkB;EACjC,IAAI,UAAU,OAAO,cAAc,QAAQ;EAC3C,MAAM,SAAS,SAAS,IAAI,QAAQ,QAAQ,MAAM,UAAU,MAAM,YAAY,yBAAyB;EACvG,MAAM,EAAE,QAAQ,UAAU,SAAS,SAAS,IAAI,QAAQ,OAAO,OAAO,EAAE,IAAI;GAAE,QAAQ;GAAU,OAAO;EAAwC;EAC/I,IAAI,WAAW,UAAU;EACzB,cAAc,QAAQ;EACtB,MAAM,OAAO,OAAO,QAAQ,QAAQ,QAAQ,QAAQ;EACpD,QAAQ,MAAM,iDAAiD,OAAO;EACtE,IAAI,yBAAyB,KAAK,OAAO,KAAK,CAAC,GAC7C,QAAQ,MAAM,UAAU,KAAK,0FAA0F;EAEzH,QAAQ,KAAK,CAAC;CAChB,GAAG,GAAG;CACN,IAAI,WAAW;CACf,MAAM,WAAW,YAAY;EAC3B,IAAI,UAAU,OAAO,QAAQ,KAAK,CAAC;EACnC,WAAW;EACX,MAAM,SAAS,KAAK;EACpB,QAAQ,KAAK,CAAC;CAChB;CACA,QAAQ,GAAG,UAAU,QAAQ;CAC7B,QAAQ,GAAG,WAAW,QAAQ;AAChC,OAAO,IAAI,YAAY,QAAQ;CAC7B,IAAI,CAAC,WAAW,KAAK,SAAS,MAAM,CAAC,GAAG;EACtC,QAAQ,MAAM,4BAA4B,QAAQ,uCAAuC;EACzF,QAAQ,KAAK,CAAC;CAChB;CACA,QAAQ,KAAK,MAAM,YAAY;EAAE;EAAS,MAAM,OAAO;EAAM,MAAM,OAAO;CAAK,CAAC,CAAC;AACnF,OAAO,IAAI,YAAY,WAAW,QAAQ,SAAS;CACjD,MAAM,SAAS,MAAM,SAAyB,KAAK,SAAS,MAAM,CAAC;CACnE,IAAI,CAAC,QAAQ;EACX,QAAQ,MAAM,4BAA4B,QAAQ,iFAAiF;EACnI,QAAQ,KAAK,CAAC;CAChB;CACA,MAAM,UAAU,OAAO,QAAQ,MAAM,UAAU,MAAM,YAAY,0BAA0B;CAC3F,MAAM,SAAS,OAAO,QAAQ,MAAM,UAAU,MAAM,YAAY,yBAAyB;CACzF,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,SAAS,SAAS,QAAQ,OAAO,MAAM,CAAC;CAEpF,MAAM,MAAM,UADC,QAAQ,QAAQ,SAAS,aAAa,CAAC,QAAQ,QAAQ,OAAO,cAAc,OAAO,OAAO,KAC5E,GAAG,QAAQ,QAAQ,QAAQ;CACtD,QAAQ,IAAI,oBAAoB,OAAO;CACvC,QAAQ,IAAI,YAAY,WAAW,KAAK,KAAK,GAAG;CAChD,QAAQ,IAAI,kFAAkF;AAChG,OAAO;CACL,QAAQ,MAAM,KAAK;CACnB,QAAQ,KAAK,CAAC;AAChB"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/start.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Context, InstanceConfig } from "@mushinjs/core";
|
|
2
|
+
import { SignInInfo } from "@mushinjs/plugin-console";
|
|
3
|
+
export declare const DEFAULT_PORT = 5160;
|
|
4
|
+
/**
|
|
5
|
+
* Imports a plugin entry. Published packages are JavaScript; a plugin in the instance's plugins/ folder may be
|
|
6
|
+
* TypeScript, so tsx starts the first time one is imported, and an instance without one never loads it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function importPlugin(specifier: string): Promise<any>;
|
|
9
|
+
export declare function defaultConfig(): InstanceConfig;
|
|
10
|
+
export interface StartOptions {
|
|
11
|
+
baseDir: string;
|
|
12
|
+
/** Overrides the server port from the config file, for tests and side-by-side instances. */
|
|
13
|
+
port?: number;
|
|
14
|
+
quiet?: boolean;
|
|
15
|
+
/** Called when the dashboard is up, with the sign-in link details. */
|
|
16
|
+
onSignIn?: (info: SignInInfo) => void;
|
|
17
|
+
/**
|
|
18
|
+
* For `mushin test`: bots stay offline, the web server, dashboard, Agent, MCP servers and
|
|
19
|
+
* statistics do not load, the database lives in memory, and the config file is never written.
|
|
20
|
+
*/
|
|
21
|
+
offline?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Boots an instance: root context, action registry, then every app plugin in the config file. */
|
|
24
|
+
export declare function start(options: StartOptions): Promise<{
|
|
25
|
+
ctx: Context;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
}>;
|
package/dist/start.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Actions, Context, Logger, PluginManager, currentPackageName, readJson, writeJsonAtomic } from "@mushinjs/core";
|
|
6
|
+
// src/start.ts
|
|
7
|
+
const DEFAULT_PORT = 5160;
|
|
8
|
+
/** True when this file runs as TypeScript: from the repository's sources, where tsx is already loaded. */
|
|
9
|
+
const fromSource = import.meta.url.endsWith(".ts");
|
|
10
|
+
let tsx;
|
|
11
|
+
/**
|
|
12
|
+
* Imports a plugin entry. Published packages are JavaScript; a plugin in the instance's plugins/ folder may be
|
|
13
|
+
* TypeScript, so tsx starts the first time one is imported, and an instance without one never loads it.
|
|
14
|
+
*/
|
|
15
|
+
async function importPlugin(specifier) {
|
|
16
|
+
if (!fromSource && /\.[cm]?tsx?$/.test(specifier)) {
|
|
17
|
+
tsx ??= import("tsx/esm/api").then(({ register }) => register());
|
|
18
|
+
await tsx;
|
|
19
|
+
}
|
|
20
|
+
return import(specifier);
|
|
21
|
+
}
|
|
22
|
+
function defaultConfig() {
|
|
23
|
+
return { plugins: [
|
|
24
|
+
{
|
|
25
|
+
id: "server",
|
|
26
|
+
package: "@cordisjs/plugin-server",
|
|
27
|
+
config: {
|
|
28
|
+
host: "127.0.0.1",
|
|
29
|
+
port: DEFAULT_PORT
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "storage",
|
|
34
|
+
package: "@mushinjs/plugin-storage",
|
|
35
|
+
config: {}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "database",
|
|
39
|
+
package: "@mushinjs/plugin-database",
|
|
40
|
+
config: {}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "providers",
|
|
44
|
+
package: "@mushinjs/plugin-providers",
|
|
45
|
+
config: {}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "provider-openai",
|
|
49
|
+
package: "@mushinjs/plugin-provider-openai",
|
|
50
|
+
config: {}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "provider-anthropic",
|
|
54
|
+
package: "@mushinjs/plugin-provider-anthropic",
|
|
55
|
+
config: {}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "provider-gemini",
|
|
59
|
+
package: "@mushinjs/plugin-provider-gemini",
|
|
60
|
+
config: {}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "provider-typesafe",
|
|
64
|
+
package: "@mushinjs/plugin-provider-typesafe",
|
|
65
|
+
config: {}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "bots",
|
|
69
|
+
package: "@mushinjs/plugin-bots",
|
|
70
|
+
config: {}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "permissions",
|
|
74
|
+
package: "@mushinjs/plugin-permissions",
|
|
75
|
+
config: {}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "personas",
|
|
79
|
+
package: "@mushinjs/plugin-personas",
|
|
80
|
+
config: {}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "adapter-onebot",
|
|
84
|
+
package: "@mushinjs/plugin-adapter-onebot",
|
|
85
|
+
config: {}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "flows",
|
|
89
|
+
package: "@mushinjs/plugin-flows",
|
|
90
|
+
config: {}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "knowledge",
|
|
94
|
+
package: "@mushinjs/plugin-knowledge",
|
|
95
|
+
config: {}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "agent",
|
|
99
|
+
package: "@mushinjs/plugin-agent",
|
|
100
|
+
config: {}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "mcp",
|
|
104
|
+
package: "@mushinjs/plugin-mcp",
|
|
105
|
+
config: {}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "stats",
|
|
109
|
+
package: "@mushinjs/plugin-stats",
|
|
110
|
+
config: {}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "console",
|
|
114
|
+
package: "@mushinjs/plugin-console",
|
|
115
|
+
config: {}
|
|
116
|
+
}
|
|
117
|
+
] };
|
|
118
|
+
}
|
|
119
|
+
const levelLabels = {
|
|
120
|
+
error: ["E", 31],
|
|
121
|
+
warn: ["W", 33],
|
|
122
|
+
info: ["I", 36],
|
|
123
|
+
debug: ["D", 90]
|
|
124
|
+
};
|
|
125
|
+
/** Prints `HH:MM:SS [I] name message` lines, colored when stdout is a terminal. */
|
|
126
|
+
function consoleExporter() {
|
|
127
|
+
const colors = process.stdout.isTTY;
|
|
128
|
+
const exporter = {
|
|
129
|
+
colors: colors ? 3 : false,
|
|
130
|
+
export(message) {
|
|
131
|
+
const time = new Date(message.ts).toTimeString().slice(0, 8);
|
|
132
|
+
const [label, color] = levelLabels[message.type] ?? ["?", 0];
|
|
133
|
+
const level = colors ? `\x1b[${color}m[${label}]\x1b[0m` : `[${label}]`;
|
|
134
|
+
const name = colors ? `\x1b[1m${message.name}\x1b[0m` : message.name;
|
|
135
|
+
(message.type === "error" ? process.stderr : process.stdout).write(`${time} ${level} ${name} ${Logger.format(exporter, message)}\n`);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
return exporter;
|
|
139
|
+
}
|
|
140
|
+
/** Plugins `mushin test` leaves out: they serve people or reach other programs, and flows do not need them. */
|
|
141
|
+
const OFFLINE_SKIP = /* @__PURE__ */ new Set([
|
|
142
|
+
"@cordisjs/plugin-server",
|
|
143
|
+
"@mushinjs/plugin-console",
|
|
144
|
+
"@mushinjs/plugin-agent",
|
|
145
|
+
"@mushinjs/plugin-mcp",
|
|
146
|
+
"@mushinjs/plugin-stats"
|
|
147
|
+
]);
|
|
148
|
+
function offlineConfig(config) {
|
|
149
|
+
return {
|
|
150
|
+
...config,
|
|
151
|
+
plugins: config.plugins.filter((entry) => !OFFLINE_SKIP.has(entry.package)).map((entry) => entry.package === "@mushinjs/plugin-database" ? {
|
|
152
|
+
...entry,
|
|
153
|
+
config: {
|
|
154
|
+
...entry.config,
|
|
155
|
+
driver: "memory"
|
|
156
|
+
}
|
|
157
|
+
} : entry)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** Boots an instance: root context, action registry, then every app plugin in the config file. */
|
|
161
|
+
async function start(options) {
|
|
162
|
+
const baseDir = resolve(options.baseDir);
|
|
163
|
+
await mkdir(baseDir, { recursive: true });
|
|
164
|
+
const configPath = join(baseDir, "mushin.config.json");
|
|
165
|
+
let config = await readJson(configPath);
|
|
166
|
+
if (!config) {
|
|
167
|
+
config = defaultConfig();
|
|
168
|
+
await writeJsonAtomic(configPath, config);
|
|
169
|
+
} else {
|
|
170
|
+
let changed = false;
|
|
171
|
+
for (const entry of config.plugins) {
|
|
172
|
+
const name = currentPackageName(entry.package);
|
|
173
|
+
if (name !== entry.package) [entry.package, changed] = [name, true];
|
|
174
|
+
}
|
|
175
|
+
const missing = defaultConfig().plugins.filter((entry) => !config.plugins.some((item) => item.package === entry.package));
|
|
176
|
+
if (missing.length) {
|
|
177
|
+
config.plugins.push(...missing);
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
if (changed) await writeJsonAtomic(configPath, config);
|
|
181
|
+
}
|
|
182
|
+
if (options.port !== void 0) {
|
|
183
|
+
const server = config.plugins.find((entry) => entry.package === "@cordisjs/plugin-server");
|
|
184
|
+
if (server) server.config = {
|
|
185
|
+
...server.config,
|
|
186
|
+
port: options.port
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const ctx = new Context();
|
|
190
|
+
ctx.baseDir = baseDir;
|
|
191
|
+
ctx.offline = !!options.offline;
|
|
192
|
+
if (options.offline) config = offlineConfig(config);
|
|
193
|
+
if (!options.quiet) ctx.logger.exporter(consoleExporter());
|
|
194
|
+
if (options.onSignIn) ctx.on("console/ready", options.onSignIn);
|
|
195
|
+
const actions = ctx.plugin(Actions);
|
|
196
|
+
await actions;
|
|
197
|
+
const manager = ctx.plugin(PluginManager, {
|
|
198
|
+
configPath: options.offline ? join(await mkdtemp(join(tmpdir(), "mushin-offline-")), "mushin.config.json") : configPath,
|
|
199
|
+
config,
|
|
200
|
+
nodeModules: [fileURLToPath(new URL("../node_modules", import.meta.url)), join(baseDir, "node_modules")],
|
|
201
|
+
localDirs: [join(baseDir, "plugins")],
|
|
202
|
+
importer: importPlugin
|
|
203
|
+
});
|
|
204
|
+
await manager;
|
|
205
|
+
return {
|
|
206
|
+
ctx,
|
|
207
|
+
async stop() {
|
|
208
|
+
await manager.dispose();
|
|
209
|
+
await actions.dispose();
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export { DEFAULT_PORT, defaultConfig, importPlugin, start };
|
|
214
|
+
|
|
215
|
+
//# sourceMappingURL=start.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start.js","names":[],"sources":["../src/start.ts"],"sourcesContent":["import { mkdir, mkdtemp } from 'node:fs/promises'\r\nimport { tmpdir } from 'node:os'\r\nimport { join, resolve } from 'node:path'\r\nimport { fileURLToPath } from 'node:url'\r\nimport { Actions, Context, currentPackageName, type Exporter, type InstanceConfig, Logger, type Message, PluginManager, readJson, writeJsonAtomic } from '@mushinjs/core'\r\nimport type { SignInInfo } from '@mushinjs/plugin-console'\r\n\r\nexport const DEFAULT_PORT = 5160\r\n\r\n/** True when this file runs as TypeScript: from the repository's sources, where tsx is already loaded. */\r\nconst fromSource = import.meta.url.endsWith('.ts')\r\nlet tsx: Promise<unknown> | undefined\r\n\r\n/**\r\n * Imports a plugin entry. Published packages are JavaScript; a plugin in the instance's plugins/ folder may be\r\n * TypeScript, so tsx starts the first time one is imported, and an instance without one never loads it.\r\n */\r\nexport async function importPlugin(specifier: string) {\r\n if (!fromSource && /\\.[cm]?tsx?$/.test(specifier)) {\r\n tsx ??= import('tsx/esm/api').then(({ register }) => register())\r\n await tsx\r\n }\r\n return import(specifier)\r\n}\r\n\r\nexport function defaultConfig(): InstanceConfig {\r\n return {\r\n plugins: [\r\n { id: 'server', package: '@cordisjs/plugin-server', config: { host: '127.0.0.1', port: DEFAULT_PORT } },\r\n { id: 'storage', package: '@mushinjs/plugin-storage', config: {} },\r\n { id: 'database', package: '@mushinjs/plugin-database', config: {} },\r\n { id: 'providers', package: '@mushinjs/plugin-providers', config: {} },\r\n { id: 'provider-openai', package: '@mushinjs/plugin-provider-openai', config: {} },\r\n { id: 'provider-anthropic', package: '@mushinjs/plugin-provider-anthropic', config: {} },\r\n { id: 'provider-gemini', package: '@mushinjs/plugin-provider-gemini', config: {} },\r\n { id: 'provider-typesafe', package: '@mushinjs/plugin-provider-typesafe', config: {} },\r\n { id: 'bots', package: '@mushinjs/plugin-bots', config: {} },\r\n { id: 'permissions', package: '@mushinjs/plugin-permissions', config: {} },\r\n { id: 'personas', package: '@mushinjs/plugin-personas', config: {} },\r\n { id: 'adapter-onebot', package: '@mushinjs/plugin-adapter-onebot', config: {} },\r\n { id: 'flows', package: '@mushinjs/plugin-flows', config: {} },\r\n { id: 'knowledge', package: '@mushinjs/plugin-knowledge', config: {} },\r\n { id: 'agent', package: '@mushinjs/plugin-agent', config: {} },\r\n { id: 'mcp', package: '@mushinjs/plugin-mcp', config: {} },\r\n { id: 'stats', package: '@mushinjs/plugin-stats', config: {} },\r\n { id: 'console', package: '@mushinjs/plugin-console', config: {} },\r\n ],\r\n }\r\n}\r\n\r\nconst levelLabels: Record<string, [string, number]> = {\r\n error: ['E', 31],\r\n warn: ['W', 33],\r\n info: ['I', 36],\r\n debug: ['D', 90],\r\n}\r\n\r\n/** Prints `HH:MM:SS [I] name message` lines, colored when stdout is a terminal. */\r\nfunction consoleExporter(): Exporter {\r\n const colors = process.stdout.isTTY\r\n const exporter: Exporter = {\r\n colors: colors ? 3 : false,\r\n export(message: Message) {\r\n const time = new Date(message.ts).toTimeString().slice(0, 8)\r\n const [label, color] = levelLabels[message.type] ?? ['?', 0]\r\n const level = colors ? `\\x1b[${color}m[${label}]\\x1b[0m` : `[${label}]`\r\n const name = colors ? `\\x1b[1m${message.name}\\x1b[0m` : message.name\r\n const stream = message.type === 'error' ? process.stderr : process.stdout\r\n stream.write(`${time} ${level} ${name} ${Logger.format(exporter, message)}\\n`)\r\n },\r\n }\r\n return exporter\r\n}\r\n\r\nexport interface StartOptions {\r\n baseDir: string\r\n /** Overrides the server port from the config file, for tests and side-by-side instances. */\r\n port?: number\r\n quiet?: boolean\r\n /** Called when the dashboard is up, with the sign-in link details. */\r\n onSignIn?: (info: SignInInfo) => void\r\n /**\r\n * For `mushin test`: bots stay offline, the web server, dashboard, Agent, MCP servers and\r\n * statistics do not load, the database lives in memory, and the config file is never written.\r\n */\r\n offline?: boolean\r\n}\r\n\r\n/** Plugins `mushin test` leaves out: they serve people or reach other programs, and flows do not need them. */\r\nconst OFFLINE_SKIP = new Set(['@cordisjs/plugin-server', '@mushinjs/plugin-console', '@mushinjs/plugin-agent', '@mushinjs/plugin-mcp', '@mushinjs/plugin-stats'])\r\n\r\nfunction offlineConfig(config: InstanceConfig): InstanceConfig {\r\n return {\r\n ...config,\r\n plugins: config.plugins\r\n .filter((entry) => !OFFLINE_SKIP.has(entry.package))\r\n .map((entry) => (entry.package === '@mushinjs/plugin-database' ? { ...entry, config: { ...entry.config, driver: 'memory' } } : entry)),\r\n }\r\n}\r\n\r\n/** Boots an instance: root context, action registry, then every app plugin in the config file. */\r\nexport async function start(options: StartOptions) {\r\n const baseDir = resolve(options.baseDir)\r\n await mkdir(baseDir, { recursive: true })\r\n const configPath = join(baseDir, 'mushin.config.json')\r\n let config = await readJson<InstanceConfig>(configPath)\r\n if (!config) {\r\n config = defaultConfig()\r\n await writeJsonAtomic(configPath, config)\r\n } else {\r\n let changed = false\r\n for (const entry of config.plugins) {\r\n const name = currentPackageName(entry.package)\r\n if (name !== entry.package) [entry.package, changed] = [name, true]\r\n }\r\n // Built-in plugins added in newer versions join existing instances; entries the user disabled stay disabled.\r\n const missing = defaultConfig().plugins.filter((entry) => !config!.plugins.some((item) => item.package === entry.package))\r\n if (missing.length) {\r\n config.plugins.push(...missing)\r\n changed = true\r\n }\r\n if (changed) await writeJsonAtomic(configPath, config)\r\n }\r\n if (options.port !== undefined) {\r\n const server = config.plugins.find((entry) => entry.package === '@cordisjs/plugin-server')\r\n if (server) server.config = { ...server.config, port: options.port }\r\n }\r\n\r\n const ctx = new Context()\r\n ctx.baseDir = baseDir\r\n ctx.offline = !!options.offline\r\n if (options.offline) config = offlineConfig(config)\r\n if (!options.quiet) ctx.logger.exporter(consoleExporter())\r\n\r\n if (options.onSignIn) ctx.on('console/ready', options.onSignIn)\r\n\r\n const actions = ctx.plugin(Actions)\r\n await actions\r\n const manager = ctx.plugin(PluginManager, {\r\n // Offline runs change the plugin list in memory; saving it would lose the real one.\r\n configPath: options.offline ? join(await mkdtemp(join(tmpdir(), 'mushin-offline-')), 'mushin.config.json') : configPath,\r\n config,\r\n nodeModules: [\r\n fileURLToPath(new URL('../node_modules', import.meta.url)),\r\n join(baseDir, 'node_modules'),\r\n ],\r\n localDirs: [join(baseDir, 'plugins')],\r\n importer: importPlugin,\r\n })\r\n await manager\r\n\r\n return {\r\n ctx,\r\n async stop() {\r\n await manager.dispose()\r\n await actions.dispose()\r\n },\r\n }\r\n}\r\n"],"mappings":"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"}
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface TestCommandOptions {
|
|
2
|
+
baseDir: string;
|
|
3
|
+
/** Only this flow, by id or name. */
|
|
4
|
+
flow?: string;
|
|
5
|
+
/** Print one JSON summary instead of lines. */
|
|
6
|
+
json?: boolean;
|
|
7
|
+
print?: (line: string) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `mushin test`: boots the instance offline, runs the saved test cases of every flow that has
|
|
11
|
+
* some and prints the results. Returns the exit code: 1 when a case failed, 0 otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export declare function testCommand(options: TestCommandOptions): Promise<1 | 0>;
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { start } from "./start.js";
|
|
2
|
+
// src/test.ts
|
|
3
|
+
const SETTLED = /* @__PURE__ */ new Set([
|
|
4
|
+
"active",
|
|
5
|
+
"failed",
|
|
6
|
+
"missing",
|
|
7
|
+
"disabled"
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* `mushin test`: boots the instance offline, runs the saved test cases of every flow that has
|
|
11
|
+
* some and prints the results. Returns the exit code: 1 when a case failed, 0 otherwise.
|
|
12
|
+
*/
|
|
13
|
+
async function testCommand(options) {
|
|
14
|
+
const print = options.print ?? ((line) => console.log(line));
|
|
15
|
+
const instance = await start({
|
|
16
|
+
baseDir: options.baseDir,
|
|
17
|
+
quiet: true,
|
|
18
|
+
offline: true
|
|
19
|
+
});
|
|
20
|
+
const { ctx } = instance;
|
|
21
|
+
try {
|
|
22
|
+
const deadline = Date.now() + 2e4;
|
|
23
|
+
while (!ctx.manager.entries.every((entry) => SETTLED.has(ctx.manager.status(entry.id).status)) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
|
|
24
|
+
const flows = ctx.get("flows");
|
|
25
|
+
if (!flows) {
|
|
26
|
+
print("The flows plugin is not running in this instance.");
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const chosen = flows.list().filter((flow) => !options.flow || flow.id === options.flow || flow.name === options.flow);
|
|
30
|
+
if (options.flow && !chosen.length) {
|
|
31
|
+
print(`No flow with id or name "${options.flow}".`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
const summaries = [];
|
|
35
|
+
for (const flow of chosen) {
|
|
36
|
+
if (!(await flows.tests(flow.id)).cases.length) continue;
|
|
37
|
+
const run = await flows.runTests(flow.id, { source: "cli" });
|
|
38
|
+
summaries.push({
|
|
39
|
+
flow: flow.id,
|
|
40
|
+
name: flow.name,
|
|
41
|
+
passed: run.passed,
|
|
42
|
+
failed: run.failed,
|
|
43
|
+
uncovered: run.uncovered,
|
|
44
|
+
results: run.results.map(({ trace: _trace, ...rest }) => rest)
|
|
45
|
+
});
|
|
46
|
+
if (options.json) continue;
|
|
47
|
+
print(`${run.failed ? "✗" : "✓"} ${flow.name} (${flow.id}): ${run.passed}/${run.passed + run.failed} passed${run.uncovered.length ? `, ${run.uncovered.length} node(s) not covered` : ""}`);
|
|
48
|
+
for (const result of run.results.filter((item) => !item.passed)) {
|
|
49
|
+
print(` ✗ ${result.name}`);
|
|
50
|
+
for (const failure of result.failures) print(` ${failure.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const failed = summaries.reduce((sum, item) => sum + item.failed, 0);
|
|
54
|
+
const passed = summaries.reduce((sum, item) => sum + item.passed, 0);
|
|
55
|
+
if (options.json) print(JSON.stringify({
|
|
56
|
+
passed,
|
|
57
|
+
failed,
|
|
58
|
+
flows: summaries
|
|
59
|
+
}, null, 2));
|
|
60
|
+
else if (!summaries.length) print("No flow has test cases yet. Add some in the flow editor's Tests tab.");
|
|
61
|
+
else print(`\n${passed} passed, ${failed} failed in ${summaries.length} flow(s)`);
|
|
62
|
+
return failed ? 1 : 0;
|
|
63
|
+
} finally {
|
|
64
|
+
await instance.stop();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export { testCommand };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=test.js.map
|
package/dist/test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.js","names":[],"sources":["../src/test.ts"],"sourcesContent":["import type {} from '@mushinjs/plugin-flows'\r\nimport { start } from './start'\r\n\r\nexport interface TestCommandOptions {\r\n baseDir: string\r\n /** Only this flow, by id or name. */\r\n flow?: string\r\n /** Print one JSON summary instead of lines. */\r\n json?: boolean\r\n print?: (line: string) => void\r\n}\r\n\r\nconst SETTLED = new Set(['active', 'failed', 'missing', 'disabled'])\r\n\r\n/**\r\n * `mushin test`: boots the instance offline, runs the saved test cases of every flow that has\r\n * some and prints the results. Returns the exit code: 1 when a case failed, 0 otherwise.\r\n */\r\nexport async function testCommand(options: TestCommandOptions) {\r\n const print = options.print ?? ((line: string) => console.log(line))\r\n const instance = await start({ baseDir: options.baseDir, quiet: true, offline: true })\r\n const { ctx } = instance\r\n try {\r\n const deadline = Date.now() + 20_000\r\n while (!ctx.manager.entries.every((entry) => SETTLED.has(ctx.manager.status(entry.id).status)) && Date.now() < deadline) {\r\n await new Promise((resolve) => setTimeout(resolve, 50))\r\n }\r\n const flows = ctx.get('flows')\r\n if (!flows) {\r\n print('The flows plugin is not running in this instance.')\r\n return 1\r\n }\r\n const chosen = flows.list().filter((flow) => !options.flow || flow.id === options.flow || flow.name === options.flow)\r\n if (options.flow && !chosen.length) {\r\n print(`No flow with id or name \"${options.flow}\".`)\r\n return 1\r\n }\r\n const summaries = []\r\n for (const flow of chosen) {\r\n if (!(await flows.tests(flow.id)).cases.length) continue\r\n const run = await flows.runTests(flow.id, { source: 'cli' })\r\n summaries.push({ flow: flow.id, name: flow.name, passed: run.passed, failed: run.failed, uncovered: run.uncovered, results: run.results.map(({ trace: _trace, ...rest }) => rest) })\r\n if (options.json) continue\r\n print(`${run.failed ? '✗' : '✓'} ${flow.name} (${flow.id}): ${run.passed}/${run.passed + run.failed} passed${run.uncovered.length ? `, ${run.uncovered.length} node(s) not covered` : ''}`)\r\n for (const result of run.results.filter((item) => !item.passed)) {\r\n print(` ✗ ${result.name}`)\r\n for (const failure of result.failures) print(` ${failure.message}`)\r\n }\r\n }\r\n const failed = summaries.reduce((sum, item) => sum + item.failed, 0)\r\n const passed = summaries.reduce((sum, item) => sum + item.passed, 0)\r\n if (options.json) print(JSON.stringify({ passed, failed, flows: summaries }, null, 2))\r\n else if (!summaries.length) print('No flow has test cases yet. Add some in the flow editor\\'s Tests tab.')\r\n else print(`\\n${passed} passed, ${failed} failed in ${summaries.length} flow(s)`)\r\n return failed ? 1 : 0\r\n } finally {\r\n await instance.stop()\r\n }\r\n}\r\n"],"mappings":";;AAYA,MAAM,0BAAU,IAAI,IAAI;CAAC;CAAU;CAAU;CAAW;AAAU,CAAC;;;;;AAMnE,eAAsB,YAAY,SAA6B;CAC7D,MAAM,QAAQ,QAAQ,WAAW,SAAiB,QAAQ,IAAI,IAAI;CAClE,MAAM,WAAW,MAAM,MAAM;EAAE,SAAS,QAAQ;EAAS,OAAO;EAAM,SAAS;CAAK,CAAC;CACrF,MAAM,EAAE,QAAQ;CAChB,IAAI;EACF,MAAM,WAAW,KAAK,IAAI,IAAI;EAC9B,OAAO,CAAC,IAAI,QAAQ,QAAQ,OAAO,UAAU,QAAQ,IAAI,IAAI,QAAQ,OAAO,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,IAAI,UAC7G,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;EAExD,MAAM,QAAQ,IAAI,IAAI,OAAO;EAC7B,IAAI,CAAC,OAAO;GACV,MAAM,mDAAmD;GACzD,OAAO;EACT;EACA,MAAM,SAAS,MAAM,KAAK,CAAC,CAAC,QAAQ,SAAS,CAAC,QAAQ,QAAQ,KAAK,OAAO,QAAQ,QAAQ,KAAK,SAAS,QAAQ,IAAI;EACpH,IAAI,QAAQ,QAAQ,CAAC,OAAO,QAAQ;GAClC,MAAM,4BAA4B,QAAQ,KAAK,GAAG;GAClD,OAAO;EACT;EACA,MAAM,YAAY,CAAC;EACnB,KAAK,MAAM,QAAQ,QAAQ;GACzB,IAAI,EAAE,MAAM,MAAM,MAAM,KAAK,EAAE,EAAA,CAAG,MAAM,QAAQ;GAChD,MAAM,MAAM,MAAM,MAAM,SAAS,KAAK,IAAI,EAAE,QAAQ,MAAM,CAAC;GAC3D,UAAU,KAAK;IAAE,MAAM,KAAK;IAAI,MAAM,KAAK;IAAM,QAAQ,IAAI;IAAQ,QAAQ,IAAI;IAAQ,WAAW,IAAI;IAAW,SAAS,IAAI,QAAQ,KAAK,EAAE,OAAO,QAAQ,GAAG,WAAW,IAAI;GAAE,CAAC;GACnL,IAAI,QAAQ,MAAM;GAClB,MAAM,GAAG,IAAI,SAAS,MAAM,IAAI,GAAG,KAAK,KAAK,IAAI,KAAK,GAAG,KAAK,IAAI,OAAO,GAAG,IAAI,SAAS,IAAI,OAAO,SAAS,IAAI,UAAU,SAAS,KAAK,IAAI,UAAU,OAAO,wBAAwB,IAAI;GAC1L,KAAK,MAAM,UAAU,IAAI,QAAQ,QAAQ,SAAS,CAAC,KAAK,MAAM,GAAG;IAC/D,MAAM,SAAS,OAAO,MAAM;IAC5B,KAAK,MAAM,WAAW,OAAO,UAAU,MAAM,WAAW,QAAQ,SAAS;GAC3E;EACF;EACA,MAAM,SAAS,UAAU,QAAQ,KAAK,SAAS,MAAM,KAAK,QAAQ,CAAC;EACnE,MAAM,SAAS,UAAU,QAAQ,KAAK,SAAS,MAAM,KAAK,QAAQ,CAAC;EACnE,IAAI,QAAQ,MAAM,MAAM,KAAK,UAAU;GAAE;GAAQ;GAAQ,OAAO;EAAU,GAAG,MAAM,CAAC,CAAC;OAChF,IAAI,CAAC,UAAU,QAAQ,MAAM,sEAAuE;OACpG,MAAM,KAAK,OAAO,WAAW,OAAO,aAAa,UAAU,OAAO,SAAS;EAChF,OAAO,SAAS,IAAI;CACtB,UAAU;EACR,MAAM,SAAS,KAAK;CACtB;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushinjs/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0-next.20261112165327",
|
|
4
|
+
"description": "Mushin.js launcher",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mushinjs/mushin.git",
|
|
10
|
+
"directory": "packages/cli"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"mushin": "./bin/mushin.js"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin",
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@mushinjs/core": "0.2.0-next.20261112165327",
|
|
31
|
+
"cordis": "4.0.0-rc.11",
|
|
32
|
+
"@cordisjs/plugin-server": "1.7.0",
|
|
33
|
+
"tsx": "^4.23.15",
|
|
34
|
+
"@mushinjs/plugin-storage": "0.2.0-next.20261112165327",
|
|
35
|
+
"@mushinjs/plugin-database": "0.2.0-next.20261112165327",
|
|
36
|
+
"@mushinjs/plugin-providers": "0.2.0-next.20261112165327",
|
|
37
|
+
"@mushinjs/plugin-provider-openai": "0.2.0-next.20261112165327",
|
|
38
|
+
"@mushinjs/plugin-provider-anthropic": "0.2.0-next.20261112165327",
|
|
39
|
+
"@mushinjs/plugin-provider-gemini": "0.2.0-next.20261112165327",
|
|
40
|
+
"@mushinjs/plugin-provider-typesafe": "0.2.0-next.20261112165327",
|
|
41
|
+
"@mushinjs/plugin-bots": "0.2.0-next.20261112165327",
|
|
42
|
+
"@mushinjs/plugin-adapter-onebot": "0.2.0-next.20261112165327",
|
|
43
|
+
"@mushinjs/plugin-agent": "0.2.0-next.20261112165327",
|
|
44
|
+
"@mushinjs/plugin-console": "0.2.0-next.20261112165327",
|
|
45
|
+
"@mushinjs/bot-plugin-echo": "0.2.0-next.20261112165327",
|
|
46
|
+
"@mushinjs/bot-plugin-llm-chat": "0.2.0-next.20261112165327",
|
|
47
|
+
"@mushinjs/bot-plugin-companion": "0.2.0-next.20261112165327",
|
|
48
|
+
"@mushinjs/plugin-flows": "0.2.0-next.20261112165327",
|
|
49
|
+
"@mushinjs/bot-plugin-flows": "0.2.0-next.20261112165327",
|
|
50
|
+
"@mushinjs/plugin-demo": "0.2.0-next.20261112165327",
|
|
51
|
+
"@mushinjs/plugin-stats": "0.2.0-next.20261112165327",
|
|
52
|
+
"@mushinjs/plugin-permissions": "0.2.0-next.20261112165327",
|
|
53
|
+
"@mushinjs/plugin-personas": "0.2.0-next.20261112165327",
|
|
54
|
+
"@mushinjs/plugin-mcp": "0.2.0-next.20261112165327",
|
|
55
|
+
"@mushinjs/plugin-terminal": "0.2.0-next.20261112165327",
|
|
56
|
+
"@mushinjs/plugin-knowledge": "0.2.0-next.20261112165327"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"vite": "^8.3.4",
|
|
60
|
+
"ws": "^8.22.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"dev": "tsx watch --clear-screen=false --exclude \"../dashboard/**\" src/dev.ts start --dir ../../instance",
|
|
64
|
+
"demo": "tsx tests/helpers/demo.ts"
|
|
65
|
+
}
|
|
6
66
|
}
|
package/README.md
DELETED