@yanglaofish/fishtools 0.5.3 → 0.6.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 +3 -3
- package/manual/README.md +3 -3
- package/manual/docs/modules.md +2 -0
- package/manual/docs/plans/2026-10-13-effective-inputs-and-tables.md +39 -0
- package/manual/docs/plans/2026-10-13-plugin-defaults.md +45 -0
- package/manual/docs/plans/2026-10-14-issue7-recovery.md +33 -0
- package/manual/docs/plans/2026-10-14-issue8-10-workflows.md +26 -0
- package/manual/docs/plans/2026-10-14-issue9-completion.md +47 -0
- package/manual/docs/plans/2026-10-14-managed-execution.md +58 -0
- package/manual/docs/plans/2026-10-14-plugin-discovery-history.md +41 -0
- package/manual/docs/plans/2026-10-14-structured-reports.md +77 -0
- package/manual/docs/plans/2026-10-14-unreleased-review.md +47 -0
- package/manual/docs/plugin-development.md +161 -8
- package/manual/docs/releases.md +25 -6
- package/manual/docs/superpowers/plans/2026-10-13-tui-readable-sessions.md +2 -2
- package/manual/docs/superpowers/reviews/2026-10-13-tui-sessions-review.md +2 -0
- package/manual/docs/superpowers/reviews/2026-10-14-0.6.0-release-review.md +19 -0
- package/manual/docs/verification.md +17 -0
- package/manual/packages/core/README.md +6 -0
- package/manual/packages/core/src/application.js +15 -9
- package/manual/packages/core/src/plugin-catalog.js +20 -2
- package/manual/packages/core/src/plugin-loading.js +24 -0
- package/manual/packages/core/src/version.js +1 -1
- package/manual/packages/fst-cli/README.md +5 -1
- package/manual/packages/fst-cli/src/terminal/command-menu.js +3 -2
- package/manual/packages/fst-cli/src/terminal/file-autocomplete.js +106 -0
- package/manual/packages/fst-cli/src/terminal/log-viewer.js +4 -3
- package/manual/packages/fst-cli/src/terminal/navigation.js +23 -1
- package/manual/packages/fst-cli/src/terminal/presentation.js +2 -1
- package/manual/packages/fst-cli/src/terminal/report-presentation.js +38 -0
- package/manual/packages/fst-cli/src/terminal/result-table.js +66 -0
- package/manual/packages/fst-cli/src/terminal/run-feed.js +42 -17
- package/manual/packages/fst-cli/src/terminal/run-presentation.js +51 -17
- package/manual/packages/fst-cli/src/terminal/run-viewer.js +15 -12
- package/manual/packages/fst-cli/src/terminal/schema.js +114 -4
- package/manual/packages/fst-cli/src/terminal/session-history.js +2 -2
- package/manual/packages/fst-cli/src/terminal/session.js +40 -15
- package/manual/packages/fst-cli/src/terminal/view.js +80 -68
- package/manual/packages/fst-cli/src/terminal/workflow-menu.js +3 -1
- package/manual/packages/fst-devtools/README.md +2 -0
- package/manual/packages/fst-devtools/src/plugin-quality.js +4 -1
- package/manual/packages/fst-devtools/src/plugin-tools.js +1 -0
- package/manual/packages/fst-devtools/src/process-usage.js +62 -0
- package/manual/packages/fst-platform/README.md +5 -0
- package/manual/packages/fst-platform/package.json +1 -0
- package/manual/packages/fst-platform/src/managed-process.js +36 -0
- package/manual/packages/fst-platform/src/process.js +7 -2
- package/manual/packages/fst-plugin/README.md +4 -0
- package/manual/packages/fst-plugin/src/contracts.js +3 -0
- package/manual/packages/fst-plugin/src/host.js +38 -23
- package/manual/packages/fst-plugin/src/runtime.js +14 -8
- package/manual/packages/fst-workflow/src/engine.js +1 -1
- package/manual/packages/fst-workspace/README.md +4 -0
- package/manual/packages/fst-workspace/src/plugin-registry.js +1 -1
- package/manual/packages/fst-workspace/src/plugins.js +5 -3
- package/manual/packages/fst-workspace/src/workflows.js +27 -9
- package/manual/packages/sdk/README.md +10 -0
- package/manual/packages/sdk/inputs.js +53 -0
- package/manual/packages/sdk/package.json +2 -0
- package/manual/packages/sdk/reports.js +53 -0
- package/node_modules/@mytools/core/README.md +6 -0
- package/node_modules/@mytools/core/src/application.js +15 -9
- package/node_modules/@mytools/core/src/plugin-catalog.js +20 -2
- package/node_modules/@mytools/core/src/plugin-loading.js +24 -0
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/README.md +5 -1
- package/node_modules/@mytools/fst-cli/src/terminal/command-menu.js +3 -2
- package/node_modules/@mytools/fst-cli/src/terminal/file-autocomplete.js +106 -0
- package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +4 -3
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +23 -1
- package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +2 -1
- package/node_modules/@mytools/fst-cli/src/terminal/report-presentation.js +38 -0
- package/node_modules/@mytools/fst-cli/src/terminal/result-table.js +66 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +42 -17
- package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +51 -17
- package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +15 -12
- package/node_modules/@mytools/fst-cli/src/terminal/schema.js +114 -4
- package/node_modules/@mytools/fst-cli/src/terminal/session-history.js +2 -2
- package/node_modules/@mytools/fst-cli/src/terminal/session.js +40 -15
- package/node_modules/@mytools/fst-cli/src/terminal/view.js +80 -68
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +3 -1
- package/node_modules/@mytools/fst-devtools/README.md +2 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +4 -1
- package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +1 -0
- package/node_modules/@mytools/fst-devtools/src/process-usage.js +62 -0
- package/node_modules/@mytools/fst-platform/README.md +5 -0
- package/node_modules/@mytools/fst-platform/package.json +1 -0
- package/node_modules/@mytools/fst-platform/src/managed-process.js +36 -0
- package/node_modules/@mytools/fst-platform/src/process.js +7 -2
- package/node_modules/@mytools/fst-plugin/README.md +4 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +3 -0
- package/node_modules/@mytools/fst-plugin/src/host.js +38 -23
- package/node_modules/@mytools/fst-plugin/src/runtime.js +14 -8
- package/node_modules/@mytools/fst-workflow/src/engine.js +1 -1
- package/node_modules/@mytools/fst-workspace/README.md +4 -0
- package/node_modules/@mytools/fst-workspace/src/plugin-registry.js +1 -1
- package/node_modules/@mytools/fst-workspace/src/plugins.js +5 -3
- package/node_modules/@mytools/fst-workspace/src/workflows.js +27 -9
- package/node_modules/@mytools/sdk/README.md +10 -0
- package/node_modules/@mytools/sdk/inputs.js +53 -0
- package/node_modules/@mytools/sdk/package.json +2 -0
- package/node_modules/@mytools/sdk/reports.js +53 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1032 -1032
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {FlowError} from './index.js';
|
|
2
|
+
import {assertSchema,validate} from './validation.js';
|
|
3
|
+
|
|
4
|
+
const text={type:'string',pattern:'\\S'};
|
|
5
|
+
const alerts={type:'array',items:{type:'object',properties:{level:{enum:['info','warning','error']},message:text},required:['level','message'],additionalProperties:false}};
|
|
6
|
+
const conclusions={summary:text,alerts,recommendations:{type:'array',items:text}};
|
|
7
|
+
export const reportSchema={
|
|
8
|
+
type:'object',
|
|
9
|
+
properties:{
|
|
10
|
+
kind:{const:'fst-report'},version:{const:1},title:text,...conclusions,
|
|
11
|
+
sections:{type:'array',items:{type:'object',properties:{title:text,...conclusions,data:true,columns:{type:'array',items:text,minItems:1,uniqueItems:true},titles:{type:'object',additionalProperties:text}},required:['title'],additionalProperties:false}},
|
|
12
|
+
},
|
|
13
|
+
required:['kind','version','title'],additionalProperties:false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// 与落盘 JSON 边界一致,提前拒绝会被 JSON.stringify 静默改值的内容。
|
|
17
|
+
function assertJson(value,ancestors=new Set()){
|
|
18
|
+
if(value===null||typeof value==='string'||typeof value==='boolean'||typeof value==='number'&&Number.isFinite(value))return;
|
|
19
|
+
if(typeof value!=='object'||!value||ancestors.has(value)||(!Array.isArray(value)&&![Object.prototype,null].includes(Object.getPrototypeOf(value))))throw new Error('报告必须为无循环引用的 JSON 数据');
|
|
20
|
+
ancestors.add(value);
|
|
21
|
+
try{
|
|
22
|
+
const list=Array.isArray(value),keys=Reflect.ownKeys(value);
|
|
23
|
+
if(list&&keys.length!==value.length+1)throw new Error('报告数组不能含空槽或附加属性');
|
|
24
|
+
for(const key of keys){
|
|
25
|
+
if(list&&key==='length')continue;
|
|
26
|
+
const field=Object.getOwnPropertyDescriptor(value,key);
|
|
27
|
+
if(typeof key!=='string'||!field.enumerable||!Object.hasOwn(field,'value')||list&&(!Number.isInteger(Number(key))||Number(key)<0||Number(key)>=value.length||String(Number(key))!==key))throw new Error('报告只能包含 JSON 可保留的普通数据属性');
|
|
28
|
+
assertJson(field.value,ancestors);
|
|
29
|
+
}
|
|
30
|
+
}finally{ancestors.delete(value);}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function inspectReport(value){
|
|
34
|
+
try{
|
|
35
|
+
if(!value||typeof value!=='object'||Array.isArray(value))return {status:'none'};
|
|
36
|
+
const kind=Object.getOwnPropertyDescriptor(value,'kind');
|
|
37
|
+
if(kind&&!Object.hasOwn(kind,'value'))throw new Error('报告标记不能为访问器');
|
|
38
|
+
if(kind?.value!=='fst-report')return {status:'none'};
|
|
39
|
+
assertJson(value);
|
|
40
|
+
if(Number.isInteger(value.version)&&value.version!==1)return {status:'unsupported',message:`不支持报告版本 ${value.version},已回退字段阅读。`};
|
|
41
|
+
const check=assertSchema(reportSchema,'报告',{useDefaults:false});
|
|
42
|
+
if(!check(value))return {status:'invalid',message:'报告格式无效,已回退字段阅读。',details:structuredClone(check.errors)};
|
|
43
|
+
return {status:'valid',report:value};
|
|
44
|
+
}catch(error){return {status:'invalid',message:`报告格式无效:${error.message},已回退字段阅读。`};}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createReport(content){
|
|
48
|
+
try{assertJson(content);}catch(error){throw new FlowError(`报告格式无效:${error.message}`,{code:'REPORT_INVALID'});}
|
|
49
|
+
const value={kind:'fst-report',version:1,...content};
|
|
50
|
+
const result=inspectReport(value);
|
|
51
|
+
if(result.status!=='valid')throw new FlowError(result.message??'报告格式无效',{code:'REPORT_INVALID',details:result.details});
|
|
52
|
+
return validate(reportSchema,value,'报告',{useDefaults:false});
|
|
53
|
+
}
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
|
|
28
28
|
`createApplication({projectDir,pluginRegistryDir})` 可让多个工作区读取同一插件库;CLI 默认接入用户级全局库,程序未传此选项时保留工作区模式。分发包始终使用自身工作区安装状态。`listPlugins()` 返回 scope/source 和待配置原因;`pluginDefinition(id)` 返回作者声明。`app.resultsDir` 是业务文件根,`app.outputDir` 保留为日志根;插件 `ctx.outputDir` 与 `ctx.logDir` 分别对应本次业务目录和日志目录。
|
|
29
29
|
|
|
30
|
+
已安装条目的加载或单插件契约错误隔离为 `runtime.status: 'unavailable'`,`listPlugins()` 返回 `loadError` 与来源,传递依赖者同样不可用。直接调用或检查引用它的流程抛出 `PLUGIN_UNAVAILABLE`;无关插件继续装配。纯卸载允许保留其他旧故障;新安装/链接严格检查。修复来源后须重新启动。此恢复不覆盖预置插件、明确的跨插件图错误和运行时初始化错误。
|
|
31
|
+
|
|
30
32
|
`app.startShell(script,options)` 执行临时命令,`app.startAction(id,input,context)` 调用任意动作,两者均纳入运行记录与关闭等待。`startCommand(name,input,{expectedPlugin,...context})` 可固定命令提供者,启动时再次检查,防止参数填写期间被其他同名能力替换。`configureCommandPresets` 和 `configureShortcuts` 在备份、冲突检查与变更锁内保存项目设置;命令快捷项在用户明确设置时绑定提供者 ID,普通安装/卸载和未改变目标的整组保存不会重新绑定;已有未绑定项必须明确提供 plugin 或在界面重新选择目标。注册项变化通过定向宿主更新生效,导出快照保留所需定义与有效快捷项。
|
|
31
33
|
|
|
32
34
|
本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
|
|
@@ -37,6 +39,10 @@
|
|
|
37
39
|
node --test tests/modules.test.js tests/application.test.js tests/lifecycle.test.js tests/module-interfaces.test.js
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
## 输入预览
|
|
43
|
+
|
|
44
|
+
应用接口 `app.previewInput(target, input)` 读取目标动作或命令的当前有效输入及来源;`target` 为 `插件ID:动作名` 或 `/命令名`。由宿主使用当前有效配置解析,返回脱离原对象的 `{value, fields}`,不回写输入,也不暴露整份配置。预览允许缺参,执行时仍作完整校验。验证见 `tests/effective-inputs.test.js`。
|
|
45
|
+
|
|
40
46
|
## 开发监督进程
|
|
41
47
|
|
|
42
48
|
`createDevelopmentSupervisor({projectDir,entry,args,env,configFile,onEvent})` 启动文件监听 worker 和应用子进程,返回 `status()`、幂等 `close()`。父进程不加载插件运行模块;每次重启使用全新的 Node 进程,完整更新 ESM 导入依赖。`bindDevelopmentChild(app,{details,closePresentation})` 通过 IPC 报告就绪,收到重启请求后等待 `app.developmentStatus()` 的运行和管理操作均为空闲,再调用 `app.close()` 及界面清理。
|
|
@@ -7,6 +7,7 @@ import { createHost } from '@mytools/fst-plugin';
|
|
|
7
7
|
import { WorkflowStore } from '@mytools/fst-workspace/workflows';
|
|
8
8
|
import { runWorkflow } from '@mytools/fst-workflow';
|
|
9
9
|
import { runProcess } from '@mytools/fst-platform';
|
|
10
|
+
import {createProcessRunner} from '@mytools/fst-platform/managed-process';
|
|
10
11
|
import {runShell} from '@mytools/fst-platform/shell';
|
|
11
12
|
import {COMMANDS_PLUGIN_ID,commandsPluginRoot,createCommandsPlugin,validateCommandPresets} from '@mytools/commands';
|
|
12
13
|
import {validateShortcuts,describeShortcuts,bindShortcutCommands} from '@mytools/fst-workspace/shortcuts';
|
|
@@ -27,6 +28,7 @@ import {validateNetworkPolicy,inspectNetworkPolicy,resolveNetworkPolicy,buildPro
|
|
|
27
28
|
import {resolvePrivilegeRequirement,checkStaticPrivilege} from '@mytools/fst-plugin/privileges';
|
|
28
29
|
import {registerWorkspace,registryWorkflowReferences,registryPluginReferences} from '@mytools/fst-workspace/registry';
|
|
29
30
|
import {mergePluginEntries,preparePluginCatalog} from './plugin-catalog.js';
|
|
31
|
+
import {loadInstalledEntry} from './plugin-loading.js';
|
|
30
32
|
|
|
31
33
|
function declaredCommands(entries){
|
|
32
34
|
return entries.flatMap(entry=>{const plugin=entry.plugin??entry;return Object.keys(plugin.commands??{}).map(name=>({name,plugin:plugin.id}));});
|
|
@@ -73,8 +75,9 @@ export async function createApplication({projectDir=process.cwd(),workspace,plug
|
|
|
73
75
|
developmentStatus:()=>({running:launching+writingRuns.size+operations.size,busy,closing:!!closing}),
|
|
74
76
|
catalog:()=>host.catalog(),
|
|
75
77
|
pluginDefinition:id=>host.definitions.get(id)??pending.get(id)?.entry.plugin,
|
|
76
|
-
listPlugins:()=>catalogEntries.map(entry=>{const p=entry.plugin??entry,scope=entry.scope??'preset',installed=scope==='global'?state:scope==='workspace'?(shared?localState:state):{};return {id:p.id,version:p.version,title:p.title,description:p.description,scope,source:entry.root,platforms:p.platforms,architectures:p.architectures,environments:p.environments,config:p.config,requirements:p.requirements,privileges:pluginPrivileges(p),runtime:{...(pending.has(p.id)?{status:'unconfigured'}:host.runtimeStatus().find(r=>r.id===p.id)),...(p.runtime?{updatePolicy:p.runtime.updatePolicy??'maintenance'}:{})},...(pending.has(p.id)?{configurationError:pending.get(p.id).message}:{}),removable:!!(installed.dependencies?.[p.id]||installed.links?.[p.id]),development:!!installed.links?.[p.id],warnings:host.warnings.filter(item=>item.plugin===p.id).map(item=>item.message)};}),
|
|
78
|
+
listPlugins:()=>catalogEntries.map(entry=>{const p=entry.plugin??entry,scope=entry.scope??'preset',installed=scope==='global'?state:scope==='workspace'?(shared?localState:state):{};return {id:p.id,version:p.version,title:p.title,description:p.description,scope,source:entry.root,platforms:p.platforms,architectures:p.architectures,environments:p.environments,config:p.config,requirements:p.requirements,privileges:pluginPrivileges(p),runtime:{...(pending.has(p.id)?{status:pending.get(p.id).status??'unconfigured'}:host.runtimeStatus().find(r=>r.id===p.id)),...(p.runtime?{updatePolicy:p.runtime.updatePolicy??'maintenance'}:{})},...(pending.has(p.id)?(pending.get(p.id).status==='unavailable'?{loadError:pending.get(p.id).error}:{configurationError:pending.get(p.id).message}):{}),removable:!!(installed.dependencies?.[p.id]||installed.links?.[p.id]),development:!!installed.links?.[p.id],warnings:host.warnings.filter(item=>item.plugin===p.id).map(item=>item.message)};}),
|
|
77
79
|
listCommands:()=>host.commands(),
|
|
80
|
+
previewInput:(target,input)=>host.previewInput(target,input),
|
|
78
81
|
listShortcuts:()=>describeShortcuts(project.shortcuts??{},{actions:host.catalog(),commands:host.commands()}),
|
|
79
82
|
async configureShortcuts(value){return updateUserCommands({shortcuts:value});},
|
|
80
83
|
async configureCommandPresets(value,options={}){
|
|
@@ -86,7 +89,7 @@ export async function createApplication({projectDir=process.cwd(),workspace,plug
|
|
|
86
89
|
async removeWorkflow(id,options){return mutateWorkflow(()=>store.remove(id,options));},
|
|
87
90
|
async checkWorkflow(id){
|
|
88
91
|
host.assertHealthy();
|
|
89
|
-
const closure=await store.resolve([id],host);
|
|
92
|
+
const closure=await store.resolve([id],{...host,actions:{get(action){assertConfigured(action.slice(0,action.lastIndexOf(':')));return host.actions.get(action);}}});
|
|
90
93
|
return {...closure,privileges:workflowPrivileges(closure,id)};
|
|
91
94
|
},
|
|
92
95
|
subscribe(fn){emitter.on('event',fn);return ()=>emitter.off('event',fn);},
|
|
@@ -125,8 +128,7 @@ export async function createApplication({projectDir=process.cwd(),workspace,plug
|
|
|
125
128
|
const env=buildProcessEnv(policy);
|
|
126
129
|
return app.executeTracked('!命令',async(_host,context)=>{
|
|
127
130
|
const result=await runShell(script,{shell,temporaryDir:context.outputDir,cwd:projectDir,env,extendEnv:false,signal:context.signal,timeout,outputMode:'stream',summaryBytes:65536,
|
|
128
|
-
|
|
129
|
-
onOutput:({stream,message})=>context.log(message,undefined,{stream,audience:'user'})});
|
|
131
|
+
runProcess:createProcessRunner({signal:context.signal,log:context.log,audience:'user'})});
|
|
130
132
|
if(result.exitCode!==0)throw new FlowError(`命令执行失败,退出码 ${result.exitCode}`,{code:'EXECUTION_FAILED',details:result});
|
|
131
133
|
return result;
|
|
132
134
|
},{kind:'shell',title:'系统命令'});
|
|
@@ -331,11 +333,15 @@ export async function createApplication({projectDir=process.cwd(),workspace,plug
|
|
|
331
333
|
}catch(error){await fileProbe.close();await opened.release();await localOpened?.release();throw error;}
|
|
332
334
|
async function loadInstalled(current,base,excluded=new Set()){
|
|
333
335
|
const loaded=[];
|
|
334
|
-
for(const name of Object.keys(current.dependencies??{}))if(!excluded.has(name)&&(!maintenance||name!==omitInstalledPlugin))loaded.push(await
|
|
335
|
-
for(const [name,source] of Object.entries(current.links??{}))if(!excluded.has(name)&&(!maintenance||name!==omitInstalledPlugin))loaded.push(await
|
|
336
|
+
for(const name of Object.keys(current.dependencies??{}))if(!excluded.has(name)&&(!maintenance||name!==omitInstalledPlugin))loaded.push(await loadInstalledEntry(name,name,current.root));
|
|
337
|
+
for(const [name,source] of Object.entries(current.links??{}))if(!excluded.has(name)&&(!maintenance||name!==omitInstalledPlugin))loaded.push(await loadInstalledEntry(name,source,base,{linked:true}));
|
|
336
338
|
return loaded;
|
|
337
339
|
}
|
|
338
|
-
function assertConfigured(id){
|
|
340
|
+
function assertConfigured(id){
|
|
341
|
+
const failure=pending.get(id);if(!failure)return;
|
|
342
|
+
if(failure.status==='unavailable')throw new FlowError(`插件 ${id} 不可用(${failure.entry.root??'未知来源'}):${failure.message}`,{code:'PLUGIN_UNAVAILABLE',details:{plugin:id,source:failure.entry.root,error:failure.error}});
|
|
343
|
+
throw new FlowError(`插件 ${id} 尚未配置:${failure.message}`,{code:'PLUGIN_NOT_CONFIGURED',details:{plugin:id}});
|
|
344
|
+
}
|
|
339
345
|
for(const name of ['startRun','startCommand','startAction','startShell','startWeb','saveWorkflow','duplicateWorkflow','removeWorkflow','installPlugin','linkPlugin','removePlugin','configurePlugin','configureNetworkPolicy','configureLogs','configureCommandPresets','configureShortcuts','setMaintenance','pack']){
|
|
340
346
|
const original=app[name],isRun=['startRun','startCommand','startAction','startShell'].includes(name);
|
|
341
347
|
app[name]=async(...args)=>{
|
|
@@ -466,7 +472,7 @@ export async function createApplication({projectDir=process.cwd(),workspace,plug
|
|
|
466
472
|
}
|
|
467
473
|
async function findPluginWorkflowReferences(id){
|
|
468
474
|
const refs=[];
|
|
469
|
-
for(const workflow of await store.list()){
|
|
475
|
+
for(const workflow of await store.list({strict:true})){
|
|
470
476
|
const actions=[...workflowActions(workflow.steps)].filter(action=>action.startsWith(`${id}:`));
|
|
471
477
|
if(actions.length)refs.push({id:workflow.id,title:workflow.title??workflow.id,actions});
|
|
472
478
|
}
|
|
@@ -510,7 +516,7 @@ export async function createApplication({projectDir=process.cwd(),workspace,plug
|
|
|
510
516
|
const workflows=shared&&targetDir===registryDir?await registryWorkflowReferences(registryDir,name):await findPluginWorkflowReferences(name);
|
|
511
517
|
if(workflows.length&&!options.force)throw new FlowError(`插件 ${name} 仍被工作流引用:${workflows.map(w=>w.id).join('、')}`,{code:'PLUGIN_IN_USE',details:{workflows}});
|
|
512
518
|
}
|
|
513
|
-
const next=await installGeneration(targetDir,{...operation,sourceDir:projectDir,state:shared&&targetDir===registryDir?{...targetState,global:true}:targetState});
|
|
519
|
+
const next=await installGeneration(targetDir,{...operation,sourceDir:projectDir,state:shared&&targetDir===registryDir?{...targetState,global:true}:targetState,recoverInstalled:loadInstalledEntry});
|
|
514
520
|
const nextLocal=shared?(targetDir===projectDir?next.loaded:localEntries):next.loaded,nextGlobal=shared?(targetDir===registryDir?next.loaded:globalEntries):[];
|
|
515
521
|
const nextState=targetDir===registryDir?next.state:state;
|
|
516
522
|
const nextCatalog=entriesWithPresets(mergePluginEntries(initial,nextLocal,nextGlobal,nextState.disabledLegacy,{shared}),latest.commandPresets??{}),activeConfig={...project.config,...config},effectiveConfig={...latest.config,...config};
|
|
@@ -18,9 +18,28 @@ export function mergePluginEntries(initial,local,global,disabledLegacy=[],{share
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function preparePluginCatalog(entries,config){
|
|
21
|
-
const pending=new Map()
|
|
21
|
+
const pending=new Map(),unavailable=new Set();
|
|
22
|
+
for(const entry of entries)if(entry.unavailable){
|
|
23
|
+
const id=entry.plugin.id;unavailable.add(id);
|
|
24
|
+
pending.set(id,{entry,status:'unavailable',message:entry.unavailable.message,error:entry.unavailable});
|
|
25
|
+
}
|
|
26
|
+
// 故障依赖的消费者显式不可用,直到用户修复来源并重新启动。
|
|
27
|
+
let changed;
|
|
28
|
+
do{
|
|
29
|
+
changed=false;
|
|
30
|
+
for(const entry of entries){
|
|
31
|
+
const plugin=entry.plugin??entry;
|
|
32
|
+
const missing=Object.keys(plugin.dependencies??{}).filter(id=>pending.has(id));
|
|
33
|
+
if(!pending.has(plugin.id)&&missing.length){
|
|
34
|
+
const message=`依赖插件不可用:${missing.join('、')}`;
|
|
35
|
+
pending.set(plugin.id,{entry,status:'unavailable',message,error:{message,phase:'dependency',dependencies:missing}});changed=true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}while(changed);
|
|
39
|
+
validatePluginDefinitions(entries,{unavailable,blocked:new Set(pending.keys())});
|
|
22
40
|
for(const entry of entries){
|
|
23
41
|
const plugin=entry.plugin??entry;
|
|
42
|
+
if(pending.has(plugin.id))continue;
|
|
24
43
|
if(plugin.config!==undefined)assertSchema(plugin.config,`${plugin.id} 配置`);
|
|
25
44
|
try{validate(plugin.config,config[plugin.id]??{},`${plugin.id} 配置`);}
|
|
26
45
|
catch(error){
|
|
@@ -29,7 +48,6 @@ export function preparePluginCatalog(entries,config){
|
|
|
29
48
|
}
|
|
30
49
|
}
|
|
31
50
|
// 缺配置的服务提供者暂不启用,相关消费者也等待;无关插件正常装配。
|
|
32
|
-
let changed;
|
|
33
51
|
do{
|
|
34
52
|
changed=false;
|
|
35
53
|
for(const entry of entries){
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {loadPlugin} from '@mytools/fst-plugin/loader';
|
|
3
|
+
import {validatePluginDefinition} from '@mytools/fst-plugin';
|
|
4
|
+
import {assertSchema} from '@mytools/sdk/schema';
|
|
5
|
+
import {FlowError} from '@mytools/sdk';
|
|
6
|
+
import {errorRecord} from '@mytools/sdk/diagnostics';
|
|
7
|
+
|
|
8
|
+
// 已安装插件加载失败时保留注册身份,避免错误导出破坏管理和卸载入口。
|
|
9
|
+
export async function loadInstalledEntry(id,spec,base,{linked=false}={}){
|
|
10
|
+
let loaded,phase='load';
|
|
11
|
+
const source=linked?path.resolve(base,spec):path.join(base,'node_modules',id);
|
|
12
|
+
try{
|
|
13
|
+
loaded=await loadPlugin(spec,base);phase='contract';
|
|
14
|
+
const plugin=validatePluginDefinition(loaded);
|
|
15
|
+
if(plugin.id!==id)throw new FlowError(`插件导出标识 ${plugin.id} 与已安装标识 ${id} 不一致`,{code:'VALIDATION'});
|
|
16
|
+
if(plugin.config!==undefined)assertSchema(plugin.config,`${id} 配置`);
|
|
17
|
+
return loaded;
|
|
18
|
+
}catch(error){
|
|
19
|
+
const original=loaded?.plugin,plugin={id};
|
|
20
|
+
for(const key of ['version','title','description'])if(typeof original?.[key]==='string')plugin[key]=original[key];
|
|
21
|
+
if(original?.dependencies&&typeof original.dependencies==='object'&&!Array.isArray(original.dependencies))plugin.dependencies=original.dependencies;
|
|
22
|
+
return {plugin,root:loaded?.root??source,unavailable:{...errorRecord(error),phase}};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// 由 npm version 的生命周期脚本同步,内部插件 API 版本独立维护。
|
|
2
|
-
export const VERSION="0.
|
|
2
|
+
export const VERSION="0.6.0";
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
fst 命令入口、交互终端、输入呈现和运行观察。通过公开应用和模块接口工作,业务执行不在界面中重复实现。
|
|
8
8
|
|
|
9
|
+
支持 SDK 的 `fst-report` v1 业务报告:主面板默认显示结论、分组和表格,以缩进区分报告标题、结论和组内数据。执行入口、输入及实际指令始终可见;Ctrl+O 展开原始输出与诊断,运行详情中的 `m` 提供专用阅读,`j` 保留原始 JSON。报告的提醒等级只代表插件的业务判断,不改变运行状态。旧文本/Markdown/结构化输出保持兼容;损坏和未知版本报告提示后回退字段阅读。
|
|
10
|
+
|
|
9
11
|
内部依赖:`@mytools/sdk`、`@mytools/core`、`@mytools/fst-workspace`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-devtools`、`@mytools/fst-logs`。
|
|
10
12
|
## 公开入口
|
|
11
13
|
|
|
@@ -35,7 +37,9 @@ fst 命令入口、交互终端、输入呈现和运行观察。通过公开应
|
|
|
35
37
|
|
|
36
38
|
TUI 支持 `!` 系统命令、`/commands` 常用命令注册和 `/shortcuts` 任意插件能力快捷名。主输入框与命令编辑框使用 Shift+Enter / Alt+Enter 换行、Enter 提交;真实多行粘贴保持换行及缩进。使用说明可通过 `fst docs commands` 离线阅读。
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
空 `/` 只列精简入口;输入前缀后搜索全部已注册命令,例如 `/env` 可找到 `envprobe.*`,`/envprobe.` 可继续缩小范围。唯一且没有名称冲突的命令命名空间也可打开所属插件能力菜单;原插件 ID 入口保留。从插件管理或能力菜单实际启动命令后,其完整命令名进入上下键历史及持久会话;取消参数或启动失败不新增执行条目,参数保留在运行记录中,不拼入可召回命令。
|
|
41
|
+
|
|
42
|
+
运行和步骤附近提示 Ctrl+O 展开原始输出与诊断;输入及受管进程的实际程序、参数、目录和原始命令默认可见。stdout/stderr(包括感叹号透传)默认折叠,print 用户反馈和业务报告缩进展示。`/logs` 中按 m 单独格式化阅读输出,j 仍保留原始 JSON。总耗时在执行期间每秒更新,结束或关闭界面后回收刷新计时器。平台声明提示直接显示在 TUI。
|
|
39
43
|
|
|
40
44
|
`/history` 或 `/tools` → 历史会话恢复输入、反馈与结果,不重跑指令;与已有插件或快捷名 `history` 冲突时保留原入口。通过 `fst-logs/sessions` 在所属工作区保存有界快照与完整追加交互记录,只读包改用可写输出目录。较早的内容分页读取,运行日志仍独立保存并遵循其保留策略。恢复创建当前会话的继续记录,不覆盖来源会话;未提交草稿和配置表单不保存。
|
|
41
45
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {randomUUID} from 'node:crypto';
|
|
2
2
|
import {isDeepStrictEqual} from 'node:util';
|
|
3
3
|
import {isCommandName} from '@mytools/fst-plugin/contracts';
|
|
4
|
-
import {editValue} from './schema.js';
|
|
4
|
+
import {editValue,editInput} from './schema.js';
|
|
5
5
|
|
|
6
6
|
export async function runShortcut(session,entry,args=[]){
|
|
7
7
|
if(!entry.available)throw new Error(`快捷名称 /${entry.name} 已失效:${entry.reason??'目标不存在'}`);
|
|
@@ -153,7 +153,8 @@ async function editShortcut(session,oldName){
|
|
|
153
153
|
}else if(choice==='input'){
|
|
154
154
|
const metadata=target.action?app.catalog().find(action=>action.id===target.action):app.listCommands().find(command=>command.name===target.command&&command.plugin===target.plugin);
|
|
155
155
|
if(!metadata){ui.show('目标已失效,请先重新选择目标。');continue;}
|
|
156
|
-
|
|
156
|
+
const preview=app.previewInput?value=>app.previewInput(target.action??`/${target.command}`,value):undefined;
|
|
157
|
+
target.input=await editInput(ui,metadata.input??{type:'object',properties:{},additionalProperties:false},target.input??{},{baseDir:session.baseDir,required:true,label:'快捷名称默认参数',preview});
|
|
157
158
|
}else if(choice==='save'){
|
|
158
159
|
const next=structuredClone(app.project.shortcuts??{});if(oldName)delete next[oldName];
|
|
159
160
|
if(Object.hasOwn(next,name)){ui.show('快捷名称已经存在,请选择其他名称。');continue;}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import {CombinedAutocompleteProvider} from '@mariozechner/pi-tui';
|
|
4
|
+
import {createFileProbe} from '@mytools/fst-platform/fs';
|
|
5
|
+
import {cleanText as safe} from './theme.js';
|
|
6
|
+
|
|
7
|
+
function budget(value,fallback,max){return Number.isSafeInteger(value)&&value>0?Math.min(value,max):fallback;}
|
|
8
|
+
export function createAutocompleteProbe(options={}){
|
|
9
|
+
return options.probe??createFileProbe({timeout:budget(options.timeout,1200,10000),maxConcurrent:1,maxPending:1,allowRemote:options.allowRemote===true});
|
|
10
|
+
}
|
|
11
|
+
function remote(value){
|
|
12
|
+
const normalized=value.replaceAll('/','\\');
|
|
13
|
+
if(/^\\\\[?.]\\/u.test(normalized))return /^\\\\[?.]\\UNC(?:\\|$)/iu.test(normalized)||/^\\\\[?.]\\GLOBALROOT\\Device\\(?:Mup|LanmanRedirector|WebDavRedirector)(?:\\|$)/iu.test(normalized);
|
|
14
|
+
return /^\\\\/u.test(normalized)||/^\\(?:\?\?\\UNC|Device\\(?:Mup|LanmanRedirector|WebDavRedirector))(?:\\|$)/iu.test(normalized);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 同步接口只查有界缓存;所有文件 I/O 交给可取消的隔离探测进程。
|
|
18
|
+
export class FileAutocomplete extends CombinedAutocompleteProvider{
|
|
19
|
+
constructor(commands=[],baseDir=process.cwd(),workflows=[],options={}){
|
|
20
|
+
super(commands,baseDir);this.baseDir=baseDir;this.workflows=workflows;
|
|
21
|
+
this.defaultCommands=new CombinedAutocompleteProvider(commands.filter(command=>!command.searchOnly),baseDir);
|
|
22
|
+
this.files=options.files!==false;this.allowRemote=options.allowRemote===true;
|
|
23
|
+
this.entryLimit=budget(options.entryLimit,1000,10000);this.maxItems=budget(options.maxItems,100,100);
|
|
24
|
+
this.cacheLimit=budget(options.cacheLimit,16,64);this.cacheTtl=budget(options.cacheTtl,5000,60000);
|
|
25
|
+
this.timeout=budget(options.timeout,1200,10000);this.debounceMs=options.debounceMs===0?0:budget(options.debounceMs,40,500);
|
|
26
|
+
this.probe=createAutocompleteProbe(options);this.ownsProbe=options.ownsProbe!==false;
|
|
27
|
+
this.onUpdate=options.onUpdate;this.cache=new Map();this.closed=false;
|
|
28
|
+
}
|
|
29
|
+
cancelPending(){
|
|
30
|
+
const pending=this.pending;this.pending=null;
|
|
31
|
+
if(pending){clearTimeout(pending.timer);pending.controller.abort();}
|
|
32
|
+
}
|
|
33
|
+
close(){
|
|
34
|
+
if(this.closing)return this.closing;
|
|
35
|
+
this.closed=true;this.cancelPending();this.cache.clear();this.onUpdate=null;
|
|
36
|
+
return this.closing=Promise.resolve(this.ownsProbe?this.probe.close():undefined);
|
|
37
|
+
}
|
|
38
|
+
schedule(directory,snapshot){
|
|
39
|
+
if(this.pending?.directory===directory){this.pending.snapshot=snapshot;return;}
|
|
40
|
+
this.cancelPending();
|
|
41
|
+
const pending={directory,snapshot,controller:new AbortController()};this.pending=pending;
|
|
42
|
+
pending.timer=setTimeout(async()=>{
|
|
43
|
+
let result;
|
|
44
|
+
try{result=await this.probe.readDirectory(directory,{limit:this.entryLimit,timeout:this.timeout,signal:pending.controller.signal});}
|
|
45
|
+
catch{result={status:'error',incomplete:true,reason:'probeFailed'};}
|
|
46
|
+
if(this.closed||this.pending!==pending||pending.controller.signal.aborted)return;
|
|
47
|
+
this.pending=null;
|
|
48
|
+
// 错误和超时也短暂缓存,避免按键触发无限重试。
|
|
49
|
+
const cached={status:result.status,incomplete:result.incomplete,reason:result.reason,
|
|
50
|
+
value:result.status==='ok'?result.value.slice(0,this.entryLimit):[],expires:Date.now()+this.cacheTtl};
|
|
51
|
+
this.cache.delete(directory);this.cache.set(directory,cached);
|
|
52
|
+
while(this.cache.size>this.cacheLimit)this.cache.delete(this.cache.keys().next().value);
|
|
53
|
+
this.lastResult={status:cached.status,incomplete:cached.incomplete,reason:cached.reason};
|
|
54
|
+
this.onUpdate?.(pending.snapshot);
|
|
55
|
+
},this.debounceMs);
|
|
56
|
+
}
|
|
57
|
+
getSuggestions(lines,line,col){
|
|
58
|
+
if(this.closed)return null;
|
|
59
|
+
const text=(lines[line]??'').slice(0,col);
|
|
60
|
+
const prefix=text.match(/(?:^|\s)(@"[^"\n]*|@[^\s]*)$/)?.[1];
|
|
61
|
+
if(!prefix){
|
|
62
|
+
if(text==='/'){this.cancelPending();return this.defaultCommands.getSuggestions(lines,line,col);}
|
|
63
|
+
const run=text.match(/^\/run\s+([^@"\s]*)$/);
|
|
64
|
+
if(run){
|
|
65
|
+
this.cancelPending();
|
|
66
|
+
const query=run[1].toLowerCase(),items=this.workflows.filter(flow=>`${flow.id} ${flow.title??''}`.toLowerCase().includes(query)).slice(0,this.maxItems).map(flow=>({value:flow.id,label:flow.title??flow.id,description:flow.id,workflow:true}));
|
|
67
|
+
return items.length?{prefix:run[1],items}:null;
|
|
68
|
+
}
|
|
69
|
+
// 只把命令交给上游,不能回落到它的同步文件系统实现。
|
|
70
|
+
if(/^\/[^\s/]*(?:\s.*)?$/.test(text)){this.cancelPending();return super.getSuggestions(lines,line,col);}
|
|
71
|
+
}
|
|
72
|
+
if(!this.files)return null;
|
|
73
|
+
const filePrefix=prefix??this.extractPathPrefix(text,false);
|
|
74
|
+
if(filePrefix===null){this.cancelPending();return null;}
|
|
75
|
+
const raw=filePrefix.replace(/^@/,'').replace(/^"/,'').replaceAll('\\','/');
|
|
76
|
+
// 解析前判定,避免 POSIX resolve 抹掉 UNC 的双斜杠或 Windows 设备前缀。
|
|
77
|
+
if(!this.allowRemote&&(remote(raw)||remote(this.baseDir))){
|
|
78
|
+
this.cancelPending();this.lastResult={status:'skipped',incomplete:true,reason:'remotePath'};return null;
|
|
79
|
+
}
|
|
80
|
+
const expanded=raw.startsWith('~/')?path.join(homedir(),raw.slice(2)):raw==='~'?homedir():raw;
|
|
81
|
+
const absolute=path.resolve(this.baseDir,expanded||'.');
|
|
82
|
+
const directory=!raw||raw==='~'||raw.endsWith('/')?absolute:path.dirname(absolute);
|
|
83
|
+
const query=!raw||raw==='~'||raw.endsWith('/')?'':path.basename(absolute).toLowerCase();
|
|
84
|
+
const cached=this.cache.get(directory);
|
|
85
|
+
if(!cached||cached.expires<=Date.now()){
|
|
86
|
+
this.schedule(directory,{lines:[...lines],line,col});return null;
|
|
87
|
+
}
|
|
88
|
+
if(this.pending?.directory!==directory)this.cancelPending();
|
|
89
|
+
this.lastResult={status:cached.status,incomplete:cached.incomplete,reason:cached.reason};
|
|
90
|
+
const items=cached.value.filter(entry=>entry.name.toLowerCase().startsWith(query)).map(entry=>{
|
|
91
|
+
// 链接只作为链接候选,不逐个 stat 目标。用户显式输入 / 后才读取其内容。
|
|
92
|
+
const dir=entry.type==='directory',head=raw==='~'?'~/':raw.slice(0,raw.lastIndexOf('/')+1),file=head+entry.name+(dir?'/':'');
|
|
93
|
+
return {value:`${prefix?'@':''}${/[\s"]/.test(file)?JSON.stringify(file):file}`,label:safe(entry.name)+(dir?'/':''),description:dir?'目录':entry.type==='symlink'?'符号链接':'文件',directory:dir,file:true};
|
|
94
|
+
}).sort((a,b)=>Number(b.directory)-Number(a.directory)||a.label.localeCompare(b.label)).slice(0,this.maxItems);
|
|
95
|
+
return items.length?{prefix:filePrefix,items,incomplete:cached.incomplete}:null;
|
|
96
|
+
}
|
|
97
|
+
getForceFileSuggestions(lines,line,col){return this.getSuggestions(lines,line,col);}
|
|
98
|
+
applyCompletion(lines,line,col,item,prefix){
|
|
99
|
+
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};}
|
|
100
|
+
if(!item.file&&!prefix.startsWith('@'))return super.applyCompletion(lines,line,col,item,prefix);
|
|
101
|
+
const before=lines[line].slice(0,col-prefix.length);
|
|
102
|
+
let after=lines[line].slice(col);if(after.startsWith('"'))after=after.slice(1);
|
|
103
|
+
const completed=item.value+(item.directory?'':' '),next=[...lines];next[line]=before+completed+after;
|
|
104
|
+
return {lines:next,cursorLine:line,cursorCol:before.length+completed.length-(item.directory&&item.value.endsWith('"')?1:0)};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { Markdown, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
2
2
|
import { mouseWheel } from './run-viewer.js';
|
|
3
|
-
import {palette,markdownTheme} from './theme.js';
|
|
3
|
+
import {cleanText,palette,markdownTheme} from './theme.js';
|
|
4
4
|
|
|
5
5
|
const theme=markdownTheme();
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// 每次只向 Overlay 提交当前窗口内的行;动态结果在消费时清理控制字符。
|
|
8
8
|
export class MarkdownLogViewer{
|
|
9
9
|
constructor({title,text,terminal,onClose,requestRender}){
|
|
10
10
|
this.title=title.replace(/\s+/g,' ');this.terminal=terminal;this.onClose=onClose;this.requestRender=requestRender;
|
|
11
|
-
this.markdown=new Markdown(text,0,0,theme);this.offset=0;
|
|
11
|
+
this.source=text;this.markdown=new Markdown(typeof text==='function'?'':cleanText(text),0,0,theme);this.offset=0;
|
|
12
12
|
}
|
|
13
13
|
invalidate(){this.markdown.invalidate();}
|
|
14
14
|
layout(width){
|
|
15
|
+
if(typeof this.source==='function'&&this.sourceWidth!==width){this.markdown.setText(cleanText(this.source(width)));this.sourceWidth=width;}
|
|
15
16
|
const height=Math.max(1,this.terminal.rows);
|
|
16
17
|
const pageSize=Math.max(1,height-2);
|
|
17
18
|
// Markdown 自身缓存同一宽度的完整渲染,滚动时不重新解析日志。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {builtinCommands} from '@mytools/fst-plugin/contracts';
|
|
1
|
+
import {builtinCommands,isCommandName} from '@mytools/fst-plugin/contracts';
|
|
2
2
|
|
|
3
3
|
const visible=['workflows','run','pack','logs','plugin','commands','shortcuts','config','tools','help','exit'];
|
|
4
4
|
// 入口由插件 ID 推导;冲突时加前缀,不能覆盖内置或作者注册的命令。
|
|
@@ -18,3 +18,25 @@ export function commandMenu(plugins,commands,shortcuts=[]){
|
|
|
18
18
|
}
|
|
19
19
|
return entries;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
// 空斜杠保持精简;输入前缀后可搜索所有可执行命令及无歧义的插件命名空间。
|
|
23
|
+
export function commandCompletions(plugins,commands,shortcuts=[]){
|
|
24
|
+
const entries=commandMenu(plugins,commands,shortcuts),used=new Set(entries.map(entry=>entry.name));
|
|
25
|
+
const menuLength=entries.length,aliases=[];
|
|
26
|
+
for(const command of [...builtinCommands,...commands])if(!used.has(command.name)){
|
|
27
|
+
used.add(command.name);entries.push({name:command.name,description:command.description??command.name,searchOnly:true});
|
|
28
|
+
}
|
|
29
|
+
const namespaces=new Map();
|
|
30
|
+
for(const command of commands){
|
|
31
|
+
if(!command.name.includes('.'))continue;
|
|
32
|
+
const name=command.name.split('.')[0];
|
|
33
|
+
if(!namespaces.has(name))namespaces.set(name,new Set());
|
|
34
|
+
namespaces.get(name).add(command.plugin);
|
|
35
|
+
}
|
|
36
|
+
for(const [name,owners] of namespaces)if(owners.size===1&&!used.has(name)&&isCommandName(name)){
|
|
37
|
+
const id=[...owners][0],plugin=plugins.find(plugin=>plugin.id===id);
|
|
38
|
+
if(!plugin)continue;
|
|
39
|
+
used.add(name);aliases.push({name,plugin:id,searchOnly:true,description:`[插件] ${plugin.title??id} · 选择能力`});
|
|
40
|
+
}
|
|
41
|
+
return [...entries.slice(0,menuLength),...aliases,...entries.slice(menuLength)];
|
|
42
|
+
}
|
|
@@ -4,7 +4,7 @@ export const logo=[
|
|
|
4
4
|
'▀ ▀▀▀ ▀▀▀ ▀ ▀',
|
|
5
5
|
].join('\n');
|
|
6
6
|
|
|
7
|
-
export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed:'失败',stopped:'已停止',unconfigured:'待配置'})[plugin.runtime?.status]??'未知';
|
|
7
|
+
export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed:'失败',stopped:'已停止',unconfigured:'待配置',unavailable:'不可用'})[plugin.runtime?.status]??'未知';
|
|
8
8
|
|
|
9
9
|
export function gettingStarted(flows,{readOnly=false}={}){
|
|
10
10
|
const sample=flows.find(w=>w.id==='hello')??flows[0];
|
|
@@ -36,6 +36,7 @@ export function pluginDetails(plugin,actions,commands){
|
|
|
36
36
|
plugin.description??'作者尚未提供用途说明。',
|
|
37
37
|
`标识:${plugin.id}\n\n版本:${plugin.version}\n\n来源:${({global:'用户级全局安装或链接',workspace:'工作区安装或链接(兼容旧版)',preset:'发行包内置 / 项目预置'})[plugin.scope]??(plugin.removable?'工作区安装或链接':'发行包内置 / 项目预置')}${plugin.source?`\n\n代码位置:${plugin.source}`:''}\n\n状态:${pluginStatus(plugin)}`,
|
|
38
38
|
...(plugin.configurationError?[`配置状态:${plugin.configurationError}\n\n在当前工作区设置插件配置后启用。`]:[]),
|
|
39
|
+
...(plugin.loadError?[`不可用原因:${plugin.loadError.message}\n\n修复上述插件来源后重启,或从插件管理移除此插件。`]:[]),
|
|
39
40
|
...(plugin.runtime?.missing?.length?[`缺少服务:${plugin.runtime.missing.join(', ')}`]:[]),
|
|
40
41
|
`更新方式:${!plugin.runtime?.updatePolicy?'无运行时服务':plugin.runtime.updatePolicy==='automatic'?'可自动短暂停止并重启':'需要本次维护确认'}\n\n插件权限:${privilege(plugin,plugin)}`,
|
|
41
42
|
`声明平台:${plugin.platforms?.join(', ')??'作者未声明'}\n\n声明架构:${plugin.architectures?.join(', ')??'作者未声明'}`,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {inspectReport} from '@mytools/sdk/reports';
|
|
2
|
+
import {cleanText as clean} from './theme.js';
|
|
3
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
4
|
+
|
|
5
|
+
const levels={info:'提示',warning:'警告',error:'错误'};
|
|
6
|
+
function conclusions(value){
|
|
7
|
+
return [value.summary&&`摘要:${value.summary}`,...(value.alerts??[]).map(alert=>`${levels[alert.level]}:${alert.message}`),
|
|
8
|
+
...(value.recommendations?.length?['建议:',...value.recommendations.map((text,index)=>` ${index+1}. ${text}`)]:[])].filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
function dataSchema(section){
|
|
11
|
+
const properties=Object.fromEntries(Object.entries(section.titles??{}).map(([key,title])=>[key,{title}]));
|
|
12
|
+
return {'x-fst-table':section.columns,properties,items:{properties},additionalProperties:{properties}};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 整个输出或 report 字段均可承载报告,不猜测其他同名业务字段的含义。
|
|
16
|
+
export function outputReport(value){
|
|
17
|
+
if(value?.kind==='fst-report')return value;
|
|
18
|
+
if(value?.report?.kind==='fst-report')return value.report;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function forwardedReport(output,finalOutput){
|
|
22
|
+
const report=outputReport(output);
|
|
23
|
+
return report&&isDeepStrictEqual(report,outputReport(finalOutput))?report:undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderReport(value,{indent='',render,truncated=false}){
|
|
27
|
+
if(truncated&&value?.kind==='fst-report')return clean(`${indent}报告内容为摘要,完整结果请在 /logs 中按 p 分页阅读。\n${render(value,indent,undefined,{raw:true})}`);
|
|
28
|
+
const result=inspectReport(value);
|
|
29
|
+
if(result.status==='none')return;
|
|
30
|
+
if(result.status!=='valid')return clean(`${indent}${result.message}\n${render(value,indent,undefined,{raw:true})}`);
|
|
31
|
+
const inset=(text,padding)=>String(text).split('\n').map(line=>' '.repeat(padding)+line).join('\n');
|
|
32
|
+
const lines=[`报告:${value.title}`,...conclusions(value).map(text=>inset(text,2))];
|
|
33
|
+
for(const section of value.sections??[]){
|
|
34
|
+
lines.push('',inset(`── ${section.title} ──`,2),...conclusions(section).map(text=>inset(text,4)));
|
|
35
|
+
if(Object.hasOwn(section,'data'))lines.push(inset(render(section.data,'',dataSchema(section),{inset:4}),4));
|
|
36
|
+
}
|
|
37
|
+
return clean(lines.join('\n')).split('\n').map(line=>indent+line).join('\n');
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {visibleWidth} from '@mariozechner/pi-tui';
|
|
2
|
+
import {cleanText as clean} from './theme.js';
|
|
3
|
+
|
|
4
|
+
const heading=value=>clean(String(value).replace(/[\r\n\t]+/g,' '));
|
|
5
|
+
const record=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
6
|
+
const scalar=value=>value===null||['string','number','boolean'].includes(typeof value);
|
|
7
|
+
const empty=value=>value==null||value===''||typeof value==='object'&&Object.keys(value).length===0;
|
|
8
|
+
const text=value=>value===undefined?'未提供':value===null?'空值':value===''?'空字符串':typeof value==='boolean'?(value?'是':'否'):clean(value);
|
|
9
|
+
const sameKeys=(a,b)=>a.length===b.length&&a.every(key=>b.includes(key));
|
|
10
|
+
|
|
11
|
+
// 表格只选择阅读列;完整的长值、复杂值和其余字段在表后按行号展开。
|
|
12
|
+
export function resultTable(value,{schema,width,indent,label,render}){
|
|
13
|
+
const available=width-visibleWidth(indent);
|
|
14
|
+
if(!Number.isFinite(available)||available<40)return;
|
|
15
|
+
const list=Array.isArray(value),entries=list?value.map((item,index)=>[String(index+1),item]):Object.entries(value);
|
|
16
|
+
if(entries.length<2||entries.some(([,item])=>!record(item)))return;
|
|
17
|
+
const firstKeys=Object.keys(entries[0][1]),declared=schema?.['x-fst-table'];
|
|
18
|
+
const explicit=Array.isArray(declared)&&declared.length>0&&declared.every(key=>typeof key==='string')&&new Set(declared).size===declared.length;
|
|
19
|
+
if(!explicit&&!entries.every(([,item])=>sameKeys(firstKeys,Object.keys(item))))return;
|
|
20
|
+
const rowSchema=list?(Array.isArray(schema?.items)?undefined:schema?.items):schema?.additionalProperties;
|
|
21
|
+
const candidates=(explicit?declared:firstKeys).filter(key=>entries.every(([,item])=>item[key]===undefined||scalar(item[key]))&&entries.some(([,item])=>item[key]!==undefined&&(explicit||item[key]!==null&&item[key]!=='')));
|
|
22
|
+
if(candidates.length<(explicit?1:2))return;
|
|
23
|
+
const duplicate=!explicit&&!list?candidates.find(name=>entries.every(([key,item])=>item[name]===key)):undefined;
|
|
24
|
+
const common=!explicit?candidates.filter(key=>key!==duplicate&&entries.every(([,item])=>item[key]===entries[0][1][key])&&visibleWidth(text(entries[0][1][key]))<=24&&!/[\r\n\t]/.test(text(entries[0][1][key]))):[];
|
|
25
|
+
const ordered=candidates.filter(key=>key!==duplicate&&!common.includes(key));
|
|
26
|
+
if(!explicit)ordered.sort((a,b)=>Number(entries.some(([,item])=>/[\r\n\t]/.test(text(item[a]))||visibleWidth(text(item[a]))>24))-Number(entries.some(([,item])=>/[\r\n\t]/.test(text(item[b]))||visibleWidth(text(item[b]))>24)));
|
|
27
|
+
// 条目键也可能很长,序号始终可定位;完整键在表后的对应条目显示。
|
|
28
|
+
const identity={key:null,title:list?'序号':duplicate?heading(label(duplicate,rowSchema?.properties?.[duplicate])):'条目',values:entries.map(([key])=>list?key:clean(key)),width:0};
|
|
29
|
+
const columns=[identity,...ordered.map(key=>({key,title:heading(label(key,rowSchema?.properties?.[key])),values:entries.map(([,item])=>text(item[key]))}))];
|
|
30
|
+
for(const column of columns)column.width=column.values.reduce((size,value)=>Math.max(size,/[\r\n\t]/.test(value)||visibleWidth(value)>24?Math.max(6,Math.min(24,visibleWidth(value))):visibleWidth(value)),visibleWidth(column.title));
|
|
31
|
+
const selected=[];let used=1;
|
|
32
|
+
for(const column of columns){
|
|
33
|
+
if(used+column.width+3>available||selected.length>=7){if(column===identity)return;continue;}
|
|
34
|
+
selected.push(column);used+=column.width+3;
|
|
35
|
+
}
|
|
36
|
+
if(selected.length<2&&!common.length)return;
|
|
37
|
+
let folded=false;
|
|
38
|
+
const detailRows=[],border=(left,middle,right)=>indent+left+selected.map(column=>'─'.repeat(column.width+2)).join(middle)+right;
|
|
39
|
+
const line=values=>indent+'│'+values.map((value,index)=>' '+value+' '.repeat(selected[index].width-visibleWidth(value)+1)).join('│')+'│';
|
|
40
|
+
const lines=[border('┌','┬','┐'),line(selected.map(column=>column.title)),border('├','┼','┤')];
|
|
41
|
+
entries.forEach(([key,item],index)=>{
|
|
42
|
+
const details=Object.create(null);let longKey=false;
|
|
43
|
+
const values=selected.map(column=>{
|
|
44
|
+
const value=column.values[index];
|
|
45
|
+
if(/[\r\n\t]/.test(value)||visibleWidth(value)>column.width){
|
|
46
|
+
if(column.key===null)longKey=true;else details[column.key]=item[column.key];
|
|
47
|
+
return '见下文';
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
});
|
|
51
|
+
for(const [name,value] of Object.entries(item))if(name!==duplicate&&!common.includes(name)&&!selected.some(column=>column.key===name)){
|
|
52
|
+
if(empty(value))folded=true;else details[name]=value;
|
|
53
|
+
}
|
|
54
|
+
lines.push(line(values));
|
|
55
|
+
if(longKey||Object.keys(details).length){
|
|
56
|
+
const identityText=list?`[${index+1}]`:`[${index+1}] ${clean(key)}`;
|
|
57
|
+
const body=render(details,indent+' ',rowSchema);
|
|
58
|
+
detailRows.push(`${indent}${identityText} · 补充字段${longKey?'(完整条目键)':''}${body.includes('\n')?'\n'+body:':'+body.trimStart()}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
lines.push(border('└','┴','┘'));
|
|
62
|
+
if(common.length)lines.push(`${indent}共有字段:${common.map(key=>`${label(key,rowSchema?.properties?.[key])}:${text(entries[0][1][key])}`).join(' · ')}`);
|
|
63
|
+
if(folded)lines.push(`${indent}空字段已折叠;/logs → j 查看原始数据。`);
|
|
64
|
+
if(detailRows.length)lines.push(`${indent}补充字段(对应表格行,保留全文):`,...detailRows);
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|