@yanglaofish/fishtools 0.1.4 → 0.3.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 +79 -25
- package/manual/LICENSE +21 -0
- package/manual/README.md +199 -0
- package/manual/THIRD_PARTY_NOTICES.md +32 -0
- package/manual/docs/examples/action-plugin/LICENSE +21 -0
- package/manual/docs/examples/action-plugin/README.md +38 -0
- package/manual/docs/examples/action-plugin/index.js +18 -0
- package/manual/docs/examples/action-plugin/input.json +1 -0
- package/manual/docs/examples/action-plugin/package.json +9 -0
- package/manual/docs/examples/pipeline-reader/LICENSE +21 -0
- package/manual/docs/examples/pipeline-reader/index.js +14 -0
- package/manual/docs/examples/pipeline-reader/package.json +7 -0
- package/manual/docs/examples/pipeline-writer/LICENSE +21 -0
- package/manual/docs/examples/pipeline-writer/index.js +17 -0
- package/manual/docs/examples/pipeline-writer/package.json +7 -0
- package/manual/docs/plans/2026-10-10-workbench-ui.md +27 -0
- package/{docs → manual/docs}/plugin-development.md +172 -20
- package/{docs → manual/docs}/releases.md +32 -2
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +167 -0
- package/manual/docs/roadmap.md +90 -0
- package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +115 -0
- package/manual/docs/superpowers/plans/2026-10-10-cordis-services.md +34 -0
- package/manual/docs/superpowers/plans/2026-10-10-fst-terminal.md +33 -0
- package/manual/docs/superpowers/plans/2026-10-10-portable-plugin-contract.md +35 -0
- package/manual/docs/superpowers/plans/2026-10-11-control-observation.md +77 -0
- package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +40 -0
- package/manual/docs/superpowers/plans/2026-10-11-terminal-hierarchy.md +40 -0
- package/manual/docs/superpowers/specs/2026-10-09-workflow-cli-design.md +377 -0
- package/manual/docs/superpowers/specs/2026-10-10-cordis-services-design.md +88 -0
- package/manual/docs/superpowers/specs/2026-10-10-portable-plugin-contract.md +31 -0
- package/manual/docs/superpowers/specs/2026-10-10-run-logs.md +28 -0
- package/manual/docs/superpowers/specs/2026-10-11-collapsed-plugin-output.md +23 -0
- package/manual/docs/superpowers/specs/2026-10-11-control-observation.md +37 -0
- package/{docs → manual/docs}/verification.md +97 -1
- package/manual/examples/service-plugins/README.md +20 -0
- package/manual/examples/service-plugins/consumer/LICENSE +21 -0
- package/manual/examples/service-plugins/consumer/index.js +14 -0
- package/manual/examples/service-plugins/consumer/package.json +1 -0
- package/manual/examples/service-plugins/provider/LICENSE +21 -0
- package/manual/examples/service-plugins/provider/index.js +19 -0
- package/manual/examples/service-plugins/provider/package.json +1 -0
- package/manual/packages/core/LICENSE +21 -0
- package/manual/packages/core/src/application.js +211 -0
- package/manual/packages/core/src/engine.js +93 -0
- package/manual/packages/core/src/host.js +124 -0
- package/manual/packages/core/src/runtime.js +135 -0
- package/manual/packages/core/src/terminal/run-feed.js +78 -0
- package/manual/packages/core/src/terminal/session.js +438 -0
- package/manual/packages/core/src/terminal/view.js +283 -0
- package/manual/packages/sdk/LICENSE +21 -0
- package/manual/packages/sdk/index.js +14 -0
- package/manual/packages/sdk/package.json +1 -0
- package/manual/plugins/environment/LICENSE +21 -0
- package/manual/plugins/environment/index.js +92 -0
- package/manual/plugins/environment/package.json +1 -0
- package/manual/plugins/examples/LICENSE +21 -0
- package/manual/plugins/examples/index.js +23 -0
- package/manual/plugins/examples/package.json +1 -0
- package/manual/plugins/examples/scripts/echo.py +5 -0
- package/manual/tests/application.test.js +75 -0
- package/node_modules/@mytools/core/bin/fst.js +12 -4
- package/node_modules/@mytools/core/src/application.js +11 -5
- package/node_modules/@mytools/core/src/cli-run.js +6 -0
- package/node_modules/@mytools/core/src/contracts.js +15 -2
- package/node_modules/@mytools/core/src/control.js +63 -0
- package/node_modules/@mytools/core/src/documentation.js +65 -0
- package/node_modules/@mytools/core/src/engine.js +80 -26
- package/node_modules/@mytools/core/src/host.js +15 -2
- package/node_modules/@mytools/core/src/markdown-log.js +9 -6
- package/node_modules/@mytools/core/src/process.js +49 -15
- package/node_modules/@mytools/core/src/project.js +1 -1
- package/node_modules/@mytools/core/src/run-logs.js +1 -1
- package/node_modules/@mytools/core/src/terminal/conditions.js +32 -0
- package/node_modules/@mytools/core/src/terminal/log-viewer.js +7 -8
- package/node_modules/@mytools/core/src/terminal/model.js +33 -4
- package/node_modules/@mytools/core/src/terminal/navigation.js +17 -0
- package/node_modules/@mytools/core/src/terminal/presentation.js +8 -10
- package/node_modules/@mytools/core/src/terminal/run-feed.js +78 -0
- package/node_modules/@mytools/core/src/terminal/run-presentation.js +60 -0
- package/node_modules/@mytools/core/src/terminal/run-viewer.js +104 -0
- package/node_modules/@mytools/core/src/terminal/session.js +212 -57
- package/node_modules/@mytools/core/src/terminal/theme.js +29 -0
- package/node_modules/@mytools/core/src/terminal/view.js +185 -33
- package/node_modules/@mytools/core/src/terminal/workflow-menu.js +50 -0
- package/node_modules/@mytools/core/src/validation.js +17 -4
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/core/src/workflows.js +41 -19
- package/node_modules/@mytools/core/templates/control-demo.yaml +47 -0
- package/node_modules/@mytools/environment/index.js +14 -2
- package/node_modules/@mytools/pack/index.js +6 -2
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +780 -780
- package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +70 -0
- package/node_modules/@mytools/web/public/assets/{index-BeV6IqTW.css → index-vuZvrUTT.css} +1 -1
- package/node_modules/@mytools/web/public/index.html +2 -2
- package/package.json +2 -2
- package/node_modules/@mytools/web/public/assets/index-TgsBnZgG.js +0 -70
- /package/{docs → manual/docs}/deployment.md +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { TUI, Text, Container, Editor, SelectList, ProcessTerminal, CombinedAutocompleteProvider, matchesKey, truncateToWidth, visibleWidth } from '@mariozechner/pi-tui';
|
|
5
|
+
import { MarkdownLogViewer } from './log-viewer.js';
|
|
6
|
+
import { RunViewer, mouseOn, mouseOff, mouseWheel } from './run-viewer.js';
|
|
7
|
+
import {RunFeed} from './run-feed.js';
|
|
8
|
+
import { logo } from './presentation.js';
|
|
9
|
+
import { VERSION } from '../version.js';
|
|
10
|
+
import {cleanText as safe,palette,menuTheme as theme,fitLine,shortenPath} from './theme.js';
|
|
11
|
+
|
|
12
|
+
export const cancelled=()=>Object.assign(new Error('已返回主菜单'),{code:'PROMPT_CANCELLED'});
|
|
13
|
+
// 上游菜单按字符数分配描述宽度,中文双宽字符需要再按终端列宽收口。
|
|
14
|
+
function fit(component){const render=component.render.bind(component);component.render=width=>render(width).map(line=>truncateToWidth(line,width));return component;}
|
|
15
|
+
|
|
16
|
+
// Pi 的 @ 模糊搜索依赖额外的 fd 程序。这里按目录补全,离线目标机无需安装 fd。
|
|
17
|
+
export class FileAutocomplete extends CombinedAutocompleteProvider{
|
|
18
|
+
constructor(commands,baseDir,workflows=[]){super(commands,baseDir);this.baseDir=baseDir;this.workflows=workflows;}
|
|
19
|
+
getSuggestions(lines,line,col){
|
|
20
|
+
const run=lines[line].slice(0,col).match(/^\/run\s+([^@"\s]*)$/);
|
|
21
|
+
if(run){
|
|
22
|
+
const query=run[1].toLowerCase(),items=this.workflows.filter(flow=>`${flow.id} ${flow.title??''}`.toLowerCase().includes(query)).map(flow=>({value:flow.id,label:flow.title??flow.id,description:flow.id,workflow:true}));
|
|
23
|
+
return items.length?{prefix:run[1],items}:null;
|
|
24
|
+
}
|
|
25
|
+
const prefix=lines[line].slice(0,col).match(/(?:^|\s)(@"[^"\n]*|@[^\s]*)$/)?.[1];
|
|
26
|
+
if(!prefix)return super.getSuggestions(lines,line,col);
|
|
27
|
+
const raw=prefix.slice(1).replace(/^"/,'').replaceAll('\\','/');
|
|
28
|
+
const expanded=raw.startsWith('~/')?path.join(homedir(),raw.slice(2)):raw;
|
|
29
|
+
const absolute=path.resolve(this.baseDir,expanded||'.');
|
|
30
|
+
const directory=!raw||raw.endsWith('/')?absolute:path.dirname(absolute);
|
|
31
|
+
const query=!raw||raw.endsWith('/')?'':path.basename(absolute).toLowerCase();
|
|
32
|
+
try{
|
|
33
|
+
const items=readdirSync(directory,{withFileTypes:true}).filter(e=>e.name.toLowerCase().startsWith(query)).map(e=>{
|
|
34
|
+
let dir=e.isDirectory();if(e.isSymbolicLink()){try{dir=statSync(path.join(directory,e.name)).isDirectory();}catch{}}
|
|
35
|
+
const head=raw.slice(0,raw.lastIndexOf('/')+1),file=head+e.name+(dir?'/':'');
|
|
36
|
+
return {value:`@${/[\s"]/.test(file)?JSON.stringify(file):file}`,label:safe(e.name)+(dir?'/':''),description:dir?'目录':'文件',directory:dir};
|
|
37
|
+
}).sort((a,b)=>(b.description==='目录')-(a.description==='目录')||a.label.localeCompare(b.label)).slice(0,100);
|
|
38
|
+
return items.length?{prefix,items}:null;
|
|
39
|
+
}catch{return null;}
|
|
40
|
+
}
|
|
41
|
+
getForceFileSuggestions(lines,line,col){return this.getSuggestions(lines,line,col);}
|
|
42
|
+
applyCompletion(lines,line,col,item,prefix){
|
|
43
|
+
if(item.workflow){const next=[...lines],before=lines[line].slice(0,col-prefix.length),text=item.value+' ';next[line]=before+text+lines[line].slice(col);return {lines:next,cursorLine:line,cursorCol:before.length+text.length};}
|
|
44
|
+
if(!prefix.startsWith('@'))return super.applyCompletion(lines,line,col,item,prefix);
|
|
45
|
+
const before=lines[line].slice(0,col-prefix.length);
|
|
46
|
+
let after=lines[line].slice(col);if(after.startsWith('"'))after=after.slice(1);
|
|
47
|
+
const completed=item.value+(item.directory?'':' '),next=[...lines];next[line]=before+completed+after;
|
|
48
|
+
return {lines:next,cursorLine:line,cursorCol:before.length+completed.length-(item.directory&&item.value.endsWith('"')?1:0)};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class TerminalView{
|
|
53
|
+
constructor({baseDir,previewLabel,terminal=new ProcessTerminal()}={}){
|
|
54
|
+
this.baseDir=baseDir;this.terminal=terminal;this.tui=new TUI(terminal);this.logs=[];this.runViews=new Map();
|
|
55
|
+
this.historyOffset=0;this.commandHistory=[];this.commandHistoryIndex=null;this.feed=new RunFeed(()=>this.refreshTranscript());
|
|
56
|
+
const originalStart=terminal.start.bind(terminal);
|
|
57
|
+
terminal.start=(input,resize)=>originalStart(data=>{
|
|
58
|
+
if(this.homeInput&&!this.runViewer&&!this.logViewer){
|
|
59
|
+
if(this.browseCommands(data))return;
|
|
60
|
+
if(matchesKey(data,'ctrl+o')){this.feed.expanded=!this.feed.expanded;this.historyOffset=0;this.refreshTranscript();return;}
|
|
61
|
+
const wheel=mouseWheel(data),page=Math.max(1,Math.floor(this.terminal.rows/2));
|
|
62
|
+
if(wheel||matchesKey(data,'pageUp')||matchesKey(data,'pageDown')){
|
|
63
|
+
this.historyOffset=Math.max(0,this.historyOffset+(wheel?-wheel*3:matchesKey(data,'pageUp')?page:-page));this.tui.requestRender();return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if(matchesKey(data,'f4')&&this.homeInput&&this.pending&&!this.runViewer&&!this.logViewer){
|
|
67
|
+
// 快捷入口不提交或丢弃当前草稿,返回首页时继续编辑。
|
|
68
|
+
this.homeDraft=this.editor.getText();this.pending.resolve('/logs');return;
|
|
69
|
+
}
|
|
70
|
+
if(matchesKey(data,'ctrl+c')){if(this.runViewer){this.runViewer.onClose();return;}if(this.cancelRun)this.cancelRun();this.pending?.reject(cancelled());return;}
|
|
71
|
+
if(matchesKey(data,'escape')&&this.pending&&!this.logViewer&&!this.runViewer){
|
|
72
|
+
// 编辑器自己处理补全菜单 Escape,第二次 Escape 返回上一级。
|
|
73
|
+
if(this.editor?.isShowingAutocomplete?.()){input(data);return;}
|
|
74
|
+
this.pending.reject(cancelled());return;
|
|
75
|
+
}
|
|
76
|
+
input(data);
|
|
77
|
+
},resize);
|
|
78
|
+
this.header=new Text('',1,0);this.transcript=new Text('',1,0);this.prompt=new Container();this.hint=new Text('',1,0);
|
|
79
|
+
this.hint.render=width=>['',fitLine(' '+palette.muted(this.homeInput?(width<45?`/ 操作 · Ctrl+O ${this.feed.expanded?'收起':'展开'}`:`/ 操作 · Ctrl+O ${this.feed.expanded?'收起':'展开'}详情 · /logs 回看 · Ctrl+C 退出`):(width<45?'↑↓ 选择 Enter 确认 Esc 返回':'↑↓ 选择 Enter 确认 Tab 补全 Esc 返回 Ctrl+C 退出')),width)];
|
|
80
|
+
const renderTranscript=this.transcript.render.bind(this.transcript);
|
|
81
|
+
this.headingLines=(width,mark='FISHtools')=>{
|
|
82
|
+
const room=Math.max(1,width-2),lines=[''];
|
|
83
|
+
const summary=(this.summaryText??'').split('\n');
|
|
84
|
+
const workspace=summary.find(line=>line.startsWith('当前工作区:'));
|
|
85
|
+
const details=summary.filter(line=>line!==workspace&&!line.startsWith('[F4')&&!line.startsWith('欢迎使用'));
|
|
86
|
+
if(mark===logo){
|
|
87
|
+
const side=[palette.text('FISHtools')+' '+palette.muted(`v${VERSION}`),palette.text('欢迎使用 · 插件编排工作台'),palette.muted(details[0]??'')];
|
|
88
|
+
lines.push(...logo.split('\n').map((line,i)=>' '+palette.brand(line)+' '+side[i]));
|
|
89
|
+
}else lines.push(' '+palette.brand('FISH')+palette.text('tools')+' '+palette.muted(`v${VERSION}`));
|
|
90
|
+
if(previewLabel)lines.push(' '+palette.brand(safe(previewLabel)));
|
|
91
|
+
if(workspace){
|
|
92
|
+
const label='当前工作区:',folder=shortenPath(workspace.slice(label.length),Math.max(1,room-visibleWidth(label)));
|
|
93
|
+
lines.push(' '+palette.muted(label)+palette.text(folder));
|
|
94
|
+
}
|
|
95
|
+
lines.push(' '+palette.muted('位置:')+palette.brand('主面板')+(this.commandLocation?palette.muted(' › ')+palette.text(this.commandLocation):''));
|
|
96
|
+
if(this.activeRuns?.())lines.push(' '+palette.muted(`${this.activeRuns()} 个运行中 · /logs 查看详情`));
|
|
97
|
+
lines.push('');return lines.map(line=>fitLine(line,width));
|
|
98
|
+
};
|
|
99
|
+
// 先保证输入与菜单可见,空间充足才展示三行字标。
|
|
100
|
+
this.header.render=width=>{
|
|
101
|
+
const full=this.headingLines(width,logo),other=renderTranscript(width).length+this.prompt.render(width).length+this.hint.render(width).length;
|
|
102
|
+
let lines=width>=55&&this.terminal.rows>=18&&full.length+other<=this.terminal.rows&&!this.feed.entries.length?full:this.headingLines(width);
|
|
103
|
+
const budget=Math.max(0,this.terminal.rows-this.prompt.render(width).length-this.hint.render(width).length-1);
|
|
104
|
+
if(lines.length>budget)lines=lines.filter(line=>safe(line).trim());
|
|
105
|
+
if(lines.length>budget)lines=lines.slice(0,budget);
|
|
106
|
+
return lines;
|
|
107
|
+
};
|
|
108
|
+
this.transcript.render=width=>{
|
|
109
|
+
const lines=renderTranscript(width),budget=Math.max(0,this.terminal.rows-this.header.render(width).length-this.prompt.render(width).length-this.hint.render(width).length);
|
|
110
|
+
if(lines.length<=budget){this.historyOffset=0;return lines;}
|
|
111
|
+
if(!budget)return [];
|
|
112
|
+
if(this.feed.entries.length){
|
|
113
|
+
const count=Math.max(0,budget-1),max=Math.max(0,lines.length-count);this.historyOffset=Math.min(this.historyOffset,max);
|
|
114
|
+
const end=lines.length-this.historyOffset,start=Math.max(0,end-count);
|
|
115
|
+
return [...lines.slice(start,end),fitLine(' '+palette.muted(`PgUp/PgDn/滚轮 查看过程 · ${this.historyOffset?'历史位置':'跟随最新'}`),width)];
|
|
116
|
+
}
|
|
117
|
+
const excerpt=this.logs.length<=1?lines.slice(0,budget-1):lines.slice(-(budget-1)||lines.length);
|
|
118
|
+
return [...excerpt,fitLine(' '+palette.muted('… /help 完整帮助 · /logs 运行日志'),width)];
|
|
119
|
+
};
|
|
120
|
+
this.tui.addChild(this.header);this.tui.addChild(this.transcript);this.tui.addChild(this.prompt);this.tui.addChild(this.hint);
|
|
121
|
+
}
|
|
122
|
+
start(){
|
|
123
|
+
if(this.started)return;this.started=true;
|
|
124
|
+
// 独立屏幕避免全屏阅读与主终端的滚动历史混在一起。
|
|
125
|
+
this.terminal.write('\x1b[?1049h\x1b[2J\x1b[H');this.tui.start();
|
|
126
|
+
}
|
|
127
|
+
browseCommands(data){
|
|
128
|
+
const direction=matchesKey(data,'up')?-1:matchesKey(data,'down')?1:0;
|
|
129
|
+
// 补全菜单保留方向键;多行编辑保留光标移动,不把配置值放进命令历史。
|
|
130
|
+
if(!direction||!this.editor||this.editor.isShowingAutocomplete()||this.editor.getText().includes('\n')||!this.commandHistory.length)return false;
|
|
131
|
+
const current=this.commandHistoryIndex??this.commandHistory.length;
|
|
132
|
+
const next=Math.max(0,Math.min(this.commandHistory.length,current+direction));
|
|
133
|
+
if(next===current)return true;
|
|
134
|
+
if(this.commandHistoryIndex===null)this.commandHistoryDraft=this.editor.getText();
|
|
135
|
+
this.commandHistoryIndex=next;
|
|
136
|
+
this.editor.setText(next===this.commandHistory.length?this.commandHistoryDraft:this.commandHistory[next]);
|
|
137
|
+
if(next===this.commandHistory.length)this.commandHistoryIndex=null;
|
|
138
|
+
this.tui.requestRender();return true;
|
|
139
|
+
}
|
|
140
|
+
async close(){
|
|
141
|
+
if(this.closed)return;this.closed=true;
|
|
142
|
+
this.feed.close();
|
|
143
|
+
// Pi 的渲染排在 nextTick;先排空最后一帧,再恢复终端,避免退出后隐藏光标。
|
|
144
|
+
await new Promise(resolve=>process.nextTick(resolve));this.tui.stop();
|
|
145
|
+
if(this.started)this.terminal.write(mouseOff+'\x1b[?1049l');
|
|
146
|
+
if(this.terminal instanceof ProcessTerminal)process.stdin.pause();
|
|
147
|
+
}
|
|
148
|
+
summary(text){this.summaryText=safe(text);this.tui.requestRender();}
|
|
149
|
+
beginCommand(text){
|
|
150
|
+
const name=text.trim().split(/\s+/)[0].replace(/^\//,'');
|
|
151
|
+
const labels={workflows:'工作流',run:'运行工作流',plugin:'插件管理',plugins:'插件管理',config:'工具配置',pack:'打包',logs:'执行记录',runs:'执行记录',tools:'工具',help:'帮助',exit:'退出'};
|
|
152
|
+
this.commandLocation=labels[name]??'/'+name;this.historyOffset=0;
|
|
153
|
+
this.clearMessages();this.feed.message('› '+safe(text));
|
|
154
|
+
}
|
|
155
|
+
endCommand(){
|
|
156
|
+
this.commandLocation=null;this.tui.requestRender();
|
|
157
|
+
}
|
|
158
|
+
transcriptText(){return [...this.logs,this.feed.text()].filter(Boolean).join('\n\n');}
|
|
159
|
+
refreshTranscript(){
|
|
160
|
+
const width=this.terminal.columns,previous=this.transcript.render===undefined?0:new Text(safe(this.lastTranscript??''),1,0).render(width).length;
|
|
161
|
+
const text=safe(this.transcriptText());
|
|
162
|
+
if(this.historyOffset>0)this.historyOffset=Math.max(0,this.historyOffset+new Text(text,1,0).render(width).length-previous);
|
|
163
|
+
this.lastTranscript=text;this.transcript.setText(text.split('\n').map(line=>line.startsWith('✗ ')?palette.error(line):/^[›✓●] /.test(line)?palette.brand(line):line.startsWith('↩ ')?palette.muted(line):line).join('\n'));this.tui.requestRender();
|
|
164
|
+
}
|
|
165
|
+
trackRun(options){this.feed.track(options);}
|
|
166
|
+
clearMessages(){this.logs=[];this.refreshTranscript();}
|
|
167
|
+
show(message,{kind='message'}={}){
|
|
168
|
+
const text=safe(message).slice(0,6000);
|
|
169
|
+
const formatted=kind==='welcome'?text.split('\n').map((line,i)=>i===0?palette.text(line):palette.muted(line)).join('\n'):text;
|
|
170
|
+
if(kind!=='welcome'){this.feed.message(formatted);return;}
|
|
171
|
+
this.logs.push(formatted);this.logs=this.logs.slice(-6);this.refreshTranscript();
|
|
172
|
+
}
|
|
173
|
+
async viewMarkdown(title,text){
|
|
174
|
+
const previousPending=this.pending??null,previousViewer=this.logViewer;
|
|
175
|
+
const children=this.tui.children;
|
|
176
|
+
let overlay;
|
|
177
|
+
try{
|
|
178
|
+
await new Promise((resolve,reject)=>{
|
|
179
|
+
this.pending={resolve,reject};
|
|
180
|
+
this.logViewer=new MarkdownLogViewer({title:safe(title),text:safe(text),terminal:this.terminal,onClose:resolve,requestRender:()=>this.tui.requestRender()});
|
|
181
|
+
this.terminal.write(mouseOn);
|
|
182
|
+
// Overlay 的基底为空,旧 transcript 不再撑高滚动区域;关闭后原样恢复。
|
|
183
|
+
this.tui.children=[];
|
|
184
|
+
overlay=this.tui.showOverlay(this.logViewer,{width:'100%',maxHeight:'100%',anchor:'top-left',margin:0});
|
|
185
|
+
this.tui.requestRender(true);
|
|
186
|
+
});
|
|
187
|
+
}finally{
|
|
188
|
+
this.tui.children=children;this.pending=previousPending;this.logViewer=previousViewer;
|
|
189
|
+
overlay?.hide();this.tui.requestRender(true);
|
|
190
|
+
this.terminal.write(mouseOff);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async observeRun({app,runId,record,flow,workflows=[],initialView}){
|
|
194
|
+
const previousPending=this.pending??null,children=this.tui.children;let overlay,unsubscribe;
|
|
195
|
+
const initial=app.getRun(runId)??record??await app.readRun(runId);
|
|
196
|
+
if(!initial)throw new Error('这次运行记录已不存在');
|
|
197
|
+
try{
|
|
198
|
+
await new Promise((resolve,reject)=>{
|
|
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()});
|
|
201
|
+
Object.assign(this.runViewer,this.runViews.get(runId)??{});
|
|
202
|
+
if(initialView!==undefined){this.runViewer.view=initialView;this.runViewer.offset=0;}
|
|
203
|
+
unsubscribe=app.subscribe(event=>{if(event.runId===runId)this.runViewer?.update(app.getRun(runId));});
|
|
204
|
+
this.tui.children=[];this.terminal.write(mouseOn);
|
|
205
|
+
overlay=this.tui.showOverlay(this.runViewer,{width:'100%',maxHeight:'100%',anchor:'top-left',margin:0});this.tui.requestRender(true);
|
|
206
|
+
});
|
|
207
|
+
}finally{
|
|
208
|
+
if(this.runViewer){const {view,selected,offset,follow,allLogs,raw}=this.runViewer;this.runViews.set(runId,{view,selected,offset,follow,allLogs,raw});}
|
|
209
|
+
if(this.runViews.size>30)this.runViews.delete(this.runViews.keys().next().value);
|
|
210
|
+
unsubscribe?.();this.runViewer=null;this.tui.children=children;this.pending=previousPending;overlay?.hide();this.terminal.write(mouseOff);this.tui.requestRender(true);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async ask(message,component){
|
|
214
|
+
fit(component);
|
|
215
|
+
const title=new Text(palette.brand('› ')+palette.text(safe(message)),1,0);
|
|
216
|
+
const renderTitle=title.render.bind(title);
|
|
217
|
+
title.render=width=>[fitLine(' '+palette.border('─'.repeat(Math.max(1,width-2))),width),...renderTitle(width)];
|
|
218
|
+
if(message.startsWith('fst >'))title.render=()=>[];
|
|
219
|
+
this.prompt.clear();this.prompt.addChild(title);this.prompt.addChild(component);this.tui.setFocus(component);this.tui.requestRender();
|
|
220
|
+
try{return await new Promise((resolve,reject)=>{this.pending={resolve,reject};component.onSubmit=text=>{
|
|
221
|
+
if(this.homeInput&&text.trim()&&this.commandHistory.at(-1)!==text.trim()){
|
|
222
|
+
this.commandHistory.push(text.trim());if(this.commandHistory.length>100)this.commandHistory.shift();
|
|
223
|
+
}
|
|
224
|
+
resolve(text);
|
|
225
|
+
};component.onSelect=item=>resolve(item.value);component.onCancel=()=>reject(cancelled());});}
|
|
226
|
+
finally{if(this.homeInput)this.terminal.write(mouseOff);this.pending=null;this.editor=null;this.homeInput=false;this.prompt.clear();this.tui.requestRender();}
|
|
227
|
+
}
|
|
228
|
+
input(message,{value='',commands=[],files=true,workflows=[]}={}){
|
|
229
|
+
const editor=new Editor(this.tui,{borderColor:palette.border,selectList:theme},{paddingX:1});
|
|
230
|
+
const renderEditor=editor.render.bind(editor);
|
|
231
|
+
editor.render=width=>{
|
|
232
|
+
if(width<8)return renderEditor(width);
|
|
233
|
+
const inner=width-4,lines=renderEditor(inner);let closed=false;
|
|
234
|
+
return lines.map((line,i)=>{
|
|
235
|
+
if(i===0)return ' '+palette.border('╭'+'─'.repeat(inner)+'╮');
|
|
236
|
+
if(!closed&&/^─+$/.test(safe(line))){closed=true;return ' '+palette.border('╰'+'─'.repeat(inner)+'╯');}
|
|
237
|
+
if(closed)return ' '+line;
|
|
238
|
+
return ' '+palette.border('│')+line+' '.repeat(Math.max(0,inner-visibleWidth(line)))+palette.border('│');
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
this.homeInput=message.startsWith('fst >');
|
|
242
|
+
this.commandHistoryIndex=null;
|
|
243
|
+
if(this.homeInput)editor.onChange=text=>{
|
|
244
|
+
if(this.commandHistoryIndex!==null&&text!==this.commandHistory[this.commandHistoryIndex])this.commandHistoryIndex=null;
|
|
245
|
+
};
|
|
246
|
+
if(this.homeInput)this.terminal.write(mouseOn);
|
|
247
|
+
this.editor=editor;editor.setText(this.homeInput?(this.homeDraft??value):value);
|
|
248
|
+
if(this.homeInput)this.homeDraft=undefined;
|
|
249
|
+
if(files||commands.length)editor.setAutocompleteProvider(new FileAutocomplete(commands,this.baseDir,workflows));
|
|
250
|
+
return this.ask(message,editor);
|
|
251
|
+
}
|
|
252
|
+
async select(message,items){
|
|
253
|
+
if(!items.length)throw new Error('当前没有可选择的项目');
|
|
254
|
+
const list=new SelectList(items.map(i=>({...i,label:safe(i.label).replace(/\s+/g,' '),description:i.description?safe(i.description).replace(/\s+/g,' '):undefined})),8,theme);
|
|
255
|
+
list.render=width=>{
|
|
256
|
+
const reserved=this.headingLines(width).length+new Text(palette.text(safe(message)),1,0).render(width).length+this.hint.render(width).length+2;
|
|
257
|
+
list.maxVisible=Math.max(1,Math.min(8,this.terminal.rows-reserved));
|
|
258
|
+
const start=Math.max(0,Math.min(list.selectedIndex-Math.floor(list.maxVisible/2),list.filteredItems.length-list.maxVisible));
|
|
259
|
+
const end=Math.min(start+list.maxVisible,list.filteredItems.length),lines=[];
|
|
260
|
+
for(let i=start;i<end;i++){
|
|
261
|
+
const item=list.filteredItems[i],selected=i===list.selectedIndex,available=Math.max(1,width-4);
|
|
262
|
+
const labelWidth=item.description&&width>=56?Math.min(32,Math.floor(available*0.45)):available;
|
|
263
|
+
const label=fitLine(item.label||item.value,labelWidth),prefix=selected?'› ':' ';
|
|
264
|
+
let line=' '+(selected?palette.brand(prefix+label):palette.text(prefix+label));
|
|
265
|
+
if(item.description&&width>=56)line+=' '.repeat(Math.max(2,labelWidth-visibleWidth(label)+2))+palette.muted(fitLine(item.description,Math.max(1,available-labelWidth-2)));
|
|
266
|
+
lines.push(fitLine(line,width));
|
|
267
|
+
}
|
|
268
|
+
if(start>0||end<list.filteredItems.length)lines.push(palette.muted(` ${list.selectedIndex+1} / ${list.filteredItems.length}`));
|
|
269
|
+
return lines.length?lines:[palette.muted(' 没有匹配项')];
|
|
270
|
+
};
|
|
271
|
+
const value=await this.ask(message,list);
|
|
272
|
+
if(this.commandLocation){
|
|
273
|
+
const item=items.find(item=>item.value===value);
|
|
274
|
+
if(item&&value!=='back'&&value!=='cancel')this.feed.message(`· ${safe(item.label??item.value)}`);
|
|
275
|
+
}
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
async confirm(message){return await this.select(message,[{value:'no',label:'否,返回'},{value:'yes',label:'是,继续'}])==='yes';}
|
|
279
|
+
busy(message,cancel){
|
|
280
|
+
this.cancelRun=cancel;this.prompt.clear();this.prompt.addChild(new Text(palette.text(safe(message)),1,0));this.tui.setFocus(this.prompt);this.tui.requestRender();
|
|
281
|
+
return ()=>{this.cancelRun=null;this.prompt.clear();this.tui.requestRender();};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -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,14 @@
|
|
|
1
|
+
export const SDK_VERSION = '0.1.0';
|
|
2
|
+
|
|
3
|
+
export function definePlugin(plugin) {
|
|
4
|
+
return Object.freeze({ sdk: '^0.1.0', actions: {}, ...plugin });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class FlowError extends Error {
|
|
8
|
+
constructor(message, { code = 'EXECUTION_FAILED', details, cause } = {}) {
|
|
9
|
+
super(message, { cause });
|
|
10
|
+
this.name = 'FlowError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.details = details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@mytools/sdk","version":"0.1.0","type":"module","main":"index.js","exports":"./index.js","files":["index.js","README.md"],"license":"MIT"}
|
|
@@ -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,92 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
import { definePlugin, FlowError } from '@mytools/sdk';
|
|
5
|
+
|
|
6
|
+
const pythonProbe=String.raw`import sys,json,importlib.metadata,importlib.util
|
|
7
|
+
names=json.loads(sys.argv[1])
|
|
8
|
+
packages=[]
|
|
9
|
+
for item in names:
|
|
10
|
+
name=item['name']
|
|
11
|
+
try:
|
|
12
|
+
version=importlib.metadata.version(name)
|
|
13
|
+
module=item.get('module')
|
|
14
|
+
if module: __import__(module)
|
|
15
|
+
packages.append({'name':name,'version':version,'ok':True})
|
|
16
|
+
except Exception as e:
|
|
17
|
+
packages.append({'name':name,'ok':False,'error':str(e)})
|
|
18
|
+
print(json.dumps({'executable':sys.executable,'version':sys.version.split()[0],'prefix':sys.prefix,'packages':packages}))`;
|
|
19
|
+
|
|
20
|
+
const plugin={
|
|
21
|
+
id:'@mytools/environment',version:'0.1.0',title:'环境检查',description:'检查目标机器的系统、Python/pip 与昇腾 NPU 环境,缺失必需条件时停止工作流。',
|
|
22
|
+
platforms:['win32','linux'],commands:{'env.check':{description:'检查系统、Python/pip 或 NPU',action:'check'}},
|
|
23
|
+
actions:{
|
|
24
|
+
check:{
|
|
25
|
+
title:'检查目标环境',description:'始终报告系统信息;按需检查指定 Python 及其软件包、pip 和 NPU。只检查,不安装或修改环境。',
|
|
26
|
+
input:{type:'object',properties:{
|
|
27
|
+
python:{type:'object',title:'Python 环境',required:['executable'],properties:{
|
|
28
|
+
executable:{type:'string',title:'解释器路径',description:'填写目标环境的 python/python.exe 绝对路径'},
|
|
29
|
+
version:{type:'string',title:'Python 版本要求',default:'>=3.9'},
|
|
30
|
+
pip:{type:'boolean',title:'要求 pip 可用',default:false},
|
|
31
|
+
packages:{type:'array',title:'要求的软件包',default:[],items:{type:'object',required:['name'],properties:{name:{type:'string',title:'包名'},version:{type:'string',title:'版本范围'},module:{type:'string',title:'需要尝试导入的模块(可选)'}}}},
|
|
32
|
+
}},
|
|
33
|
+
npu:{type:'boolean',title:'检查昇腾 NPU',default:false},
|
|
34
|
+
npuCommand:{type:'string',title:'npu-smi 路径',default:'npu-smi'},
|
|
35
|
+
},additionalProperties:false},
|
|
36
|
+
output:{type:'object',properties:{ok:{type:'boolean',title:'检查通过'},system:{type:'object',title:'系统信息'},python:{type:'object',title:'Python 环境',properties:{executable:{type:'string',title:'已检查的解释器'},version:{type:'string',title:'Python 版本'}}},checks:{type:'array',title:'检查明细',items:{type:'object'}}},required:['ok','system','checks']},
|
|
37
|
+
async run(input,{runProcess,signal}) {
|
|
38
|
+
const report={ok:true,system:{platform:os.platform(),architecture:os.arch(),kernel:os.release()},checks:[]};
|
|
39
|
+
if(process.platform==='linux'){
|
|
40
|
+
try{report.system.distribution=await readFile('/etc/os-release','utf8');}catch{}
|
|
41
|
+
}
|
|
42
|
+
const add=(name,ok,actual,status='error',expected)=>{report.checks.push({name,status:ok?'available':status,actual,expected});if(!ok)report.ok=false;};
|
|
43
|
+
const probe=async(file,args)=>runProcess(file,args,{signal,timeout:15000});
|
|
44
|
+
if(input.python){
|
|
45
|
+
const requested=input.python;
|
|
46
|
+
try {
|
|
47
|
+
const result=await probe(requested.executable,['-c',pythonProbe,JSON.stringify(requested.packages??[])]);
|
|
48
|
+
if(result.exitCode!==0)throw new Error(result.stderr||'Python 查询失败');
|
|
49
|
+
report.python=JSON.parse(result.stdout);
|
|
50
|
+
const parsed=semver.coerce(report.python.version);
|
|
51
|
+
const compatible=!!parsed && semver.satisfies(parsed,requested.version??'>=3.9');
|
|
52
|
+
add('python',compatible,report.python.version,'incompatible',requested.version??'>=3.9');
|
|
53
|
+
for(const pkg of report.python.packages){
|
|
54
|
+
const range=requested.packages.find(p=>p.name===pkg.name)?.version;
|
|
55
|
+
const version=semver.coerce(pkg.version??'');
|
|
56
|
+
add(`python.package:${pkg.name}`,pkg.ok&&(!range||(version&&semver.satisfies(version,range))),pkg.version??pkg.error,pkg.ok?'incompatible':'not_found',range);
|
|
57
|
+
}
|
|
58
|
+
if(requested.pip){
|
|
59
|
+
const pip=await probe(report.python.executable,['-m','pip','--version']);
|
|
60
|
+
add('pip',pip.exitCode===0,pip.stdout||pip.stderr,'not_found');
|
|
61
|
+
}
|
|
62
|
+
} catch(error){
|
|
63
|
+
if(signal?.aborted)throw error;
|
|
64
|
+
add('python',false,error.message,error.code==='ENOENT'?'not_found':'error','目标机可用解释器');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if(input.npu){
|
|
68
|
+
if(process.platform!=='linux')add('npu',false,'当前 NPU 适配仅支持 Linux','unsupported');
|
|
69
|
+
else try{
|
|
70
|
+
const r=await probe(input.npuCommand??'npu-smi',['info','-l']);
|
|
71
|
+
const count=r.stdout.match(/Total\s+NPU\s+count\s*:\s*(\d+)/i);
|
|
72
|
+
add('npu',r.exitCode===0&&Number(count?.[1])>0,r.stdout||r.stderr,r.exitCode===0?'not_found':'error','当前进程可见的昇腾 NPU');
|
|
73
|
+
}catch(error){if(signal?.aborted)throw error;add('npu',false,error.message,error.code==='ENOENT'?'not_found':'error');}
|
|
74
|
+
}
|
|
75
|
+
if(!report.ok)throw new FlowError('目标环境检查未通过,请根据检查明细处理后重试',{code:'ENVIRONMENT_FAILED',details:report});
|
|
76
|
+
return report;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
// 报告模式将“检测完成”与“检测通过”分开;旧 check 保持失败即终止的兼容语义。
|
|
82
|
+
plugin.actions.report={
|
|
83
|
+
...plugin.actions.check,
|
|
84
|
+
title:'生成环境报告',description:'返回结构化检测结果;未通过时返回 ok:false,由工作流条件决定是否继续。',
|
|
85
|
+
conditions:{passed:{title:'环境检查通过',path:'ok',op:'eq',value:true}},
|
|
86
|
+
async run(input,context){
|
|
87
|
+
try{return await plugin.actions.check.run(input,context);}
|
|
88
|
+
catch(error){if(error.code==='ENVIRONMENT_FAILED')return error.details;throw error;}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
plugin.commands['env.report']={description:'检测环境并返回报告(不因检查未通过抛错)',action:'report'};
|
|
92
|
+
export default definePlugin(plugin);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@mytools/environment","version":"0.1.0","type":"module","main":"index.js","files":["index.js"],"license":"MIT","dependencies":{"@mytools/sdk":"0.1.0","semver":"7.7.4"}}
|
|
@@ -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,23 @@
|
|
|
1
|
+
import { definePlugin, FlowError } from '@mytools/sdk';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const text={type:'string',title:'文本'};
|
|
6
|
+
const textInput={type:'object',required:['text'],properties:{text},additionalProperties:false};
|
|
7
|
+
export default definePlugin({
|
|
8
|
+
id:'@mytools/examples',version:'0.1.0',title:'基础示例',description:'文本处理、文件输出和目标 Python 调用示例,用于组合第一个工作流。',
|
|
9
|
+
platforms:['win32','linux'],architectures:['x64','arm64'],
|
|
10
|
+
commands:{
|
|
11
|
+
'text.upper':{description:'将文本转为大写',action:'uppercase'},
|
|
12
|
+
'text.ask':{description:'交互输入文本并转大写',async run(_input,{ui,signal}){const text=await ui.input('输入文本');signal.throwIfAborted();return {text:text.toUpperCase()};}},
|
|
13
|
+
},
|
|
14
|
+
actions:{
|
|
15
|
+
uppercase:{title:'文本转大写',description:'将输入文本中的英文字母转成大写,输出可以继续传给其他步骤。',input:textInput,output:textInput,async run({text}){return {text:text.toUpperCase()};}},
|
|
16
|
+
append:{title:'追加文本',description:'在一段文本末尾追加内容。',input:{type:'object',required:['text','suffix'],properties:{text,suffix:{type:'string',title:'追加内容',default:' ✓'}},additionalProperties:false},output:textInput,async run({text,suffix}){return {text:text+suffix};}},
|
|
17
|
+
save:{title:'保存文本文件',description:'把文本保存到本次运行的结果目录,输出文件路径。',input:textInput,output:{type:'object',properties:{path:{type:'string',title:'结果文件路径'}},required:['path']},async run({text},{outputDir}){const file=path.join(outputDir,'result.txt');await writeFile(file,text,'utf8');return {path:file};}},
|
|
18
|
+
pythonEcho:{title:'调用 Python 脚本',description:'使用目标环境解释器执行随插件分发的 Python 脚本,返回解释器和处理结果。',input:{type:'object',required:['executable','text'],properties:{executable:{type:'string',title:'Python 解释器',description:'建议选择环境检查步骤输出的 python.executable'},text},additionalProperties:false},output:{type:'object',properties:{text,executable:{type:'string',title:'实际解释器'},version:{type:'string',title:'Python 版本'}},required:['text','executable','version']},async run({executable,text},{runProcess,signal,resourceDir}){
|
|
19
|
+
const r=await runProcess(executable,[path.join(resourceDir,'scripts','echo.py'),text],{signal});
|
|
20
|
+
if(r.exitCode!==0)throw new FlowError(`Python 脚本失败:${r.stderr}`);return JSON.parse(r.stdout);
|
|
21
|
+
}},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@mytools/examples","version":"0.1.0","type":"module","main":"index.js","files":["index.js","scripts"],"license":"MIT","dependencies":{"@mytools/sdk":"0.1.0"}}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createApplication } from '../packages/core/src/application.js';
|
|
6
|
+
import { temporary, testPlugin, object } from './helpers.js';
|
|
7
|
+
import {validate} from '../packages/core/src/validation.js';
|
|
8
|
+
|
|
9
|
+
test('同 ID schema 隔离编译,保留内嵌引用且不串用旧约束',()=>{
|
|
10
|
+
const schema=type=>({$id:'https://fishtools.test/review/config',type:'object',definitions:{value:{type}},properties:{value:{$ref:'#/definitions/value'}},required:['value']});
|
|
11
|
+
assert.deepEqual(validate(schema('string'),{value:'old'},'旧配置'),{value:'old'});
|
|
12
|
+
assert.deepEqual(validate(schema('integer'),{value:2},'新配置'),{value:2});
|
|
13
|
+
assert.throws(()=>validate(schema('integer'),{value:'old'},'新配置'),/integer/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('外部带 $id 配置插件可升级,后续安装与卸载不被旧 schema 阻塞',async t=>{
|
|
17
|
+
const root=await temporary(t),projectDir=path.join(root,'project');
|
|
18
|
+
const fixture=async(version)=>{
|
|
19
|
+
const directory=path.join(root,'plugin-'+version);await mkdir(directory);
|
|
20
|
+
await writeFile(path.join(directory,'package.json'),JSON.stringify({name:'review-schema-plugin',version,type:'module',main:'index.js'}));
|
|
21
|
+
const config={$id:'https://fishtools.test/external/config',type:'object',properties:{version:{type:'string',const:version,default:version}}};
|
|
22
|
+
await writeFile(path.join(directory,'index.js'),`export default {id:'review-schema-plugin',version:${JSON.stringify(version)},config:${JSON.stringify(config)},commands:{'review.version':{action:'version'}},actions:{version:{input:{type:'object'},output:{type:'object'},async run(_, {config}){return config;}}}};`);
|
|
23
|
+
return directory;
|
|
24
|
+
};
|
|
25
|
+
const first=await fixture('1.0.0'),second=await fixture('1.0.1'),extra=path.join(root,'unrelated');await mkdir(extra);
|
|
26
|
+
await writeFile(path.join(extra,'package.json'),JSON.stringify({name:'review-unrelated',version:'1.0.0',type:'module',main:'index.js'}));
|
|
27
|
+
await writeFile(path.join(extra,'index.js'),"export default {id:'review-unrelated',version:'1.0.0'};");
|
|
28
|
+
const app=await createApplication({projectDir,plugins:[]});t.after(()=>app.close());
|
|
29
|
+
await app.installPlugin(first);await app.installPlugin(extra);await app.removePlugin('review-unrelated');await app.installPlugin(second);
|
|
30
|
+
assert.equal((await (await app.startCommand('review.version')).done).output.version,'1.0.1');
|
|
31
|
+
await assert.rejects(app.configurePlugin('review-schema-plugin',{version:'1.0.0'}),/配置/);
|
|
32
|
+
await app.close();
|
|
33
|
+
const restarted=await createApplication({projectDir});t.after(()=>restarted.close());
|
|
34
|
+
assert.equal((await (await restarted.startCommand('review.version')).done).output.version,'1.0.1');
|
|
35
|
+
await restarted.removePlugin('review-schema-plugin');assert.equal(restarted.listPlugins().length,0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('运行快照独立于之后保存,状态与日志持久化',async t=>{
|
|
39
|
+
const projectDir=await temporary(t);
|
|
40
|
+
const app=await createApplication({projectDir,plugins:[testPlugin()]}); t.after(()=>app.close());
|
|
41
|
+
const w={id:'hello',schemaVersion:1,title:'示例',inputs:object(),steps:[{id:'convert',action:'@test/text:upper',with:{text:'hello'}}],outputs:{text:{$ref:'steps.convert.output.text'}}};
|
|
42
|
+
await app.saveWorkflow(w);
|
|
43
|
+
const run=await app.startRun('hello',{});
|
|
44
|
+
await app.saveWorkflow({...w,steps:[{...w.steps[0],with:{text:'changed'}}]});
|
|
45
|
+
await run.done;
|
|
46
|
+
assert.equal(app.getRun(run.id).status,'succeeded');
|
|
47
|
+
assert.deepEqual(app.getRun(run.id).output,{text:'HELLO'});
|
|
48
|
+
const events=(await readFile(path.join(app.outputDir,run.id,'events.jsonl'),'utf8')).trim().split('\n').map(JSON.parse);
|
|
49
|
+
assert.ok(events.some(e=>e.type==='run.succeeded'));
|
|
50
|
+
assert.ok(!events.some(e=>Object.hasOwn(e,'output')));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('仓库外本地插件可以安装、加载并执行,移除后目录同步',async t=>{
|
|
54
|
+
const root=await temporary(t), pluginDir=path.join(root,'外部插件'),projectDir=path.join(root,'project');
|
|
55
|
+
await mkdir(pluginDir,{recursive:true});
|
|
56
|
+
await writeFile(path.join(pluginDir,'package.json'),JSON.stringify({name:'external-flow-test',version:'1.0.0',type:'module',main:'index.js'}));
|
|
57
|
+
await writeFile(path.join(pluginDir,'index.js'),`export default {id:'external-flow-test',version:'1.0.0',sdk:'^0.1.0',actions:{hello:{title:'外部动作',input:{type:'object'},output:{type:'object'},run:async()=>({value:42})}}};`);
|
|
58
|
+
const app=await createApplication({projectDir,plugins:[]});t.after(()=>app.close());
|
|
59
|
+
await app.installPlugin(pluginDir);
|
|
60
|
+
assert.ok(app.catalog().some(a=>a.id==='external-flow-test:hello'));
|
|
61
|
+
await app.saveWorkflow({id:'external',schemaVersion:1,inputs:object(),steps:[{id:'hello',action:'external-flow-test:hello',with:{}}],outputs:{answer:{$ref:'steps.hello.output.value'}}});
|
|
62
|
+
const run=await app.startRun('external',{});await run.done;
|
|
63
|
+
assert.deepEqual(app.getRun(run.id).output,{answer:42});
|
|
64
|
+
await app.removePlugin('external-flow-test');
|
|
65
|
+
assert.ok(!app.catalog().some(a=>a.id==='external-flow-test:hello'));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('外部配置覆盖项目值并在调用前验证',async t=>{
|
|
69
|
+
const projectDir=await temporary(t);
|
|
70
|
+
await writeFile(path.join(projectDir,'flow.project.json'),JSON.stringify({config:{configured:{prefix:'项目'}}}));
|
|
71
|
+
const plugin={id:'configured',version:'1.0.0',config:{type:'object',properties:{prefix:{type:'string'}},required:['prefix'],additionalProperties:false},actions:{show:{input:object(),output:object({prefix:{type:'string'}},['prefix']),run:async(_input,{config})=>config}}};
|
|
72
|
+
const app=await createApplication({projectDir,plugins:[plugin],config:{configured:{prefix:'目标'}}});t.after(()=>app.close());
|
|
73
|
+
assert.deepEqual(await app.host.invoke('configured:show',{}),{prefix:'目标'});
|
|
74
|
+
await assert.rejects(createApplication({projectDir,plugins:[plugin],config:{configured:{prefix:42}}}),/配置/);
|
|
75
|
+
});
|
|
@@ -5,8 +5,9 @@ import path from 'node:path';
|
|
|
5
5
|
import { createApplication } from '../src/application.js';
|
|
6
6
|
import { verifyBundle } from '../src/bundle.js';
|
|
7
7
|
import { resolveProject, initializeProject } from '../src/project.js';
|
|
8
|
-
import {waitForCliRun} from '../src/cli-run.js';
|
|
8
|
+
import {waitForCliRun,cliRunResult} from '../src/cli-run.js';
|
|
9
9
|
import {VERSION} from '../src/version.js';
|
|
10
|
+
import {listDocumentation,readDocumentation,findDocumentationRoot,exportDocumentation} from '../src/documentation.js';
|
|
10
11
|
|
|
11
12
|
const program=new Command().name('fst').description('Fishtools · 插件式工作流工作台').version(VERSION);
|
|
12
13
|
program.option('--maintenance','维护模式:不启动有状态服务插件');
|
|
@@ -14,13 +15,20 @@ program.option('--project <directory>','项目目录(默认当前项目或用
|
|
|
14
15
|
async function app(){const o=program.opts();return createApplication({projectDir:await resolveProject(o.project,o.workspace),outputDir:o.output,maintenance:!!o.maintenance,config:o.config?JSON.parse(await readFile(o.config,'utf8')):{}});}
|
|
15
16
|
async function use(fn){const a=await app();try{return await fn(a);}finally{await a.close();}}
|
|
16
17
|
program.command('init [directory]').description('创建项目,保留已有配置与工作流').action(async directory=>console.log(`Fishtools 项目:${await initializeProject(path.resolve(directory??process.cwd()))}`));
|
|
18
|
+
program.command('docs [topic]').description('阅读或导出随安装提供的离线文档').option('--path','只打印文档路径').option('--export <directory>','导出完整资料到新目录').action(async(topic,options)=>{
|
|
19
|
+
if(options.export){if(topic||options.path)throw new Error('--export 不可与文档主题或 --path 同用');console.log(await exportDocumentation(options.export));return;}
|
|
20
|
+
if(topic){const document=await readDocumentation(topic);console.log(options.path?document.path:document.text);return;}
|
|
21
|
+
if(options.path){console.log(await findDocumentationRoot());return;}
|
|
22
|
+
console.log((await listDocumentation()).map(document=>`${document.id}\t${document.title}`).join('\n'));
|
|
23
|
+
console.log('\nfst docs <主题> 阅读正文;fst docs <主题> --path 查看路径;fst docs --export <新目录> 导出全部资料');
|
|
24
|
+
});
|
|
17
25
|
const workflow=program.command('workflow').description('查看和检查工作流');
|
|
18
26
|
workflow.command('list').action(()=>use(async a=>console.log(JSON.stringify(await a.listWorkflows(),null,2))));
|
|
19
27
|
workflow.command('check <id>').action(id=>use(async a=>{await a.checkWorkflow(id);console.log('工作流校验通过');}));
|
|
20
28
|
program.command('run <id>').option('--input <file>','JSON 输入文件').action((id,o)=>use(async a=>{
|
|
21
29
|
const run=await a.startRun(id,o.input?JSON.parse(await readFile(o.input,'utf8')):{});
|
|
22
|
-
const
|
|
23
|
-
process.exitCode=
|
|
30
|
+
const result=cliRunResult(await waitForCliRun(a,run));console.log(JSON.stringify(result.value,null,2));
|
|
31
|
+
process.exitCode=result.exitCode;
|
|
24
32
|
}));
|
|
25
33
|
const plugin=program.command('plugin').description('管理插件');
|
|
26
34
|
plugin.command('list').action(()=>use(a=>console.log(JSON.stringify(a.listPlugins(),null,2))));
|
|
@@ -36,7 +44,7 @@ packOptions(program.command('portable').description('导出完整便携工具、
|
|
|
36
44
|
program.command('command [name]').description('列出插件命令或执行动作命令').option('--input <file>','JSON 输入').action((name,o)=>use(async a=>{
|
|
37
45
|
if(!name){console.log(JSON.stringify(a.listCommands(),null,2));return;}
|
|
38
46
|
const run=await a.startCommand(name,o.input?JSON.parse(await readFile(o.input,'utf8')):{});
|
|
39
|
-
const result=await waitForCliRun(a,run);console.log(JSON.stringify(result.
|
|
47
|
+
const result=cliRunResult(await waitForCliRun(a,run));console.log(JSON.stringify(result.value,null,2));process.exitCode=result.exitCode;
|
|
40
48
|
}));
|
|
41
49
|
program.command('configure <plugin> <file>').description('从 JSON 保存插件配置').action((id,file)=>use(async a=>{await a.configurePlugin(id,JSON.parse(await readFile(file,'utf8')));console.log('插件配置已保存');}));
|
|
42
50
|
async function web(o){
|