@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,52 @@
|
|
|
1
|
+
import { validate } from '@mytools/sdk/schema';
|
|
2
|
+
import { fileReference } from './model.js';
|
|
3
|
+
|
|
4
|
+
export const needsJsonEditor=schema=>['oneOf','anyOf','allOf','if','$ref','patternProperties'].some(key=>schema?.[key]!==undefined);
|
|
5
|
+
|
|
6
|
+
// 值只在整个编辑过程完成后返回,Escape 不会留下半个修改。
|
|
7
|
+
export async function editValue(ui,schema={},value,{baseDir,required=false,label=schema.title??'值'}={}){
|
|
8
|
+
if(value===undefined&&schema.default!==undefined)value=structuredClone(schema.default);
|
|
9
|
+
if(schema.enum&&!needsJsonEditor(schema)){
|
|
10
|
+
const index=await ui.select(label,schema.enum.map((v,i)=>({value:String(i),label:JSON.stringify(v)})));
|
|
11
|
+
return schema.enum[Number(index)];
|
|
12
|
+
}
|
|
13
|
+
if(schema.type==='boolean'&&!needsJsonEditor(schema))return (await ui.select(label,[{value:'true',label:'是 / true'},{value:'false',label:'否 / false'}]))==='true';
|
|
14
|
+
if(schema.type==='object'&&schema.properties&&!needsJsonEditor(schema)){
|
|
15
|
+
const result=structuredClone(value??{}),props=Object.entries(schema.properties);
|
|
16
|
+
while(true){
|
|
17
|
+
const key=await ui.select(`${label} · 选择字段`,[
|
|
18
|
+
...props.map(([key,s])=>({value:`field:${key}`,label:`${s.title??key}${schema.required?.includes(key)?' *':''}`,description:JSON.stringify(result[key])??'未填写'})),
|
|
19
|
+
{value:'__done',label:'完成填写'},
|
|
20
|
+
]);
|
|
21
|
+
if(key==='__done'){
|
|
22
|
+
try{return validate(schema,result,label);}catch(e){ui.show(e.message);continue;}
|
|
23
|
+
}
|
|
24
|
+
const field=key.slice(6);
|
|
25
|
+
const next=await editValue(ui,schema.properties[field],result[field],{baseDir,required:schema.required?.includes(field),label:schema.properties[field].title??field});
|
|
26
|
+
if(next===undefined)delete result[field];else result[field]=next;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if(schema.type==='array'&&schema.items&&!Array.isArray(schema.items)&&!needsJsonEditor(schema)){
|
|
30
|
+
const result=structuredClone(value??[]);
|
|
31
|
+
while(true){
|
|
32
|
+
const choice=await ui.select(label,[...result.map((v,i)=>({value:String(i),label:`${i+1}. ${JSON.stringify(v)}`})),{value:'add',label:'添加一项'},{value:'done',label:'完成列表'}]);
|
|
33
|
+
if(choice==='done'){try{return validate(schema,result,label);}catch(e){ui.show(e.message);continue;}}
|
|
34
|
+
if(choice==='add'){const item=await editValue(ui,schema.items,undefined,{baseDir,required:true,label:'新项'});result.push(item);}
|
|
35
|
+
else{
|
|
36
|
+
const index=Number(choice),action=await ui.select('修改列表项',[{value:'edit',label:'编辑'},{value:'remove',label:'移除'}]);
|
|
37
|
+
if(action==='remove')result.splice(index,1);else result[index]=await editValue(ui,schema.items,result[index],{baseDir,required:true,label:`第 ${index+1} 项`});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
while(true){
|
|
42
|
+
const string=schema.type==='string';
|
|
43
|
+
const initial=value===undefined?'':string?String(value):JSON.stringify(value);
|
|
44
|
+
const raw=await ui.input(`${label}${required?'(必填)':''}${string?' · @文件引用路径,@@表示字面量@':' · 输入 JSON 值'}${schema.description?'\n'+schema.description:''}`,{value:string&&initial.startsWith('@')?'@'+initial:initial,files:string});
|
|
45
|
+
try{
|
|
46
|
+
if(!raw&&!required&&schema.type!=='string')return undefined;
|
|
47
|
+
if(!raw&&required&&schema.type!=='string')throw new Error('请填写必填值');
|
|
48
|
+
const result=string?fileReference(raw,baseDir):JSON.parse(raw);
|
|
49
|
+
return validate(schema,result,label);
|
|
50
|
+
}catch(e){ui.show(`输入无效:${e.message}`);}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {stripVTControlCharacters} from 'node:util';
|
|
2
|
+
import {truncateToWidth,visibleWidth} from '@mariozechner/pi-tui';
|
|
3
|
+
|
|
4
|
+
// 先清理外部文本,再由受控主题函数添加颜色;不接受插件传入的 ANSI。
|
|
5
|
+
export const cleanText=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
6
|
+
const color=(rgb,text)=>Object.hasOwn(process.env,'NO_COLOR')?text:`\x1b[38;2;${rgb}m${text}\x1b[39m`;
|
|
7
|
+
export const palette={
|
|
8
|
+
brand:text=>color('78;201;176',text),
|
|
9
|
+
error:text=>color('244;117;117',text),
|
|
10
|
+
text:text=>color('229;233;237',text),
|
|
11
|
+
muted:text=>color('139;149;158',text),
|
|
12
|
+
border:text=>color('74;101;99',text),
|
|
13
|
+
};
|
|
14
|
+
export const menuTheme={selectedPrefix:palette.brand,selectedText:palette.brand,description:palette.muted,scrollInfo:palette.muted,noMatch:palette.muted};
|
|
15
|
+
export const fitLine=(text,width)=>truncateToWidth(text,Math.max(1,width),'…');
|
|
16
|
+
|
|
17
|
+
export function shortenPath(value,width){
|
|
18
|
+
if(visibleWidth(value)<=width)return value;
|
|
19
|
+
// 中间省略保留盘符与项目名;完整目录仍在帮助页中展示。
|
|
20
|
+
const chars=[...value],head=chars.splice(0,Math.min(3,Math.max(0,width-2))).join('');
|
|
21
|
+
while(chars.length&&visibleWidth(head+'…'+chars.join(''))>width)chars.shift();
|
|
22
|
+
return fitLine(head+'…'+chars.join(''),width);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function markdownTheme(){
|
|
26
|
+
const plain=text=>text;
|
|
27
|
+
return {heading:palette.brand,link:palette.brand,linkUrl:palette.muted,code:palette.brand,codeBlock:plain,codeBlockBorder:palette.border,quote:plain,quoteBorder:palette.border,hr:palette.border,listBullet:palette.brand,
|
|
28
|
+
bold:palette.text,italic:plain,strikethrough:plain,underline:plain};
|
|
29
|
+
}
|
|
@@ -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);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export async function workflowMenu(session){
|
|
2
|
+
const {app,ui}=session;
|
|
3
|
+
while(true){
|
|
4
|
+
const workflows=await app.listWorkflows();
|
|
5
|
+
const choice=await ui.select('工作流 · 选择后编辑或执行',[
|
|
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} 创建副本` })):[]),
|
|
8
|
+
...(!app.project.readOnly?[{value:'new',label:'+ 新建工作流'}]:[]),
|
|
9
|
+
...(session.dirty?[{value:'draft',label:`继续编辑草稿:${session.flow.title}`}]:[]),
|
|
10
|
+
{value:'back',label:'返回主面板'},
|
|
11
|
+
]);
|
|
12
|
+
if(choice==='back')return;
|
|
13
|
+
try{
|
|
14
|
+
const id=choice.startsWith('workflow:')?choice.slice(9):null;
|
|
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;}
|
|
20
|
+
else if(choice!=='draft'&&session.flow?.id!==id){
|
|
21
|
+
if(!await session.discard())continue;
|
|
22
|
+
session.flow=structuredClone(workflows.find(flow=>flow.id===id));session.dirty=false;
|
|
23
|
+
}
|
|
24
|
+
if(await editWorkflow(session)==='running')return;
|
|
25
|
+
}catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function editWorkflow(session){
|
|
30
|
+
const {app,ui}=session;
|
|
31
|
+
while(true){
|
|
32
|
+
const flow=session.current();
|
|
33
|
+
const choice=await ui.select(`${flow.title??flow.id} · ${flow.steps.length} 步 · ${session.dirty?'未保存':'已保存'}`, [
|
|
34
|
+
{value:'run',label:'运行工作流'},
|
|
35
|
+
...(!app.project.readOnly?[
|
|
36
|
+
{value:'add',label:'添加步骤 / 控制流'}, {value:'steps',label:'编辑步骤、参数与顺序'},
|
|
37
|
+
{value:'outputs',label:'设置最终输出'},{value:'save',label:'保存工作流'},
|
|
38
|
+
{value:'duplicate',label:'复制为新工作流(包含当前未保存修改)'},
|
|
39
|
+
{value:'delete',label:'删除工作流(保留可恢复备份)'},
|
|
40
|
+
]:[{value:'steps',label:'查看步骤'}]),
|
|
41
|
+
{value:'back',label:'返回工作流列表(保留草稿)'},
|
|
42
|
+
]);
|
|
43
|
+
if(choice==='back')return;
|
|
44
|
+
try{
|
|
45
|
+
if(choice==='delete'){
|
|
46
|
+
if(!await ui.confirm(`删除 ${flow.title??flow.id}?已保存文件会移入备份,未保存修改将放弃。`))continue;
|
|
47
|
+
const exists=(await app.listWorkflows()).some(w=>w.id===flow.id);
|
|
48
|
+
const backup=exists?await app.removeWorkflow(flow.id):null;
|
|
49
|
+
session.flow=null;session.dirty=false;ui.show(backup?`工作流已移入备份:${backup}`:'未保存的草稿已删除。');return;
|
|
50
|
+
}
|
|
51
|
+
if(choice==='run'){
|
|
52
|
+
if(await session.startRun([],{draft:true}))return 'running';
|
|
53
|
+
}else if(choice==='duplicate')await duplicateIntoSession(session,flow);
|
|
54
|
+
else await session.execute('/'+choice);
|
|
55
|
+
}catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
|
|
56
|
+
}
|
|
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.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 开发工具模块
|
|
2
|
+
|
|
3
|
+
包名:`@mytools/fst-devtools`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
|
|
4
|
+
|
|
5
|
+
## 职责与边界
|
|
6
|
+
|
|
7
|
+
插件脚手架、validate、lint、离线文档发现与导出。不管理常驻宿主或用户运行状态。
|
|
8
|
+
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/fst-platform`、`@mytools/fst-plugin`。
|
|
10
|
+
## 公开入口
|
|
11
|
+
|
|
12
|
+
| 导入路径 | 实现位置 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `@mytools/fst-devtools` | `./src/plugin-tools.js` |
|
|
15
|
+
| `@mytools/fst-devtools/documentation` | `./src/documentation.js` |
|
|
16
|
+
| `@mytools/fst-devtools/package.json` | `./package.json` |
|
|
17
|
+
|
|
18
|
+
仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
|
|
19
|
+
|
|
20
|
+
## 维护与验证
|
|
21
|
+
|
|
22
|
+
本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
|
|
23
|
+
|
|
24
|
+
在仓库根目录执行:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
node --test tests/modules.test.js tests/plugin-tools.test.js tests/plugin-lint.test.js tests/documentation.test.js
|
|
28
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mytools/fst-devtools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/plugin-tools.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/plugin-tools.js",
|
|
9
|
+
"./documentation": "./src/documentation.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=24.15.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@mytools/sdk": "0.1.0",
|
|
23
|
+
"@mytools/fst-platform": "0.1.0",
|
|
24
|
+
"@mytools/fst-plugin": "0.1.0",
|
|
25
|
+
"semver": "7.7.4",
|
|
26
|
+
"validate-npm-package-name": "8.0.0",
|
|
27
|
+
"acorn": "8.19.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import {access,readFile,readdir,lstat,mkdir,copyFile,realpath} from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
const topics=[
|
|
6
|
+
['plugins','插件开发规范','docs/plugin-development.md'],
|
|
7
|
+
['deployment','安装与部署','docs/deployment.md'],
|
|
8
|
+
['control','控制流与运行观察规范','docs/superpowers/specs/2026-10-11-control-observation.md'],
|
|
9
|
+
['services','服务插件示例','examples/service-plugins/README.md'],
|
|
10
|
+
['example','可安装的零依赖动作插件','docs/examples/action-plugin/README.md'],
|
|
11
|
+
['roadmap','路线图','docs/roadmap.md'],
|
|
12
|
+
['modules','模块架构与维护','docs/modules.md'],
|
|
13
|
+
['generation','大日志、开发重载与隔离文件探测','docs/kernel-generation.md'],
|
|
14
|
+
['readme','Fishtools 使用说明','README.md'],
|
|
15
|
+
];
|
|
16
|
+
// 保留文档链接所需的目录布局;参考代码仅供阅读,不是第二份运行时。
|
|
17
|
+
const assets=['README.md','LICENSE','THIRD_PARTY_NOTICES.md','docs','examples/service-plugins','plugins/examples','plugins/environment','plugins/web/README.md','packages/sdk','packages/core','packages/pack','packages/fst-logs','packages/fst-platform','packages/fst-workflow','packages/fst-plugin','packages/fst-workspace','packages/fst-devtools','packages/fst-cli','tests/application.test.js'];
|
|
18
|
+
const excluded=new Set(['node_modules','dist','.git','.cache','__pycache__','.pytest_cache','coverage','public']);
|
|
19
|
+
const inside=(parent,child)=>{const relative=path.relative(parent,child);return relative===''||(!relative.startsWith(`..${path.sep}`)&&relative!=='..'&&!path.isAbsolute(relative));};
|
|
20
|
+
|
|
21
|
+
export async function findDocumentationRoot(){
|
|
22
|
+
let directory=path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
for(;;){
|
|
24
|
+
for(const candidate of [directory,path.join(directory,'manual')]){
|
|
25
|
+
try{await access(path.join(candidate,'docs/plugin-development.md'));await access(path.join(candidate,'LICENSE'));return candidate;}
|
|
26
|
+
catch(error){if(error.code!=='ENOENT'&&error.code!=='ENOTDIR')throw error;}
|
|
27
|
+
}
|
|
28
|
+
const parent=path.dirname(directory);if(parent===directory)break;directory=parent;
|
|
29
|
+
}
|
|
30
|
+
throw new Error('当前安装缺少离线文档,请重新安装完整的 Fishtools 发行包');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function listDocumentation(){
|
|
34
|
+
const root=await findDocumentationRoot();
|
|
35
|
+
return topics.map(([id,title,file])=>({id,title,path:path.join(root,file)}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readDocumentation(topic='plugins'){
|
|
39
|
+
const entry=(await listDocumentation()).find(item=>item.id===topic);
|
|
40
|
+
if(!entry)throw new Error(`未知文档主题:${topic}。运行 fst docs 查看可用主题`);
|
|
41
|
+
return {...entry,text:await readFile(entry.path,'utf8')};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function copyDocumentation(destination,{sourceRoot}={}){
|
|
45
|
+
const source=await realpath(sourceRoot??await findDocumentationRoot());
|
|
46
|
+
const target=path.resolve(destination);
|
|
47
|
+
try{await lstat(target);throw new Error(`文档导出目标已存在:${target}`);}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
48
|
+
// 目标可位于仓库 dist 下,但不能嵌入即将复制的资料目录。
|
|
49
|
+
await mkdir(path.dirname(target),{recursive:true});
|
|
50
|
+
const canonicalTarget=path.join(await realpath(path.dirname(target)),path.basename(target));
|
|
51
|
+
for(const asset of assets)if(inside(path.join(source,asset),canonicalTarget))throw new Error('文档导出目标不能位于原资料目录内');
|
|
52
|
+
await mkdir(target);
|
|
53
|
+
const copy=async(from,to)=>{
|
|
54
|
+
const info=await lstat(from);
|
|
55
|
+
if(info.isSymbolicLink())throw new Error(`离线文档不允许符号链接:${from}`);
|
|
56
|
+
if(info.isDirectory()){
|
|
57
|
+
await mkdir(to,{recursive:true});
|
|
58
|
+
for(const entry of await readdir(from))if(!excluded.has(entry))await copy(path.join(from,entry),path.join(to,entry));
|
|
59
|
+
}else if(info.isFile()){
|
|
60
|
+
await mkdir(path.dirname(to),{recursive:true});await copyFile(from,to);
|
|
61
|
+
}else throw new Error(`离线文档包含不支持的文件:${from}`);
|
|
62
|
+
};
|
|
63
|
+
for(const asset of assets)await copy(path.join(source,asset),path.join(target,asset));
|
|
64
|
+
return target;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function exportDocumentation(destination){return copyDocumentation(destination);}
|