@yanglaofish/fishtools 0.3.0 → 0.5.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/README.md +33 -5
- package/bin/fst.js +1 -1
- package/manual/README.md +33 -5
- package/manual/docs/deployment.md +3 -3
- package/manual/docs/examples/script-plugin/LICENSE +21 -0
- package/manual/docs/examples/script-plugin/README.md +64 -0
- package/manual/docs/examples/script-plugin/index.js +39 -0
- package/manual/docs/examples/script-plugin/package.json +9 -0
- package/manual/docs/examples/script-plugin/scripts/benchmark.py +23 -0
- package/manual/docs/kernel-generation.md +97 -0
- package/manual/docs/modules.md +57 -0
- package/manual/docs/plugin-development.md +163 -12
- package/manual/docs/releases.md +23 -0
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
- package/manual/docs/roadmap.md +16 -3
- package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
- package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
- package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
- package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
- package/manual/docs/superpowers/plans/2026-10-12-plugin-developer-experience.md +50 -0
- package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
- package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
- package/manual/docs/superpowers/reviews/2026-10-12-plugin-release-review.md +20 -0
- package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
- package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
- package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
- package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
- package/manual/docs/superpowers/specs/2026-10-12-plugin-developer-experience.md +22 -0
- package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
- package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
- package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
- package/manual/docs/verification.md +58 -1
- package/manual/packages/core/README.md +44 -0
- package/manual/packages/core/package.json +38 -0
- package/manual/packages/core/src/application.js +268 -87
- package/manual/packages/core/src/bundle.js +2 -0
- package/manual/packages/core/src/development-watch-worker.js +69 -0
- package/manual/packages/core/src/development.js +146 -0
- package/manual/packages/core/src/environments.js +2 -0
- package/manual/packages/core/src/export-snapshot.js +42 -0
- package/manual/packages/core/src/plugin-update.js +108 -0
- package/manual/packages/core/src/process.js +2 -0
- package/manual/packages/core/src/resources.js +31 -0
- package/manual/packages/core/src/version.js +2 -0
- package/manual/packages/fst-cli/LICENSE +21 -0
- package/manual/packages/fst-cli/README.md +54 -0
- package/manual/packages/fst-cli/bin/fst.js +124 -0
- package/manual/packages/fst-cli/package.json +49 -0
- package/manual/packages/fst-cli/src/development.js +53 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-feed.js +31 -5
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
- package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
- package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
- package/manual/packages/fst-devtools/LICENSE +21 -0
- package/manual/packages/fst-devtools/README.md +28 -0
- package/manual/packages/fst-devtools/package.json +29 -0
- package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
- package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
- package/manual/packages/fst-devtools/src/plugin-tools.js +152 -0
- package/manual/packages/fst-logs/LICENSE +21 -0
- package/manual/packages/fst-logs/README.md +56 -0
- package/manual/packages/fst-logs/package.json +24 -0
- package/manual/packages/fst-logs/src/log-bounds.js +91 -0
- package/manual/packages/fst-logs/src/run-logs.js +239 -0
- package/manual/packages/fst-platform/LICENSE +21 -0
- package/manual/packages/fst-platform/README.md +67 -0
- package/manual/packages/fst-platform/package.json +33 -0
- package/manual/packages/fst-platform/src/bundle.js +17 -0
- package/manual/packages/fst-platform/src/environments.js +67 -0
- package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
- package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
- package/manual/packages/fst-platform/src/file-probe.js +6 -0
- package/manual/packages/fst-platform/src/json.js +5 -0
- package/manual/packages/fst-platform/src/network.js +199 -0
- package/manual/packages/fst-platform/src/npm.js +11 -0
- package/manual/packages/fst-platform/src/process.js +110 -0
- package/manual/packages/fst-plugin/LICENSE +21 -0
- package/manual/packages/fst-plugin/README.md +29 -0
- package/manual/packages/fst-plugin/package.json +29 -0
- package/manual/packages/fst-plugin/src/contracts.js +86 -0
- package/manual/packages/fst-plugin/src/host.js +232 -0
- package/manual/packages/fst-plugin/src/loader.js +21 -0
- package/manual/packages/fst-plugin/src/privileges.js +91 -0
- package/manual/packages/{core → fst-plugin}/src/runtime.js +20 -4
- package/manual/packages/fst-workflow/LICENSE +21 -0
- package/manual/packages/fst-workflow/README.md +28 -0
- package/manual/packages/fst-workflow/package.json +24 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
- package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
- package/manual/packages/fst-workspace/LICENSE +21 -0
- package/manual/packages/fst-workspace/README.md +32 -0
- package/manual/packages/fst-workspace/package.json +34 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
- package/manual/packages/fst-workspace/src/index.js +3 -0
- package/manual/packages/fst-workspace/src/plugin-installations.js +84 -0
- package/manual/packages/fst-workspace/src/plugins.js +35 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +41 -21
- package/manual/packages/pack/LICENSE +21 -0
- package/manual/packages/pack/README.md +31 -0
- package/manual/packages/pack/archive.js +15 -0
- package/manual/packages/pack/dependencies.js +105 -0
- package/manual/packages/pack/environments.js +50 -0
- package/manual/packages/pack/index.js +103 -0
- package/manual/packages/pack/package.json +29 -0
- package/manual/packages/pack/runtime.js +61 -0
- package/manual/packages/sdk/README.md +31 -0
- package/manual/packages/sdk/conditions.js +1 -0
- package/manual/packages/sdk/package.json +23 -1
- package/manual/packages/sdk/schema-path.js +31 -0
- package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +22 -9
- package/manual/plugins/web/README.md +29 -0
- package/manual/tests/application.test.js +6 -4
- package/node_modules/@mytools/core/README.md +44 -0
- package/node_modules/@mytools/core/package.json +17 -15
- package/node_modules/@mytools/core/src/application.js +268 -87
- package/node_modules/@mytools/core/src/bundle.js +2 -17
- package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
- package/node_modules/@mytools/core/src/development.js +146 -0
- package/node_modules/@mytools/core/src/environments.js +2 -29
- package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
- package/node_modules/@mytools/core/src/plugin-update.js +108 -0
- package/node_modules/@mytools/core/src/process.js +2 -71
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/LICENSE +21 -0
- package/node_modules/@mytools/fst-cli/README.md +54 -0
- package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
- package/node_modules/@mytools/fst-cli/package.json +49 -0
- package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
- package/node_modules/@mytools/fst-cli/src/development.js +53 -0
- package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
- package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
- package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
- package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
- package/{manual/packages/core → node_modules/@mytools/fst-cli}/src/terminal/run-feed.js +31 -5
- package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
- package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
- package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
- package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
- package/node_modules/@mytools/fst-devtools/README.md +28 -0
- package/node_modules/@mytools/fst-devtools/package.json +29 -0
- package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
- package/node_modules/@mytools/fst-logs/LICENSE +21 -0
- package/node_modules/@mytools/fst-logs/README.md +56 -0
- package/node_modules/@mytools/fst-logs/package.json +24 -0
- package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
- package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
- package/node_modules/@mytools/fst-logs/src/run-logs.js +239 -0
- package/node_modules/@mytools/fst-platform/LICENSE +21 -0
- package/node_modules/@mytools/fst-platform/README.md +67 -0
- package/node_modules/@mytools/fst-platform/package.json +33 -0
- package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
- package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
- package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
- package/node_modules/@mytools/fst-platform/src/json.js +5 -0
- package/node_modules/@mytools/fst-platform/src/network.js +199 -0
- package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
- package/node_modules/@mytools/fst-platform/src/process.js +110 -0
- package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
- package/node_modules/@mytools/fst-plugin/README.md +29 -0
- package/node_modules/@mytools/fst-plugin/package.json +29 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
- package/node_modules/@mytools/fst-plugin/src/host.js +232 -0
- package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
- package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
- package/node_modules/@mytools/{core → fst-plugin}/src/runtime.js +20 -4
- package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
- package/node_modules/@mytools/fst-workflow/README.md +28 -0
- package/node_modules/@mytools/fst-workflow/package.json +24 -0
- package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
- package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
- package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
- package/node_modules/@mytools/fst-workspace/README.md +32 -0
- package/node_modules/@mytools/fst-workspace/package.json +34 -0
- package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
- package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
- package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
- package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
- package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
- package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
- package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
- package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
- package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
- package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
- package/node_modules/@mytools/pack/README.md +31 -0
- package/node_modules/@mytools/pack/dependencies.js +15 -3
- package/node_modules/@mytools/pack/environments.js +1 -1
- package/node_modules/@mytools/pack/index.js +33 -25
- package/node_modules/@mytools/pack/package.json +29 -1
- package/node_modules/@mytools/sdk/README.md +31 -0
- package/node_modules/@mytools/sdk/conditions.js +1 -0
- package/node_modules/@mytools/sdk/diagnostics.js +27 -0
- package/node_modules/@mytools/sdk/package.json +23 -1
- package/node_modules/@mytools/sdk/schema-path.js +31 -0
- package/node_modules/@mytools/sdk/validation.js +78 -0
- package/node_modules/@mytools/web/README.md +29 -0
- package/node_modules/@mytools/web/package.json +7 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1275 -1076
- package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
- package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
- package/node_modules/@mytools/web/public/index.html +2 -2
- package/node_modules/@mytools/web/server.js +24 -17
- package/node_modules/@mytools/web/src/model.js +84 -0
- package/node_modules/acorn/CHANGELOG.md +1012 -0
- package/node_modules/acorn/LICENSE +21 -0
- package/node_modules/acorn/README.md +309 -0
- package/node_modules/acorn/bin/acorn +4 -0
- package/node_modules/acorn/dist/acorn.d.mts +897 -0
- package/node_modules/acorn/dist/acorn.d.ts +897 -0
- package/node_modules/acorn/dist/acorn.js +6370 -0
- package/node_modules/acorn/dist/acorn.mjs +6341 -0
- package/node_modules/acorn/dist/bin.js +90 -0
- package/node_modules/acorn/package.json +50 -0
- package/package.json +117 -101
- package/manual/packages/core/src/host.js +0 -124
- package/node_modules/@mytools/core/bin/fst.js +0 -70
- package/node_modules/@mytools/core/src/contracts.js +0 -41
- package/node_modules/@mytools/core/src/host.js +0 -124
- package/node_modules/@mytools/core/src/plugins.js +0 -55
- package/node_modules/@mytools/core/src/run-logs.js +0 -63
- package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
- package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mytools/fst-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/terminal/session.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/terminal/session.js",
|
|
9
|
+
"./entry": "./bin/fst.js",
|
|
10
|
+
"./run": "./src/cli-run.js",
|
|
11
|
+
"./terminal/conditions": "./src/terminal/conditions.js",
|
|
12
|
+
"./terminal/log-viewer": "./src/terminal/log-viewer.js",
|
|
13
|
+
"./terminal/model": "./src/terminal/model.js",
|
|
14
|
+
"./terminal/navigation": "./src/terminal/navigation.js",
|
|
15
|
+
"./terminal/presentation": "./src/terminal/presentation.js",
|
|
16
|
+
"./terminal/run-feed": "./src/terminal/run-feed.js",
|
|
17
|
+
"./terminal/run-presentation": "./src/terminal/run-presentation.js",
|
|
18
|
+
"./terminal/run-viewer": "./src/terminal/run-viewer.js",
|
|
19
|
+
"./terminal/schema": "./src/terminal/schema.js",
|
|
20
|
+
"./terminal/session": "./src/terminal/session.js",
|
|
21
|
+
"./terminal/theme": "./src/terminal/theme.js",
|
|
22
|
+
"./terminal/view": "./src/terminal/view.js",
|
|
23
|
+
"./terminal/workflow-menu": "./src/terminal/workflow-menu.js",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"bin",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=24.15.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@mytools/sdk": "0.1.0",
|
|
38
|
+
"@mytools/core": "0.1.0",
|
|
39
|
+
"@mytools/fst-workspace": "0.1.0",
|
|
40
|
+
"@mytools/fst-platform": "0.1.0",
|
|
41
|
+
"@mytools/fst-plugin": "0.1.0",
|
|
42
|
+
"@mytools/fst-devtools": "0.1.0",
|
|
43
|
+
"@mariozechner/pi-tui": "0.50.0",
|
|
44
|
+
"commander": "14.0.3"
|
|
45
|
+
},
|
|
46
|
+
"bin": {
|
|
47
|
+
"fst": "bin/fst.js"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// 工作流与插件命令共享取消及落盘收尾。
|
|
2
|
+
export async function waitForCliRun(app,run,signals=process){
|
|
3
|
+
const cancel=()=>app.cancelRun(run.id);signals.once('SIGINT',cancel);
|
|
4
|
+
try{return await run.done??app.getRun(run.id);}
|
|
5
|
+
finally{signals.removeListener('SIGINT',cancel);}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function cliRunResult(run){
|
|
9
|
+
const value=run.status==='stopped'?{status:'stopped',reason:run.reason}:run.error??run.output;
|
|
10
|
+
const exitCode=['succeeded','stopped'].includes(run.status)?0:run.status==='cancelled'?130:1;
|
|
11
|
+
return {value,exitCode};
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {randomBytes} from 'node:crypto';
|
|
2
|
+
import {createDevelopmentSupervisor,bindDevelopmentChild} from '@mytools/core/development';
|
|
3
|
+
|
|
4
|
+
export async function startDevelopment({projectDir,entry,args,configFile,mode,port}){
|
|
5
|
+
console.error('开发模式:源码变化后等待当前业务结束并重启;终端未保存的编辑草稿不会保留。');
|
|
6
|
+
const supervisor=await createDevelopmentSupervisor({projectDir,entry,args,configFile,
|
|
7
|
+
env:{FISHTOOLS_DEV_PROJECT:projectDir,FISHTOOLS_DEV_MODE:mode,FISHTOOLS_DEV_TOKEN:randomBytes(24).toString('hex'),FISHTOOLS_DEV_PORT:String(port)},
|
|
8
|
+
onEvent(event){
|
|
9
|
+
if(event.type==='ready')console.error(event.recovered?'开发应用已恢复,源码监听继续。':`开发应用已就绪(第 ${event.generation} 代)。`);
|
|
10
|
+
else if(event.type==='waiting')console.error('开发源码已修改,等待当前运行及管理操作结束。');
|
|
11
|
+
else if(event.type==='restarting')console.error('开发应用准备重启。');
|
|
12
|
+
else if(event.type==='failed')console.error(`开发应用启动失败:${event.message};修复源码后自动重试。`);
|
|
13
|
+
else if(['forced','blocked','watchError'].includes(event.type))console.error(`开发模式:${event.message}`);
|
|
14
|
+
if(event.type==='exited'&&mode==='terminal'&&process.stdout.isTTY){
|
|
15
|
+
process.stdin.setRawMode?.(false);process.stdout.write('\x1b[?1006l\x1b[?1000l\x1b[?25h\x1b[?1049l');
|
|
16
|
+
}
|
|
17
|
+
}});
|
|
18
|
+
const stop=()=>{void supervisor.close().catch(error=>{console.error(error.message);process.exitCode=2;}).finally(()=>{
|
|
19
|
+
process.removeListener('SIGINT',stop);process.removeListener('SIGTERM',stop);
|
|
20
|
+
// 子进程被强制回收时,监督器恢复终端的常用状态。
|
|
21
|
+
if(mode==='terminal'&&process.stdout.isTTY)process.stdout.write('\x1b[?1006l\x1b[?1000l\x1b[?25h\x1b[?1049l');
|
|
22
|
+
});};
|
|
23
|
+
process.once('SIGINT',stop);process.once('SIGTERM',stop);
|
|
24
|
+
return supervisor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runDevelopmentChild(app,{mode,webOptions}){
|
|
28
|
+
const web={...webOptions,port:Number(process.env.FISHTOOLS_DEV_PORT??webOptions.port),token:process.env.FISHTOOLS_DEV_TOKEN};
|
|
29
|
+
let binding;
|
|
30
|
+
const stop=()=>{void binding?.close();};
|
|
31
|
+
if(mode==='web'){
|
|
32
|
+
const server=await app.startWeb(web);console.log(`Fishtools:${server.url}`);
|
|
33
|
+
binding=bindDevelopmentChild(app,{details:{port:server.port},closePresentation:()=>{
|
|
34
|
+
process.removeListener('SIGINT',stop);process.removeListener('SIGTERM',stop);
|
|
35
|
+
}});
|
|
36
|
+
process.on('SIGINT',stop);process.on('SIGTERM',stop);return;
|
|
37
|
+
}
|
|
38
|
+
const [{TerminalView},{TerminalSession}]=await Promise.all([import('./terminal/view.js'),import('./terminal/session.js')]);
|
|
39
|
+
const ui=new TerminalView({baseDir:process.cwd()}),session=new TerminalSession(app,ui,{webOptions:web});
|
|
40
|
+
let presentation;
|
|
41
|
+
try{
|
|
42
|
+
ui.start();presentation=session.run();
|
|
43
|
+
binding=bindDevelopmentChild(app,{closePresentation:async()=>{
|
|
44
|
+
// 监督器已经确认业务空闲;开发重启直接结束草稿和 pending 提示。
|
|
45
|
+
session.exiting=true;session.dirty=false;
|
|
46
|
+
ui.pending?.reject(Object.assign(new Error('开发应用重启'),{code:'PROMPT_CANCELLED'}));
|
|
47
|
+
await presentation;
|
|
48
|
+
await ui.close();process.removeListener('SIGINT',stop);process.removeListener('SIGTERM',stop);
|
|
49
|
+
}});
|
|
50
|
+
process.on('SIGINT',stop);process.on('SIGTERM',stop);
|
|
51
|
+
await presentation;
|
|
52
|
+
}finally{await ui.close();await binding?.close();}
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { outputSources, precedingSteps, stepMetadata } from './model.js';
|
|
2
|
+
import { editValue } from './schema.js';
|
|
3
|
+
|
|
4
|
+
export function conditionSources(flow,index,catalog,workflows){
|
|
5
|
+
const flatten=(schema,prefix)=>Object.entries(schema?.properties??{}).flatMap(([key,value])=>[{value:`${prefix}.${key}`,label:value.title??`${prefix}.${key}`,schema:value},...flatten(value,`${prefix}.${key}`)]);
|
|
6
|
+
return [...flatten(flow.inputs,'inputs'),...outputSources(flow,index,catalog,workflows)];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 条件使用结构化表单构造;用户不需要了解底层 JSON 协议。
|
|
10
|
+
export async function editCondition(ui,flow,index,catalog,workflows,{baseDir,current}={}){
|
|
11
|
+
const presets=precedingSteps(flow,index).flatMap(step=>Object.entries(stepMetadata(step,catalog,workflows)?.conditions??{}).map(([key,preset])=>({value:`preset:steps.${step.id}.output:${key}`,label:`${step.title??step.id} · ${preset.title??key}`,condition:{left:{$ref:`steps.${step.id}.output.${preset.path}`},op:preset.op,...(preset.op==='exists'?{}:{right:preset.value})}})));
|
|
12
|
+
const mode=await ui.select('设置判断条件',[
|
|
13
|
+
...presets,{value:'compare',label:'选择字段与比较方式'},{value:'all',label:'全部条件成立(且)'},{value:'any',label:'任一条件成立(或)'},{value:'not',label:'条件取反'},...(current?[{value:'keep',label:'保留当前条件'}]:[]),
|
|
14
|
+
]);
|
|
15
|
+
if(mode==='keep')return current;
|
|
16
|
+
const preset=presets.find(p=>p.value===mode);if(preset)return structuredClone(preset.condition);
|
|
17
|
+
if(mode==='not')return {not:await editCondition(ui,flow,index,catalog,workflows,{baseDir})};
|
|
18
|
+
if(mode==='all'||mode==='any'){
|
|
19
|
+
const children=[];
|
|
20
|
+
do{children.push(await editCondition(ui,flow,index,catalog,workflows,{baseDir}));}while(await ui.confirm('继续添加判断条件?'));
|
|
21
|
+
return {[mode]:children};
|
|
22
|
+
}
|
|
23
|
+
const sources=conditionSources(flow,index,catalog,workflows);
|
|
24
|
+
if(!sources.length)throw new Error('请先添加运行参数或产生输出的前置步骤');
|
|
25
|
+
const source=await ui.select('选择要判断的字段',sources),schema=sources.find(s=>s.value===source)?.schema??{};
|
|
26
|
+
const op=await ui.select('比较方式',[['eq','等于'],['ne','不等于'],['gt','大于'],['gte','大于等于'],['lt','小于'],['lte','小于等于'],['contains','包含'],['exists','存在']].map(([value,label])=>({value,label})));
|
|
27
|
+
if(op==='exists')return {left:{$ref:source},op};
|
|
28
|
+
const type=op==='contains'&&schema.type==='array'?schema.items?.type:schema.type;
|
|
29
|
+
const chosen=['string','number','integer','boolean'].includes(type)?type:await ui.select('比较值类型',[{value:'string',label:'文本'},{value:'number',label:'数字'},{value:'boolean',label:'是 / 否'}]);
|
|
30
|
+
const right=await editValue(ui,{type:chosen},current?.right,{baseDir,required:true,label:'比较值'});
|
|
31
|
+
return {left:{$ref:source},op,right};
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Markdown, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
2
|
+
import { mouseWheel } from './run-viewer.js';
|
|
3
|
+
import {palette,markdownTheme} from './theme.js';
|
|
4
|
+
|
|
5
|
+
const theme=markdownTheme();
|
|
6
|
+
|
|
7
|
+
// 文本已由 TerminalView 的 safe 清理;每次只向 Overlay 提交当前窗口内的行。
|
|
8
|
+
export class MarkdownLogViewer{
|
|
9
|
+
constructor({title,text,terminal,onClose,requestRender}){
|
|
10
|
+
this.title=title.replace(/\s+/g,' ');this.terminal=terminal;this.onClose=onClose;this.requestRender=requestRender;
|
|
11
|
+
this.markdown=new Markdown(text,0,0,theme);this.offset=0;
|
|
12
|
+
}
|
|
13
|
+
invalidate(){this.markdown.invalidate();}
|
|
14
|
+
layout(width){
|
|
15
|
+
const height=Math.max(1,this.terminal.rows);
|
|
16
|
+
const pageSize=Math.max(1,height-2);
|
|
17
|
+
// Markdown 自身缓存同一宽度的完整渲染,滚动时不重新解析日志。
|
|
18
|
+
const lines=this.markdown.render(Math.max(1,width));
|
|
19
|
+
const maxOffset=Math.max(0,lines.length-pageSize);
|
|
20
|
+
this.offset=Math.min(maxOffset,Math.max(0,this.offset));
|
|
21
|
+
return {height,pageSize,lines,maxOffset};
|
|
22
|
+
}
|
|
23
|
+
render(width){
|
|
24
|
+
const {height,pageSize,lines}=this.layout(width);
|
|
25
|
+
const body=lines.slice(this.offset,this.offset+pageSize);
|
|
26
|
+
while(body.length<pageSize)body.push('');
|
|
27
|
+
const start=lines.length?this.offset+1:0,end=Math.min(this.offset+pageSize,lines.length);
|
|
28
|
+
const footer=palette.muted(`Esc/q 返回 · ↑↓/滚轮 · PgUp/PgDn · Home/End · ${start}–${end}/${lines.length}`);
|
|
29
|
+
const screen=height===1?body:height===2?[...body,footer]:[palette.brand(this.title),...body,footer];
|
|
30
|
+
return screen.map(line=>truncateToWidth(line,width));
|
|
31
|
+
}
|
|
32
|
+
handleInput(data){
|
|
33
|
+
if(['escape','enter','q'].some(key=>matchesKey(data,key))){this.onClose();return;}
|
|
34
|
+
const {pageSize,maxOffset}=this.layout(this.terminal.columns);
|
|
35
|
+
if(mouseWheel(data))this.offset+=mouseWheel(data)*3;
|
|
36
|
+
else if(matchesKey(data,'up'))this.offset--;
|
|
37
|
+
else if(matchesKey(data,'down'))this.offset++;
|
|
38
|
+
else if(matchesKey(data,'pageUp'))this.offset-=pageSize;
|
|
39
|
+
else if(matchesKey(data,'pageDown'))this.offset+=pageSize;
|
|
40
|
+
else if(matchesKey(data,'home'))this.offset=0;
|
|
41
|
+
else if(matchesKey(data,'end'))this.offset=maxOffset;
|
|
42
|
+
else return;
|
|
43
|
+
this.offset=Math.min(maxOffset,Math.max(0,this.offset));this.requestRender();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
export {builtinCommands as commands} from '@mytools/fst-plugin/contracts';
|
|
7
|
+
|
|
8
|
+
export function parseCommand(text){
|
|
9
|
+
const args=[];let token='',quote='',started=false;
|
|
10
|
+
for(const char of text.trim()){
|
|
11
|
+
if(quote){if(char===quote)quote='';else token+=char;started=true;}
|
|
12
|
+
else if(char==='"'||char==="'"){quote=char;started=true;}
|
|
13
|
+
else if(/\s/.test(char)){if(started){args.push(token);token='';started=false;}}
|
|
14
|
+
else{token+=char;started=true;}
|
|
15
|
+
}
|
|
16
|
+
if(quote)throw new Error('文件路径的引号未闭合');
|
|
17
|
+
if(started)args.push(token);
|
|
18
|
+
if(!args[0]?.startsWith('/'))throw Object.assign(new Error('这不是斜杠命令。输入 / 选择操作;执行工作流请用 /run 工作流ID。'),{code:'COMMAND_INPUT'});
|
|
19
|
+
args[0]=args[0].slice(1);return args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fileReference(text,baseDir){
|
|
23
|
+
if(text.startsWith('@@'))return text.slice(1);
|
|
24
|
+
if(!text.startsWith('@'))return text;
|
|
25
|
+
let file=text.slice(1).trim();
|
|
26
|
+
if((file.startsWith('"')&&file.endsWith('"'))||(file.startsWith("'")&&file.endsWith("'")))file=file.slice(1,-1);
|
|
27
|
+
if(!file)throw new Error('@ 后请选择文件路径');
|
|
28
|
+
if(file==='~')file=homedir();else if(file.startsWith('~/')||file.startsWith('~\\'))file=path.join(homedir(),file.slice(2));
|
|
29
|
+
return path.resolve(baseDir,file);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readRunInput(reference,baseDir){
|
|
33
|
+
if(!reference.startsWith('@')||reference.startsWith('@@'))throw new Error('运行参数文件需要使用 @路径.json');
|
|
34
|
+
const value=JSON.parse(await readFile(fileReference(reference,baseDir),'utf8'));
|
|
35
|
+
if(!value||typeof value!=='object'||Array.isArray(value))throw new Error('运行参数文件必须是 JSON 对象');
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const newWorkflow=(title,description)=>({schemaVersion:1,id:`flow-${randomUUID().slice(0,8)}`,title,description,inputs:{type:'object',properties:{}},steps:[],outputs:{}});
|
|
40
|
+
|
|
41
|
+
// 路径从根 steps 开始,例如 [1, 'branch', 'then', 0]。
|
|
42
|
+
export const stepAt=(flow,index)=>[...(Array.isArray(index)?index:[index])].reduce((node,key)=>node[key],flow.steps);
|
|
43
|
+
export const sequenceAt=(flow,parts=[])=>parts.reduce((node,key)=>node[key],flow.steps);
|
|
44
|
+
export function precedingSteps(flow,index){
|
|
45
|
+
const parts=Array.isArray(index)?index:[index];let sequence=flow.steps;const result=[];
|
|
46
|
+
for(let i=0;i<parts.length;i+=3){
|
|
47
|
+
// 并行子项同属一批,不能把前面的兄弟当成已完成步骤。
|
|
48
|
+
if(i===0||parts[i-2]!=='parallel')result.push(...sequence.slice(0,parts[i]));
|
|
49
|
+
if(i+1<parts.length)sequence=sequence[parts[i]][parts[i+1]][parts[i+2]];
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setBinding(flow,index,key,mode,schema,value){
|
|
55
|
+
const step=stepAt(flow,index),old=step.with?.[key];step.with??={};
|
|
56
|
+
if(mode==='runtime'){
|
|
57
|
+
flow.inputs??={type:'object',properties:{}};flow.inputs.properties??={};
|
|
58
|
+
const name=`fst_${step.id}_${key}`;
|
|
59
|
+
flow.inputs.properties[name]={...schema,title:schema.title??key};
|
|
60
|
+
flow.inputs.required=[...new Set([...(flow.inputs.required??[]),name])];
|
|
61
|
+
step.with[key]={$ref:`inputs.${name}`};
|
|
62
|
+
}else if(mode==='fixed'){
|
|
63
|
+
if(value===undefined)delete step.with[key];else step.with[key]=value;
|
|
64
|
+
}else step.with[key]={$ref:mode};
|
|
65
|
+
if(old?.$ref?.startsWith('inputs.fst_')){
|
|
66
|
+
const refs=[];
|
|
67
|
+
const scan=v=>{if(v&&typeof v==='object'){if(typeof v.$ref==='string')refs.push(v.$ref);for(const child of Object.values(v))scan(child);}};
|
|
68
|
+
scan(flow.steps);scan(flow.outputs);
|
|
69
|
+
if(!refs.includes(old.$ref)){
|
|
70
|
+
const name=old.$ref.slice('inputs.'.length);delete flow.inputs.properties[name];
|
|
71
|
+
flow.inputs.required=(flow.inputs.required??[]).filter(k=>k!==name);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function pruneRuntimeInputs(flow){
|
|
77
|
+
const refs=new Set();
|
|
78
|
+
const scan=value=>{if(value&&typeof value==='object'){if(typeof value.$ref==='string')refs.add(value.$ref);Object.values(value).forEach(scan);}};
|
|
79
|
+
scan(flow.steps);scan(flow.outputs);
|
|
80
|
+
for(const name of Object.keys(flow.inputs?.properties??{})){
|
|
81
|
+
if(name.startsWith('fst_')&&!refs.has(`inputs.${name}`)){
|
|
82
|
+
delete flow.inputs.properties[name];flow.inputs.required=(flow.inputs.required??[]).filter(key=>key!==name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function stepMetadata(step,catalog,workflows){
|
|
88
|
+
if(step.branch||step.parallel){
|
|
89
|
+
const children=step.parallel?.steps??[...(step.branch.then??[]),...(step.branch.else??[])];
|
|
90
|
+
return {title:step.title??(step.branch?'条件分支':'并行组'),output:{type:'object',properties:Object.fromEntries(children.map(child=>[child.id,stepMetadata(child,catalog,workflows)?.output??{}]))}};
|
|
91
|
+
}
|
|
92
|
+
if(step.stop)return {title:step.title??'提前结束',output:{type:'object',properties:{}}};
|
|
93
|
+
if(step.action)return catalog.find(a=>a.id===step.action);
|
|
94
|
+
const flow=workflows.find(w=>`${w.id}.yaml`===step.workflow?.replace(/^\.\//,''));
|
|
95
|
+
return flow?{title:flow.title??flow.id,description:flow.description,input:flow.inputs,output:{type:'object',properties:Object.fromEntries(Object.keys(flow.outputs??{}).map(k=>[k,{}]))}}:null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function outputSources(flow,index,catalog,workflows){
|
|
99
|
+
const flatten=(schema,prefix='')=>Object.entries(schema?.properties??{}).flatMap(([key,s])=>{const name=prefix?`${prefix}.${key}`:key;return [{name,schema:s},...flatten(s,name)];});
|
|
100
|
+
return precedingSteps(flow,index).flatMap((step,i)=>{
|
|
101
|
+
const meta=stepMetadata(step,catalog,workflows),base=`steps.${step.id}.output`,label=`${i+1}. ${meta?.title??step.id}`;
|
|
102
|
+
return [{value:base,label:`${label} → 完整输出`,schema:meta?.output},...flatten(meta?.output).map(f=>({value:`${base}.${f.name}`,label:`${label} → ${f.schema.title??f.name}`,schema:f.schema}))];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {builtinCommands} from '@mytools/fst-plugin/contracts';
|
|
2
|
+
|
|
3
|
+
const visible=['workflows','run','pack','logs','plugin','config','tools','help','exit'];
|
|
4
|
+
// 入口由插件 ID 推导;冲突时加前缀,不能覆盖内置或作者注册的命令。
|
|
5
|
+
export function commandMenu(plugins,commands){
|
|
6
|
+
const entries=builtinCommands.filter(command=>visible.includes(command.name)).map(command=>({...command,description:`[内置] ${command.description}`}));
|
|
7
|
+
const used=new Set([...builtinCommands.map(c=>c.name),...commands.map(c=>c.name)]);
|
|
8
|
+
const groups=plugins.filter(plugin=>commands.some(c=>c.plugin===plugin.id)).sort((a,b)=>a.id.localeCompare(b.id));
|
|
9
|
+
for(const plugin of groups){
|
|
10
|
+
const leaf=plugin.id.split('/').at(-1).replace(/[^\w.-]/g,'-');
|
|
11
|
+
let name=/^[a-zA-Z]/.test(leaf)?leaf:`plugin-${leaf}`;
|
|
12
|
+
if(groups.filter(p=>p.id.split('/').at(-1)===leaf).length>1)name=plugin.id.replace(/^@/,'').replace(/[^\w.-]/g,'-');
|
|
13
|
+
while(used.has(name))name='plugin-'+name;
|
|
14
|
+
used.add(name);entries.push({name,plugin:plugin.id,description:`[插件] ${plugin.title??plugin.id} · ${plugin.id}`});
|
|
15
|
+
}
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const logo=[
|
|
2
|
+
'█▀▀ ▀█▀ █▀▀ █ █',
|
|
3
|
+
'█▀ █ ▀▀█ █▀█',
|
|
4
|
+
'▀ ▀▀▀ ▀▀▀ ▀ ▀',
|
|
5
|
+
].join('\n');
|
|
6
|
+
|
|
7
|
+
export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed:'失败',stopped:'已停止'})[plugin.runtime?.status]??'未知';
|
|
8
|
+
|
|
9
|
+
export function gettingStarted(flows,{readOnly=false}={}){
|
|
10
|
+
const sample=flows.find(w=>w.id==='hello')??flows[0];
|
|
11
|
+
return [
|
|
12
|
+
sample?`试运行 /run ${sample.id}\n${sample.description??sample.title??'按提示填写参数,执行后查看结果。'}`:'当前工作区还没有工作流。',
|
|
13
|
+
'',
|
|
14
|
+
`/workflows ${readOnly?'浏览工作流':'管理工作流'} /plugin 插件 /config 工具配置`,
|
|
15
|
+
`/logs 回看执行记录、输入输出与日志 /help 帮助`,
|
|
16
|
+
].join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fields(schema){
|
|
20
|
+
if(!schema)return '未声明';
|
|
21
|
+
if(!schema.properties)return `类型:${schema.type??'由插件约束定义'}`;
|
|
22
|
+
const entries=Object.entries(schema.properties);
|
|
23
|
+
return entries.length?entries.map(([key,s])=>`${s.title??key} (${key}, ${s.type??'任意类型'}${schema.required?.includes(key)?', 必填':''})${s.description?`:${s.description}`:''}`).join(';'):'无';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function privilege(item,plugin){
|
|
27
|
+
const declaration=item.requirements?.privilege?.[process.platform]??plugin.requirements?.privilege?.[process.platform];
|
|
28
|
+
if(!declaration)return '未声明';
|
|
29
|
+
const names={user:'普通用户',root:'root',administrator:'管理员',resource:'特定资源权限'};
|
|
30
|
+
return `${names[declaration.level]??declaration.level}${declaration.reason?`;原因:${declaration.reason}`:''}${declaration.remediation?`;处理:${declaration.remediation}`:''}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function pluginDetails(plugin,actions,commands){
|
|
34
|
+
return [
|
|
35
|
+
`# ${plugin.title??plugin.id}`,
|
|
36
|
+
plugin.description??'作者尚未提供用途说明。',
|
|
37
|
+
`标识:${plugin.id}\n\n版本:${plugin.version}\n\n来源:${plugin.removable?'自行安装(可移除)':'发行包内置 / 项目预置'}\n\n状态:${pluginStatus(plugin)}`,
|
|
38
|
+
...(plugin.runtime?.missing?.length?[`缺少服务:${plugin.runtime.missing.join(', ')}`]:[]),
|
|
39
|
+
`更新方式:${!plugin.runtime?.updatePolicy?'无运行时服务':plugin.runtime.updatePolicy==='automatic'?'可自动短暂停止并重启':'需要本次维护确认'}\n\n插件权限:${privilege(plugin,plugin)}`,
|
|
40
|
+
`声明平台:${plugin.platforms?.join(', ')??'作者未声明'}\n\n声明架构:${plugin.architectures?.join(', ')??'作者未声明'}`,
|
|
41
|
+
'## 可用于编排的能力',
|
|
42
|
+
...(actions.length?actions.map(a=>`### ${a.title??a.id}\n\n${a.description??''}\n\n能力 ID:${a.id}\n\n输入:${fields(a.input)}\n\n输出:${fields(a.output)}\n\n权限:${privilege(a,plugin)}`):['此插件没有提供编排动作,可能提供 Web、共享服务或交互命令。']),
|
|
43
|
+
'## 斜杠命令',commands.length?commands.map(c=>`- /${c.name}:${c.description??'未提供说明'}`).join('\n'):'没有注册斜杠命令。',
|
|
44
|
+
'## 插件配置',plugin.config?fields(plugin.config):'无需配置。',
|
|
45
|
+
'## 环境声明',
|
|
46
|
+
...[plugin,...actions].flatMap(item=>Object.entries(item.environments??{}).map(([name,e])=>`- ${item.id} / ${name}:${({host:'使用宿主机环境',bundled:'随包携带',tool:'使用 Fishtools 自带环境'})[e.source]??e.source}`)),
|
|
47
|
+
'环境与平台由插件作者声明;具体执行结果请在 /logs 查看。',
|
|
48
|
+
'编排用法:/workflows 新建工作流 → 添加步骤并选择上述能力 → 填写参数 → 保存 → 运行。也可在主面板用 /run 选择已保存的工作流。',
|
|
49
|
+
].join('\n\n');
|
|
50
|
+
}
|
|
@@ -3,7 +3,7 @@ import {runStatuses} from './run-viewer.js';
|
|
|
3
3
|
|
|
4
4
|
// 主面板按事件到达顺序追加;运行快照另存,配置菜单不会清掉执行记录。
|
|
5
5
|
export class RunFeed{
|
|
6
|
-
constructor(refresh){this
|
|
6
|
+
constructor(refresh,{maxEvents=1000,maxBytes=1048576,maxRuns=50}={}){Object.assign(this,{refresh,maxEvents,maxBytes,maxRuns});this.runs=new Map();this.entries=[];this.subscriptions=new Map();this.expanded=false;this.dropped=0;}
|
|
7
7
|
track({app,runId,title,input}){
|
|
8
8
|
if(this.runs.has(runId))return;
|
|
9
9
|
const record=app.getRun(runId),item={app,record,title,input,count:0,ended:false,steps:new Map(),catalog:new Map((app.catalog?.()??[]).map(action=>[action.id,action]))};
|
|
@@ -26,12 +26,15 @@ export class RunFeed{
|
|
|
26
26
|
this.refresh();
|
|
27
27
|
}
|
|
28
28
|
update(item,event){
|
|
29
|
-
|
|
29
|
+
let events;
|
|
30
|
+
if(event?.type&&!['run.succeeded','run.failed','run.cancelled','run.stopped'].includes(event.type)){item.record.events.push(event);events=[event];item.count++;}
|
|
30
31
|
else{
|
|
31
32
|
const record=item.app.getRun(item.record.id);if(!record)return;
|
|
32
33
|
item.record={...record,events:[...(record.events??[])]};
|
|
34
|
+
events=record.events.filter((e,index)=>e.seq!==undefined?e.seq>item.count:index>=item.count);
|
|
35
|
+
item.count=record.eventCount??record.events.length;
|
|
33
36
|
}
|
|
34
|
-
const {record}=item
|
|
37
|
+
const {record}=item;
|
|
35
38
|
for(const event of events)if(event.type.startsWith('step.'))item.steps.set(event.callPath,{...item.steps.get(event.callPath),...event});
|
|
36
39
|
if(events.length){
|
|
37
40
|
const last=this.entries.at(-1);
|
|
@@ -39,12 +42,34 @@ export class RunFeed{
|
|
|
39
42
|
else this.entries.push({type:'events',item,events});
|
|
40
43
|
}
|
|
41
44
|
if(record.status!=='running'&&!item.ended){item.ended=true;this.entries.push({type:'result',item});}
|
|
45
|
+
this.trim();
|
|
46
|
+
}
|
|
47
|
+
trim(){
|
|
48
|
+
let count=0,bytes=0;
|
|
49
|
+
for(let index=this.entries.length-1;index>=0;index--){
|
|
50
|
+
const entry=this.entries[index];if(!entry.events)continue;
|
|
51
|
+
let keep=entry.events.length;
|
|
52
|
+
for(let i=entry.events.length-1;i>=0;i--){
|
|
53
|
+
const size=Buffer.byteLength(JSON.stringify(entry.events[i]));
|
|
54
|
+
if(count>=this.maxEvents||bytes+size>this.maxBytes){keep=entry.events.length-i-1;this.dropped+=i+1;break;}
|
|
55
|
+
count++;bytes+=size;
|
|
56
|
+
}
|
|
57
|
+
if(keep!==entry.events.length)entry.events=keep?entry.events.slice(-keep):[];
|
|
58
|
+
}
|
|
59
|
+
this.entries=this.entries.filter(entry=>!entry.events||entry.events.length);
|
|
60
|
+
for(const item of this.runs.values()){
|
|
61
|
+
item.record.events=this.entries.filter(e=>e.item===item&&e.events).flatMap(e=>e.events);
|
|
62
|
+
while(item.steps.size>this.maxEvents)item.steps.delete(item.steps.keys().next().value);
|
|
63
|
+
}
|
|
64
|
+
const ended=[...this.runs.values()].filter(item=>item.ended);
|
|
65
|
+
for(const item of ended.slice(0,Math.max(0,ended.length-this.maxRuns))){this.runs.delete(item.record.id);this.entries=this.entries.filter(e=>e.item!==item);this.dropped++;}
|
|
66
|
+
const messages=this.entries.filter(e=>e.type==='message');for(const entry of messages.slice(0,Math.max(0,messages.length-100)))this.entries.splice(this.entries.indexOf(entry),1);
|
|
42
67
|
}
|
|
43
68
|
release(app){
|
|
44
69
|
if([...this.runs.values()].some(item=>item.app===app&&!item.ended))return;
|
|
45
70
|
this.subscriptions.get(app)?.();this.subscriptions.delete(app);
|
|
46
71
|
}
|
|
47
|
-
message(text){this.entries.push({type:'message',text});this.refresh();}
|
|
72
|
+
message(text){this.entries.push({type:'message',text:String(text).slice(-65536)});this.trim();this.refresh();}
|
|
48
73
|
compactEvents(entry,seen){
|
|
49
74
|
const {item}=entry,rows=[];
|
|
50
75
|
for(const event of entry.events){
|
|
@@ -61,7 +86,7 @@ export class RunFeed{
|
|
|
61
86
|
}
|
|
62
87
|
return rows.length?`${item.title??item.record.workflowId} · ${item.record.id.slice(0,8)}\n${rows.join('\n')}`:'';
|
|
63
88
|
}
|
|
64
|
-
text(){const seen=new Set();return this.entries.map(entry=>{
|
|
89
|
+
text(){const seen=new Set();return (this.dropped?'主面板仅保留近期摘要;完整日志可在 /logs 中按 p 分页阅读。\n\n':'')+this.entries.map(entry=>{
|
|
65
90
|
if(entry.type==='message')return entry.text;
|
|
66
91
|
const {record,title,input}=entry.item,label=`${title??record.workflowId} · ${record.id.slice(0,8)}`;
|
|
67
92
|
if(entry.type==='start')return [
|
|
@@ -71,6 +96,7 @@ export class RunFeed{
|
|
|
71
96
|
].filter(Boolean).join('\n');
|
|
72
97
|
if(entry.type==='events')return this.expanded?`${label}\n${readableTimeline(entry.events,{values:true})}`:this.compactEvents(entry,seen);
|
|
73
98
|
return [`结果 · ${label} · ${runStatuses[record.status]??record.status}`,
|
|
99
|
+
...(record.warnings??[]).map(warning=>`警告:${warning.message}`),
|
|
74
100
|
this.expanded&&record.output!==undefined?readableValue(record.output):'',record.error&&(this.expanded||![...entry.item.steps.values()].some(step=>step.type==='step.failed'))?readableError(record.error,{details:this.expanded}):'',record.reason??'',
|
|
75
101
|
].filter(Boolean).join('\n');
|
|
76
102
|
}).filter(Boolean).join('\n\n');}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 阅读层只处理展示;原始事件、输入输出与错误仍保留在运行记录中。
|
|
2
|
+
export function readableValue(value,indent=''){
|
|
3
|
+
if(value===undefined)return '暂无记录';
|
|
4
|
+
if(value===null)return 'null';
|
|
5
|
+
if(typeof value!=='object')return String(value);
|
|
6
|
+
const entries=Array.isArray(value)?value.map((v,i)=>[`[${i+1}]`,v]):Object.entries(value);
|
|
7
|
+
if(!entries.length)return Array.isArray(value)?'(空列表)':'(空对象)';
|
|
8
|
+
return entries.map(([key,item])=>{
|
|
9
|
+
const nested=item!==null&&typeof item==='object';
|
|
10
|
+
return `${indent}${key}:${nested?'\n'+readableValue(item,indent+' '):String(item).replaceAll('\n','\n'+indent+' ')}`;
|
|
11
|
+
}).join('\n');
|
|
12
|
+
}
|
|
13
|
+
export function readableError(error,{details=false}={}){
|
|
14
|
+
if(!error)return '';
|
|
15
|
+
const chain=[];for(let current=error;current;current=current.cause)chain.push(current);
|
|
16
|
+
const program=chain.find(e=>e.details?.executable)?.details.executable;
|
|
17
|
+
const code=chain.find(e=>e.code==='ENOENT')?.code??error.code;
|
|
18
|
+
const summary=[`原因:${code==='ENOENT'&&program?'未找到所需程序':error.message??'未知错误'}`,error.plugin&&`插件:${error.plugin}`,code&&`错误码:${code}`,program&&`程序:${program}`,code==='ENOENT'&&'请检查程序是否安装、路径是否正确以及 PATH 配置。'].filter(Boolean).join('\n');
|
|
19
|
+
if(!details)return summary;
|
|
20
|
+
const diagnostic=chain.flatMap((item,index)=>[
|
|
21
|
+
`${index===0?'错误':'上游原因 '+index}:${item.message??'未知错误'}`,
|
|
22
|
+
item.code&&`错误码:${item.code}`,
|
|
23
|
+
item.details!==undefined&&`细节:\n${readableValue(item.details)}`,
|
|
24
|
+
item.stack&&`调用栈:\n${item.stack}`,
|
|
25
|
+
].filter(Boolean));
|
|
26
|
+
return `${summary}\n\n${diagnostic.join('\n')}`;
|
|
27
|
+
}
|
|
28
|
+
const eventNames={'step.started':'步骤开始','step.succeeded':'步骤成功','step.failed':'步骤失败','step.cancelled':'步骤取消','step.skipped':'步骤跳过','step.stopped':'步骤停止','run.started':'运行开始','run.succeeded':'运行成功','run.failed':'运行失败','run.cancelled':'运行取消','run.stopped':'提前结束'};
|
|
29
|
+
const time=value=>{const date=new Date(value);return Number.isFinite(+date)?date.toLocaleTimeString('zh-CN',{hour12:false}):'--:--:--';};
|
|
30
|
+
export function readableTimeline(events,{callPath,values=false}={}){
|
|
31
|
+
const rows=[];let pending;
|
|
32
|
+
const push=(event,message)=>{
|
|
33
|
+
const prefix=`${time(event.time)} [${event.callPath??'运行'}${event.stream?' · '+event.stream:''}]`;
|
|
34
|
+
const lines=String(message).replaceAll('\r\n','\n').replaceAll('\r','\n').split('\n');
|
|
35
|
+
if(lines.at(-1)==='')lines.pop();
|
|
36
|
+
for(const line of lines)rows.push(`${prefix} ${line}`);
|
|
37
|
+
};
|
|
38
|
+
const flush=()=>{if(pending){if(pending.text)push(pending.event,pending.text);pending=null;}};
|
|
39
|
+
for(const event of events??[]){
|
|
40
|
+
if(callPath&&event.callPath!==callPath)continue;
|
|
41
|
+
if(event.type==='log'&&event.stream){
|
|
42
|
+
const key=`${event.callPath}\0${event.stream}`;
|
|
43
|
+
if(pending?.key!==key){flush();pending={key,event,text:''};}
|
|
44
|
+
pending.text+=event.message??'';
|
|
45
|
+
const end=pending.text.lastIndexOf('\n');
|
|
46
|
+
if(end>=0){push(pending.event,pending.text.slice(0,end+1));pending.text=pending.text.slice(end+1);pending.event=event;}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if(event.type==='log'){flush();push(event,`${event.message??''}${event.data!==undefined?'\n'+readableValue(event.data):''}`);continue;}
|
|
50
|
+
if(eventNames[event.type]){
|
|
51
|
+
const detail=event.error?readableError(event.error,{details:values}):event.reason??event.message;
|
|
52
|
+
flush();push(event,`${eventNames[event.type]}${event.action?' · '+event.action:''}${event.durationMs!==undefined?` · ${event.durationMs} ms`:''}${detail?'\n'+detail:''}`);
|
|
53
|
+
if(values&&event.type.startsWith('step.')){
|
|
54
|
+
if(event.input!==undefined&&Object.keys(event.input??{}).length)push(event,'输入\n'+readableValue(event.input));
|
|
55
|
+
if(event.output!==undefined)push(event,'输出\n'+readableValue(event.output));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
flush();return rows.join('\n')||'等待执行记录。插件通过 log 或 runProcess 输出的内容会显示在这里。';
|
|
60
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { Text, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
3
|
+
import {palette} from './theme.js';
|
|
4
|
+
import {readableValue,readableError,readableTimeline} from './run-presentation.js';
|
|
5
|
+
|
|
6
|
+
export const runStatuses={pending:'待执行',unknown:'摘要已省略',running:'运行中',succeeded:'成功',failed:'失败',cancelled:'已取消',skipped:'已跳过',stopped:'已停止'};
|
|
7
|
+
const safe=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
8
|
+
export const mouseWheel=data=>{const match=/^\x1b\[<(\d+);\d+;\d+[Mm]$/.exec(data);return match&&(Number(match[1])&64)?(Number(match[1])&1?1:-1):0;};
|
|
9
|
+
export const mouseOn='\x1b[?1000h\x1b[?1006h';
|
|
10
|
+
export const mouseOff='\x1b[?1006l\x1b[?1000l';
|
|
11
|
+
|
|
12
|
+
export class RunViewer{
|
|
13
|
+
constructor({record,flow,workflows=[],catalog=[],terminal,onClose,onCancel,onReadLogs,requestRender}){
|
|
14
|
+
Object.assign(this,{terminal,onClose,onCancel,onReadLogs,requestRender,catalog});this.view=0;this.selected=0;this.offset=0;this.follow=true;this.allLogs=true;this.raw=false;this.confirmCancel=false;
|
|
15
|
+
this.planned=[];
|
|
16
|
+
const walk=(steps,prefix,ancestors)=>{
|
|
17
|
+
for(const step of steps??[]){
|
|
18
|
+
const callPath=`${prefix}/${step.id}`;this.planned.push({callPath,stepId:step.id,action:step.action,title:step.title,status:'pending'});
|
|
19
|
+
if(step.branch){walk(step.branch.then,callPath,ancestors);walk(step.branch.else,callPath,ancestors);}
|
|
20
|
+
if(step.parallel)walk(step.parallel.steps,callPath,ancestors);
|
|
21
|
+
if(step.workflow){const child=workflows.find(w=>`${w.id}.yaml`===step.workflow.replace(/^\.\//,''));if(child&&!ancestors.has(child.id))walk(child.steps,callPath,new Set([...ancestors,child.id]));}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
if(flow)walk(flow.steps,flow.id,new Set([flow.id]));
|
|
25
|
+
this.update(record);
|
|
26
|
+
}
|
|
27
|
+
invalidate(){}
|
|
28
|
+
update(record){
|
|
29
|
+
if(!record)return;
|
|
30
|
+
this.record=record;const nodes=new Map(this.planned.map(node=>[node.callPath,{...node,...(record.stepsTruncated&&record.status!=='running'?{status:'unknown'}:{})}]));
|
|
31
|
+
for(const event of [...(record.events??[]),...(record.stepSummaries??[])].sort((a,b)=>(a.seq??0)-(b.seq??0))){
|
|
32
|
+
if(!event.type.startsWith('step.')||!event.callPath)continue;
|
|
33
|
+
const node=nodes.get(event.callPath)??{callPath:event.callPath,stepId:event.stepId};
|
|
34
|
+
Object.assign(node,event,{status:event.type==='step.started'?'running':event.type.slice(5)});
|
|
35
|
+
node.title??=this.catalog.find(action=>action.id===node.action)?.title;nodes.set(event.callPath,node);
|
|
36
|
+
}
|
|
37
|
+
this.nodes=[...nodes.values()];this.selected=Math.max(0,Math.min(this.selected,this.nodes.length-1));
|
|
38
|
+
this.requestRender();
|
|
39
|
+
}
|
|
40
|
+
content(){
|
|
41
|
+
const node=this.nodes[this.selected],status=runStatuses[this.record.status]??this.record.status;
|
|
42
|
+
if(this.view===0)return [
|
|
43
|
+
`运行:${status}${this.record.reason?` · ${this.record.reason}`:''}`,
|
|
44
|
+
this.record.error?readableError(this.record.error):'',
|
|
45
|
+
...(this.record.warnings??[]).map(warning=>`警告:${warning.message}`),
|
|
46
|
+
this.record.output!==undefined?'\n最终输出\n'+readableValue(this.record.output):'',
|
|
47
|
+
'\n步骤 · Enter 查看详情',
|
|
48
|
+
...this.nodes.map((n,i)=>`${i===this.selected?'›':' '} ${runStatuses[n.status]??n.status} · ${n.title??n.callPath}${n.durationMs!==undefined?` · ${n.durationMs} ms`:''}${n.reason?' · '+n.reason:''}`),
|
|
49
|
+
!this.nodes.length?'此运行没有工作流步骤,可在日志页查看插件输出。':'',
|
|
50
|
+
].filter(Boolean).join('\n');
|
|
51
|
+
if(this.view===1){
|
|
52
|
+
const format=value=>this.raw?JSON.stringify(value??null,null,2):readableValue(value);
|
|
53
|
+
const error=node?node.error:this.record.error;
|
|
54
|
+
return [`步骤:${node?.callPath??'整个运行'}`,node?.action?'能力:'+node.action:'',error?'\n错误\n'+(this.raw?format(error):readableError(error)):'',
|
|
55
|
+
node?'\n输入\n'+format(node.input):'', '\n输出\n'+format(node?node.output:this.record.output),
|
|
56
|
+
!error&&(node?.reason||node?.message)?'\n说明:'+(node.reason??node.message):'',this.raw?'\n原始 JSON · 包含完整错误详情与调用栈':'\nj 查看原始 JSON 与完整诊断',
|
|
57
|
+
].filter(Boolean).join('\n');
|
|
58
|
+
}
|
|
59
|
+
return (this.record.eventsTruncated?'当前为近期日志摘要,按 p 分页查看完整落盘日志。\n\n':'')+readableTimeline(this.record.events,{callPath:this.allLogs?undefined:node?.callPath});
|
|
60
|
+
}
|
|
61
|
+
layout(width){
|
|
62
|
+
const height=Math.max(1,this.terminal.rows),pageSize=Math.max(1,height-4);
|
|
63
|
+
const lines=new Text(safe(this.content()),0,0).render(Math.max(1,width));
|
|
64
|
+
const maxOffset=Math.max(0,lines.length-pageSize);
|
|
65
|
+
this.offset=this.follow&&this.view===2?maxOffset:Math.max(0,Math.min(this.offset,maxOffset));
|
|
66
|
+
return {height,pageSize,lines,maxOffset};
|
|
67
|
+
}
|
|
68
|
+
render(width){
|
|
69
|
+
const {height,pageSize,lines}=this.layout(width),node=this.nodes[this.selected];
|
|
70
|
+
const body=lines.slice(this.offset,this.offset+pageSize);while(body.length<pageSize)body.push('');
|
|
71
|
+
const tabs=`${['1 总览','2 输入输出','3 日志'].map((s,i)=>this.view===i?palette.brand(`[${s}]`):palette.muted(s)).join(' ')} ${palette.muted('·')} ${palette.text(safe(runStatuses[this.record.status]??this.record.status))} ${palette.muted('· '+safe(this.record.workflowId??'运行'))}`;
|
|
72
|
+
const selection=`[ / ] 切换步骤:${node?.callPath??'暂无步骤'}${this.view===2?` · ${this.allLogs?'全部日志':'本步骤日志'} · ${this.follow?'跟随尾部':'暂停跟随'}`:''}`;
|
|
73
|
+
const footer=this.confirmCancel?'取消整个运行?y 确认 · 任意其他键返回':'q/Esc 返回 · x 取消 · p 完整日志 · 1/2/3 切换';
|
|
74
|
+
const shortcuts=width<60?'↑↓/滚轮 · [ ] 步骤 · j 原始数据':`↑↓/滚轮 翻页 · [ ] 步骤 · j 原始数据 · a 全部/本步骤 · End 跟随 · ${this.offset+1}–${Math.min(this.offset+pageSize,lines.length)}/${lines.length}`;
|
|
75
|
+
const styledBody=body.map(line=>line.startsWith('›')?palette.brand(line):line);
|
|
76
|
+
const styledFooter=palette.muted(safe(footer));
|
|
77
|
+
const screen=height===1?styledBody:height===2?[styledBody[0],styledFooter]:height===3?[tabs,styledBody[0],styledFooter]:height===4?[tabs,styledBody[0],styledFooter,palette.muted(shortcuts)]:[tabs,palette.muted(safe(selection)),...styledBody,styledFooter,palette.muted(shortcuts)];
|
|
78
|
+
return screen.map(line=>truncateToWidth(line,width));
|
|
79
|
+
}
|
|
80
|
+
handleInput(data){
|
|
81
|
+
if(data==='p'&&this.onReadLogs){this.onReadLogs();return;}
|
|
82
|
+
if(this.confirmCancel){this.confirmCancel=false;if(data==='y')this.onCancel();this.requestRender();return;}
|
|
83
|
+
if(['escape','q'].some(key=>matchesKey(data,key))){this.onClose();return;}
|
|
84
|
+
if(matchesKey(data,'enter')){if(this.view===0){this.view=1;this.offset=0;this.requestRender();}return;}
|
|
85
|
+
if(data==='j'){this.raw=!this.raw;this.view=1;this.offset=0;this.requestRender();return;}
|
|
86
|
+
if(data==='x'&&this.record.status==='running'){this.confirmCancel=true;this.requestRender();return;}
|
|
87
|
+
if(['1','2','3'].includes(data)){this.view=Number(data)-1;this.offset=0;this.follow=true;this.requestRender();return;}
|
|
88
|
+
if(data==='['||data===']'){
|
|
89
|
+
this.selected=Math.max(0,Math.min(this.nodes.length-1,this.selected+(data===']'?1:-1)));this.offset=0;
|
|
90
|
+
if(this.view===0){const {lines,pageSize,maxOffset}=this.layout(this.terminal.columns);const row=lines.findIndex(line=>line.startsWith('›'));this.offset=Math.min(maxOffset,Math.max(0,row-Math.floor(pageSize/2)));}
|
|
91
|
+
this.requestRender();return;
|
|
92
|
+
}
|
|
93
|
+
if(data==='a'){this.allLogs=!this.allLogs;this.offset=0;this.requestRender();return;}
|
|
94
|
+
if(data==='f'){this.follow=!this.follow;this.requestRender();return;}
|
|
95
|
+
const {pageSize,maxOffset}=this.layout(this.terminal.columns),wheel=mouseWheel(data);
|
|
96
|
+
if(wheel)this.offset+=wheel*3;
|
|
97
|
+
else if(matchesKey(data,'up'))this.offset--;
|
|
98
|
+
else if(matchesKey(data,'down'))this.offset++;
|
|
99
|
+
else if(matchesKey(data,'pageUp'))this.offset-=pageSize;
|
|
100
|
+
else if(matchesKey(data,'pageDown'))this.offset+=pageSize;
|
|
101
|
+
else if(matchesKey(data,'home'))this.offset=0;
|
|
102
|
+
else if(matchesKey(data,'end')){this.offset=maxOffset;this.follow=true;this.requestRender();return;}
|
|
103
|
+
else return;
|
|
104
|
+
this.follow=false;this.offset=Math.max(0,Math.min(maxOffset,this.offset));this.requestRender();
|
|
105
|
+
}
|
|
106
|
+
}
|