@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.
Files changed (97) hide show
  1. package/README.md +79 -25
  2. package/manual/LICENSE +21 -0
  3. package/manual/README.md +199 -0
  4. package/manual/THIRD_PARTY_NOTICES.md +32 -0
  5. package/manual/docs/examples/action-plugin/LICENSE +21 -0
  6. package/manual/docs/examples/action-plugin/README.md +38 -0
  7. package/manual/docs/examples/action-plugin/index.js +18 -0
  8. package/manual/docs/examples/action-plugin/input.json +1 -0
  9. package/manual/docs/examples/action-plugin/package.json +9 -0
  10. package/manual/docs/examples/pipeline-reader/LICENSE +21 -0
  11. package/manual/docs/examples/pipeline-reader/index.js +14 -0
  12. package/manual/docs/examples/pipeline-reader/package.json +7 -0
  13. package/manual/docs/examples/pipeline-writer/LICENSE +21 -0
  14. package/manual/docs/examples/pipeline-writer/index.js +17 -0
  15. package/manual/docs/examples/pipeline-writer/package.json +7 -0
  16. package/manual/docs/plans/2026-10-10-workbench-ui.md +27 -0
  17. package/{docs → manual/docs}/plugin-development.md +172 -20
  18. package/{docs → manual/docs}/releases.md +32 -2
  19. package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +167 -0
  20. package/manual/docs/roadmap.md +90 -0
  21. package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +115 -0
  22. package/manual/docs/superpowers/plans/2026-10-10-cordis-services.md +34 -0
  23. package/manual/docs/superpowers/plans/2026-10-10-fst-terminal.md +33 -0
  24. package/manual/docs/superpowers/plans/2026-10-10-portable-plugin-contract.md +35 -0
  25. package/manual/docs/superpowers/plans/2026-10-11-control-observation.md +77 -0
  26. package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +40 -0
  27. package/manual/docs/superpowers/plans/2026-10-11-terminal-hierarchy.md +40 -0
  28. package/manual/docs/superpowers/specs/2026-10-09-workflow-cli-design.md +377 -0
  29. package/manual/docs/superpowers/specs/2026-10-10-cordis-services-design.md +88 -0
  30. package/manual/docs/superpowers/specs/2026-10-10-portable-plugin-contract.md +31 -0
  31. package/manual/docs/superpowers/specs/2026-10-10-run-logs.md +28 -0
  32. package/manual/docs/superpowers/specs/2026-10-11-collapsed-plugin-output.md +23 -0
  33. package/manual/docs/superpowers/specs/2026-10-11-control-observation.md +37 -0
  34. package/{docs → manual/docs}/verification.md +97 -1
  35. package/manual/examples/service-plugins/README.md +20 -0
  36. package/manual/examples/service-plugins/consumer/LICENSE +21 -0
  37. package/manual/examples/service-plugins/consumer/index.js +14 -0
  38. package/manual/examples/service-plugins/consumer/package.json +1 -0
  39. package/manual/examples/service-plugins/provider/LICENSE +21 -0
  40. package/manual/examples/service-plugins/provider/index.js +19 -0
  41. package/manual/examples/service-plugins/provider/package.json +1 -0
  42. package/manual/packages/core/LICENSE +21 -0
  43. package/manual/packages/core/src/application.js +211 -0
  44. package/manual/packages/core/src/engine.js +93 -0
  45. package/manual/packages/core/src/host.js +124 -0
  46. package/manual/packages/core/src/runtime.js +135 -0
  47. package/manual/packages/core/src/terminal/run-feed.js +78 -0
  48. package/manual/packages/core/src/terminal/session.js +438 -0
  49. package/manual/packages/core/src/terminal/view.js +283 -0
  50. package/manual/packages/sdk/LICENSE +21 -0
  51. package/manual/packages/sdk/index.js +14 -0
  52. package/manual/packages/sdk/package.json +1 -0
  53. package/manual/plugins/environment/LICENSE +21 -0
  54. package/manual/plugins/environment/index.js +92 -0
  55. package/manual/plugins/environment/package.json +1 -0
  56. package/manual/plugins/examples/LICENSE +21 -0
  57. package/manual/plugins/examples/index.js +23 -0
  58. package/manual/plugins/examples/package.json +1 -0
  59. package/manual/plugins/examples/scripts/echo.py +5 -0
  60. package/manual/tests/application.test.js +75 -0
  61. package/node_modules/@mytools/core/bin/fst.js +12 -4
  62. package/node_modules/@mytools/core/src/application.js +11 -5
  63. package/node_modules/@mytools/core/src/cli-run.js +6 -0
  64. package/node_modules/@mytools/core/src/contracts.js +15 -2
  65. package/node_modules/@mytools/core/src/control.js +63 -0
  66. package/node_modules/@mytools/core/src/documentation.js +65 -0
  67. package/node_modules/@mytools/core/src/engine.js +80 -26
  68. package/node_modules/@mytools/core/src/host.js +15 -2
  69. package/node_modules/@mytools/core/src/markdown-log.js +9 -6
  70. package/node_modules/@mytools/core/src/process.js +49 -15
  71. package/node_modules/@mytools/core/src/project.js +1 -1
  72. package/node_modules/@mytools/core/src/run-logs.js +1 -1
  73. package/node_modules/@mytools/core/src/terminal/conditions.js +32 -0
  74. package/node_modules/@mytools/core/src/terminal/log-viewer.js +7 -8
  75. package/node_modules/@mytools/core/src/terminal/model.js +33 -4
  76. package/node_modules/@mytools/core/src/terminal/navigation.js +17 -0
  77. package/node_modules/@mytools/core/src/terminal/presentation.js +8 -10
  78. package/node_modules/@mytools/core/src/terminal/run-feed.js +78 -0
  79. package/node_modules/@mytools/core/src/terminal/run-presentation.js +60 -0
  80. package/node_modules/@mytools/core/src/terminal/run-viewer.js +104 -0
  81. package/node_modules/@mytools/core/src/terminal/session.js +212 -57
  82. package/node_modules/@mytools/core/src/terminal/theme.js +29 -0
  83. package/node_modules/@mytools/core/src/terminal/view.js +185 -33
  84. package/node_modules/@mytools/core/src/terminal/workflow-menu.js +50 -0
  85. package/node_modules/@mytools/core/src/validation.js +17 -4
  86. package/node_modules/@mytools/core/src/version.js +1 -1
  87. package/node_modules/@mytools/core/src/workflows.js +41 -19
  88. package/node_modules/@mytools/core/templates/control-demo.yaml +47 -0
  89. package/node_modules/@mytools/environment/index.js +14 -2
  90. package/node_modules/@mytools/pack/index.js +6 -2
  91. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +780 -780
  92. package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +70 -0
  93. package/node_modules/@mytools/web/public/assets/{index-BeV6IqTW.css → index-vuZvrUTT.css} +1 -1
  94. package/node_modules/@mytools/web/public/index.html +2 -2
  95. package/package.json +2 -2
  96. package/node_modules/@mytools/web/public/assets/index-TgsBnZgG.js +0 -70
  97. /package/{docs → manual/docs}/deployment.md +0 -0
