@yanglaofish/fishtools 0.4.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/kernel-generation.md +97 -0
- package/manual/docs/modules.md +57 -0
- package/manual/docs/plugin-development.md +120 -10
- package/manual/docs/releases.md +12 -0
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
- package/manual/docs/roadmap.md +14 -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/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-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-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 +42 -2
- package/manual/packages/core/README.md +44 -0
- package/manual/packages/core/package.json +38 -0
- package/manual/packages/core/src/application.js +242 -85
- 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/{node_modules/@mytools/core → manual/packages/fst-cli}/bin/fst.js +55 -23
- 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/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/plugin-tools.js +24 -5
- 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/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/contracts.js +30 -3
- 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/{node_modules/@mytools/core → manual/packages/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/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugins.js +6 -36
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +40 -7
- 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/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 -17
- package/node_modules/@mytools/core/src/application.js +242 -85
- 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 -67
- 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 -80
- 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/{manual/packages/core → node_modules/@mytools/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 +7 -2
- 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 +1398 -1199
- 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/node_modules/{minipass-flush/node_modules/minipass → tar}/node_modules/yallist/README.md +8 -7
- package/node_modules/tar/node_modules/yallist/package.json +68 -0
- package/node_modules/yallist/README.md +7 -8
- package/node_modules/yallist/package.json +12 -51
- package/package.json +131 -115
- package/manual/packages/core/src/host.js +0 -139
- package/node_modules/@mytools/core/src/host.js +0 -139
- 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/minipass-flush/node_modules/minipass/node_modules/yallist/package.json +0 -29
- /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/plugin-installations.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
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/schema-path.js +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/LICENSE.md +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/package.json +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/package.json +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/LICENSE +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/iterator.js +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/yallist.js +0 -0
|
@@ -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
|
+
}
|
|
@@ -3,7 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
|
|
6
|
-
export {builtinCommands as commands} from '
|
|
6
|
+
export {builtinCommands as commands} from '@mytools/fst-plugin/contracts';
|
|
7
7
|
|
|
8
8
|
export function parseCommand(text){
|
|
9
9
|
const args=[];let token='',quote='',started=false;
|
|
@@ -23,15 +23,23 @@ function fields(schema){
|
|
|
23
23
|
return entries.length?entries.map(([key,s])=>`${s.title??key} (${key}, ${s.type??'任意类型'}${schema.required?.includes(key)?', 必填':''})${s.description?`:${s.description}`:''}`).join(';'):'无';
|
|
24
24
|
}
|
|
25
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
|
+
|
|
26
33
|
export function pluginDetails(plugin,actions,commands){
|
|
27
34
|
return [
|
|
28
35
|
`# ${plugin.title??plugin.id}`,
|
|
29
36
|
plugin.description??'作者尚未提供用途说明。',
|
|
30
37
|
`标识:${plugin.id}\n\n版本:${plugin.version}\n\n来源:${plugin.removable?'自行安装(可移除)':'发行包内置 / 项目预置'}\n\n状态:${pluginStatus(plugin)}`,
|
|
31
38
|
...(plugin.runtime?.missing?.length?[`缺少服务:${plugin.runtime.missing.join(', ')}`]:[]),
|
|
39
|
+
`更新方式:${!plugin.runtime?.updatePolicy?'无运行时服务':plugin.runtime.updatePolicy==='automatic'?'可自动短暂停止并重启':'需要本次维护确认'}\n\n插件权限:${privilege(plugin,plugin)}`,
|
|
32
40
|
`声明平台:${plugin.platforms?.join(', ')??'作者未声明'}\n\n声明架构:${plugin.architectures?.join(', ')??'作者未声明'}`,
|
|
33
41
|
'## 可用于编排的能力',
|
|
34
|
-
...(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)}`):['此插件没有提供编排动作,可能提供 Web、共享服务或交互命令。']),
|
|
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、共享服务或交互命令。']),
|
|
35
43
|
'## 斜杠命令',commands.length?commands.map(c=>`- /${c.name}:${c.description??'未提供说明'}`).join('\n'):'没有注册斜杠命令。',
|
|
36
44
|
'## 插件配置',plugin.config?fields(plugin.config):'无需配置。',
|
|
37
45
|
'## 环境声明',
|
|
@@ -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');}
|
|
@@ -3,15 +3,15 @@ import { Text, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
|
3
3
|
import {palette} from './theme.js';
|
|
4
4
|
import {readableValue,readableError,readableTimeline} from './run-presentation.js';
|
|
5
5
|
|
|
6
|
-
export const runStatuses={pending:'待执行',running:'运行中',succeeded:'成功',failed:'失败',cancelled:'已取消',skipped:'已跳过',stopped:'已停止'};
|
|
6
|
+
export const runStatuses={pending:'待执行',unknown:'摘要已省略',running:'运行中',succeeded:'成功',failed:'失败',cancelled:'已取消',skipped:'已跳过',stopped:'已停止'};
|
|
7
7
|
const safe=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
8
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
9
|
export const mouseOn='\x1b[?1000h\x1b[?1006h';
|
|
10
10
|
export const mouseOff='\x1b[?1006l\x1b[?1000l';
|
|
11
11
|
|
|
12
12
|
export class RunViewer{
|
|
13
|
-
constructor({record,flow,workflows=[],catalog=[],terminal,onClose,onCancel,requestRender}){
|
|
14
|
-
Object.assign(this,{terminal,onClose,onCancel,requestRender,catalog});this.view=0;this.selected=0;this.offset=0;this.follow=true;this.allLogs=true;this.raw=false;this.confirmCancel=false;
|
|
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
15
|
this.planned=[];
|
|
16
16
|
const walk=(steps,prefix,ancestors)=>{
|
|
17
17
|
for(const step of steps??[]){
|
|
@@ -27,8 +27,8 @@ export class RunViewer{
|
|
|
27
27
|
invalidate(){}
|
|
28
28
|
update(record){
|
|
29
29
|
if(!record)return;
|
|
30
|
-
this.record=record;const nodes=new Map(this.planned.map(node=>[node.callPath,{...node}]));
|
|
31
|
-
for(const event of record.events??[]){
|
|
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
32
|
if(!event.type.startsWith('step.')||!event.callPath)continue;
|
|
33
33
|
const node=nodes.get(event.callPath)??{callPath:event.callPath,stepId:event.stepId};
|
|
34
34
|
Object.assign(node,event,{status:event.type==='step.started'?'running':event.type.slice(5)});
|
|
@@ -42,6 +42,7 @@ export class RunViewer{
|
|
|
42
42
|
if(this.view===0)return [
|
|
43
43
|
`运行:${status}${this.record.reason?` · ${this.record.reason}`:''}`,
|
|
44
44
|
this.record.error?readableError(this.record.error):'',
|
|
45
|
+
...(this.record.warnings??[]).map(warning=>`警告:${warning.message}`),
|
|
45
46
|
this.record.output!==undefined?'\n最终输出\n'+readableValue(this.record.output):'',
|
|
46
47
|
'\n步骤 · Enter 查看详情',
|
|
47
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:''}`),
|
|
@@ -55,7 +56,7 @@ export class RunViewer{
|
|
|
55
56
|
!error&&(node?.reason||node?.message)?'\n说明:'+(node.reason??node.message):'',this.raw?'\n原始 JSON · 包含完整错误详情与调用栈':'\nj 查看原始 JSON 与完整诊断',
|
|
56
57
|
].filter(Boolean).join('\n');
|
|
57
58
|
}
|
|
58
|
-
return readableTimeline(this.record.events,{callPath:this.allLogs?undefined:node?.callPath});
|
|
59
|
+
return (this.record.eventsTruncated?'当前为近期日志摘要,按 p 分页查看完整落盘日志。\n\n':'')+readableTimeline(this.record.events,{callPath:this.allLogs?undefined:node?.callPath});
|
|
59
60
|
}
|
|
60
61
|
layout(width){
|
|
61
62
|
const height=Math.max(1,this.terminal.rows),pageSize=Math.max(1,height-4);
|
|
@@ -69,7 +70,7 @@ export class RunViewer{
|
|
|
69
70
|
const body=lines.slice(this.offset,this.offset+pageSize);while(body.length<pageSize)body.push('');
|
|
70
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??'运行'))}`;
|
|
71
72
|
const selection=`[ / ] 切换步骤:${node?.callPath??'暂无步骤'}${this.view===2?` · ${this.allLogs?'全部日志':'本步骤日志'} · ${this.follow?'跟随尾部':'暂停跟随'}`:''}`;
|
|
72
|
-
const footer=this.confirmCancel?'取消整个运行?y 确认 · 任意其他键返回':'q/Esc 返回 · x 取消 · 1/2/3 切换';
|
|
73
|
+
const footer=this.confirmCancel?'取消整个运行?y 确认 · 任意其他键返回':'q/Esc 返回 · x 取消 · p 完整日志 · 1/2/3 切换';
|
|
73
74
|
const shortcuts=width<60?'↑↓/滚轮 · [ ] 步骤 · j 原始数据':`↑↓/滚轮 翻页 · [ ] 步骤 · j 原始数据 · a 全部/本步骤 · End 跟随 · ${this.offset+1}–${Math.min(this.offset+pageSize,lines.length)}/${lines.length}`;
|
|
74
75
|
const styledBody=body.map(line=>line.startsWith('›')?palette.brand(line):line);
|
|
75
76
|
const styledFooter=palette.muted(safe(footer));
|
|
@@ -77,6 +78,7 @@ export class RunViewer{
|
|
|
77
78
|
return screen.map(line=>truncateToWidth(line,width));
|
|
78
79
|
}
|
|
79
80
|
handleInput(data){
|
|
81
|
+
if(data==='p'&&this.onReadLogs){this.onReadLogs();return;}
|
|
80
82
|
if(this.confirmCancel){this.confirmCancel=false;if(data==='y')this.onCancel();this.requestRender();return;}
|
|
81
83
|
if(['escape','q'].some(key=>matchesKey(data,key))){this.onClose();return;}
|
|
82
84
|
if(matchesKey(data,'enter')){if(this.view===0){this.view=1;this.offset=0;this.requestRender();}return;}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validate } from '
|
|
1
|
+
import { validate } from '@mytools/sdk/schema';
|
|
2
2
|
import { fileReference } from './model.js';
|
|
3
3
|
|
|
4
4
|
export const needsJsonEditor=schema=>['oneOf','anyOf','allOf','if','$ref','patternProperties'].some(key=>schema?.[key]!==undefined);
|
|
@@ -9,10 +9,20 @@ import {gettingStarted,pluginStatus,pluginDetails} from './presentation.js';
|
|
|
9
9
|
import {readableValue,readableError} from './run-presentation.js';
|
|
10
10
|
import {commandMenu} from './navigation.js';
|
|
11
11
|
import {workflowMenu} from './workflow-menu.js';
|
|
12
|
+
import {resolvePrivilegeRequirement,checkStaticPrivilege} from '@mytools/fst-plugin/privileges';
|
|
12
13
|
|
|
13
14
|
export class TerminalSession{
|
|
14
15
|
constructor(app,ui,{baseDir=process.cwd(),webOptions={}}={}){this.app=app;this.ui=ui;this.baseDir=baseDir;this.webOptions=webOptions;this.flow=null;this.dirty=false;this.exiting=false;this.web=null;ui.activeRuns=()=>app.listRuns().filter(run=>run.status==='running').length;}
|
|
15
16
|
writable(){if(this.app.project.readOnly)throw new Error('这是只读运行包,请在完整工作区中编排、安装或打包');}
|
|
17
|
+
async withMaintenance(operation,options={}){
|
|
18
|
+
try{return await operation(options);}
|
|
19
|
+
catch(error){
|
|
20
|
+
if(error.code!=='MAINTENANCE_REQUIRED')throw error;
|
|
21
|
+
const names=error.details?.plugins?.join('、')??'相关插件';
|
|
22
|
+
if(!await this.ui.confirm(`本次修改会短暂停止并重启 ${names}。现在继续吗?`))return null;
|
|
23
|
+
return operation({...options,allowMaintenance:true});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
16
26
|
current(){if(!this.flow)throw new Error('先用 /workflows 打开或新建工作流');return this.flow;}
|
|
17
27
|
commands(){return commandMenu(this.app.listPlugins?.()??[],this.app.listCommands());}
|
|
18
28
|
async discard(){return !this.dirty||await this.ui.confirm('当前修改未保存,是否放弃修改?');}
|
|
@@ -71,7 +81,7 @@ export class TerminalSession{
|
|
|
71
81
|
'## 全部操作',this.commands().map(c=>`- /${c.name}:${c.description}`).join('\n'),
|
|
72
82
|
].join('\n\n'));return;
|
|
73
83
|
case 'docs':{
|
|
74
|
-
const {listDocumentation,readDocumentation}=await import('
|
|
84
|
+
const {listDocumentation,readDocumentation}=await import('@mytools/fst-devtools/documentation');
|
|
75
85
|
const topics=await listDocumentation();
|
|
76
86
|
const topic=await this.ui.select('离线文档', [...topics.map(item=>({value:item.id,label:item.title})),{value:'back',label:'返回工作台'}]);
|
|
77
87
|
if(topic!=='back'){const document=await readDocumentation(topic);await this.ui.viewMarkdown(document.title,document.text);}
|
|
@@ -110,7 +120,12 @@ export class TerminalSession{
|
|
|
110
120
|
if(args.length>1)throw new Error(`用法:/${command.name} [@参数.json]`);
|
|
111
121
|
const input=args.length?await readRunInput(args[0],this.baseDir):command.input?await editValue(this.ui,command.input,{}, {baseDir:this.baseDir,required:true,label:command.description}):{};
|
|
112
122
|
const ui=Object.fromEntries(['input','select','show','confirm'].map(name=>[name,this.ui[name].bind(this.ui)]));
|
|
113
|
-
const
|
|
123
|
+
const definition=this.app.host.definitions.get(command.plugin);
|
|
124
|
+
const requirement=resolvePrivilegeRequirement(definition,command.action);
|
|
125
|
+
const unknown=requirement.declared&&checkStaticPrivilege(requirement).status==='unknown';
|
|
126
|
+
const allowUnknownPrivileges=unknown?await this.ui.confirm(`/${command.name} 的权限需要运行时核查。若检查结果仍无法确认,是否允许本次继续?\n${requirement.reason??''}\n${requirement.remediation??''}`):false;
|
|
127
|
+
if(unknown&&!allowUnknownPrivileges)return;
|
|
128
|
+
const run=await this.app.startCommand(command.name,input,{ui,allowUnknownPrivileges});
|
|
114
129
|
if(this.ui.trackRun){
|
|
115
130
|
this.ui.clearMessages?.();this.ui.trackRun({app:this.app,runId:run.id,title:command.description??command.name,input});
|
|
116
131
|
if(command.action){void run.done.catch(error=>{if(!this.ui.closed)this.ui.show(`运行记录保存失败:${error.message}`);});return;}
|
|
@@ -127,19 +142,22 @@ export class TerminalSession{
|
|
|
127
142
|
const id=selectedId??await this.ui.select('选择要配置的插件',plugins.map(p=>({value:p.id,label:p.title??p.id})));
|
|
128
143
|
const plugin=plugins.find(p=>p.id===id);
|
|
129
144
|
const value=await editValue(this.ui,plugin.config,this.app.project.config?.[id]??{}, {baseDir:this.baseDir,required:true,label:plugin.title??id});
|
|
130
|
-
await this.app.configurePlugin(id,value)
|
|
145
|
+
const result=await this.withMaintenance(options=>this.app.configurePlugin(id,value,options));
|
|
146
|
+
this.ui.show(result===null?'已取消修改。':`插件配置已保存到当前工作区。原项目配置备份:${result.backup}`);
|
|
131
147
|
}
|
|
132
148
|
async logs(){
|
|
133
149
|
const statuses=runStatuses;
|
|
134
150
|
while(true){
|
|
135
151
|
const history=(await this.app.listRunHistory()).sort((a,b)=>(b.status==='running')-(a.status==='running'));
|
|
136
|
-
if(!history.length)
|
|
152
|
+
if(!history.length)this.ui.show('还没有执行记录。运行工作流后,可在 /logs 回看。');
|
|
137
153
|
const titles=new Map((await this.app.listWorkflows()).map(flow=>[flow.id,flow.title??flow.id]));
|
|
138
154
|
const choice=await this.ui.select('执行记录 · 选择一次执行,查看步骤、输入输出与日志',[
|
|
139
155
|
...history.map(r=>({value:`run:${r.id}`,label:`${statuses[r.status]??r.status} · ${titles.get(r.workflowId)??r.workflowId}`,description:`${new Date(r.startedAt).toLocaleString()} · ${r.id.slice(0,8)}`})),
|
|
156
|
+
{value:'usage',label:'查看日志体积',description:'按需统计,不清理文件'},
|
|
140
157
|
{value:'back',label:'返回'},
|
|
141
158
|
]);
|
|
142
159
|
if(choice==='back')return;
|
|
160
|
+
if(choice==='usage'){await this.logUsage();continue;}
|
|
143
161
|
const id=choice.slice(4),record=history.find(r=>r.id===id);
|
|
144
162
|
try{
|
|
145
163
|
if(this.ui.observeRun)await this.openRun(record,{initialView:2});
|
|
@@ -285,9 +303,12 @@ export class TerminalSession{
|
|
|
285
303
|
if(choice==='saved')flow=saved;
|
|
286
304
|
else await this.execute('/save');
|
|
287
305
|
}
|
|
288
|
-
await this.app.checkWorkflow(flow.id);
|
|
306
|
+
const report=await this.app.checkWorkflow(flow.id);
|
|
307
|
+
const unknown=report.privileges.filter(item=>item.requirement.declared&&item.static.status==='unknown');
|
|
308
|
+
const allowUnknownPrivileges=unknown.length?await this.ui.confirm(`这些动作需要在运行时核查权限:${unknown.map(item=>item.action).join('、')}。若仍无法确认,是否允许本次继续?`):false;
|
|
309
|
+
if(unknown.length&&!allowUnknownPrivileges)return false;
|
|
289
310
|
const input=reference?await readRunInput(reference,this.baseDir):(needsJsonEditor(flow.inputs)||Object.keys(flow.inputs?.properties??{}).length)?await editValue(this.ui,flow.inputs,{}, {baseDir:this.baseDir,label:'本次运行参数',required:true}):{};
|
|
290
|
-
const run=await this.app.startRun(flow.id,input);
|
|
311
|
+
const run=await this.app.startRun(flow.id,input,{allowUnknownPrivileges});
|
|
291
312
|
if(this.ui.trackRun){
|
|
292
313
|
this.ui.clearMessages?.();this.ui.trackRun({app:this.app,runId:run.id,title:flow.title??flow.id,input});
|
|
293
314
|
void run.done.catch(error=>{if(!this.ui.closed)this.ui.show(`运行记录保存失败:${error.message}`);});return true;
|
|
@@ -319,20 +340,116 @@ export class TerminalSession{
|
|
|
319
340
|
const keep=this.app.logSettings().keepRuns;
|
|
320
341
|
const choice=await this.ui.select('Fishtools 配置',[
|
|
321
342
|
{value:'info',label:'查看工作区信息'},
|
|
343
|
+
{value:'network',label:'代理设置',description:'分别设置整个工作区、插件或某个动作'},
|
|
344
|
+
{value:'usage',label:'查看日志体积'},
|
|
322
345
|
...(!this.app.project.readOnly?[{value:'logs',label:'日志保留次数',description:keep===null?'不自动清理':`最近 ${keep} 次`}]:[]),
|
|
323
346
|
{value:'back',label:'返回主面板'},
|
|
324
347
|
]);
|
|
325
348
|
if(choice==='back')return;
|
|
326
349
|
if(choice==='info')this.ui.show(`工作区:${this.app.projectDir}\n日志目录:${this.app.outputDir}\n模式:${this.app.project.readOnly?'只读运行包':'可编辑工作区'}`);
|
|
350
|
+
else if(choice==='network')await this.networkSettings();
|
|
351
|
+
else if(choice==='usage')await this.logUsage();
|
|
327
352
|
else await this.logSettings();
|
|
328
353
|
}
|
|
329
354
|
}
|
|
355
|
+
async chooseNetworkRule(label){
|
|
356
|
+
const profiles=Object.keys(this.app.networkPolicy().profiles??{});
|
|
357
|
+
const value=await this.ui.select(label,[
|
|
358
|
+
{value:'inherit',label:'沿用系统代理环境'},
|
|
359
|
+
{value:'direct',label:'直连,不使用代理'},
|
|
360
|
+
...profiles.map(name=>({value:`profile:${name}`,label:`使用代理配置:${name}`})),
|
|
361
|
+
{value:'back',label:'返回'},
|
|
362
|
+
]);
|
|
363
|
+
if(value==='back')return null;
|
|
364
|
+
return value.startsWith('profile:')?{mode:'profile',profile:value.slice(8)}:{mode:value};
|
|
365
|
+
}
|
|
366
|
+
async networkProfiles(){
|
|
367
|
+
while(true){
|
|
368
|
+
const policy=this.app.networkPolicy(),profiles=policy.profiles??{};
|
|
369
|
+
const choice=await this.ui.select('代理配置(地址可用环境变量引用,凭据请勿写入项目)',[
|
|
370
|
+
...Object.keys(profiles).map(name=>({value:`edit:${name}`,label:`修改 ${name}`})),
|
|
371
|
+
...(!this.app.project.readOnly?[{value:'create',label:'+ 新建代理配置'},...Object.keys(profiles).map(name=>({value:`delete:${name}`,label:`删除 ${name}`}))]:[]),
|
|
372
|
+
{value:'back',label:'返回'},
|
|
373
|
+
]);
|
|
374
|
+
if(choice==='back')return;
|
|
375
|
+
this.writable();
|
|
376
|
+
if(choice.startsWith('delete:')){
|
|
377
|
+
const name=choice.slice(7);
|
|
378
|
+
if(await this.ui.confirm(`删除代理配置 ${name}?引用它的规则需要先改为其他模式。`)){
|
|
379
|
+
delete profiles[name];const saved=await this.app.configureNetworkPolicy({...policy,profiles});
|
|
380
|
+
this.ui.show(`代理配置 ${name} 已删除。原项目配置备份:${saved.backup}`);
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const name=choice==='create'?(await this.ui.input('代理配置名称(字母开头)',{files:false})).trim():choice.slice(5);
|
|
385
|
+
if(!name)throw new Error('代理配置名称不能为空');
|
|
386
|
+
const current=profiles[name]??{},next={};
|
|
387
|
+
for(const [field,label] of [['http','HTTP 请求'],['https','HTTPS 请求'],['noProxy','不走代理的目标(例如 localhost,127.0.0.1)']]){
|
|
388
|
+
const direct=field==='noProxy'?'noProxy':`${field}Url`,reference=`${field}Env`;
|
|
389
|
+
const source=await this.ui.select(`${name} · ${label}`,[{value:'none',label:'不单独设置'},{value:'url',label:field==='noProxy'?'直接填写目标列表':'直接填写代理地址(不能带凭据)'},{value:'env',label:'读取环境变量(可用于有凭据的代理)'}]);
|
|
390
|
+
if(source!=='none'){
|
|
391
|
+
const key=source==='env'?reference:direct;
|
|
392
|
+
next[key]=(await this.ui.input(source==='env'?'环境变量名称':label,{value:current[key]??'',files:false})).trim();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const saved=await this.app.configureNetworkPolicy({...policy,profiles:{...profiles,[name]:next}});
|
|
396
|
+
this.ui.show(`代理配置 ${name} 已保存。原项目配置备份:${saved.backup}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async networkSettings(){
|
|
400
|
+
while(true){
|
|
401
|
+
const policy=this.app.networkPolicy(),summary=JSON.stringify(policy,null,2);
|
|
402
|
+
const choice=await this.ui.select('代理设置 · 动作规则优先于插件,插件优先于工作区',[
|
|
403
|
+
{value:'view',label:'查看当前规则'},
|
|
404
|
+
...(!this.app.project.readOnly?[{value:'default',label:'设置工作区默认模式'},{value:'plugin',label:'设置某个插件的默认模式'},{value:'action',label:'设置某个动作的模式'}]:[]),
|
|
405
|
+
{value:'profiles',label:'管理代理配置'},
|
|
406
|
+
{value:'back',label:'返回'},
|
|
407
|
+
]);
|
|
408
|
+
if(choice==='back')return;
|
|
409
|
+
if(choice==='view'){
|
|
410
|
+
const report=this.app.networkReport();
|
|
411
|
+
const mode=item=>item.mode==='profile'?`代理配置 ${item.profile}`:item.mode==='direct'?'直连':'继承系统环境';
|
|
412
|
+
const effective=report.entries.map(item=>`${item.id}:${mode(item)}(来源:${{action:'动作',plugin:'插件',workspace:'工作区',system:'系统默认'}[item.source]})`);
|
|
413
|
+
const stale=[...report.stalePlugins.map(id=>`插件 ${id}`),...report.staleActions.map(id=>`动作 ${id}`)];
|
|
414
|
+
this.ui.show(`已保存规则:\n${summary}\n\n最终生效模式:\n${effective.join('\n')||'暂无已安装插件'}\n\n失效引用:\n${stale.join('\n')||'无'}`);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if(choice==='profiles'){await this.networkProfiles();continue;}
|
|
418
|
+
const next=structuredClone(policy);
|
|
419
|
+
if(choice==='default'){
|
|
420
|
+
const rule=await this.chooseNetworkRule('工作区默认模式');if(!rule)continue;
|
|
421
|
+
next.default=rule;
|
|
422
|
+
}else{
|
|
423
|
+
const installed=this.app.listPlugins();
|
|
424
|
+
if(!installed.length){this.ui.show('当前没有已安装插件。');continue;}
|
|
425
|
+
const id=await this.ui.select('选择插件',installed.map(p=>({value:p.id,label:p.title??p.id})));
|
|
426
|
+
next.plugins??={};next.plugins[id]??={};
|
|
427
|
+
if(choice==='plugin'){
|
|
428
|
+
const rule=await this.chooseNetworkRule(`${id} 默认模式`);if(!rule)continue;
|
|
429
|
+
next.plugins[id].default=rule;
|
|
430
|
+
}else{
|
|
431
|
+
const actions=this.app.catalog().filter(a=>a.plugin===id);
|
|
432
|
+
if(!actions.length){this.ui.show('此插件没有可编排动作。');continue;}
|
|
433
|
+
const action=await this.ui.select('选择动作',actions.map(a=>({value:a.id.slice(id.length+1),label:a.title??a.id})));
|
|
434
|
+
const rule=await this.chooseNetworkRule(`${id}:${action} 模式`);if(!rule)continue;
|
|
435
|
+
next.plugins[id].actions??={};next.plugins[id].actions[action]=rule;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const saved=await this.app.configureNetworkPolicy(next);this.ui.show(`代理规则已保存。原项目配置备份:${saved.backup}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async logUsage(){
|
|
442
|
+
const usage=await this.app.logUsage(),keep=this.app.logSettings().keepRuns;
|
|
443
|
+
const units=['B','KiB','MiB','GiB','TiB'];let size=usage.bytes,index=0;
|
|
444
|
+
while(size>=1024&&index<units.length-1){size/=1024;index++;}
|
|
445
|
+
this.ui.show(`日志目录:${usage.directory}\n日志文件体积:${index?size.toFixed(2):size} ${units[index]}(${usage.bytes} 字节,${usage.files} 个文件)${usage.incomplete?',部分路径无法读取,统计不完整':''}\n统计该目录中所有运行的 run.md / events.jsonl / result.json,包含进行中的运行;不含业务文件,不跟随链接。共享目录可能包含其他项目。\n这是文件长度快照,运行中可能继续增长;并非磁盘实际分配空间。\n本项目保留策略:${keep===null?'不自动清理':`最近 ${keep} 次已结束运行`};没有单次运行体积上限。`);
|
|
446
|
+
}
|
|
330
447
|
async logSettings(){
|
|
331
448
|
const keep=this.app.logSettings().keepRuns;
|
|
332
449
|
const mode=await this.ui.select('保留策略(整个项目合计,仅清理日志,保留业务文件)',[{value:'count',label:'仅保留最近 N 次已结束运行'},{value:'all',label:'不自动清理'},{value:'back',label:'返回'}]);
|
|
333
450
|
if(mode==='back')return;
|
|
334
451
|
const count=mode==='all'?null:Number(await this.ui.input('保留次数(例如 10)',{value:String(keep??10),files:false}));
|
|
335
|
-
await this.app.configureLogs(count);this.ui.show('
|
|
452
|
+
const saved=await this.app.configureLogs(count);this.ui.show(`日志保留设置已保存。原项目配置备份:${saved.backup}${saved.warning?`\n${saved.warning.message}`:''}`);
|
|
336
453
|
}
|
|
337
454
|
async openRun(entry,{initialView}={}){
|
|
338
455
|
const record=this.app.getRun(entry.id)??await this.app.readRun(entry.id),workflows=await this.app.listWorkflows();
|
|
@@ -358,7 +475,8 @@ export class TerminalSession{
|
|
|
358
475
|
const text=await this.ui.input(action==='npm'?'npm 包名(支持 @scope/name)':'输入 @ 并选择插件目录或 .tgz',{files:action==='local'});
|
|
359
476
|
const source=action==='local'?fileReference(text,this.baseDir):text.trim();if(!source)throw new Error('插件来源不能为空');
|
|
360
477
|
if(!await this.ui.confirm(`安装并加载可信插件:${source}?`))return;
|
|
361
|
-
const done=this.ui.busy('正在安装插件…');try{await this.app.installPlugin(source);}finally{done();}
|
|
478
|
+
const done=this.ui.busy('正在安装插件…');let result;try{result=await this.withMaintenance(options=>this.app.installPlugin(source,options));}finally{done();}
|
|
479
|
+
if(result===null){this.ui.show('已取消安装。');continue;}
|
|
362
480
|
}
|
|
363
481
|
this.ui.show('插件列表已更新。');
|
|
364
482
|
}
|
|
@@ -383,7 +501,17 @@ export class TerminalSession{
|
|
|
383
501
|
else if(choice==='configure')await this.configure(id);
|
|
384
502
|
else if(choice.startsWith('command:'))await this.pluginCommand(commands.find(c=>c.name===choice.slice(8)),[]);
|
|
385
503
|
else if(choice==='remove'){
|
|
386
|
-
this.writable();if(await this.ui.confirm(`移除 ${id}
|
|
504
|
+
this.writable();if(await this.ui.confirm(`移除 ${id}?`)){
|
|
505
|
+
let removed;
|
|
506
|
+
try{removed=await this.withMaintenance(options=>this.app.removePlugin(id,options));}
|
|
507
|
+
catch(error){
|
|
508
|
+
if(error.code!=='PLUGIN_IN_USE'||!error.details?.workflows?.length)throw error;
|
|
509
|
+
const names=error.details.workflows.map(w=>w.title??w.id).join('、');
|
|
510
|
+
if(!await this.ui.confirm(`以下工作流仍使用 ${id}:${names}。移除后这些工作流将无法运行,仍要移除吗?`))continue;
|
|
511
|
+
removed=await this.withMaintenance(options=>this.app.removePlugin(id,options),{force:true});
|
|
512
|
+
}
|
|
513
|
+
if(removed){this.ui.show('插件已移除。');return;}
|
|
514
|
+
}
|
|
387
515
|
}
|
|
388
516
|
}
|
|
389
517
|
}
|
|
@@ -6,7 +6,7 @@ import { MarkdownLogViewer } from './log-viewer.js';
|
|
|
6
6
|
import { RunViewer, mouseOn, mouseOff, mouseWheel } from './run-viewer.js';
|
|
7
7
|
import {RunFeed} from './run-feed.js';
|
|
8
8
|
import { logo } from './presentation.js';
|
|
9
|
-
import { VERSION } from '
|
|
9
|
+
import { VERSION } from '@mytools/core/version';
|
|
10
10
|
import {cleanText as safe,palette,menuTheme as theme,fitLine,shortenPath} from './theme.js';
|
|
11
11
|
|
|
12
12
|
export const cancelled=()=>Object.assign(new Error('已返回主菜单'),{code:'PROMPT_CANCELLED'});
|
|
@@ -191,13 +191,13 @@ export class TerminalView{
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
async observeRun({app,runId,record,flow,workflows=[],initialView}){
|
|
194
|
-
const previousPending=this.pending??null,children=this.tui.children;let overlay,unsubscribe;
|
|
194
|
+
const previousPending=this.pending??null,children=this.tui.children;let overlay,unsubscribe,readPages=false;
|
|
195
195
|
const initial=app.getRun(runId)??record??await app.readRun(runId);
|
|
196
196
|
if(!initial)throw new Error('这次运行记录已不存在');
|
|
197
197
|
try{
|
|
198
198
|
await new Promise((resolve,reject)=>{
|
|
199
199
|
this.pending={resolve,reject};
|
|
200
|
-
this.runViewer=new RunViewer({record:initial,flow:initial.status==='running'?flow:undefined,workflows,catalog:app.catalog?.()??[],terminal:this.terminal,onClose:resolve,onCancel:()=>app.cancelRun(runId),requestRender:()=>this.tui.requestRender()});
|
|
200
|
+
this.runViewer=new RunViewer({record:initial,flow:initial.status==='running'?flow:undefined,workflows,catalog:app.catalog?.()??[],terminal:this.terminal,onClose:resolve,onCancel:()=>app.cancelRun(runId),onReadLogs:app.readLogPage?()=>{readPages=true;resolve();}:undefined,requestRender:()=>this.tui.requestRender()});
|
|
201
201
|
Object.assign(this.runViewer,this.runViews.get(runId)??{});
|
|
202
202
|
if(initialView!==undefined){this.runViewer.view=initialView;this.runViewer.offset=0;}
|
|
203
203
|
unsubscribe=app.subscribe(event=>{if(event.runId===runId)this.runViewer?.update(app.getRun(runId));});
|
|
@@ -209,6 +209,23 @@ export class TerminalView{
|
|
|
209
209
|
if(this.runViews.size>30)this.runViews.delete(this.runViews.keys().next().value);
|
|
210
210
|
unsubscribe?.();this.runViewer=null;this.tui.children=children;this.pending=previousPending;overlay?.hide();this.terminal.write(mouseOff);this.tui.requestRender(true);
|
|
211
211
|
}
|
|
212
|
+
if(readPages)await this.viewLogPages(app,runId);
|
|
213
|
+
}
|
|
214
|
+
async viewLogPages(app,id){
|
|
215
|
+
let cursor=0;const previous=[];
|
|
216
|
+
for(;;){
|
|
217
|
+
const page=await app.readLogPage(id,{cursor,limit:65536});
|
|
218
|
+
await this.viewMarkdown(`完整日志 · ${cursor}–${page.nextCursor} / ${page.totalBytes} 字节`,page.text||'本页暂无内容');
|
|
219
|
+
const choice=await this.select('完整日志分页',[
|
|
220
|
+
...(page.hasMore?[{value:'next',label:'下一页'}]:[]),
|
|
221
|
+
...(previous.length?[{value:'previous',label:'上一页'}]:[]),
|
|
222
|
+
{value:'refresh',label:'刷新本页'},{value:'first',label:'回到首页'},{value:'back',label:'返回'},
|
|
223
|
+
]);
|
|
224
|
+
if(choice==='back')return;
|
|
225
|
+
if(choice==='next'){previous.push(cursor);if(previous.length>1000)previous.shift();cursor=page.nextCursor;}
|
|
226
|
+
if(choice==='previous')cursor=previous.pop()??0;
|
|
227
|
+
if(choice==='first'){cursor=0;previous.length=0;}
|
|
228
|
+
}
|
|
212
229
|
}
|
|
213
230
|
async ask(message,component){
|
|
214
231
|
fit(component);
|
package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js
RENAMED
|
@@ -4,6 +4,7 @@ export async function workflowMenu(session){
|
|
|
4
4
|
const workflows=await app.listWorkflows();
|
|
5
5
|
const choice=await ui.select('工作流 · 选择后编辑或执行',[
|
|
6
6
|
...workflows.map(flow=>({value:`workflow:${flow.id}`,label:flow.title??flow.id,description:`${flow.id} · ${flow.steps.length} 步`})),
|
|
7
|
+
...(!app.project.readOnly?workflows.map(flow=>({value:`duplicate:${flow.id}`,label:`复制 ${flow.title??flow.id} 为新工作流`,description:`从已保存版本 ${flow.id} 创建副本` })):[]),
|
|
7
8
|
...(!app.project.readOnly?[{value:'new',label:'+ 新建工作流'}]:[]),
|
|
8
9
|
...(session.dirty?[{value:'draft',label:`继续编辑草稿:${session.flow.title}`}]:[]),
|
|
9
10
|
{value:'back',label:'返回主面板'},
|
|
@@ -11,7 +12,11 @@ export async function workflowMenu(session){
|
|
|
11
12
|
if(choice==='back')return;
|
|
12
13
|
try{
|
|
13
14
|
const id=choice.startsWith('workflow:')?choice.slice(9):null;
|
|
14
|
-
if(choice
|
|
15
|
+
if(choice.startsWith('duplicate:')){
|
|
16
|
+
if(!await session.discard())continue;
|
|
17
|
+
await duplicateIntoSession(session,workflows.find(flow=>flow.id===choice.slice(10)));
|
|
18
|
+
}
|
|
19
|
+
else if(choice==='new'){if(await session.execute('/new')===false)continue;}
|
|
15
20
|
else if(choice!=='draft'&&session.flow?.id!==id){
|
|
16
21
|
if(!await session.discard())continue;
|
|
17
22
|
session.flow=structuredClone(workflows.find(flow=>flow.id===id));session.dirty=false;
|
|
@@ -30,6 +35,7 @@ async function editWorkflow(session){
|
|
|
30
35
|
...(!app.project.readOnly?[
|
|
31
36
|
{value:'add',label:'添加步骤 / 控制流'}, {value:'steps',label:'编辑步骤、参数与顺序'},
|
|
32
37
|
{value:'outputs',label:'设置最终输出'},{value:'save',label:'保存工作流'},
|
|
38
|
+
{value:'duplicate',label:'复制为新工作流(包含当前未保存修改)'},
|
|
33
39
|
{value:'delete',label:'删除工作流(保留可恢复备份)'},
|
|
34
40
|
]:[{value:'steps',label:'查看步骤'}]),
|
|
35
41
|
{value:'back',label:'返回工作流列表(保留草稿)'},
|
|
@@ -44,7 +50,16 @@ async function editWorkflow(session){
|
|
|
44
50
|
}
|
|
45
51
|
if(choice==='run'){
|
|
46
52
|
if(await session.startRun([],{draft:true}))return 'running';
|
|
47
|
-
}else
|
|
53
|
+
}else if(choice==='duplicate')await duplicateIntoSession(session,flow);
|
|
54
|
+
else await session.execute('/'+choice);
|
|
48
55
|
}catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
|
|
49
56
|
}
|
|
50
57
|
}
|
|
58
|
+
|
|
59
|
+
async function duplicateIntoSession(session,source){
|
|
60
|
+
const title=(await session.ui.input('新工作流名称',{value:`${source.title??source.id} 副本`,files:false})).trim();
|
|
61
|
+
if(!title)throw new Error('名称不能为空');
|
|
62
|
+
const {definition}=await session.app.duplicateWorkflow(source,{title});
|
|
63
|
+
session.flow=structuredClone(definition);session.dirty=false;
|
|
64
|
+
session.ui.show(`已创建副本:${definition.title}(${definition.id})。原工作流 ${source.id} 未修改。可在父工作流中依次加入原版和副本两个子流程步骤,比较输出和耗时。`);
|
|
65
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mytools contributors
|
|
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.
|