@yanglaofish/fishtools 0.1.3 → 0.2.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 +53 -20
- package/manual/LICENSE +21 -0
- package/manual/README.md +173 -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/plans/2026-10-10-workbench-ui.md +27 -0
- package/{docs → manual/docs}/plugin-development.md +151 -19
- package/{docs → manual/docs}/releases.md +18 -1
- package/manual/docs/roadmap.md +85 -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/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-control-observation.md +37 -0
- package/{docs → manual/docs}/verification.md +20 -0
- 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/host.js +122 -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 +45 -0
- package/node_modules/@mytools/core/bin/fst.js +12 -4
- package/node_modules/@mytools/core/src/application.js +9 -4
- 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 +13 -2
- package/node_modules/@mytools/core/src/markdown-log.js +8 -5
- package/node_modules/@mytools/core/src/process.js +35 -14
- 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 +32 -3
- package/node_modules/@mytools/core/src/terminal/presentation.js +42 -0
- package/node_modules/@mytools/core/src/terminal/run-viewer.js +86 -0
- package/node_modules/@mytools/core/src/terminal/session.js +150 -31
- package/node_modules/@mytools/core/src/terminal/theme.js +28 -0
- package/node_modules/@mytools/core/src/terminal/view.js +92 -13
- package/node_modules/@mytools/core/src/validation.js +15 -4
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/core/src/workflows.js +30 -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 +2 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1094 -1095
- 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
|
@@ -38,8 +38,21 @@ export async function readRunInput(reference,baseDir){
|
|
|
38
38
|
|
|
39
39
|
export const newWorkflow=(title,description)=>({schemaVersion:1,id:`flow-${randomUUID().slice(0,8)}`,title,description,inputs:{type:'object',properties:{}},steps:[],outputs:{}});
|
|
40
40
|
|
|
41
|
+
// 路径从根 steps 开始,例如 [1, 'branch', 'then', 0]。
|
|
42
|
+
export const stepAt=(flow,index)=>[...(Array.isArray(index)?index:[index])].reduce((node,key)=>node[key],flow.steps);
|
|
43
|
+
export const sequenceAt=(flow,parts=[])=>parts.reduce((node,key)=>node[key],flow.steps);
|
|
44
|
+
export function precedingSteps(flow,index){
|
|
45
|
+
const parts=Array.isArray(index)?index:[index];let sequence=flow.steps;const result=[];
|
|
46
|
+
for(let i=0;i<parts.length;i+=3){
|
|
47
|
+
// 并行子项同属一批,不能把前面的兄弟当成已完成步骤。
|
|
48
|
+
if(i===0||parts[i-2]!=='parallel')result.push(...sequence.slice(0,parts[i]));
|
|
49
|
+
if(i+1<parts.length)sequence=sequence[parts[i]][parts[i+1]][parts[i+2]];
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
export function setBinding(flow,index,key,mode,schema,value){
|
|
42
|
-
const step=flow
|
|
55
|
+
const step=stepAt(flow,index),old=step.with?.[key];step.with??={};
|
|
43
56
|
if(mode==='runtime'){
|
|
44
57
|
flow.inputs??={type:'object',properties:{}};flow.inputs.properties??={};
|
|
45
58
|
const name=`fst_${step.id}_${key}`;
|
|
@@ -60,15 +73,31 @@ export function setBinding(flow,index,key,mode,schema,value){
|
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
|
|
76
|
+
export function pruneRuntimeInputs(flow){
|
|
77
|
+
const refs=new Set();
|
|
78
|
+
const scan=value=>{if(value&&typeof value==='object'){if(typeof value.$ref==='string')refs.add(value.$ref);Object.values(value).forEach(scan);}};
|
|
79
|
+
scan(flow.steps);scan(flow.outputs);
|
|
80
|
+
for(const name of Object.keys(flow.inputs?.properties??{})){
|
|
81
|
+
if(name.startsWith('fst_')&&!refs.has(`inputs.${name}`)){
|
|
82
|
+
delete flow.inputs.properties[name];flow.inputs.required=(flow.inputs.required??[]).filter(key=>key!==name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
export function stepMetadata(step,catalog,workflows){
|
|
88
|
+
if(step.branch||step.parallel){
|
|
89
|
+
const children=step.parallel?.steps??[...(step.branch.then??[]),...(step.branch.else??[])];
|
|
90
|
+
return {title:step.title??(step.branch?'条件分支':'并行组'),output:{type:'object',properties:Object.fromEntries(children.map(child=>[child.id,stepMetadata(child,catalog,workflows)?.output??{}]))}};
|
|
91
|
+
}
|
|
92
|
+
if(step.stop)return {title:step.title??'提前结束',output:{type:'object',properties:{}}};
|
|
64
93
|
if(step.action)return catalog.find(a=>a.id===step.action);
|
|
65
|
-
const flow=workflows.find(w
|
|
94
|
+
const flow=workflows.find(w=>`${w.id}.yaml`===step.workflow?.replace(/^\.\//,''));
|
|
66
95
|
return flow?{title:flow.title??flow.id,description:flow.description,input:flow.inputs,output:{type:'object',properties:Object.fromEntries(Object.keys(flow.outputs??{}).map(k=>[k,{}]))}}:null;
|
|
67
96
|
}
|
|
68
97
|
|
|
69
98
|
export function outputSources(flow,index,catalog,workflows){
|
|
70
99
|
const flatten=(schema,prefix='')=>Object.entries(schema?.properties??{}).flatMap(([key,s])=>{const name=prefix?`${prefix}.${key}`:key;return [{name,schema:s},...flatten(s,name)];});
|
|
71
|
-
return flow
|
|
100
|
+
return precedingSteps(flow,index).flatMap((step,i)=>{
|
|
72
101
|
const meta=stepMetadata(step,catalog,workflows),base=`steps.${step.id}.output`,label=`${i+1}. ${meta?.title??step.id}`;
|
|
73
102
|
return [{value:base,label:`${label} → 完整输出`,schema:meta?.output},...flatten(meta?.output).map(f=>({value:`${base}.${f.name}`,label:`${label} → ${f.schema.title??f.name}`,schema:f.schema}))];
|
|
74
103
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const logo=[
|
|
2
|
+
'█▀▀ ▀█▀ █▀▀ █ █',
|
|
3
|
+
'█▀ █ ▀▀█ █▀█',
|
|
4
|
+
'▀ ▀▀▀ ▀▀▀ ▀ ▀',
|
|
5
|
+
].join('\n');
|
|
6
|
+
|
|
7
|
+
export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed:'失败',stopped:'已停止'})[plugin.runtime?.status]??'未知';
|
|
8
|
+
|
|
9
|
+
export function gettingStarted(flows,{readOnly=false}={}){
|
|
10
|
+
const sample=flows.find(w=>w.id==='hello')??flows[0];
|
|
11
|
+
return [
|
|
12
|
+
sample?`试运行 /run ${sample.id}\n${sample.description??sample.title??'按提示填写参数,执行后查看结果。'}`:'当前工作区还没有工作流。',
|
|
13
|
+
'',
|
|
14
|
+
`/workflows 工作流 /plugins 插件`,
|
|
15
|
+
`${readOnly?'只读运行包': '/new 新建'} /runs 运行观察 /help 帮助`,
|
|
16
|
+
].join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fields(schema){
|
|
20
|
+
if(!schema)return '未声明';
|
|
21
|
+
if(!schema.properties)return `类型:${schema.type??'由插件约束定义'}`;
|
|
22
|
+
const entries=Object.entries(schema.properties);
|
|
23
|
+
return entries.length?entries.map(([key,s])=>`${s.title??key} (${key}, ${s.type??'任意类型'}${schema.required?.includes(key)?', 必填':''})${s.description?`:${s.description}`:''}`).join(';'):'无';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function pluginDetails(plugin,actions,commands){
|
|
27
|
+
return [
|
|
28
|
+
`# ${plugin.title??plugin.id}`,
|
|
29
|
+
plugin.description??'作者尚未提供用途说明。',
|
|
30
|
+
`标识:${plugin.id}\n\n版本:${plugin.version}\n\n来源:${plugin.removable?'自行安装(可移除)':'发行包内置 / 项目预置'}\n\n状态:${pluginStatus(plugin)}`,
|
|
31
|
+
...(plugin.runtime?.missing?.length?[`缺少服务:${plugin.runtime.missing.join(', ')}`]:[]),
|
|
32
|
+
`声明平台:${plugin.platforms?.join(', ')??'作者未声明'}\n\n声明架构:${plugin.architectures?.join(', ')??'作者未声明'}`,
|
|
33
|
+
'## 可用于编排的能力',
|
|
34
|
+
...(actions.length?actions.map(a=>`### ${a.title??a.id}\n\n${a.description??''}\n\n能力 ID:${a.id}\n\n输入:${fields(a.input)}\n\n输出:${fields(a.output)}`):['此插件没有提供编排动作,可能提供 Web、共享服务或交互命令。']),
|
|
35
|
+
'## 斜杠命令',commands.length?commands.map(c=>`- /${c.name}:${c.description??'未提供说明'}`).join('\n'):'没有注册斜杠命令。',
|
|
36
|
+
'## 插件配置',plugin.config?fields(plugin.config):'无需配置。',
|
|
37
|
+
'## 环境声明',
|
|
38
|
+
...[plugin,...actions].flatMap(item=>Object.entries(item.environments??{}).map(([name,e])=>`- ${item.id} / ${name}:${({host:'使用宿主机环境',bundled:'随包携带',tool:'使用 Fishtools 自带环境'})[e.source]??e.source}`)),
|
|
39
|
+
'环境与平台由插件作者声明;具体执行结果请在 /logs 查看。',
|
|
40
|
+
'编排用法:/new 新建工作流 → /add 选择上述能力 → 填写参数 → /save → /run。',
|
|
41
|
+
].join('\n\n');
|
|
42
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { Text, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
3
|
+
import {palette} from './theme.js';
|
|
4
|
+
|
|
5
|
+
export const runStatuses={pending:'待执行',running:'运行中',succeeded:'成功',failed:'失败',cancelled:'已取消',skipped:'已跳过',stopped:'已停止'};
|
|
6
|
+
const safe=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
7
|
+
export const mouseWheel=data=>{const match=/^\x1b\[<(\d+);\d+;\d+[Mm]$/.exec(data);return match&&(Number(match[1])&64)?(Number(match[1])&1?1:-1):0;};
|
|
8
|
+
export const mouseOn='\x1b[?1000h\x1b[?1006h';
|
|
9
|
+
export const mouseOff='\x1b[?1006l\x1b[?1000l';
|
|
10
|
+
|
|
11
|
+
export class RunViewer{
|
|
12
|
+
constructor({record,flow,workflows=[],terminal,onClose,onCancel,requestRender}){
|
|
13
|
+
Object.assign(this,{terminal,onClose,onCancel,requestRender});this.view=0;this.selected=0;this.offset=0;this.follow=true;this.allLogs=false;this.confirmCancel=false;
|
|
14
|
+
this.planned=[];
|
|
15
|
+
const walk=(steps,prefix,ancestors)=>{
|
|
16
|
+
for(const step of steps??[]){
|
|
17
|
+
const callPath=`${prefix}/${step.id}`;this.planned.push({callPath,stepId:step.id,action:step.action,title:step.title,status:'pending'});
|
|
18
|
+
if(step.branch){walk(step.branch.then,callPath,ancestors);walk(step.branch.else,callPath,ancestors);}
|
|
19
|
+
if(step.parallel)walk(step.parallel.steps,callPath,ancestors);
|
|
20
|
+
if(step.workflow){const child=workflows.find(w=>`${w.id}.yaml`===step.workflow.replace(/^\.\//,''));if(child&&!ancestors.has(child.id))walk(child.steps,callPath,new Set([...ancestors,child.id]));}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
if(flow)walk(flow.steps,flow.id,new Set([flow.id]));
|
|
24
|
+
this.update(record);
|
|
25
|
+
}
|
|
26
|
+
invalidate(){}
|
|
27
|
+
update(record){
|
|
28
|
+
if(!record)return;
|
|
29
|
+
this.record=record;const nodes=new Map(this.planned.map(node=>[node.callPath,{...node}]));
|
|
30
|
+
for(const event of record.events??[]){
|
|
31
|
+
if(!event.type.startsWith('step.')||!event.callPath)continue;
|
|
32
|
+
const node=nodes.get(event.callPath)??{callPath:event.callPath,stepId:event.stepId};
|
|
33
|
+
Object.assign(node,event,{status:event.type==='step.started'?'running':event.type.slice(5)});nodes.set(event.callPath,node);
|
|
34
|
+
}
|
|
35
|
+
this.nodes=[...nodes.values()];this.selected=Math.max(0,Math.min(this.selected,this.nodes.length-1));
|
|
36
|
+
this.requestRender();
|
|
37
|
+
}
|
|
38
|
+
content(){
|
|
39
|
+
const node=this.nodes[this.selected],status=runStatuses[this.record.status]??this.record.status;
|
|
40
|
+
if(this.view===0)return [`运行:${status}${this.record.reason?` · ${this.record.reason}`:''}`,this.record.error?`错误:${this.record.error.message}`:'',...this.nodes.map((n,i)=>`${i===this.selected?'›':' '} ${runStatuses[n.status]??n.status} · ${n.callPath}${n.action?` · ${n.action}`:''}${n.message?` · ${n.message}`:''}`)].filter(Boolean).join('\n');
|
|
41
|
+
if(this.view===1)return `步骤:${node?.callPath??'运行结果'}\n输入\n${JSON.stringify(node?.input??{},null,2)}\n输出\n${JSON.stringify(node?.output??{},null,2)}${node?.error?`\n错误\n${JSON.stringify(node.error,null,2)}`:''}${node?.reason||node?.message?`\n说明:${node.reason??node.message}`:''}\n运行状态:${status}${this.record.reason?` · ${this.record.reason}`:''}\n汇总输出\n${JSON.stringify(this.record.output??{},null,2)}`;
|
|
42
|
+
const logs=(this.record.events??[]).filter(e=>e.type==='log'&&(this.allLogs||!node||e.callPath===node.callPath));
|
|
43
|
+
return logs.length?logs.map(e=>`${this.allLogs?`[${e.callPath??'运行'}] `:''}[${e.stream??'日志'}] ${e.message??''}${e.data?` ${JSON.stringify(e.data)}`:''}`).join('\n'):'尚无日志';
|
|
44
|
+
}
|
|
45
|
+
layout(width){
|
|
46
|
+
const height=Math.max(1,this.terminal.rows),pageSize=Math.max(1,height-3);
|
|
47
|
+
const lines=new Text(safe(this.content()),0,0).render(Math.max(1,width));
|
|
48
|
+
const maxOffset=Math.max(0,lines.length-pageSize);
|
|
49
|
+
this.offset=this.follow&&this.view===2?maxOffset:Math.max(0,Math.min(this.offset,maxOffset));
|
|
50
|
+
return {height,pageSize,lines,maxOffset};
|
|
51
|
+
}
|
|
52
|
+
render(width){
|
|
53
|
+
const {height,pageSize,lines}=this.layout(width),node=this.nodes[this.selected];
|
|
54
|
+
const body=lines.slice(this.offset,this.offset+pageSize);while(body.length<pageSize)body.push('');
|
|
55
|
+
const tabs=`${['1 总览','2 输入输出','3 日志'].map((s,i)=>this.view===i?palette.brand(`[${s}]`):palette.muted(s)).join(' ')} ${palette.muted('·')} ${palette.text(safe(runStatuses[this.record.status]??this.record.status))} ${palette.muted('· '+safe(this.record.workflowId??'运行'))}`;
|
|
56
|
+
const selection=`[ / ] 切换步骤:${node?.callPath??'暂无步骤'}${this.view===2?` · ${this.allLogs?'全部日志':'本步骤日志'} · ${this.follow?'跟随尾部':'暂停跟随'}`:''}`;
|
|
57
|
+
const footer=this.confirmCancel?'取消整个运行?y 确认 · 任意其他键返回':`q/Esc 关闭 · x 取消 · ↑↓/滚轮 PgUp/PgDn · End 跟随 · a 全部日志 · ${this.offset+1}–${Math.min(this.offset+pageSize,lines.length)}/${lines.length}`;
|
|
58
|
+
const styledBody=body.map(line=>line.startsWith('›')?palette.brand(line):line);
|
|
59
|
+
const styledFooter=palette.muted(safe(footer));
|
|
60
|
+
const screen=height===1?styledBody:height===2?[styledBody[0],styledFooter]:height===3?[tabs,styledBody[0],styledFooter]:[tabs,palette.muted(safe(selection)),...styledBody,styledFooter];
|
|
61
|
+
return screen.map(line=>truncateToWidth(line,width));
|
|
62
|
+
}
|
|
63
|
+
handleInput(data){
|
|
64
|
+
if(this.confirmCancel){this.confirmCancel=false;if(data==='y')this.onCancel();this.requestRender();return;}
|
|
65
|
+
if(['escape','q','enter'].some(key=>matchesKey(data,key))){this.onClose();return;}
|
|
66
|
+
if(data==='x'&&this.record.status==='running'){this.confirmCancel=true;this.requestRender();return;}
|
|
67
|
+
if(['1','2','3'].includes(data)){this.view=Number(data)-1;this.offset=0;this.follow=true;this.requestRender();return;}
|
|
68
|
+
if(data==='['||data===']'){
|
|
69
|
+
this.selected=Math.max(0,Math.min(this.nodes.length-1,this.selected+(data===']'?1:-1)));this.offset=0;
|
|
70
|
+
if(this.view===0)this.offset=Math.max(0,this.selected-Math.floor((this.terminal.rows-3)/2));
|
|
71
|
+
this.requestRender();return;
|
|
72
|
+
}
|
|
73
|
+
if(data==='a'){this.allLogs=!this.allLogs;this.offset=0;this.requestRender();return;}
|
|
74
|
+
if(data==='f'){this.follow=!this.follow;this.requestRender();return;}
|
|
75
|
+
const {pageSize,maxOffset}=this.layout(this.terminal.columns),wheel=mouseWheel(data);
|
|
76
|
+
if(wheel)this.offset+=wheel*3;
|
|
77
|
+
else if(matchesKey(data,'up'))this.offset--;
|
|
78
|
+
else if(matchesKey(data,'down'))this.offset++;
|
|
79
|
+
else if(matchesKey(data,'pageUp'))this.offset-=pageSize;
|
|
80
|
+
else if(matchesKey(data,'pageDown'))this.offset+=pageSize;
|
|
81
|
+
else if(matchesKey(data,'home'))this.offset=0;
|
|
82
|
+
else if(matchesKey(data,'end')){this.offset=maxOffset;this.follow=true;this.requestRender();return;}
|
|
83
|
+
else return;
|
|
84
|
+
this.follow=false;this.offset=Math.max(0,Math.min(maxOffset,this.offset));this.requestRender();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { commands, parseCommand, fileReference, readRunInput, newWorkflow, setBinding, stepMetadata, outputSources } from './model.js';
|
|
3
|
+
import { commands, parseCommand, fileReference, readRunInput, newWorkflow, setBinding, stepMetadata, outputSources, stepAt, sequenceAt, pruneRuntimeInputs } from './model.js';
|
|
4
|
+
import { editCondition } from './conditions.js';
|
|
5
|
+
import { runStatuses } from './run-viewer.js';
|
|
4
6
|
import { editValue, needsJsonEditor } from './schema.js';
|
|
7
|
+
import {gettingStarted,pluginStatus,pluginDetails} from './presentation.js';
|
|
5
8
|
|
|
6
9
|
export class TerminalSession{
|
|
7
10
|
constructor(app,ui,{baseDir=process.cwd(),webOptions={}}={}){this.app=app;this.ui=ui;this.baseDir=baseDir;this.webOptions=webOptions;this.flow=null;this.dirty=false;this.exiting=false;this.web=null;}
|
|
@@ -9,14 +12,23 @@ export class TerminalSession{
|
|
|
9
12
|
current(){if(!this.flow)throw new Error('先用 /workflows 打开工作流,或用 /new 新建');return this.flow;}
|
|
10
13
|
commands(){return [...commands,...this.app.listCommands()];}
|
|
11
14
|
async discard(){return !this.dirty||await this.ui.confirm('当前修改未保存,是否放弃修改?');}
|
|
15
|
+
async mayExit(){
|
|
16
|
+
if(!await this.discard())return false;
|
|
17
|
+
const active=this.app.listRuns().filter(run=>run.status==='running');
|
|
18
|
+
if(active.length){
|
|
19
|
+
if(!await this.ui.confirm(`仍有 ${active.length} 个后台任务。退出工作台将取消这些任务,是否退出?`))return false;
|
|
20
|
+
for(const run of active)this.app.cancelRun(run.id);
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
12
24
|
async run(){
|
|
13
|
-
this.ui.show(
|
|
25
|
+
this.ui.show(gettingStarted(await this.app.listWorkflows(),this.app.project),{kind:'welcome'});
|
|
14
26
|
try{
|
|
15
27
|
while(!this.exiting){
|
|
16
|
-
this.ui.summary(
|
|
28
|
+
this.ui.summary(`欢迎使用 Fishtools · 插件编排工作台\n当前工作区:${this.app.projectDir}\n${this.app.listPlugins().length} 个插件 · ${(await this.app.listWorkflows()).length} 个工作流${this.flow?` · ${this.flow.title??this.flow.id} · ${this.dirty?'未保存':'已保存'}`:''}`);
|
|
17
29
|
let text;
|
|
18
|
-
try{text=await this.ui.input('fst > 输入 /
|
|
19
|
-
catch(e){if(e.code!=='PROMPT_CANCELLED')throw e;try{if(await this.
|
|
30
|
+
try{text=await this.ui.input('fst > 输入 / 选择操作,@ 引用文件',{commands:this.commands()});}
|
|
31
|
+
catch(e){if(e.code!=='PROMPT_CANCELLED')throw e;try{if(await this.mayExit())break;}catch{}continue;}
|
|
20
32
|
if(!text.trim())continue;
|
|
21
33
|
try{await this.execute(text);}catch(e){this.ui.show(e.code==='PROMPT_CANCELLED'?'已返回,未完成的修改已放弃。':`操作失败:${e.message}`);}
|
|
22
34
|
}
|
|
@@ -28,8 +40,27 @@ export class TerminalSession{
|
|
|
28
40
|
if(pluginCommand)return this.pluginCommand(pluginCommand,args);
|
|
29
41
|
if(command!=='run'&&args.length)throw new Error(`/${command} 使用交互选择,请不要附加参数`);
|
|
30
42
|
switch(command){
|
|
31
|
-
case 'help':this.ui.
|
|
32
|
-
|
|
43
|
+
case 'help':await this.ui.viewMarkdown('Fishtools 使用指南',[
|
|
44
|
+
'# Fishtools',
|
|
45
|
+
'将插件能力组合为工作流,在终端内编辑、运行并查看结果。',
|
|
46
|
+
`当前工作区:${this.app.projectDir}`,
|
|
47
|
+
`文件路径起点:${this.baseDir}`,
|
|
48
|
+
'## 常用操作',gettingStarted(await this.app.listWorkflows(),this.app.project),
|
|
49
|
+
'## 编排工作流','/new 新建 → /add 添加步骤 → /steps 编辑参数、条件与分组 → /save 保存 → /run 执行。',
|
|
50
|
+
'## 观察运行','/runs 打开观察面板。1 / 2 / 3 切换步骤总览、输入输出和日志,[ / ] 切换步骤。↑↓、PgUp/PgDn 或滚轮浏览;End 恢复尾部跟随。q / Esc 关闭后任务在后台继续,x 后 y 明确取消。',
|
|
51
|
+
'## 文件与运行参数','输入 @ 后按 Tab 选择文件;@@ 表示字面量 @。运行命令的 @参数.json 会读取 JSON 对象作为输入。',
|
|
52
|
+
'/run 工作流ID @"参数 文件.json"',
|
|
53
|
+
'## 离线文档','输入 /docs 阅读插件开发、部署、控制流与服务文档。独立命令 fst docs --path 查看资料位置,fst docs --export 目标目录 导出资料。',
|
|
54
|
+
'## 全部操作',this.commands().map(c=>`- /${c.name}:${c.description}`).join('\n'),
|
|
55
|
+
].join('\n\n'));return;
|
|
56
|
+
case 'docs':{
|
|
57
|
+
const {listDocumentation,readDocumentation}=await import('../documentation.js');
|
|
58
|
+
const topics=await listDocumentation();
|
|
59
|
+
const topic=await this.ui.select('离线文档', [...topics.map(item=>({value:item.id,label:item.title})),{value:'back',label:'返回工作台'}]);
|
|
60
|
+
if(topic!=='back'){const document=await readDocumentation(topic);await this.ui.viewMarkdown(document.title,document.text);}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
case 'exit':if(await this.mayExit())this.exiting=true;return;
|
|
33
64
|
case 'workflows':{
|
|
34
65
|
const flows=await this.app.listWorkflows();
|
|
35
66
|
if(!flows.length){this.ui.show('没有工作流,输入 /new 新建。');return;}
|
|
@@ -48,6 +79,7 @@ export class TerminalSession{
|
|
|
48
79
|
case 'outputs':return this.outputs();
|
|
49
80
|
case 'save':this.writable();await this.app.saveWorkflow(this.current());this.dirty=false;this.ui.show(`已保存:${this.flow.title}`);return;
|
|
50
81
|
case 'run':return this.startRun(args);
|
|
82
|
+
case 'runs':return this.runs();
|
|
51
83
|
case 'logs':return this.logs();
|
|
52
84
|
case 'plugins':return this.plugins();
|
|
53
85
|
case 'config':return this.configure();
|
|
@@ -69,17 +101,17 @@ export class TerminalSession{
|
|
|
69
101
|
try{const result=await run.done;this.ui.show(`${result.status}\n${JSON.stringify(result.error??result.output,null,2)}\n输入 /logs 在终端内查看日志\n可读日志:${path.join(this.app.outputDir,run.id,'run.md')}`);}
|
|
70
102
|
finally{unsubscribe();done();}
|
|
71
103
|
}
|
|
72
|
-
async configure(){
|
|
104
|
+
async configure(selectedId){
|
|
73
105
|
this.writable();
|
|
74
106
|
const plugins=this.app.listPlugins().filter(p=>p.config);
|
|
75
107
|
if(!plugins.length){this.ui.show('当前插件没有声明可配置字段。');return;}
|
|
76
|
-
const id=await this.ui.select('选择要配置的插件',plugins.map(p=>({value:p.id,label:p.title??p.id})));
|
|
108
|
+
const id=selectedId??await this.ui.select('选择要配置的插件',plugins.map(p=>({value:p.id,label:p.title??p.id})));
|
|
77
109
|
const plugin=plugins.find(p=>p.id===id);
|
|
78
110
|
const value=await editValue(this.ui,plugin.config,this.app.project.config?.[id]??{}, {baseDir:this.baseDir,required:true,label:plugin.title??id});
|
|
79
111
|
await this.app.configurePlugin(id,value);this.ui.show('插件配置已保存到当前工作区。');
|
|
80
112
|
}
|
|
81
113
|
async logs(){
|
|
82
|
-
const statuses=
|
|
114
|
+
const statuses=runStatuses;
|
|
83
115
|
while(true){
|
|
84
116
|
const history=await this.app.listRunHistory(),keep=this.app.logSettings().keepRuns;
|
|
85
117
|
const choice=await this.ui.select(`运行日志 · ${keep===null?'不自动清理':`项目保留最近 ${keep} 次`}`,[
|
|
@@ -100,17 +132,67 @@ export class TerminalSession{
|
|
|
100
132
|
}
|
|
101
133
|
async add(){
|
|
102
134
|
this.writable();const flow=structuredClone(this.current()),catalog=this.app.catalog(),workflows=await this.app.listWorkflows();
|
|
103
|
-
|
|
135
|
+
await this.addNode(flow,[],catalog,workflows);
|
|
136
|
+
this.flow=flow;this.dirty=true;this.ui.show('步骤已添加。/add 继续添加,/steps 调整,/save 保存。');
|
|
137
|
+
}
|
|
138
|
+
async addNode(flow,parent,catalog,workflows){
|
|
139
|
+
this.writable();
|
|
140
|
+
const choice=await this.ui.select('添加能力、子工作流或控制节点',[
|
|
104
141
|
...catalog.map(a=>({value:`action:${a.id}`,label:a.title??a.id,description:a.description})),
|
|
105
142
|
...workflows.filter(w=>w.id!==flow.id).map(w=>({value:`workflow:${w.id}`,label:`工作流:${w.title}`,description:w.description})),
|
|
143
|
+
{value:'control:branch',label:'条件分支'},{value:'control:parallel',label:'并行组'},{value:'control:stop',label:'提前结束'},
|
|
106
144
|
]);
|
|
107
|
-
const step={id:`step-${randomUUID().slice(0,8)}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
145
|
+
const sequence=sequenceAt(flow,parent),step={id:`step-${randomUUID().slice(0,8)}`},index=[...parent,sequence.length];
|
|
146
|
+
sequence.push(step);
|
|
147
|
+
if(choice==='control:branch')step.branch={condition:await editCondition(this.ui,flow,index,catalog,workflows,{baseDir:this.baseDir}),then:[],else:[]};
|
|
148
|
+
else if(choice==='control:parallel')step.parallel={concurrency:4,steps:[]};
|
|
149
|
+
else if(choice==='control:stop')step.stop={reason:''};
|
|
150
|
+
else{step.with={};if(choice.startsWith('action:'))step.action=choice.slice(7);else step.workflow=`./${choice.slice(9)}.yaml`;}
|
|
151
|
+
await this.editNode(flow,index,catalog,workflows);
|
|
152
|
+
}
|
|
153
|
+
async editNode(flow,index,catalog,workflows){
|
|
154
|
+
this.writable();const step=stepAt(flow,index);
|
|
155
|
+
if(step.stop){step.stop.reason=await this.ui.input('提前结束原因',{value:step.stop.reason,files:false});return;}
|
|
156
|
+
if(step.parallel){
|
|
157
|
+
const value=Number(await this.ui.input('最大并发数(1–32)',{value:String(step.parallel.concurrency??4),files:false}));
|
|
158
|
+
if(!Number.isInteger(value)||value<1||value>32)throw new Error('并发数必须为 1–32 的整数');
|
|
159
|
+
step.parallel.concurrency=value;await this.editSequence(flow,[...index,'parallel','steps'],catalog,workflows);return;
|
|
160
|
+
}
|
|
161
|
+
if(step.branch){
|
|
162
|
+
step.branch.else??=[];
|
|
163
|
+
while(true){
|
|
164
|
+
const choice=await this.ui.select('条件分支', [{value:'condition',label:'修改判断条件'},{value:'then',label:`条件成立 · ${step.branch.then.length} 步`},{value:'else',label:`条件不成立 · ${step.branch.else.length} 步`},{value:'done',label:'完成分支编辑'}]);
|
|
165
|
+
if(choice==='done')return;
|
|
166
|
+
if(choice==='condition')step.branch.condition=await editCondition(this.ui,flow,index,catalog,workflows,{baseDir:this.baseDir,current:step.branch.condition});
|
|
167
|
+
else await this.editSequence(flow,[...index,'branch',choice],catalog,workflows);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return this.editBindings(flow,index,catalog,workflows);
|
|
171
|
+
}
|
|
172
|
+
async editSequence(flow,parent,catalog,workflows){
|
|
173
|
+
const sequence=sequenceAt(flow,parent);
|
|
174
|
+
while(true){
|
|
175
|
+
const choice=await this.ui.select('嵌套步骤', [...sequence.map((s,i)=>({value:`step:${i}`,label:`${i+1}. ${stepMetadata(s,catalog,workflows)?.title??s.id}`})),{value:'add',label:'添加步骤'},{value:'done',label:'完成此组'}]);
|
|
176
|
+
if(choice==='done')return;
|
|
177
|
+
if(choice==='add')await this.addNode(flow,parent,catalog,workflows);
|
|
178
|
+
else await this.stepOperation(flow,[...parent,Number(choice.slice(5))],catalog,workflows);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async stepOperation(flow,index,catalog,workflows){
|
|
182
|
+
this.writable();const step=stepAt(flow,index),parent=index.slice(0,-1),position=index.at(-1),sequence=sequenceAt(flow,parent);
|
|
183
|
+
const action=await this.ui.select('步骤操作',[{value:'edit',label:step.branch||step.parallel||step.stop?'编辑控制节点与嵌套步骤':'设置参数'},{value:'when',label:'设置执行条件'},...(step.when?[{value:'clear-when',label:'移除执行条件'}]:[]),...(position>0?[{value:'up',label:'上移一步'}]:[]),...(position<sequence.length-1?[{value:'down',label:'下移一步'}]:[]),{value:'remove',label:'删除此步骤'}]);
|
|
184
|
+
if(action==='edit')await this.editNode(flow,index,catalog,workflows);
|
|
185
|
+
else if(action==='when')step.when=await editCondition(this.ui,flow,index,catalog,workflows,{baseDir:this.baseDir,current:step.when});
|
|
186
|
+
else if(action==='clear-when')delete step.when;
|
|
187
|
+
else if(action==='remove'){
|
|
188
|
+
if(!await this.ui.confirm('删除此步骤?引用它的步骤或输出需要重新设置。'))return;
|
|
189
|
+
for(const key of Object.keys(step.with??{}))setBinding(flow,index,key,'fixed',{},undefined);
|
|
190
|
+
sequence.splice(position,1);
|
|
191
|
+
pruneRuntimeInputs(flow);
|
|
192
|
+
}else{const to=position+(action==='up'?-1:1);[sequence[position],sequence[to]]=[sequence[to],sequence[position]];}
|
|
111
193
|
}
|
|
112
194
|
async editBindings(flow,index,catalog,workflows){
|
|
113
|
-
const step=flow
|
|
195
|
+
const step=stepAt(flow,index),meta=stepMetadata(step,catalog,workflows);
|
|
114
196
|
if(!meta)throw new Error('该步骤所需插件或子工作流不可用');
|
|
115
197
|
if(needsJsonEditor(meta.input)||!meta.input?.properties){
|
|
116
198
|
step.with=await editValue(this.ui,meta.input??{type:'object'},step.with,{baseDir:this.baseDir,required:true,label:`${meta.title} · 完整参数 JSON`});return;
|
|
@@ -143,13 +225,7 @@ export class TerminalSession{
|
|
|
143
225
|
const id=await this.ui.select('选择步骤',flow.steps.map((s,i)=>({value:s.id,label:`${i+1}. ${stepMetadata(s,catalog,workflows)?.title??s.id}`,description:JSON.stringify(s.with)})));
|
|
144
226
|
const index=flow.steps.findIndex(s=>s.id===id);
|
|
145
227
|
if(this.app.project.readOnly){this.ui.show(JSON.stringify(flow.steps[index],null,2));return;}
|
|
146
|
-
|
|
147
|
-
if(action==='edit')await this.editBindings(flow,index,catalog,workflows);
|
|
148
|
-
else if(action==='remove'){
|
|
149
|
-
if(!await this.ui.confirm('删除此步骤?引用它的步骤或输出需要重新设置。'))return;
|
|
150
|
-
for(const key of Object.keys(flow.steps[index].with??{}))setBinding(flow,index,key,'fixed',{},undefined);
|
|
151
|
-
flow.steps.splice(index,1);
|
|
152
|
-
}else{const to=index+(action==='up'?-1:1);[flow.steps[index],flow.steps[to]]=[flow.steps[to],flow.steps[index]];}
|
|
228
|
+
await this.stepOperation(flow,[index],catalog,workflows);
|
|
153
229
|
this.flow=flow;this.dirty=true;this.ui.show('步骤已更新,保存前可继续调整。');
|
|
154
230
|
}
|
|
155
231
|
async outputs(){
|
|
@@ -178,32 +254,75 @@ export class TerminalSession{
|
|
|
178
254
|
await this.app.checkWorkflow(flow.id);
|
|
179
255
|
const input=reference?await readRunInput(reference,this.baseDir):(needsJsonEditor(flow.inputs)||Object.keys(flow.inputs?.properties??{}).length)?await editValue(this.ui,flow.inputs,{}, {baseDir:this.baseDir,label:'本次运行参数',required:true}):{};
|
|
180
256
|
const run=await this.app.startRun(flow.id,input);
|
|
257
|
+
if(this.ui.observeRun){
|
|
258
|
+
await this.ui.observeRun({app:this.app,runId:run.id,flow,workflows:await this.app.listWorkflows()});
|
|
259
|
+
this.ui.show(`已关闭运行观察面板。输入 /runs 重新查看,/logs 查看历史日志。`);return;
|
|
260
|
+
}
|
|
181
261
|
const unsubscribe=this.app.subscribe(e=>{if(e.runId===run.id&&e.type.startsWith('step.'))this.ui.show(`${e.type} · ${e.callPath}${e.message?' · '+e.message:''}`);});
|
|
182
262
|
const done=this.ui.busy('运行中 · Ctrl+C 取消当前运行',()=>this.app.cancelRun(run.id));
|
|
183
263
|
try{
|
|
184
264
|
const result=await run.done;
|
|
185
|
-
this.ui.show(`运行${result.status
|
|
265
|
+
this.ui.show(`运行${runStatuses[result.status]??result.status}\n${JSON.stringify(result.reason??(result.status==='succeeded'?result.output:result.error),null,2)}`);
|
|
186
266
|
if(result.status==='succeeded'&&!Object.keys(result.output??{}).length)this.ui.show(JSON.stringify(result.events.filter(e=>e.type==='step.succeeded').map(e=>({step:e.callPath,output:e.output})),null,2));
|
|
187
267
|
this.ui.show(`输入 /logs 在终端内查看日志\n可读日志:${path.join(this.app.outputDir,run.id,'run.md')}\n结果目录:${path.join(this.app.outputDir,run.id)}`);
|
|
188
268
|
}finally{unsubscribe();done();}
|
|
189
269
|
}
|
|
270
|
+
async runs(){
|
|
271
|
+
const runs=this.app.listRuns();
|
|
272
|
+
if(!runs.length){this.ui.show('当前会话还没有运行。/run 执行工作流,/logs 查看历史记录。');return;}
|
|
273
|
+
const id=await this.ui.select('运行观察 · 关闭面板后后台继续', [...runs.slice().reverse().map(run=>({value:run.id,label:`${runStatuses[run.status]??run.status} · ${run.workflowId}`,description:run.id})),{value:'back',label:'返回工作台'}]);
|
|
274
|
+
if(id==='back')return;
|
|
275
|
+
const workflows=await this.app.listWorkflows(),record=this.app.getRun(id);
|
|
276
|
+
await this.ui.observeRun({app:this.app,runId:id,flow:workflows.find(flow=>flow.id===record.workflowId),workflows});
|
|
277
|
+
}
|
|
190
278
|
async plugins(){
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
279
|
+
this.ui.clearMessages?.();
|
|
280
|
+
while(true){
|
|
281
|
+
const plugins=this.app.listPlugins();
|
|
282
|
+
const action=await this.ui.select(`已安装插件(${plugins.length})· ↑↓ 选择插件查看能力与配置`,[
|
|
283
|
+
...plugins.map(p=>({value:`plugin:${p.id}`,label:`${p.title??p.id} · v${p.version} · ${pluginStatus(p)}`,description:p.description??p.id})),
|
|
284
|
+
...(!this.app.project.readOnly?[{value:'local',label:'+ 安装本地插件 / @路径'},{value:'npm',label:'+ 安装 npm 插件包'},{value:'maintenance',label:this.app.maintenance?'恢复插件服务':'进入维护模式(停止有状态服务)'},...(this.web?[{value:'stop-web',label:'停止 Web 工作台'}]:[])]:[]),{value:'back',label:'返回工作台'},
|
|
285
|
+
]);
|
|
286
|
+
if(action.startsWith('plugin:')){
|
|
287
|
+
try{await this.pluginPage(action.slice(7));}catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
194
290
|
if(action==='maintenance'){await this.app.setMaintenance(!this.app.maintenance);this.ui.show(this.app.maintenance?'已进入维护模式,可修改插件与配置。':'插件服务已恢复。');return;}
|
|
195
291
|
if(action==='stop-web'){await this.web.close();this.web=null;this.ui.show('Web 已停止。');return;}
|
|
196
292
|
if(action==='back')return;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if(await this.ui.confirm(`移除 ${id}?已有工作流可能依赖它。`))await this.app.removePlugin(id);
|
|
200
|
-
}else{
|
|
293
|
+
this.writable();
|
|
294
|
+
if(action==='local'||action==='npm'){
|
|
201
295
|
const text=await this.ui.input(action==='npm'?'npm 包名(支持 @scope/name)':'输入 @ 并选择插件目录或 .tgz',{files:action==='local'});
|
|
202
296
|
const source=action==='local'?fileReference(text,this.baseDir):text.trim();if(!source)throw new Error('插件来源不能为空');
|
|
203
297
|
if(!await this.ui.confirm(`安装并加载可信插件:${source}?`))return;
|
|
204
298
|
const done=this.ui.busy('正在安装插件…');try{await this.app.installPlugin(source);}finally{done();}
|
|
205
299
|
}
|
|
206
300
|
this.ui.show('插件列表已更新。');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async pluginPage(id){
|
|
304
|
+
while(true){
|
|
305
|
+
const plugin=this.app.listPlugins().find(p=>p.id===id);if(!plugin)return;
|
|
306
|
+
const definition=this.app.host.definitions.get(id);
|
|
307
|
+
// 详情显示作者声明;维护模式只停止执行,不能让能力说明消失。
|
|
308
|
+
const actions=Object.entries(definition.actions??{}).map(([name,a])=>({...a,id:`${id}:${name}`}));
|
|
309
|
+
const declaredCommands=Object.entries(definition.commands??{}).map(([name,c])=>({...c,name}));
|
|
310
|
+
const commands=this.app.listCommands().filter(c=>c.plugin===id);
|
|
311
|
+
const choice=await this.ui.select(`${plugin.title??id} · ${pluginStatus(plugin)} · ${actions.length} 项能力`,[
|
|
312
|
+
{value:'details',label:'查看用途、能力、输入输出和环境要求'},
|
|
313
|
+
...(!this.app.project.readOnly&&plugin.config?[{value:'configure',label:'设置插件配置'}]:[]),
|
|
314
|
+
...commands.map(c=>({value:`command:${c.name}`,label:`运行 /${c.name}`,description:c.description})),
|
|
315
|
+
...(!this.app.project.readOnly&&plugin.removable?[{value:'remove',label:'移除此插件'}]:[]),
|
|
316
|
+
{value:'back',label:'返回插件列表'},
|
|
317
|
+
]);
|
|
318
|
+
if(choice==='back')return;
|
|
319
|
+
if(choice==='details')await this.ui.viewMarkdown(plugin.title??id,pluginDetails(plugin,actions,declaredCommands));
|
|
320
|
+
else if(choice==='configure')await this.configure(id);
|
|
321
|
+
else if(choice.startsWith('command:'))await this.pluginCommand(commands.find(c=>c.name===choice.slice(8)),[]);
|
|
322
|
+
else if(choice==='remove'){
|
|
323
|
+
this.writable();if(await this.ui.confirm(`移除 ${id}?已有工作流可能依赖它。`)){await this.app.removePlugin(id);this.ui.show('插件已移除。');return;}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
207
326
|
}
|
|
208
327
|
async pack(){
|
|
209
328
|
this.writable();if(this.dirty){if(!await this.ui.confirm('先保存当前工作流再选择打包?'))return;await this.execute('/save');}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {stripVTControlCharacters} from 'node:util';
|
|
2
|
+
import {truncateToWidth,visibleWidth} from '@mariozechner/pi-tui';
|
|
3
|
+
|
|
4
|
+
// 先清理外部文本,再由受控主题函数添加颜色;不接受插件传入的 ANSI。
|
|
5
|
+
export const cleanText=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
6
|
+
const color=(rgb,text)=>Object.hasOwn(process.env,'NO_COLOR')?text:`\x1b[38;2;${rgb}m${text}\x1b[39m`;
|
|
7
|
+
export const palette={
|
|
8
|
+
brand:text=>color('78;201;176',text),
|
|
9
|
+
text:text=>color('229;233;237',text),
|
|
10
|
+
muted:text=>color('139;149;158',text),
|
|
11
|
+
border:text=>color('74;101;99',text),
|
|
12
|
+
};
|
|
13
|
+
export const menuTheme={selectedPrefix:palette.brand,selectedText:palette.brand,description:palette.muted,scrollInfo:palette.muted,noMatch:palette.muted};
|
|
14
|
+
export const fitLine=(text,width)=>truncateToWidth(text,Math.max(1,width),'…');
|
|
15
|
+
|
|
16
|
+
export function shortenPath(value,width){
|
|
17
|
+
if(visibleWidth(value)<=width)return value;
|
|
18
|
+
// 中间省略保留盘符与项目名;完整目录仍在帮助页中展示。
|
|
19
|
+
const chars=[...value],head=chars.splice(0,Math.min(3,Math.max(0,width-2))).join('');
|
|
20
|
+
while(chars.length&&visibleWidth(head+'…'+chars.join(''))>width)chars.shift();
|
|
21
|
+
return fitLine(head+'…'+chars.join(''),width);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function markdownTheme(){
|
|
25
|
+
const plain=text=>text;
|
|
26
|
+
return {heading:palette.brand,link:palette.brand,linkUrl:palette.muted,code:palette.brand,codeBlock:plain,codeBlockBorder:palette.border,quote:plain,quoteBorder:palette.border,hr:palette.border,listBullet:palette.brand,
|
|
27
|
+
bold:palette.text,italic:plain,strikethrough:plain,underline:plain};
|
|
28
|
+
}
|