@@ -1,22 +1,27 @@
1
1
  import { readdirSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { stripVTControlCharacters } from 'node:util';
5
- import { TUI, Text, Container, Editor, SelectList, ProcessTerminal, CombinedAutocompleteProvider, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
4
+ import { TUI, Text, Container, Editor, SelectList, ProcessTerminal, CombinedAutocompleteProvider, matchesKey, truncateToWidth, visibleWidth } from '@mariozechner/pi-tui';
6
5
  import { MarkdownLogViewer } from './log-viewer.js';
6
+ import { RunViewer, mouseOn, mouseOff, mouseWheel } from './run-viewer.js';
7
+ import {RunFeed} from './run-feed.js';
7
8
  import { logo } from './presentation.js';
8
9
  import { VERSION } from '../version.js';
10
+ import {cleanText as safe,palette,menuTheme as theme,fitLine,shortenPath} from './theme.js';
9
11
 
10
12
  export const cancelled=()=>Object.assign(new Error('已返回主菜单'),{code:'PROMPT_CANCELLED'});
11
- const safe=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
12
- const theme={selectedPrefix:s=>`\x1b[36m${s}\x1b[0m`,selectedText:s=>`\x1b[36m${s}\x1b[0m`,description:s=>s,scrollInfo:s=>s,noMatch:s=>s};
13
13
  // 上游菜单按字符数分配描述宽度,中文双宽字符需要再按终端列宽收口。
14
14
  function fit(component){const render=component.render.bind(component);component.render=width=>render(width).map(line=>truncateToWidth(line,width));return component;}
15
15
 
16
16
  // Pi 的 @ 模糊搜索依赖额外的 fd 程序。这里按目录补全,离线目标机无需安装 fd。
17
17
  export class FileAutocomplete extends CombinedAutocompleteProvider{
18
- constructor(commands,baseDir){super(commands,baseDir);this.baseDir=baseDir;}
18
+ constructor(commands,baseDir,workflows=[]){super(commands,baseDir);this.baseDir=baseDir;this.workflows=workflows;}
19
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
+ }
20
25
  const prefix=lines[line].slice(0,col).match(/(?:^|\s)(@"[^"\n]*|@[^\s]*)$/)?.[1];
21
26
  if(!prefix)return super.getSuggestions(lines,line,col);
22
27
  const raw=prefix.slice(1).replace(/^"/,'').replaceAll('\\','/');
@@ -35,6 +40,7 @@ export class FileAutocomplete extends CombinedAutocompleteProvider{
35
40
  }
36
41
  getForceFileSuggestions(lines,line,col){return this.getSuggestions(lines,line,col);}
37
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};}
38
44
  if(!prefix.startsWith('@'))return super.applyCompletion(lines,line,col,item,prefix);
