@yanglaofish/fishtools 0.5.0 → 0.5.2
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 +4 -2
- package/manual/README.md +4 -2
- package/manual/docs/deployment.md +2 -2
- package/manual/docs/modules.md +2 -0
- package/manual/docs/plugin-development.md +3 -1
- package/manual/docs/releases.md +44 -6
- package/manual/docs/superpowers/plans/2026-10-13-command-shortcuts.md +107 -0
- package/manual/docs/superpowers/reviews/2026-10-13-0.5.0-followup-review.md +70 -0
- package/manual/docs/superpowers/reviews/2026-10-13-command-shortcuts-review.md +63 -0
- package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +1 -1
- package/manual/docs/superpowers/specs/2026-10-13-command-shortcuts-design.md +56 -0
- package/manual/docs/superpowers/specs/2026-10-13-issue-4-workflow-menu.md +28 -0
- package/manual/docs/terminal-commands.md +64 -0
- package/manual/docs/verification.md +39 -1
- package/manual/packages/core/README.md +3 -1
- package/manual/packages/core/package.json +1 -0
- package/manual/packages/core/src/application.js +126 -20
- package/manual/packages/core/src/export-snapshot.js +20 -1
- package/manual/packages/core/src/version.js +1 -1
- package/manual/packages/fst-cli/README.md +8 -1
- package/manual/packages/fst-cli/package.json +1 -0
- package/manual/packages/fst-cli/src/terminal/command-menu.js +186 -0
- package/manual/packages/fst-cli/src/terminal/navigation.js +4 -3
- package/manual/packages/fst-cli/src/terminal/run-feed.js +24 -9
- package/manual/packages/fst-cli/src/terminal/session.js +35 -4
- package/manual/packages/fst-cli/src/terminal/view.js +43 -8
- package/manual/packages/fst-cli/src/terminal/workflow-menu.js +1 -6
- package/manual/packages/fst-devtools/src/documentation.js +2 -1
- package/manual/packages/fst-logs/README.md +11 -0
- package/manual/packages/fst-logs/package.json +2 -0
- package/manual/packages/fst-logs/src/run-logs.js +8 -23
- package/manual/packages/fst-logs/src/run-preview.js +18 -0
- package/manual/packages/fst-logs/src/step-summaries.js +28 -0
- package/manual/packages/fst-platform/README.md +17 -0
- package/manual/packages/fst-platform/package.json +1 -0
- package/manual/packages/fst-platform/src/process.js +12 -8
- package/manual/packages/fst-platform/src/shell.js +58 -0
- package/manual/packages/fst-plugin/src/contracts.js +6 -2
- package/manual/packages/fst-plugin/src/host.js +7 -4
- package/manual/packages/fst-plugin/src/runtime.js +8 -5
- package/manual/packages/fst-workspace/README.md +3 -0
- package/manual/packages/fst-workspace/package.json +1 -0
- package/manual/packages/fst-workspace/src/shortcuts.js +119 -0
- package/manual/packages/pack/index.js +1 -1
- package/manual/plugins/commands/LICENSE +21 -0
- package/manual/plugins/commands/README.md +41 -0
- package/manual/plugins/commands/index.js +94 -0
- package/manual/plugins/commands/package.json +17 -0
- package/node_modules/@mytools/commands/LICENSE +21 -0
- package/node_modules/@mytools/commands/README.md +41 -0
- package/node_modules/@mytools/commands/index.js +94 -0
- package/node_modules/@mytools/commands/package.json +17 -0
- package/node_modules/@mytools/core/README.md +3 -1
- package/node_modules/@mytools/core/package.json +1 -0
- package/node_modules/@mytools/core/src/application.js +126 -20
- package/node_modules/@mytools/core/src/export-snapshot.js +20 -1
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/README.md +8 -1
- package/node_modules/@mytools/fst-cli/package.json +1 -0
- package/node_modules/@mytools/fst-cli/src/terminal/command-menu.js +186 -0
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +4 -3
- package/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +24 -9
- package/node_modules/@mytools/fst-cli/src/terminal/session.js +35 -4
- package/node_modules/@mytools/fst-cli/src/terminal/view.js +43 -8
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +1 -6
- package/node_modules/@mytools/fst-devtools/src/documentation.js +2 -1
- package/node_modules/@mytools/fst-logs/README.md +11 -0
- package/node_modules/@mytools/fst-logs/package.json +2 -0
- package/node_modules/@mytools/fst-logs/src/run-logs.js +8 -23
- package/node_modules/@mytools/fst-logs/src/run-preview.js +18 -0
- package/node_modules/@mytools/fst-logs/src/step-summaries.js +28 -0
- package/node_modules/@mytools/fst-platform/README.md +17 -0
- package/node_modules/@mytools/fst-platform/package.json +1 -0
- package/node_modules/@mytools/fst-platform/src/process.js +12 -8
- package/node_modules/@mytools/fst-platform/src/shell.js +58 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +6 -2
- package/node_modules/@mytools/fst-plugin/src/host.js +7 -4
- package/node_modules/@mytools/fst-plugin/src/runtime.js +8 -5
- package/node_modules/@mytools/fst-workspace/README.md +3 -0
- package/node_modules/@mytools/fst-workspace/package.json +1 -0
- package/node_modules/@mytools/fst-workspace/src/shortcuts.js +119 -0
- package/node_modules/@mytools/pack/index.js +1 -1
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1212 -1187
- package/package.json +3 -1
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
# 验收记录
|
|
2
2
|
|
|
3
|
+
## 0.5.2 命令注册与快捷名称(2026-10-13)
|
|
4
|
+
|
|
5
|
+
- 分支 `codex/0.5.2-command-shortcuts`,固定基线 `68a84cd`;按用户指定目标集中实现 `!` 系统命令、`/commands` 注册动作、`/shortcuts` 任意动作或命令的中文快捷名称与默认参数。
|
|
6
|
+
主输入框和命令编辑框均为 **Shift+Enter 换行、Enter 提交**,Alt+Enter 兼容备用,粘贴保留多行、缩进和引号。
|
|
7
|
+
- 独立 Standards / Spec 审查分别确认 3 项(最高 P1)与 3 项(最高 P2),均已修正。
|
|
8
|
+
最终生产代码 `30d7472`、探针与记录补充 `7fa6178` 两次独立复审均通过;详见[审查记录](superpowers/reviews/2026-10-13-command-shortcuts-review.md)。
|
|
9
|
+
- 修正真实 Windows 长脚本参数限制、命令名称冲突漏查、快捷目标身份变化、跨实例过期工作流草稿,以及整组保存自动启用旧别名的问题。
|
|
10
|
+
全量测试还复现并修正输出超限时管道提前关闭留下后代进程的问题;原心跳断言与新增七种进程树停止探针均通过。
|
|
11
|
+
后续一次单字节文件大小变化未在 164 次诊断中复现,保留事实,不认定原因;最终同时检查进程状态和实际内容。
|
|
12
|
+
- `npm run dist:npm` 成功,本地归档 9,804,373 字节、285 个生产包。
|
|
13
|
+
指定该归档后完整 `npm test` **489 项:487 通过、0 失败、2 跳过**,175.1 秒;包含离线全局安装、脱离源码运行和再次导出。
|
|
14
|
+
本机 Windows/Node 24.15.0;POSIX 保存失败注入与未开放权限的文件 symlink 跳过,目录 junction 回归执行。
|
|
15
|
+
- [首次正式验收](https://github.com/yanglaofish/Fishtools/actions/runs/38358232278):Ubuntu **488 通过、0 失败、1 跳过**,便携包及断网只读容器通过;Windows **486 通过、2 失败、1 跳过**。
|
|
16
|
+
两项失败均为 Shell 展开 `RUNNER~1` 短路径后与测试字面路径不等,真实 `C:\PROGRA~1` 诊断复现并核对为同一目录。修正测试为 `realpath` 的目录身份比较,生产执行目录行为未变。
|
|
17
|
+
发布任务被跳过,npm 0.5.2 与 GitHub Release 尚不存在。用户明确要求同时合入 [issue #4](superpowers/specs/2026-10-13-issue-4-workflow-menu.md) 的复制入口修正:列表每个工作流只出现一次,进入管理页后复制;相关专项 **26/26** 通过。
|
|
18
|
+
合入后本地重建归档 **9,806,640 字节、285 个生产包**,指定该归档完整 `npm test` **489 项:487 通过、0 失败、2 跳过**,186.3 秒。
|
|
19
|
+
补充范围 `8223bad...c567534` 的独立 Standards / Spec 复审均通过,新增确认问题均为 0。
|
|
20
|
+
最终双平台验收重新执行,完成后核对正式发布产物;待发布 tag 更新前检查旧对象和未发布状态,不覆盖已发布版本。
|
|
21
|
+
|
|
22
|
+
## 0.5.1 全代际复核修正(2026-10-13)
|
|
23
|
+
|
|
24
|
+
- 分支 `codex/review-0.5.0-followup`,以 0.4.0 `f529244` 至 0.5.0 后验收提交 `d26a0ee` 为固定范围执行独立 Standards / Spec 审查。
|
|
25
|
+
共确认 6 项问题:直连子进程重新继承代理、清理失败后重复启动资源、嵌套 stop 的权限误拦、TUI 摘要淘汰崩溃、步骤缓存超预算、最终结果缓存超预算。
|
|
26
|
+
均先失败复现再修正;修补后的两个独立复审无剩余阻断问题,详见[审查记录](superpowers/reviews/2026-10-13-0.5.0-followup-review.md)。
|
|
27
|
+
- logs 提供统一步骤缓存与结果投影,TUI 不重新持有完整大正文;真实 2 MiB 输入/输出验证业务 done 与完整 JSONL 不受影响。
|
|
28
|
+
实际子进程验证三种代理模式、正常/失败清理恢复与 Linux 普通用户嵌套 stop 的执行边界。
|
|
29
|
+
- `npm run dist:npm` 成功,0.5.1 本地归档 9,757,761 字节、284 个生产包;用该归档执行完整 `npm test` **382 项:381 通过、0 失败、1 跳过**,166.0 秒。
|
|
30
|
+
包括离线全局安装、脱离源码运行与再次导出。Windows 文件 symlink 权限导致一项跳过,目录 junction 回归执行。
|
|
31
|
+
- 产品和 CLI 同步为 0.5.1,保持工作流与内部插件 API 格式兼容。发布 tag 固定于 `1fff499`,后续验收记录不改变发行产物。
|
|
32
|
+
- [正式发布流程](https://github.com/yanglaofish/Fishtools/actions/runs/38329427621)全部成功:同一 npm 归档在 Windows **382/382**、Ubuntu **381/382(1 项 Windows 专用跳过)**通过,零失败。
|
|
33
|
+
两平台自包含包、无 Node 的断网只读 Debian 容器检查通过;[普通主分支 CI](https://github.com/yanglaofish/Fishtools/actions/runs/38329413252)也成功。
|
|
34
|
+
- npm 官方注册表 `latest` 已为 **0.5.1**,独立下载公开 tarball 验证:9,757,768 字节,SHA-512 与注册表 integrity 一致,
|
|
35
|
+
SHA-256 `ca1463a00a8227abe6a71c0f0627f2e881902703f8a3c9bc7267ce49ea6691a9` 与 Release 的 `SHA256SUMS` 一致。
|
|
36
|
+
- [GitHub Release](https://github.com/yanglaofish/Fishtools/releases/tag/v0.5.1)已公开于仓库访问范围,包含 npm、Windows(52,457,324 字节)、Linux(58,024,913 字节)归档及校验文件。
|
|
37
|
+
发布说明补齐本轮修正及审查记录链接;发布任务首次运行即成功,没有重复上传或改动已发布版本。
|
|
38
|
+
|
|
3
39
|
## 0.5.0 发布前验证(2026-10-13)
|
|
4
40
|
|
|
5
41
|
- 分支 `codex/kernel-runtime-readiness`,固定基线 `002260b`。完成动作级代理、权限预检、工作流复制与删除、引用保护、定向插件更新及恢复;双轴审查与修复见[审查记录](superpowers/reviews/2026-10-13-kernel-runtime-release-review.md)。
|
|
6
42
|
- 审查补齐项目配置及工作流覆盖保存的备份、卸载期间的并发保护、恢复失败后的停止运行、代理实际生效模式和失效引用提示,以及权限不足时的失败日志。原有宿主关闭后能力列表行为已恢复。
|
|
7
43
|
- Windows Node 24.15.0 上 `npm run dist:npm` 成功;最终 npm 归档 `dist/yanglaofish-fishtools-0.5.0.tgz` 为 9,750,617 字节,收集 284 个生产包。Web 构建成功;仅有第三方 `use client` 指令提示。
|
|
8
44
|
- 设置 `FISHTOOLS_TEST_NPM_ARCHIVE` 指向最终归档后运行完整 `npm test`,**374 项:373 通过、0 失败、1 跳过**,164.2 秒。覆盖 npm 离线全局安装、脱离源码运行、再次导出、代理规则、权限、备份与并发卸载。跳过的是本机未开放文件符号链接权限的 Windows 用例;目录 junction 仍执行。
|
|
9
|
-
-
|
|
45
|
+
- [v0.5.0 发布工作流](https://github.com/yanglaofish/Fishtools/actions/runs/38282818908)最终全部成功:Windows **374/374**,Ubuntu **373/374**、1 项跳过;两平台便携包与无 Node 的断网只读 Linux 容器验收通过。
|
|
46
|
+
- npm 官方源已公开 `@yanglaofish/fishtools@0.5.0`,`latest` 指向 0.5.0;[GitHub Release](https://github.com/yanglaofish/Fishtools/releases/tag/v0.5.0)已公开,包含 npm、Windows、Linux 归档及 `SHA256SUMS`。
|
|
47
|
+
- npm 首次接受上传后处理超过发布脚本的 5 分钟等待上限;注册表可见后仅重试失败的发布任务,归档完整性匹配,随后完成 Release,未重复创建版本。
|
|
10
48
|
|
|
11
49
|
## 完整内核能力演进(2026-10-12,本地未发布)
|
|
12
50
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
组合各能力模块、协调运行与关闭、提供 CLI/Web 共用应用接口及导出声明快照。日志写入和缓存由 logs 负责;导出实现通过可选 pack 依赖加载。
|
|
8
8
|
|
|
9
|
-
内部依赖:`@mytools/sdk`、`@mytools/fst-platform`、`@mytools/fst-logs`、`@mytools/fst-workflow`、`@mytools/fst-plugin`、`@mytools/fst-workspace`、`@mytools/pack`。
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/fst-platform`、`@mytools/fst-logs`、`@mytools/fst-workflow`、`@mytools/fst-plugin`、`@mytools/fst-workspace`、官方 `@mytools/commands`、`@mytools/pack`。
|
|
10
10
|
`@mytools/pack` 是可选 peer 依赖;普通发行包含它,只读发行省略它。既有 `/process`、`/bundle`、`/environments` 子入口保留兼容转发。
|
|
11
11
|
|
|
12
12
|
## 公开入口
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
## 维护与验证
|
|
27
27
|
|
|
28
|
+
`app.startShell(script,options)` 执行临时命令,`app.startAction(id,input,context)` 调用任意动作,两者均纳入运行记录与关闭等待。`startCommand(name,input,{expectedPlugin,...context})` 可固定命令提供者,启动时再次检查,防止参数填写期间被其他同名能力替换。`configureCommandPresets` 和 `configureShortcuts` 在备份、冲突检查与变更锁内保存项目设置;命令快捷项在用户明确设置时绑定提供者 ID,普通安装/卸载和未改变目标的整组保存不会重新绑定;已有未绑定项必须明确提供 plugin 或在界面重新选择目标。注册项变化通过定向宿主更新生效,导出快照保留所需定义与有效快捷项。
|
|
29
|
+
|
|
28
30
|
本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
|
|
29
31
|
|
|
30
32
|
在仓库根目录执行:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, writeFile, rename, rm } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
@@ -7,6 +7,9 @@ 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 {runShell} from '@mytools/fst-platform/shell';
|
|
11
|
+
import {COMMANDS_PLUGIN_ID,commandsPluginRoot,createCommandsPlugin,validateCommandPresets} from '@mytools/commands';
|
|
12
|
+
import {validateShortcuts,describeShortcuts,bindShortcutCommands} from '@mytools/fst-workspace/shortcuts';
|
|
10
13
|
import {createFileProbe} from '@mytools/fst-platform/fs';
|
|
11
14
|
import {readJson} from '@mytools/fst-platform/json';
|
|
12
15
|
import {loadPlugin} from '@mytools/fst-plugin/loader';
|
|
@@ -20,9 +23,22 @@ import {createExportSnapshot} from './export-snapshot.js';
|
|
|
20
23
|
import {createRunLogs,validateKeepRuns} from '@mytools/fst-logs';
|
|
21
24
|
import {openPluginState,withPluginMutation,assertPluginState,savePluginState,leaseInstallation} from '@mytools/fst-workspace/installations';
|
|
22
25
|
import {planPluginUpdate,changedPluginSources} from './plugin-update.js';
|
|
23
|
-
import {validateNetworkPolicy,inspectNetworkPolicy} from '@mytools/fst-platform/network';
|
|
26
|
+
import {validateNetworkPolicy,inspectNetworkPolicy,resolveNetworkPolicy,buildProcessEnv} from '@mytools/fst-platform/network';
|
|
24
27
|
import {resolvePrivilegeRequirement,checkStaticPrivilege} from '@mytools/fst-plugin/privileges';
|
|
25
28
|
|
|
29
|
+
function declaredCommands(entries){
|
|
30
|
+
return entries.flatMap(entry=>{const plugin=entry.plugin??entry;return Object.keys(plugin.commands??{}).map(name=>({name,plugin:plugin.id}));});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function workflowActions(steps,found=new Set()){
|
|
34
|
+
for(const step of steps??[]){
|
|
35
|
+
if(step.action)found.add(step.action);
|
|
36
|
+
if(step.branch){workflowActions(step.branch.then,found);workflowActions(step.branch.else,found);}
|
|
37
|
+
if(step.parallel)workflowActions(step.parallel.steps,found);
|
|
38
|
+
}
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
export async function createApplication({projectDir=process.cwd(),workspace,outputDir,plugins,config={},maintenance=false,omitInstalledPlugin,allowUnknownPrivileges=false}={}) {
|
|
27
43
|
projectDir=await prepareBundleWorkspace(path.resolve(projectDir),workspace);
|
|
28
44
|
const project=await readJson(path.join(projectDir,'flow.project.json'),{plugins:[]});
|
|
@@ -47,9 +63,14 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
47
63
|
catalog:()=>host.catalog(),
|
|
48
64
|
listPlugins:()=>[...host.definitions.values()].map(p=>({id:p.id,version:p.version,title:p.title,description:p.description,platforms:p.platforms,architectures:p.architectures,environments:p.environments,config:p.config,requirements:p.requirements,privileges:pluginPrivileges(p),runtime:{...host.runtimeStatus().find(r=>r.id===p.id),...(p.runtime?{updatePolicy:p.runtime.updatePolicy??'maintenance'}:{})},removable:!!(state.dependencies?.[p.id]||state.links?.[p.id]),development:!!state.links?.[p.id],warnings:host.warnings.filter(item=>item.plugin===p.id).map(item=>item.message)})),
|
|
49
65
|
listCommands:()=>host.commands(),
|
|
66
|
+
listShortcuts:()=>describeShortcuts(project.shortcuts??{},{actions:host.catalog(),commands:host.commands()}),
|
|
67
|
+
async configureShortcuts(value){return updateUserCommands({shortcuts:value});},
|
|
68
|
+
async configureCommandPresets(value,options={}){
|
|
69
|
+
return updateUserCommands({commandPresets:value,...(options.shortcuts!==undefined?{shortcuts:options.shortcuts}:{})},options);
|
|
70
|
+
},
|
|
50
71
|
listWorkflows:()=>store.list(),
|
|
51
|
-
async saveWorkflow(w){return mutateWorkflow(()=>store.save(w));},
|
|
52
|
-
async duplicateWorkflow(definition,options){return mutateWorkflow(()=>store.duplicate(definition,options));},
|
|
72
|
+
async saveWorkflow(w){return mutateWorkflow(()=>store.save(w),w);},
|
|
73
|
+
async duplicateWorkflow(definition,options){return mutateWorkflow(()=>store.duplicate(definition,options),definition);},
|
|
53
74
|
async removeWorkflow(id){return mutateWorkflow(()=>store.remove(id));},
|
|
54
75
|
async checkWorkflow(id){
|
|
55
76
|
host.assertHealthy();
|
|
@@ -71,7 +92,29 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
71
92
|
async startCommand(name,input={},context={}){
|
|
72
93
|
if(busy)throw new FlowError('插件更新中,请稍后调用');
|
|
73
94
|
const networkPolicy=structuredClone(project.network??{});
|
|
74
|
-
return app.executeTracked(`/${name}`,(activeHost,runContext)=>
|
|
95
|
+
return app.executeTracked(`/${name}`,(activeHost,runContext)=>{
|
|
96
|
+
if(context.expectedPlugin!==undefined&&!activeHost.commands().some(command=>command.name===name&&command.plugin===context.expectedPlugin)){
|
|
97
|
+
throw new FlowError('命令目标插件已变更,请重新选择目标后执行',{code:'COMMAND_TARGET_CHANGED',details:{command:name,plugin:context.expectedPlugin}});
|
|
98
|
+
}
|
|
99
|
+
return activeHost.invokeCommand(name,input,{...context,...runContext,networkPolicy});
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
async startAction(id,input={},context={}){
|
|
103
|
+
if(busy)throw new FlowError('插件更新中,请稍后调用');
|
|
104
|
+
const networkPolicy=structuredClone(project.network??{});
|
|
105
|
+
return app.executeTracked(id,(activeHost,runContext)=>activeHost.invoke(id,input,{...context,...runContext,networkPolicy}));
|
|
106
|
+
},
|
|
107
|
+
async startShell(script,{shell='auto',timeout=30000}={}){
|
|
108
|
+
if(busy)throw new FlowError('项目更新中,请稍后执行命令');
|
|
109
|
+
if(!Number.isSafeInteger(timeout)||timeout<0||timeout>2147483647)throw new FlowError('命令超时必须为 0 到 2147483647 毫秒,0 表示不限时',{code:'VALIDATION'});
|
|
110
|
+
const policy=resolveNetworkPolicy(structuredClone(project.network??{}));
|
|
111
|
+
const env=buildProcessEnv(policy);
|
|
112
|
+
return app.executeTracked('!命令',async(_host,context)=>{
|
|
113
|
+
const result=await runShell(script,{shell,temporaryDir:context.outputDir,cwd:projectDir,env,extendEnv:false,signal:context.signal,timeout,outputMode:'stream',summaryBytes:65536,
|
|
114
|
+
onOutput:({stream,message})=>context.log(message,undefined,{stream})});
|
|
115
|
+
if(result.exitCode!==0)throw new FlowError(`命令执行失败,退出码 ${result.exitCode}`,{code:'EXECUTION_FAILED',details:result});
|
|
116
|
+
return result;
|
|
117
|
+
});
|
|
75
118
|
},
|
|
76
119
|
async executeTracked(id,execute){
|
|
77
120
|
const runId=randomUUID(),controller=new AbortController();
|
|
@@ -245,7 +288,11 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
245
288
|
let initial;
|
|
246
289
|
const services={application:app,runProcess,fs:fileProbe};
|
|
247
290
|
const hostProject=()=>({directory:projectDir,readOnly:!!project.readOnly});
|
|
248
|
-
const build=extra=>
|
|
291
|
+
const build=extra=>{
|
|
292
|
+
const entries=entriesWithPresets([...initial,...extra],project.commandPresets??{});
|
|
293
|
+
validateShortcuts(project.shortcuts??{},{reservedNames:declaredCommands(entries).map(command=>command.name)});
|
|
294
|
+
return createHost({plugins:entries,config:{...project.config,...config},services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:!maintenance,allowUnknownPrivileges});
|
|
295
|
+
};
|
|
249
296
|
try{
|
|
250
297
|
initial=plugins??await Promise.all((project.plugins??[]).map(spec=>loadPlugin(spec,project.baseBundle?.root??projectDir)));
|
|
251
298
|
const external=[];
|
|
@@ -253,8 +300,8 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
253
300
|
for(const [name,source] of Object.entries(state.links??{}))if(!maintenance||name!==omitInstalledPlugin)external.push(await loadPlugin(source,projectDir));
|
|
254
301
|
host=await build(external);
|
|
255
302
|
}catch(error){await fileProbe.close();await opened.release();throw error;}
|
|
256
|
-
for(const name of ['startRun','startCommand','startWeb','saveWorkflow','duplicateWorkflow','removeWorkflow','installPlugin','linkPlugin','removePlugin','configurePlugin','configureNetworkPolicy','configureLogs','setMaintenance','pack']){
|
|
257
|
-
const original=app[name],isRun=['startRun','startCommand'].includes(name);
|
|
303
|
+
for(const name of ['startRun','startCommand','startAction','startShell','startWeb','saveWorkflow','duplicateWorkflow','removeWorkflow','installPlugin','linkPlugin','removePlugin','configurePlugin','configureNetworkPolicy','configureLogs','configureCommandPresets','configureShortcuts','setMaintenance','pack']){
|
|
304
|
+
const original=app[name],isRun=['startRun','startCommand','startAction','startShell'].includes(name);
|
|
258
305
|
app[name]=async(...args)=>{
|
|
259
306
|
if(closing)throw new FlowError('应用已关闭');
|
|
260
307
|
if(isRun)launching++;
|
|
@@ -262,11 +309,16 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
262
309
|
try{return await task;}finally{operations.delete(task);if(isRun)launching--;}
|
|
263
310
|
};
|
|
264
311
|
}
|
|
265
|
-
async function mutateWorkflow(operation){
|
|
312
|
+
async function mutateWorkflow(operation,definition){
|
|
266
313
|
if(project.readOnly)throw new FlowError('只读运行包不能修改工作流');
|
|
267
314
|
if(busy)throw new FlowError('插件变更期间暂不能修改工作流',{code:'PLUGIN_BUSY'});
|
|
268
315
|
return withPluginMutation(projectDir,async()=>{
|
|
269
316
|
await assertPluginState(projectDir,state);
|
|
317
|
+
if(definition){
|
|
318
|
+
const workflow=store.validateShape(definition),latest=await readJson(path.join(projectDir,'flow.project.json'),project);
|
|
319
|
+
const missing=[...workflowActions(workflow.steps)].filter(id=>id.startsWith(`${COMMANDS_PLUGIN_ID}:`)&&!Object.hasOwn(latest.commandPresets??{},id.slice(COMMANDS_PLUGIN_ID.length+1)));
|
|
320
|
+
if(missing.length)throw new FlowError('引用的注册命令已删除,请重新选择动作后保存',{code:'COMMAND_NOT_FOUND',details:{actions:missing}});
|
|
321
|
+
}
|
|
270
322
|
return operation();
|
|
271
323
|
});
|
|
272
324
|
}
|
|
@@ -280,6 +332,55 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
280
332
|
await writeFile(backup,original,{flag:'wx',mode:0o600});
|
|
281
333
|
return backup;
|
|
282
334
|
}
|
|
335
|
+
function entriesWithPresets(entries,presets){
|
|
336
|
+
const definition=createCommandsPlugin(presets);
|
|
337
|
+
const result=entries.filter(entry=>(entry.plugin??entry).id!==COMMANDS_PLUGIN_ID);
|
|
338
|
+
if(Object.keys(definition.actions).length)result.push({plugin:definition,root:commandsPluginRoot});
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
async function updateUserCommands(changes,options={}){
|
|
342
|
+
if(project.readOnly)throw new FlowError('只读运行包不能修改注册命令或快捷名称');
|
|
343
|
+
if(busy||launching||app.listRuns().some(run=>run.status==='running'))throw new FlowError('请等待运行或安装结束');
|
|
344
|
+
const requested=structuredClone(changes);
|
|
345
|
+
if(Object.hasOwn(requested,'commandPresets'))requested.commandPresets=validateCommandPresets(requested.commandPresets);
|
|
346
|
+
busy=true;
|
|
347
|
+
try{return await withPluginMutation(projectDir,async()=>{
|
|
348
|
+
await assertPluginState(projectDir,state);
|
|
349
|
+
const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
|
|
350
|
+
if(latest.readOnly)throw new FlowError('只读运行包不能修改注册命令或快捷名称');
|
|
351
|
+
for(const field of Object.keys(requested))if(!isDeepStrictEqual(latest[field]??{},project[field]??{})){
|
|
352
|
+
throw new FlowError('注册命令或快捷名称已被另一实例修改,请重新打开工作区再保存',{code:'USER_COMMANDS_CONFLICT',details:{field}});
|
|
353
|
+
}
|
|
354
|
+
const nextProject={...latest,...requested};
|
|
355
|
+
const presets=validateCommandPresets(nextProject.commandPresets??{});
|
|
356
|
+
const oldEntries=[...host.definitions.values()].map(plugin=>({plugin,root:plugin.root}));
|
|
357
|
+
const entries=entriesWithPresets(oldEntries,presets);
|
|
358
|
+
const reservedNames=declaredCommands(entries).map(command=>command.name);
|
|
359
|
+
const shortcuts=Object.hasOwn(requested,'shortcuts')?bindShortcutCommands(nextProject.shortcuts??{},{commands:declaredCommands(entries),previous:latest.shortcuts??{}}):nextProject.shortcuts??{};
|
|
360
|
+
nextProject.shortcuts=validateShortcuts(shortcuts,{reservedNames});
|
|
361
|
+
const removed=new Set(Object.keys(project.commandPresets??{}).filter(id=>!Object.hasOwn(presets,id)).map(id=>`${COMMANDS_PLUGIN_ID}:${id}`));
|
|
362
|
+
if(removed.size){
|
|
363
|
+
const workflows=(await findPluginWorkflowReferences(COMMANDS_PLUGIN_ID)).filter(workflow=>workflow.actions.some(action=>removed.has(action)));
|
|
364
|
+
if(workflows.length)throw new FlowError(`命令仍被工作流使用:${workflows.map(workflow=>workflow.title).join('、')}`,{code:'COMMAND_IN_USE',details:{workflows}});
|
|
365
|
+
}
|
|
366
|
+
const effectiveConfig={...latest.config,...config},oldConfig={...project.config,...config};
|
|
367
|
+
const nextDefinitions=new Map(entries.map(entry=>{const plugin=entry.plugin??entry;return [plugin.id,plugin];}));
|
|
368
|
+
const changed=new Set(isDeepStrictEqual(presets,project.commandPresets??{})?[]:[COMMANDS_PLUGIN_ID]);
|
|
369
|
+
const plan=planPluginUpdate(host.definitions,nextDefinitions,oldConfig,effectiveConfig,changed);
|
|
370
|
+
assertMaintenance(plan,options);
|
|
371
|
+
const backup=await backupProjectFile(file,latest,'commands');
|
|
372
|
+
if(plan.affected.length)await host.replace({plugins:entries,config:effectiveConfig,affected:plan.affected});
|
|
373
|
+
const temp=`${file}.${randomUUID()}.tmp`;
|
|
374
|
+
try{await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);}
|
|
375
|
+
catch(error){
|
|
376
|
+
if(plan.affected.length)try{await host.replace({plugins:oldEntries,config:oldConfig,affected:plan.affected});}
|
|
377
|
+
catch(recovery){host.invalidate();throw new AggregateError([error,recovery],`命令设置保存失败且恢复失败:${error.message};${recovery.message}`);}
|
|
378
|
+
throw error;
|
|
379
|
+
}finally{await rm(temp,{force:true});}
|
|
380
|
+
for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
|
|
381
|
+
return {backup,commandPresets:structuredClone(presets),shortcuts:structuredClone(nextProject.shortcuts)};
|
|
382
|
+
});}finally{busy=false;}
|
|
383
|
+
}
|
|
283
384
|
function pluginPrivileges(plugin){
|
|
284
385
|
const report=action=>{
|
|
285
386
|
const requirement=resolvePrivilegeRequirement(plugin,action);
|
|
@@ -293,7 +394,15 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
293
394
|
if(active.length)throw new FlowError(`请先停止受影响插件提供的 Web:${active.join('、')}`,{code:'WEB_ACTIVE',details:{plugins:active}});
|
|
294
395
|
}
|
|
295
396
|
function workflowPrivileges(closure,entryId){
|
|
296
|
-
const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[];
|
|
397
|
+
const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[],stoppingFlows=new Map();
|
|
398
|
+
const mayStopFlow=id=>{
|
|
399
|
+
if(!stoppingFlows.has(id))stoppingFlows.set(id,(flows.get(id)?.steps??[]).some(mayStopStep));
|
|
400
|
+
return stoppingFlows.get(id);
|
|
401
|
+
};
|
|
402
|
+
const mayStopStep=step=>!!step.stop
|
|
403
|
+
||!!step.workflow&&mayStopFlow(store.childId(step.workflow))
|
|
404
|
+
||!!step.branch&&[...step.branch.then,...(step.branch.else??[])].some(mayStopStep)
|
|
405
|
+
||!!step.parallel&&step.parallel.steps.some(mayStopStep);
|
|
297
406
|
const visit=(id,mandatory)=>{
|
|
298
407
|
const workflow=flows.get(id);
|
|
299
408
|
if(!workflow)return;
|
|
@@ -308,8 +417,9 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
308
417
|
}
|
|
309
418
|
if(step.workflow)visit(store.childId(step.workflow),needed);
|
|
310
419
|
if(step.branch){steps(step.branch.then,false);steps(step.branch.else??[],false);}
|
|
311
|
-
|
|
312
|
-
if(step.
|
|
420
|
+
// 并行兄弟可能在动作开始前停止整组;嵌套 stop 也会传播到后续步骤。
|
|
421
|
+
if(step.parallel)steps(step.parallel.steps,needed&&!step.parallel.steps.some(mayStopStep));
|
|
422
|
+
if(mayStopStep(step))available=false;
|
|
313
423
|
}
|
|
314
424
|
};
|
|
315
425
|
steps(workflow.steps,mandatory);
|
|
@@ -318,14 +428,9 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
318
428
|
}
|
|
319
429
|
async function findPluginWorkflowReferences(id){
|
|
320
430
|
const refs=[];
|
|
321
|
-
const collect=(steps,found)=>{for(const step of steps??[]){
|
|
322
|
-
if(typeof step.action==='string'&&step.action.startsWith(`${id}:`))found.add(step.action);
|
|
323
|
-
if(step.branch){collect(step.branch.then,found);collect(step.branch.else,found);}
|
|
324
|
-
if(step.parallel)collect(step.parallel.steps,found);
|
|
325
|
-
}};
|
|
326
431
|
for(const workflow of await store.list()){
|
|
327
|
-
const
|
|
328
|
-
if(
|
|
432
|
+
const actions=[...workflowActions(workflow.steps)].filter(action=>action.startsWith(`${id}:`));
|
|
433
|
+
if(actions.length)refs.push({id:workflow.id,title:workflow.title??workflow.id,actions});
|
|
329
434
|
}
|
|
330
435
|
return refs;
|
|
331
436
|
}
|
|
@@ -362,8 +467,9 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
362
467
|
if(workflows.length&&!options.force)throw new FlowError(`插件 ${name} 仍被工作流引用:${workflows.map(w=>w.id).join('、')}`,{code:'PLUGIN_IN_USE',details:{workflows}});
|
|
363
468
|
}
|
|
364
469
|
const next=await installGeneration(projectDir,{...operation,state});
|
|
365
|
-
const nextEntries=[...initial,...next.loaded],activeConfig={...project.config,...config},effectiveConfig={...latest.config,...config};
|
|
470
|
+
const nextEntries=entriesWithPresets([...initial,...next.loaded],latest.commandPresets??{}),activeConfig={...project.config,...config},effectiveConfig={...latest.config,...config};
|
|
366
471
|
candidate=await createHost({plugins:nextEntries,config:effectiveConfig,services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:false,allowUnknownPrivileges});
|
|
472
|
+
validateShortcuts(latest.shortcuts??{},{reservedNames:declaredCommands(nextEntries).map(command=>command.name)});
|
|
367
473
|
const sourceChanges=await changedPluginSources(host.definitions,candidate.definitions,{oldInstallationRoot:state.root,newInstallationRoot:next.state.root});
|
|
368
474
|
const plan=planPluginUpdate(host.definitions,candidate.definitions,activeConfig,effectiveConfig,sourceChanges);
|
|
369
475
|
assertMaintenance(plan,options);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {fileURLToPath} from 'node:url';
|
|
2
2
|
import {FlowError} from '@mytools/sdk';
|
|
3
|
+
import {COMMANDS_PLUGIN_ID} from '@mytools/commands';
|
|
3
4
|
|
|
4
5
|
function selectedNetwork(network,ids){
|
|
5
6
|
if(!network)return undefined;
|
|
@@ -28,6 +29,24 @@ export async function createExportSnapshot({store,host,project},{entries,portabl
|
|
|
28
29
|
for(const id of closure.plugins)ids.add(id);
|
|
29
30
|
if(includeWeb){if(!web)throw new FlowError('未安装提供 Web 服务的插件');ids.add(web.id);}
|
|
30
31
|
for(const id of ids)for(const dependency of Object.keys(host.definitions.get(id)?.dependencies??{}))ids.add(dependency);
|
|
32
|
+
const usedActions=new Set();
|
|
33
|
+
const scan=steps=>{for(const step of steps??[]){
|
|
34
|
+
if(step.action)usedActions.add(step.action);
|
|
35
|
+
if(step.branch){scan(step.branch.then);scan(step.branch.else);}
|
|
36
|
+
if(step.parallel)scan(step.parallel.steps);
|
|
37
|
+
}};
|
|
38
|
+
for(const workflow of closure.workflows)scan(workflow.steps);
|
|
39
|
+
const commandPresets=Object.fromEntries(Object.entries(project.commandPresets??{}).filter(([id])=>ids.has(COMMANDS_PLUGIN_ID)&&(portable||usedActions.has(`${COMMANDS_PLUGIN_ID}:${id}`))));
|
|
40
|
+
const commandAllowed=id=>!id.startsWith(`${COMMANDS_PLUGIN_ID}:`)||Object.hasOwn(commandPresets,id.slice(COMMANDS_PLUGIN_ID.length+1));
|
|
41
|
+
const commands=new Map(host.commands().map(command=>[command.name,command]));
|
|
42
|
+
const shortcuts=Object.fromEntries(Object.entries(project.shortcuts??{}).filter(([,target])=>{
|
|
43
|
+
if(target.action){
|
|
44
|
+
const action=host.actions.get(target.action);
|
|
45
|
+
return action&&ids.has(action.plugin)&&commandAllowed(target.action);
|
|
46
|
+
}
|
|
47
|
+
const command=commands.get(target.command);
|
|
48
|
+
return command&&target.plugin===command.plugin&&ids.has(command.plugin)&&(!command.action||commandAllowed(`${command.plugin}:${command.action}`));
|
|
49
|
+
}));
|
|
31
50
|
const plugins=[...ids].map(id=>{
|
|
32
51
|
const p=host.definitions.get(id);
|
|
33
52
|
if(!p?.root)throw new FlowError('内存中临时定义的插件不能打包,请先安装为标准包');
|
|
@@ -37,6 +56,6 @@ export async function createExportSnapshot({store,host,project},{entries,portabl
|
|
|
37
56
|
['actions',Object.fromEntries(Object.entries(p.actions??{}).map(([name,action])=>[name,{environments:action.environments,requirements:action.requirements}]))],
|
|
38
57
|
]);
|
|
39
58
|
});
|
|
40
|
-
return structuredClone({entries,closure,plugins,project:{environments:project.environments,network:selectedNetwork(project.network,ids),logs:project.logs,baseBundle:project.baseBundle},
|
|
59
|
+
return structuredClone({entries,closure,plugins,project:{environments:project.environments,network:selectedNetwork(project.network,ids),logs:project.logs,baseBundle:project.baseBundle,commandPresets,shortcuts},
|
|
41
60
|
toolRoot:fileURLToPath(new URL('../../fst-cli',import.meta.url))});
|
|
42
61
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// 由 npm version 的生命周期脚本同步,内部插件 API 版本独立维护。
|
|
2
|
-
export const VERSION="0.5.
|
|
2
|
+
export const VERSION="0.5.2";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
fst 命令入口、交互终端、输入呈现和运行观察。通过公开应用和模块接口工作,业务执行不在界面中重复实现。
|
|
8
8
|
|
|
9
|
-
内部依赖:`@mytools/sdk`、`@mytools/core`、`@mytools/fst-workspace`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-devtools`。
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/core`、`@mytools/fst-workspace`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-devtools`、`@mytools/fst-logs`。
|
|
10
10
|
## 公开入口
|
|
11
11
|
|
|
12
12
|
| 导入路径 | 实现位置 |
|
|
@@ -33,6 +33,8 @@ fst 命令入口、交互终端、输入呈现和运行观察。通过公开应
|
|
|
33
33
|
|
|
34
34
|
## 维护与验证
|
|
35
35
|
|
|
36
|
+
TUI 支持 `!` 系统命令、`/commands` 常用命令注册和 `/shortcuts` 任意插件能力快捷名。主输入框与命令编辑框使用 Shift+Enter / Alt+Enter 换行、Enter 提交;真实多行粘贴保持换行及缩进。使用说明可通过 `fst docs commands` 离线阅读。
|
|
37
|
+
|
|
36
38
|
本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
|
|
37
39
|
|
|
38
40
|
在仓库根目录执行:
|
|
@@ -52,3 +54,8 @@ node --test tests/modules.test.js tests/cli.test.js tests/terminal.test.js
|
|
|
52
54
|
## 完整日志分页
|
|
53
55
|
|
|
54
56
|
`fst logs` 列出历史,`fst logs <id> --cursor 0 --limit 65536 --json` 返回正文和下一页字节游标。TUI 运行详情按 `p` 打开完整日志,每页读取后可前后翻页或刷新。主面板保留有界近期摘要;日志清理失败显示维护警告。细节见 `fst docs generation`。
|
|
57
|
+
|
|
58
|
+
主面板通过 `fst-logs/steps` 共用步骤摘要缓存,按最近更新顺序淘汰,同时限制条数和字节量。
|
|
59
|
+
摘要不保存业务 input/output;缺失的状态显示“摘要已省略”,已有错误仍可查看。
|
|
60
|
+
运行输入、最终结果与错误通过 `fst-logs/preview` 保存有界预览,避免业务完成后又缓存完整大结果。
|
|
61
|
+
展开时明确提示结果为摘要,完整结果仍在日志中;不会替换业务调用方取得的完整输出。
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
import {isCommandName} from '@mytools/fst-plugin/contracts';
|
|
3
|
+
import {editValue} from './schema.js';
|
|
4
|
+
|
|
5
|
+
export async function runShortcut(session,entry,args=[]){
|
|
6
|
+
if(!entry.available)throw new Error(`快捷名称 /${entry.name} 已失效:${entry.reason??'目标不存在'}`);
|
|
7
|
+
const {app}=session,target=entry.target;
|
|
8
|
+
if(target.command){
|
|
9
|
+
const command=app.listCommands().find(command=>command.name===target.command&&command.plugin===target.plugin);
|
|
10
|
+
if(!command)throw new Error(`快捷名称 /${entry.name} 的目标命令不存在`);
|
|
11
|
+
return session.pluginCommand({...command,name:entry.name,targetName:command.name,shortcutInput:entry.input??{}},args);
|
|
12
|
+
}
|
|
13
|
+
const action=app.catalog().find(action=>action.id===target.action);
|
|
14
|
+
if(!action)throw new Error(`快捷名称 /${entry.name} 的目标动作不存在`);
|
|
15
|
+
const plugin=action.plugin??action.id.slice(0,action.id.lastIndexOf(':'));
|
|
16
|
+
return session.pluginCommand({...action,name:entry.name,plugin,action:action.id.slice(plugin.length+1),actionId:action.id,shortcutInput:entry.input??{}},args);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function parameters(session,value={}){
|
|
20
|
+
const result=structuredClone(value),{ui}=session;
|
|
21
|
+
for(;;){
|
|
22
|
+
const choice=await ui.select('命令参数(脚本通过 FST_INPUT_参数名 环境变量读取)',[
|
|
23
|
+
...Object.entries(result).map(([name,field])=>({value:`field:${name}`,label:field.title??name,description:`${name} · ${field.type}`})),
|
|
24
|
+
{value:'new',label:'添加参数'},{value:'back',label:'完成参数设置'},
|
|
25
|
+
]);
|
|
26
|
+
if(choice==='back')return result;
|
|
27
|
+
const existing=choice.startsWith('field:')?choice.slice(6):undefined;
|
|
28
|
+
if(existing){
|
|
29
|
+
const operation=await ui.select(`参数 ${existing}`,[{value:'edit',label:'编辑'},{value:'remove',label:'删除'},{value:'back',label:'返回'}]);
|
|
30
|
+
if(operation==='back')continue;
|
|
31
|
+
if(operation==='remove'){delete result[existing];continue;}
|
|
32
|
+
}
|
|
33
|
+
const name=existing??(await ui.input('参数名(英文字母、数字、下划线,不能以数字开头)',{files:false})).trim();
|
|
34
|
+
if(!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)||['__proto__','constructor','prototype'].includes(name)){ui.show('参数名无效,请重新填写。');continue;}
|
|
35
|
+
if(!existing&&Object.hasOwn(result,name)){ui.show('参数名已经存在。');continue;}
|
|
36
|
+
const title=await ui.input('参数显示名称',{value:result[name]?.title??name,files:false});
|
|
37
|
+
const type=await ui.select('参数类型',[{value:'string',label:'文本'},{value:'number',label:'数字'},{value:'boolean',label:'是 / 否'}]);
|
|
38
|
+
const field={title,type,required:await ui.confirm('这个参数必须填写吗?')};
|
|
39
|
+
if(await ui.confirm('设置默认值吗?'))field.default=await editValue(ui,{type},result[name]?.default,{baseDir:session.baseDir,required:true,label:'默认值'});
|
|
40
|
+
result[name]=field;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function editPreset(session,id,current){
|
|
45
|
+
const {ui,app}=session,preset=structuredClone(current??{shell:'auto'});
|
|
46
|
+
preset.title=(await ui.input('常用命令名称',{value:preset.title??'',files:false})).trim();
|
|
47
|
+
preset.script=await ui.input('命令文本 · Shift+Enter 换行,Enter 提交(Alt+Enter 备用)',{value:preset.script??'',files:false,multiline:true});
|
|
48
|
+
const action=`@mytools/commands:${id}`;
|
|
49
|
+
const oldName=Object.keys(app.project.shortcuts??{}).find(name=>app.project.shortcuts[name].action===action);
|
|
50
|
+
let shortcut=oldName??(isCommandName(preset.title)?preset.title:'');
|
|
51
|
+
for(;;){
|
|
52
|
+
const choice=await ui.select('常用命令设置',[
|
|
53
|
+
{value:'save',label:'保存',description:shortcut?`快捷执行 /${shortcut}`:'只注册工作流动作'},
|
|
54
|
+
{value:'title',label:'修改名称'},{value:'script',label:'编辑多行命令'},
|
|
55
|
+
{value:'shortcut',label:'快捷名称',description:shortcut||'未设置'},
|
|
56
|
+
{value:'shell',label:'命令解释器',description:preset.shell??'auto'},
|
|
57
|
+
{value:'timeout',label:'超时时间',description:preset.timeout===0?'不限时':`${(preset.timeout??30000)/1000} 秒`},
|
|
58
|
+
{value:'parameters',label:'参数',description:`${Object.keys(preset.parameters??{}).length} 项,可不设置`},
|
|
59
|
+
{value:'root',label:'Linux root 权限要求',description:preset.requireRoot?'需要 root':'按当前用户权限执行'},
|
|
60
|
+
{value:'back',label:'取消,返回'},
|
|
61
|
+
]);
|
|
62
|
+
if(choice==='back')return;
|
|
63
|
+
if(choice==='title')preset.title=(await ui.input('常用命令名称',{value:preset.title,files:false})).trim();
|
|
64
|
+
else if(choice==='script')preset.script=await ui.input('命令文本 · Shift+Enter 换行,Enter 提交(Alt+Enter 备用)',{value:preset.script,files:false,multiline:true});
|
|
65
|
+
else if(choice==='shortcut')shortcut=(await ui.input('快捷名称(支持中文,不含空格或 /;留空不设置)',{value:shortcut,files:false})).trim();
|
|
66
|
+
else if(choice==='shell')preset.shell=await ui.select('命令解释器',[
|
|
67
|
+
{value:'auto',label:'按系统选择(Windows PowerShell / Linux sh)'},
|
|
68
|
+
{value:'powershell',label:'Windows PowerShell'},{value:'pwsh',label:'PowerShell 7(pwsh)'},
|
|
69
|
+
{value:'sh',label:'sh'},{value:'bash',label:'bash'},
|
|
70
|
+
]);
|
|
71
|
+
else if(choice==='timeout'){
|
|
72
|
+
const seconds=Number(await ui.input('超时时间(秒,0 表示不限时)',{value:String((preset.timeout??30000)/1000),files:false}));
|
|
73
|
+
if(!Number.isFinite(seconds)||!Number.isSafeInteger(seconds*1000)||seconds<0||seconds*1000>2147483647){ui.show('超时时间无效。');continue;}
|
|
74
|
+
preset.timeout=seconds*1000;
|
|
75
|
+
}else if(choice==='parameters')preset.parameters=await parameters(session,preset.parameters);
|
|
76
|
+
else if(choice==='root')preset.requireRoot=await ui.confirm('这个命令在 Linux 上必须使用 root 吗?执行前会检查,不会自动提权。');
|
|
77
|
+
else if(choice==='save'){
|
|
78
|
+
const next={...app.project.commandPresets,[id]:preset},shortcuts=structuredClone(app.project.shortcuts??{});
|
|
79
|
+
if(oldName)delete shortcuts[oldName];
|
|
80
|
+
if(shortcut){
|
|
81
|
+
if(Object.hasOwn(shortcuts,shortcut)){ui.show(`快捷名称 /${shortcut} 已存在,请选择其他名称。`);continue;}
|
|
82
|
+
shortcuts[shortcut]={...(oldName?app.project.shortcuts[oldName]:{}),action};
|
|
83
|
+
}
|
|
84
|
+
try{
|
|
85
|
+
const result=await session.withMaintenance(options=>app.configureCommandPresets(next,{...options,shortcuts}));
|
|
86
|
+
if(result===null){ui.show('已取消保存。');continue;}
|
|
87
|
+
ui.show(`常用命令已保存,修改前配置已备份。${shortcut?`输入 /${shortcut} 可直接运行。`:''}也可在工作流中添加此动作。`);
|
|
88
|
+
return;
|
|
89
|
+
}catch(error){ui.show(`保存失败:${error.message}`);}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function commandPresetsMenu(session){
|
|
95
|
+
const {app,ui}=session;
|
|
96
|
+
for(;;){
|
|
97
|
+
const choice=await ui.select('常用命令',[
|
|
98
|
+
...(!app.project.readOnly?[{value:'new',label:'注册常用命令'}]:[]),
|
|
99
|
+
...Object.entries(app.project.commandPresets??{}).map(([id,preset])=>({value:`preset:${id}`,label:preset.title,description:id})),
|
|
100
|
+
{value:'back',label:'返回主面板'},
|
|
101
|
+
]);
|
|
102
|
+
if(choice==='back')return;
|
|
103
|
+
if(choice==='new'){session.writable();await editPreset(session,`command-${randomUUID().slice(0,8)}`);continue;}
|
|
104
|
+
const id=choice.slice(7),preset=app.project.commandPresets?.[id];if(!preset)continue;
|
|
105
|
+
const operation=await ui.select(preset.title,[{value:'run',label:'运行'},
|
|
106
|
+
...(!app.project.readOnly?[{value:'edit',label:'编辑'},{value:'remove',label:'删除注册项'}]:[]),{value:'back',label:'返回'}]);
|
|
107
|
+
if(operation==='run')await runShortcut(session,{name:preset.title,target:{action:`@mytools/commands:${id}`},input:{},available:true});
|
|
108
|
+
else if(operation==='edit'){session.writable();await editPreset(session,id,preset);}
|
|
109
|
+
else if(operation==='remove'){
|
|
110
|
+
session.writable();if(!await ui.confirm(`删除 ${preset.title}?指向它的快捷名称会显示失效;引用中的工作流需先调整。`))continue;
|
|
111
|
+
const next=structuredClone(app.project.commandPresets);delete next[id];
|
|
112
|
+
try{const result=await session.withMaintenance(options=>app.configureCommandPresets(next,options));if(result!==null)ui.show('注册项已删除,修改前配置已备份。');}
|
|
113
|
+
catch(error){ui.show(`删除失败:${error.message}`);}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function targets(app){
|
|
119
|
+
return [...app.catalog().map(action=>({value:`action:${action.id}`,label:`动作:${action.title??action.id}`,description:action.id})),
|
|
120
|
+
...app.listCommands().map(command=>({value:`command:${command.name}`,label:`命令:/${command.name}`,description:command.description}))];
|
|
121
|
+
}
|
|
122
|
+
function shortcutTarget(app,selected){
|
|
123
|
+
if(selected.startsWith('action:'))return {action:selected.slice(7)};
|
|
124
|
+
const name=selected.slice(8),command=app.listCommands().find(command=>command.name===name);
|
|
125
|
+
if(!command)throw new Error('目标命令已失效,请重新选择');
|
|
126
|
+
return {command:name,plugin:command.plugin};
|
|
127
|
+
}
|
|
128
|
+
async function editShortcut(session,oldName){
|
|
129
|
+
const {app,ui}=session,old=app.project.shortcuts?.[oldName];
|
|
130
|
+
let name=(await ui.input('快捷名称(支持中文,不含空格或 /)',{value:oldName??'',files:false})).trim();
|
|
131
|
+
let target=old?structuredClone(old):{};
|
|
132
|
+
if(!old){
|
|
133
|
+
const choices=targets(app);if(!choices.length){ui.show('当前没有可选择的插件动作或命令。');return;}
|
|
134
|
+
const selected=await ui.select('选择快捷名称指向的能力',choices);
|
|
135
|
+
target=shortcutTarget(app,selected);
|
|
136
|
+
}
|
|
137
|
+
for(;;){
|
|
138
|
+
const choice=await ui.select(`快捷名称 /${name}`,[
|
|
139
|
+
{value:'save',label:'保存'},{value:'name',label:'修改快捷名称'},
|
|
140
|
+
{value:'target',label:'选择目标',description:target.action??target.command},
|
|
141
|
+
{value:'input',label:'设置默认参数',description:'可留空,运行时按需要填写'},
|
|
142
|
+
{value:'back',label:'取消,返回'},
|
|
143
|
+
]);
|
|
144
|
+
if(choice==='back')return;
|
|
145
|
+
if(choice==='name')name=(await ui.input('快捷名称',{value:name,files:false})).trim();
|
|
146
|
+
else if(choice==='target'){
|
|
147
|
+
const selected=await ui.select('选择目标',targets(app));
|
|
148
|
+
target=shortcutTarget(app,selected);
|
|
149
|
+
}else if(choice==='input'){
|
|
150
|
+
const metadata=target.action?app.catalog().find(action=>action.id===target.action):app.listCommands().find(command=>command.name===target.command&&command.plugin===target.plugin);
|
|
151
|
+
if(!metadata){ui.show('目标已失效,请先重新选择目标。');continue;}
|
|
152
|
+
target.input=await editValue(ui,metadata.input??{type:'object',properties:{},additionalProperties:false},target.input??{},{baseDir:session.baseDir,required:true,label:'快捷名称默认参数'});
|
|
153
|
+
}else if(choice==='save'){
|
|
154
|
+
const next=structuredClone(app.project.shortcuts??{});if(oldName)delete next[oldName];
|
|
155
|
+
if(Object.hasOwn(next,name)){ui.show('快捷名称已经存在,请选择其他名称。');continue;}
|
|
156
|
+
next[name]=target;
|
|
157
|
+
try{await app.configureShortcuts(next);ui.show('快捷名称已保存,修改前配置已备份。');return;}
|
|
158
|
+
catch(error){ui.show(`保存失败:${error.message}`);}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function shortcutsMenu(session){
|
|
164
|
+
const {app,ui}=session;
|
|
165
|
+
for(;;){
|
|
166
|
+
const entries=app.listShortcuts(),choice=await ui.select('自定义快捷名称',[
|
|
167
|
+
...(!app.project.readOnly?[{value:'new',label:'添加快捷名称'}]:[]),
|
|
168
|
+
...entries.map(entry=>({value:`shortcut:${entry.name}`,label:`/${entry.name}${entry.available?'':' [失效]'}`,description:entry.available?entry.description:entry.reason})),
|
|
169
|
+
{value:'back',label:'返回主面板'},
|
|
170
|
+
]);
|
|
171
|
+
if(choice==='back')return;
|
|
172
|
+
if(choice==='new'){session.writable();await editShortcut(session);continue;}
|
|
173
|
+
const name=choice.slice(9),entry=entries.find(entry=>entry.name===name);if(!entry)continue;
|
|
174
|
+
const operation=await ui.select(`/${name}`,[
|
|
175
|
+
...(entry.available?[{value:'run',label:'运行'}]:[]),
|
|
176
|
+
...(!app.project.readOnly?[{value:'edit',label:'编辑'},{value:'remove',label:'删除快捷名称'}]:[]),{value:'back',label:'返回'},
|
|
177
|
+
]);
|
|
178
|
+
if(operation==='run')await runShortcut(session,entry);
|
|
179
|
+
else if(operation==='edit'){session.writable();await editShortcut(session,name);}
|
|
180
|
+
else if(operation==='remove'){
|
|
181
|
+
session.writable();if(!await ui.confirm(`删除快捷名称 /${name}?`))continue;
|
|
182
|
+
const next=structuredClone(app.project.shortcuts??{});delete next[name];
|
|
183
|
+
await app.configureShortcuts(next);ui.show('快捷名称已删除,插件能力仍可使用。');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {builtinCommands} from '@mytools/fst-plugin/contracts';
|
|
2
2
|
|
|
3
|
-
const visible=['workflows','run','pack','logs','plugin','config','tools','help','exit'];
|
|
3
|
+
const visible=['workflows','run','pack','logs','plugin','commands','shortcuts','config','tools','help','exit'];
|
|
4
4
|
// 入口由插件 ID 推导;冲突时加前缀,不能覆盖内置或作者注册的命令。
|
|
5
|
-
export function commandMenu(plugins,commands){
|
|
5
|
+
export function commandMenu(plugins,commands,shortcuts=[]){
|
|
6
6
|
const entries=builtinCommands.filter(command=>visible.includes(command.name)).map(command=>({...command,description:`[内置] ${command.description}`}));
|
|
7
|
-
const used=new Set([...builtinCommands.map(c=>c.name),...commands.map(c=>c.name)]);
|
|
7
|
+
const used=new Set([...builtinCommands.map(c=>c.name),...commands.map(c=>c.name),...shortcuts.map(entry=>entry.name)]);
|
|
8
|
+
entries.push(...shortcuts.map(entry=>({name:entry.name,shortcut:true,description:`[快捷] ${entry.available?entry.description:`已失效:${entry.reason}`}`})));
|
|
8
9
|
const groups=plugins.filter(plugin=>commands.some(c=>c.plugin===plugin.id)).sort((a,b)=>a.id.localeCompare(b.id));
|
|
9
10
|
for(const plugin of groups){
|
|
10
11
|
const leaf=plugin.id.split('/').at(-1).replace(/[^\w.-]/g,'-');
|