39
45
  const before=lines[line].slice(0,col-prefix.length);
40
46
  let after=lines[line].slice(col);if(after.startsWith('"'))after=after.slice(1);
@@ -44,44 +50,126 @@ export class FileAutocomplete extends CombinedAutocompleteProvider{
44
50
  }
45
51
 
46
52
  export class TerminalView{
47
- constructor({baseDir,terminal=new ProcessTerminal()}={}){
48
- this.baseDir=baseDir;this.terminal=terminal;this.tui=new TUI(terminal);this.logs=[];
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());
49
56
  const originalStart=terminal.start.bind(terminal);
50
57
  terminal.start=(input,resize)=>originalStart(data=>{
51
- if(matchesKey(data,'ctrl+c')){if(this.cancelRun)this.cancelRun();this.pending?.reject(cancelled());return;}
52
- if(matchesKey(data,'escape')&&this.pending&&!this.logViewer){
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){
53
72
  // 编辑器自己处理补全菜单 Escape,第二次 Escape 返回上一级。
54
73
  if(this.editor?.isShowingAutocomplete?.()){input(data);return;}
55
74
  this.pending.reject(cancelled());return;
56
75
  }
57
76
  input(data);
58
77
  },resize);
59
- this.header=new Text('',1,1);this.transcript=new Text('',1,0);this.prompt=new Container();this.hint=new Text('↑↓ 选择 · Enter 确认 · Tab 补全 · Esc 返回 · Ctrl+C 取消/退出',1,1);
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)];
60
80
  const renderTranscript=this.transcript.render.bind(this.transcript);
61
- this.headingLines=(width,mark='FISHtools')=>new Text(`${mark} v${VERSION}\n${this.summaryText??''}`,1,1).render(width).map(line=>truncateToWidth(line,width));
62
- // 根据整屏换行后的行数选择标题,而不是只判断终端宽度。
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
+ // 先保证输入与菜单可见,空间充足才展示三行字标。
63
100
  this.header.render=width=>{
64
101
  const full=this.headingLines(width,logo),other=renderTranscript(width).length+this.prompt.render(width).length+this.hint.render(width).length;
65
- return width>=52&&full.length+other<=this.terminal.rows?full:this.headingLines(width);
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;
66
107
  };
67
108
  this.transcript.render=width=>{
68
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);
69
- if(lines.length<=budget)return lines;
110
+ if(lines.length<=budget){this.historyOffset=0;return lines;}
70
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
+ }
71
117
  const excerpt=this.logs.length<=1?lines.slice(0,budget-1):lines.slice(-(budget-1)||lines.length);
72
- return [...excerpt,truncateToWidth(' … /help 查看用法 · /logs 查看运行结果',width)];
118
+ return [...excerpt,fitLine(' '+palette.muted('… /help 完整帮助 · /logs 运行日志'),width)];
73
119
  };
74
120
  this.tui.addChild(this.header);this.tui.addChild(this.transcript);this.tui.addChild(this.prompt);this.tui.addChild(this.hint);
75
121
  }
76
- start(){this.tui.start();}
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
+ }
77
140
  async close(){
141
+ if(this.closed)return;this.closed=true;
142
+ this.feed.close();
78
143
  // Pi 的渲染排在 nextTick;先排空最后一帧,再恢复终端,避免退出后隐藏光标。
79
144
  await new Promise(resolve=>process.nextTick(resolve));this.tui.stop();
145
+ if(this.started)this.terminal.write(mouseOff+'\x1b[?1049l');
80
146
  if(this.terminal instanceof ProcessTerminal)process.stdin.pause();
81
147
  }
82
148
  summary(text){this.summaryText=safe(text);this.tui.requestRender();}
83
- clearMessages(){this.logs=[];this.transcript.setText('');this.tui.requestRender();}
84
- show(message){this.logs.push(safe(message).slice(0,6000));this.logs=this.logs.slice(-6);this.transcript.setText(this.logs.join('\n\n'));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
+ }
85
173
  async viewMarkdown(title,text){
86
174
  const previousPending=this.pending??null,previousViewer=this.logViewer;
87
175
  const children=this.tui.children;
@@ -90,6 +178,7 @@ export class TerminalView{
90
178
  await new Promise((resolve,reject)=>{
91
179
  this.pending={resolve,reject};
92
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);
93
182
  // Overlay 的基底为空,旧 transcript 不再撑高滚动区域;关闭后原样恢复。
94
183
  this.tui.children=[];
95
184
  overlay=this.tui.showOverlay(this.logViewer,{width:'100%',maxHeight:'100%',anchor:'top-left',margin:0});
@@ -98,34 +187,97 @@ export class TerminalView{
98
187
  }finally{
99
188
  this.tui.children=children;this.pending=previousPending;this.logViewer=previousViewer;
100
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);
101
211
  }
102
212
  }
103
213
  async ask(message,component){
104
214
  fit(component);
105
- this.prompt.clear();this.prompt.addChild(new Text(safe(message),1,1));this.prompt.addChild(component);this.tui.setFocus(component);this.tui.requestRender();
106
- try{return await new Promise((resolve,reject)=>{this.pending={resolve,reject};component.onSubmit=resolve;component.onSelect=item=>resolve(item.value);component.onCancel=()=>reject(cancelled());});}
107
- finally{this.pending=null;this.editor=null;this.prompt.clear();this.tui.requestRender();}
108
- }
109
- input(message,{value='',commands=[],files=true}={}){
110
- const editor=new Editor(this.tui,{borderColor:theme.selectedPrefix,selectList:theme},{paddingX:1});
111
- this.editor=editor;editor.setText(value);
112
- if(files||commands.length)editor.setAutocompleteProvider(new FileAutocomplete(commands,this.baseDir));
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));
113
250
  return this.ask(message,editor);
114
251
  }
115
- select(message,items){
252
+ async select(message,items){
116
253
  if(!items.length)throw new Error('当前没有可选择的项目');
117
- const list=new SelectList(items.map(i=>({...i,label:safe(i.label),description:i.description?safe(i.description):undefined})),8,theme);
118
- const render=list.render.bind(list);
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);
119
255
  list.render=width=>{
120
- const reserved=this.headingLines(width).length+new Text(safe(message),1,1).render(width).length+this.hint.render(width).length+1;
256
+ const reserved=this.headingLines(width).length+new Text(palette.text(safe(message)),1,0).render(width).length+this.hint.render(width).length+2;
121
257
  list.maxVisible=Math.max(1,Math.min(8,this.terminal.rows-reserved));
122
- return render(width);
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(' 没有匹配项')];
123
270
  };
124
- return this.ask(message,list);
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;
125
277
  }
126
278
  async confirm(message){return await this.select(message,[{value:'no',label:'否,返回'},{value:'yes',label:'是,继续'}])==='yes';}
127
279
  busy(message,cancel){
128
- this.cancelRun=cancel;this.prompt.clear();this.prompt.addChild(new Text(safe(message),1,1));this.tui.setFocus(this.prompt);this.tui.requestRender();
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();
129
281
  return ()=>{this.cancelRun=null;this.prompt.clear();this.tui.requestRender();};
130
282
  }
131
283
  }
@@ -0,0 +1,50 @@
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?[{value:'new',label:'+ 新建工作流'}]:[]),
8
+ ...(session.dirty?[{value:'draft',label:`继续编辑草稿:${session.flow.title}`}]:[]),
9
+ {value:'back',label:'返回主面板'},
10
+ ]);
11
+ if(choice==='back')return;
12
+ try{
13
+ const id=choice.startsWith('workflow:')?choice.slice(9):null;
14
+ if(choice==='new'){if(await session.execute('/new')===false)continue;}
15
+ else if(choice!=='draft'&&session.flow?.id!==id){
16
+ if(!await session.discard())continue;
17
+ session.flow=structuredClone(workflows.find(flow=>flow.id===id));session.dirty=false;
18
+ }
19
+ if(await editWorkflow(session)==='running')return;
20
+ }catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
21
+ }
22
+ }
23
+
24
+ async function editWorkflow(session){
25
+ const {app,ui}=session;
26
+ while(true){
27
+ const flow=session.current();
28
+ const choice=await ui.select(`${flow.title??flow.id} · ${flow.steps.length} 步 · ${session.dirty?'未保存':'已保存'}`, [
29
+ {value:'run',label:'运行工作流'},
30
+ ...(!app.project.readOnly?[
31
+ {value:'add',label:'添加步骤 / 控制流'}, {value:'steps',label:'编辑步骤、参数与顺序'},
32
+ {value:'outputs',label:'设置最终输出'},{value:'save',label:'保存工作流'},
33
+ {value:'delete',label:'删除工作流(保留可恢复备份)'},
34
+ ]:[{value:'steps',label:'查看步骤'}]),
35
+ {value:'back',label:'返回工作流列表(保留草稿)'},
36
+ ]);
37
+ if(choice==='back')return;
38
+ try{
39
+ if(choice==='delete'){
40
+ if(!await ui.confirm(`删除 ${flow.title??flow.id}?已保存文件会移入备份,未保存修改将放弃。`))continue;
41
+ const exists=(await app.listWorkflows()).some(w=>w.id===flow.id);
42
+ const backup=exists?await app.removeWorkflow(flow.id):null;
43
+ session.flow=null;session.dirty=false;ui.show(backup?`工作流已移入备份:${backup}`:'未保存的草稿已删除。');return;
44
+ }
45
+ if(choice==='run'){
46
+ if(await session.startRun([],{draft:true}))return 'running';
47
+ }else await session.execute('/'+choice);
48
+ }catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
49
+ }
50
+ }
@@ -3,12 +3,25 @@ import { FlowError } from '@mytools/sdk';
3
3
 
4
4
  const ajv = new Ajv({ allErrors: true, strict: false, useDefaults: true });
5
5
  const validators = new WeakMap();
6
+ const booleanValidators = new Map();
6
7
  export const emptyObject = { type: 'object', properties: {}, additionalProperties: false };
7
8
 
9
+ export function assertSchema(schema,label) {
10
+ if (!(typeof schema==='boolean'||schema&&typeof schema==='object'&&!Array.isArray(schema))) throw new FlowError(`${label} 必须声明有效的 JSON Schema`,{code:'VALIDATION'});
11
+ const cache=typeof schema==='boolean'?booleanValidators:validators;
12
+ let check=cache.get(schema);
13
+ if(!check){
14
+ // 每份定义独立解析 $id/$ref,避免插件重载或不同插件共用 ID 时串用约束。
15
+ // 编译结果仍按对象缓存;注册表随校验器释放,不污染后续安装批次。
16
+ try{check=new Ajv({allErrors:true,strict:false,useDefaults:true}).compile(schema);cache.set(schema,check);}
17
+ catch(cause){throw new FlowError(`${label} JSON Schema 无效:${cause.message}`,{code:'VALIDATION',cause});}
18
+ }
19
+ return check;
20
+ }
21
+
8
22
  export function validate(schema, value, label) {
9
- if (!schema) return structuredClone(value);
10
- let check = validators.get(schema);
11
- if (!check) { check = ajv.compile(schema); validators.set(schema, check); }
23
+ if (schema==null) return structuredClone(value);
24
+ const check = assertSchema(schema,label);
12
25
  const copy = structuredClone(value);
13
26
  if (!check(copy)) throw new FlowError(`${label}: ${ajv.errorsText(check.errors, { separator: ';' })}`, { code: 'VALIDATION', details: check.errors });
14
27
  return copy;
@@ -42,7 +55,7 @@ export function bind(value, context) {
42
55
  if (Object.hasOwn(value, '$ref')) {
43
56
  let current = context;
44
57
  for (const field of referenceParts(value.$ref)) {
45
- if (current == null || !Object.hasOwn(current, field)) throw new FlowError(`引用的值不存在:${value.$ref}`);
58
+ if (current == null || !Object.hasOwn(current, field)) throw new FlowError(`引用的值不存在:${value.$ref}`,{code:'MISSING_REFERENCE'});
46
59
  current = current[field];
47
60
  }
48
61
  return structuredClone(current);
@@ -1,2 +1,2 @@
1
1
  // 由 npm version 的生命周期脚本同步,内部插件 API 版本独立维护。
2
- export const VERSION="0.1.4";
2
+ export const VERSION="0.3.0";
@@ -3,7 +3,8 @@ import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import YAML from 'yaml';
5
5
  import { FlowError } from '@mytools/sdk';
6
- import { assertId, references, referenceParts, emptyObject } from './validation.js';
6
+ import { assertId, assertSchema, references, referenceParts, emptyObject } from './validation.js';
7
+ import {validateCondition,validateStepList} from './control.js';
7
8
 
8
9
  function fieldSchema(schema,fields,ref){
9
10
  let current=schema;
@@ -34,13 +35,7 @@ export class WorkflowStore {
34
35
  validateShape(w) {
35
36
  assertId(w?.id);
36
37
  if (w.schemaVersion!==1 || !Array.isArray(w.steps)) throw new FlowError('工作流格式错误:需要 schemaVersion: 1 与 steps 列表',{code:'VALIDATION'});
37
- const ids=new Set();
38
- for(const s of w.steps) {
39
- assertId(s.id);
40
- if(ids.has(s.id)) throw new FlowError(`步骤 ID 重复:${s.id}`,{code:'VALIDATION'});
41
- ids.add(s.id);
42
- if(Boolean(s.action)===Boolean(s.workflow) || (s.action && typeof s.action!=='string') || (s.workflow && typeof s.workflow!=='string')) throw new FlowError(`${s.id} 必须选择一个动作或子工作流`,{code:'VALIDATION'});
43
- }
38
+ validateStepList(w.steps,assertId);
44
39
  return structuredClone(w);
45
40
  }
46
41
  async save(definition) {
@@ -56,6 +51,17 @@ export class WorkflowStore {
56
51
  const files=await readdir(this.root);
57
52
  return Promise.all(files.filter(f=>f.endsWith('.yaml')).sort().map(f=>this.get(f.slice(0,-5))));
58
53
  }
54
+ async remove(id){
55
+ await this.get(id);
56
+ const refers=steps=>steps.some(step=>step.workflow&&this.childId(step.workflow)===id||step.branch&&(refers(step.branch.then)||refers(step.branch.else??[]))||step.parallel&&refers(step.parallel.steps));
57
+ const dependents=(await this.list()).filter(flow=>flow.id!==id&&refers(flow.steps));
58
+ if(dependents.length)throw new FlowError(`工作流仍被引用:${dependents.map(flow=>flow.title??flow.id).join('、')}`);
59
+ const directory=path.join(path.dirname(this.root),'.flow','deleted-workflows');
60
+ await mkdir(directory,{recursive:true});
61
+ const backup=path.join(directory,`${id}-${randomUUID()}.yaml`);
62
+ // 同项目内原子移动,备份保留完整原文;复制回原文件名即可恢复。
63
+ await rename(this.file(id),backup);return backup;
64
+ }
59
65
  childId(reference) {
60
66
  if(typeof reference!=='string' || !/^(\.\/)?[A-Za-z0-9][A-Za-z0-9_-]*\.yaml$/.test(reference)) throw new FlowError(`子流程必须位于工作流目录内:${reference}`,{code:'VALIDATION'});
61
67
  return reference.replace(/^\.\//,'').slice(0,-5);
@@ -66,21 +72,37 @@ export class WorkflowStore {
66
72
  if(visiting.has(id)) throw new FlowError(`工作流循环引用:${[...visiting,id].join(' → ')}`,{code:'VALIDATION'});
67
73
  if(workflows.has(id)) return;
68
74
  visiting.add(id);
69
- const w=await this.get(id), previous=new Map();
70
- const check=ref=>{
75
+ const w=await this.get(id);
76
+ const checker=previous=>ref=>{
71
77
  const p=referenceParts(ref);
72
78
  if(p[0]==='steps' && !previous.has(p[1])) throw new FlowError(`${id} 引用必须指向前序步骤:${ref}`,{code:'VALIDATION'});
73
79
  return fieldSchema(p[0]==='inputs'?(w.inputs??emptyObject):previous.get(p[1]),p.slice(p[0]==='inputs'?1:3),ref);
74
80
  };
75
- for(const s of w.steps) {
76
- references(s.with??{},check);
77
- if(s.action) {
78
- const action=host.actions.get(s.action);
79
- if(!action) throw new FlowError(`${id} → ${s.id} 缺少插件动作 ${s.action}`,{code:'VALIDATION'});
80
- plugins.add(action.plugin);
81
- previous.set(s.id,action.output);
82
- } else {const child=this.childId(s.workflow);await visit(child);previous.set(s.id,outputs.get(child));}
83
- }
81
+ const resolveList=async(steps,inherited=new Map(),parallel=false)=>{
82
+ const previous=new Map(inherited),own={};
83
+ for(const s of steps){
84
+ const check=checker(parallel?inherited:previous);
85
+ if(s.when)validateCondition(s.when,check);
86
+ let output;
87
+ if(s.action){
88
+ references(s.with??{},check);
89
+ const action=host.actions.get(s.action);
90
+ if(!action)throw new FlowError(`${id} → ${s.id} 缺少插件动作 ${s.action}`,{code:'VALIDATION'});
91
+ if(action.input===undefined||action.output===undefined)throw new FlowError(`可编排动作 ${s.action} 必须声明 input 和 output schema`,{code:'VALIDATION'});
92
+ assertSchema(action.input,`${s.action} input schema`);assertSchema(action.output,`${s.action} output schema`);
93
+ plugins.add(action.plugin);output=action.output;
94
+ }else if(s.workflow){references(s.with??{},check);const child=this.childId(s.workflow);await visit(child);output=outputs.get(child);}
95
+ else if(s.branch){
96
+ validateCondition(s.branch.condition,check);
97
+ const left=await resolveList(s.branch.then,parallel?inherited:previous),right=await resolveList(s.branch.else??[],parallel?inherited:previous);
98
+ output={type:'object',additionalProperties:false,properties:{...left.own,...right.own}};
99
+ }else if(s.parallel){const group=await resolveList(s.parallel.steps,parallel?inherited:previous,true);output={type:'object',additionalProperties:false,properties:group.own};}
100
+ else output=emptyObject;
101
+ previous.set(s.id,output);own[s.id]=output;
102
+ }
103
+ return {previous,own};
104
+ };
105
+ const {previous}=await resolveList(w.steps),check=checker(previous);
84
106
  references(w.outputs??{},check);
85
107
  const infer=value=>{
86
108
  if(value?.$ref)return check(value.$ref);
@@ -0,0 +1,47 @@
1
+ schemaVersion: 1
2
+ id: control-demo
3
+ title: 条件与并行入门
4
+ description: 检查系统后,根据你的选择继续或提前结束;继续时并行处理两段文本。运行中可切换步骤和查看日志。
5
+ inputs:
6
+ type: object
7
+ properties:
8
+ proceed:
9
+ type: boolean
10
+ title: 继续执行文本处理
11
+ default: true
12
+ required: [proceed]
13
+ steps:
14
+ - id: environment
15
+ action: "@mytools/environment:report"
16
+ with: {}
17
+ - id: guard
18
+ when:
19
+ left: {$ref: steps.environment.output.ok}
20
+ op: eq
21
+ right: false
22
+ stop:
23
+ reason: 环境检查未通过,已停止后续步骤。
24
+ - id: choose
25
+ branch:
26
+ condition:
27
+ left: {$ref: inputs.proceed}
28
+ op: eq
29
+ right: true
30
+ then:
31
+ - id: batch
32
+ parallel:
33
+ concurrency: 2
34
+ steps:
35
+ - id: first
36
+ action: "@mytools/examples:uppercase"
37
+ with: {text: "Hello, Fishtools!"}
38
+ - id: second
39
+ action: "@mytools/examples:uppercase"
40
+ with: {text: Parallel workflow}
41
+ else:
42
+ - id: finish
43
+ stop:
44
+ reason: 本次选择提前结束,没有执行文本处理。
45
+ outputs:
46
+ first: {$ref: steps.choose.output.batch.first.text}
47
+ second: {$ref: steps.choose.output.batch.second.text}
@@ -17,7 +17,7 @@ for item in names:
17
17
  packages.append({'name':name,'ok':False,'error':str(e)})
18
18
  print(json.dumps({'executable':sys.executable,'version':sys.version.split()[0],'prefix':sys.prefix,'packages':packages}))`;
19
19
 
20
- export default definePlugin({
20
+ const plugin={
21
21
  id:'@mytools/environment',version:'0.1.0',title:'环境检查',description:'检查目标机器的系统、Python/pip 与昇腾 NPU 环境,缺失必需条件时停止工作流。',
22
22
  platforms:['win32','linux'],commands:{'env.check':{description:'检查系统、Python/pip 或 NPU',action:'check'}},
23
23
  actions:{
@@ -77,4 +77,16 @@ export default definePlugin({
77
77
  },
78
78
  },
79
79
  },
80
- });
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);
@@ -9,6 +9,7 @@ import YAML from 'yaml';
9
9
  import { verifyBundle } from '@mytools/core/bundle';
10
10
  import {planEnvironments} from './environments.js';
11
11
  import {archiveDirectory} from './archive.js';
12
+ import {copyDocumentation} from '../core/src/documentation.js';
12
13
  export {verifyBundle};
13
14
 
14
15
  async function hashes(root,dir=root,result={}){
@@ -25,10 +26,12 @@ async function prepare(app,{entries,target,includeWeb=false,portable=false,envir
25
26
  if(!portable&&!entries?.length)throw new FlowError('请至少选择一个工作流');
26
27
  if(!['win32-x64','linux-x64-glibc'].includes(target))throw new FlowError('请选择支持的目标平台');
27
28
  const closure=await app.store.resolve(entries,app.host);
28
- const ids=new Set(portable?[...app.host.definitions.keys()].filter(id=>id!=='@mytools/web'||includeWeb):closure.plugins);
29
+ const webProvider=[...app.host.definitions.values()].find(plugin=>typeof plugin.services?.web==='function');
30
+ const ids=new Set(portable?[...app.host.definitions.keys()].filter(id=>id!==webProvider?.id||includeWeb):closure.plugins);
29
31
  for(const id of closure.plugins)ids.add(id);
32
+ if(includeWeb){if(!webProvider)throw new FlowError('未安装提供 Web 服务的插件');ids.add(webProvider.id);}
33
+ // Web 与其他入口使用相同的依赖闭包,包含提供者自身的传递依赖。
30
34
  for(const id of ids)for(const dependency of Object.keys(app.host.definitions.get(id)?.dependencies??{}))ids.add(dependency);
31
- if(includeWeb){if(!app.host.definitions.has('@mytools/web'))throw new FlowError('未安装官方 Web 插件');ids.add('@mytools/web');}
32
35
  const plugins=[...ids].map(id=>app.host.definitions.get(id));
33
36
  if(plugins.some(p=>!p.root))throw new FlowError('内存中临时定义的插件不能打包,请先安装为标准包');
34
37
  const overrides=structuredClone(app.project.environments??{});
@@ -84,6 +87,7 @@ export async function packApplication(app,{entries,target,outDir='dist',includeW
84
87
  for(const e of environmentPlan.entries.filter(e=>e.source==='bundled'))for(const p of packages.filter(p=>p.name===e.plugin)){
85
88
  const file=['app',p.path,e.executable].join('/');executablePaths.add(file);await chmod(path.join(directory,file),0o755);
86
89
  }
90
+ await copyDocumentation(path.join(directory,'manual'));
87
91
  const manifest={format:2,bundleId,editable:!readOnly,createdAt:new Date().toISOString(),target,runtime,entries,workflows:closure.workflows.map(w=>w.id),plugins:plugins.map(p=>({id:p.id,version:p.version,platforms:p.platforms,architectures:p.architectures})),environments:environmentPlan.entries,warnings:environmentPlan.warnings,packages,files:await hashes(directory)};
88
92
  await writeFile(path.join(directory,'manifest.json'),JSON.stringify(manifest,null,2));
89
93
  const archive=await archiveDirectory(directory,target,executablePaths);