@yanglaofish/fishtools 0.4.0 → 0.5.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 +33 -5
- package/bin/fst.js +1 -1
- package/manual/README.md +33 -5
- package/manual/docs/deployment.md +3 -3
- package/manual/docs/kernel-generation.md +97 -0
- package/manual/docs/modules.md +57 -0
- package/manual/docs/plugin-development.md +120 -10
- package/manual/docs/releases.md +12 -0
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
- package/manual/docs/roadmap.md +14 -3
- package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
- package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
- package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
- package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
- package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
- package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
- package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
- package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
- package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
- package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
- package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
- package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
- package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
- package/manual/docs/verification.md +42 -2
- package/manual/packages/core/README.md +44 -0
- package/manual/packages/core/package.json +38 -0
- package/manual/packages/core/src/application.js +242 -85
- package/manual/packages/core/src/bundle.js +2 -0
- package/manual/packages/core/src/development-watch-worker.js +69 -0
- package/manual/packages/core/src/development.js +146 -0
- package/manual/packages/core/src/environments.js +2 -0
- package/manual/packages/core/src/export-snapshot.js +42 -0
- package/manual/packages/core/src/plugin-update.js +108 -0
- package/manual/packages/core/src/process.js +2 -0
- package/manual/packages/core/src/resources.js +31 -0
- package/manual/packages/core/src/version.js +2 -0
- package/manual/packages/fst-cli/LICENSE +21 -0
- package/manual/packages/fst-cli/README.md +54 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/bin/fst.js +55 -23
- package/manual/packages/fst-cli/package.json +49 -0
- package/manual/packages/fst-cli/src/development.js +53 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-feed.js +31 -5
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
- package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
- package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
- package/manual/packages/fst-devtools/LICENSE +21 -0
- package/manual/packages/fst-devtools/README.md +28 -0
- package/manual/packages/fst-devtools/package.json +29 -0
- package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
- package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
- package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/plugin-tools.js +24 -5
- package/manual/packages/fst-logs/LICENSE +21 -0
- package/manual/packages/fst-logs/README.md +56 -0
- package/manual/packages/fst-logs/package.json +24 -0
- package/manual/packages/fst-logs/src/log-bounds.js +91 -0
- package/manual/packages/fst-logs/src/run-logs.js +239 -0
- package/manual/packages/fst-platform/LICENSE +21 -0
- package/manual/packages/fst-platform/README.md +67 -0
- package/manual/packages/fst-platform/package.json +33 -0
- package/manual/packages/fst-platform/src/bundle.js +17 -0
- package/manual/packages/fst-platform/src/environments.js +67 -0
- package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
- package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
- package/manual/packages/fst-platform/src/file-probe.js +6 -0
- package/manual/packages/fst-platform/src/json.js +5 -0
- package/manual/packages/fst-platform/src/network.js +199 -0
- package/manual/packages/fst-platform/src/npm.js +11 -0
- package/manual/packages/fst-platform/src/process.js +110 -0
- package/manual/packages/fst-plugin/LICENSE +21 -0
- package/manual/packages/fst-plugin/README.md +29 -0
- package/manual/packages/fst-plugin/package.json +29 -0
- package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/contracts.js +30 -3
- package/manual/packages/fst-plugin/src/host.js +232 -0
- package/manual/packages/fst-plugin/src/loader.js +21 -0
- package/manual/packages/fst-plugin/src/privileges.js +91 -0
- package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/runtime.js +20 -4
- package/manual/packages/fst-workflow/LICENSE +21 -0
- package/manual/packages/fst-workflow/README.md +28 -0
- package/manual/packages/fst-workflow/package.json +24 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
- package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
- package/manual/packages/fst-workspace/LICENSE +21 -0
- package/manual/packages/fst-workspace/README.md +32 -0
- package/manual/packages/fst-workspace/package.json +34 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
- package/manual/packages/fst-workspace/src/index.js +3 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugins.js +6 -36
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +40 -7
- package/manual/packages/pack/LICENSE +21 -0
- package/manual/packages/pack/README.md +31 -0
- package/manual/packages/pack/archive.js +15 -0
- package/manual/packages/pack/dependencies.js +105 -0
- package/manual/packages/pack/environments.js +50 -0
- package/manual/packages/pack/index.js +103 -0
- package/manual/packages/pack/package.json +29 -0
- package/manual/packages/pack/runtime.js +61 -0
- package/manual/packages/sdk/README.md +31 -0
- package/manual/packages/sdk/conditions.js +1 -0
- package/manual/packages/sdk/package.json +23 -1
- package/manual/plugins/web/README.md +29 -0
- package/manual/tests/application.test.js +6 -4
- package/node_modules/@mytools/core/README.md +44 -0
- package/node_modules/@mytools/core/package.json +17 -17
- package/node_modules/@mytools/core/src/application.js +242 -85
- package/node_modules/@mytools/core/src/bundle.js +2 -17
- package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
- package/node_modules/@mytools/core/src/development.js +146 -0
- package/node_modules/@mytools/core/src/environments.js +2 -67
- package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
- package/node_modules/@mytools/core/src/plugin-update.js +108 -0
- package/node_modules/@mytools/core/src/process.js +2 -80
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/LICENSE +21 -0
- package/node_modules/@mytools/fst-cli/README.md +54 -0
- package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
- package/node_modules/@mytools/fst-cli/package.json +49 -0
- package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
- package/node_modules/@mytools/fst-cli/src/development.js +53 -0
- package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
- package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
- package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
- package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
- package/{manual/packages/core → node_modules/@mytools/fst-cli}/src/terminal/run-feed.js +31 -5
- package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
- package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
- package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
- package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
- package/node_modules/@mytools/fst-devtools/README.md +28 -0
- package/node_modules/@mytools/fst-devtools/package.json +29 -0
- package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
- package/node_modules/@mytools/fst-logs/LICENSE +21 -0
- package/node_modules/@mytools/fst-logs/README.md +56 -0
- package/node_modules/@mytools/fst-logs/package.json +24 -0
- package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
- package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
- package/node_modules/@mytools/fst-logs/src/run-logs.js +239 -0
- package/node_modules/@mytools/fst-platform/LICENSE +21 -0
- package/node_modules/@mytools/fst-platform/README.md +67 -0
- package/node_modules/@mytools/fst-platform/package.json +33 -0
- package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
- package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
- package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
- package/node_modules/@mytools/fst-platform/src/json.js +5 -0
- package/node_modules/@mytools/fst-platform/src/network.js +199 -0
- package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
- package/node_modules/@mytools/fst-platform/src/process.js +110 -0
- package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
- package/node_modules/@mytools/fst-plugin/README.md +29 -0
- package/node_modules/@mytools/fst-plugin/package.json +29 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
- package/node_modules/@mytools/fst-plugin/src/host.js +232 -0
- package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
- package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
- package/{manual/packages/core → node_modules/@mytools/fst-plugin}/src/runtime.js +20 -4
- package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
- package/node_modules/@mytools/fst-workflow/README.md +28 -0
- package/node_modules/@mytools/fst-workflow/package.json +24 -0
- package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
- package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
- package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
- package/node_modules/@mytools/fst-workspace/README.md +32 -0
- package/node_modules/@mytools/fst-workspace/package.json +34 -0
- package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
- package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
- package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
- package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
- package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
- package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
- package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
- package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
- package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
- package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
- package/node_modules/@mytools/pack/README.md +31 -0
- package/node_modules/@mytools/pack/dependencies.js +7 -2
- package/node_modules/@mytools/pack/environments.js +1 -1
- package/node_modules/@mytools/pack/index.js +33 -25
- package/node_modules/@mytools/pack/package.json +29 -1
- package/node_modules/@mytools/sdk/README.md +31 -0
- package/node_modules/@mytools/sdk/conditions.js +1 -0
- package/node_modules/@mytools/sdk/diagnostics.js +27 -0
- package/node_modules/@mytools/sdk/package.json +23 -1
- package/node_modules/@mytools/sdk/schema-path.js +31 -0
- package/node_modules/@mytools/sdk/validation.js +78 -0
- package/node_modules/@mytools/web/README.md +29 -0
- package/node_modules/@mytools/web/package.json +7 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1398 -1199
- package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
- package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
- package/node_modules/@mytools/web/public/index.html +2 -2
- package/node_modules/@mytools/web/server.js +24 -17
- package/node_modules/@mytools/web/src/model.js +84 -0
- package/node_modules/acorn/CHANGELOG.md +1012 -0
- package/node_modules/acorn/LICENSE +21 -0
- package/node_modules/acorn/README.md +309 -0
- package/node_modules/acorn/bin/acorn +4 -0
- package/node_modules/acorn/dist/acorn.d.mts +897 -0
- package/node_modules/acorn/dist/acorn.d.ts +897 -0
- package/node_modules/acorn/dist/acorn.js +6370 -0
- package/node_modules/acorn/dist/acorn.mjs +6341 -0
- package/node_modules/acorn/dist/bin.js +90 -0
- package/node_modules/acorn/package.json +50 -0
- package/node_modules/{minipass-flush/node_modules/minipass → tar}/node_modules/yallist/README.md +8 -7
- package/node_modules/tar/node_modules/yallist/package.json +68 -0
- package/node_modules/yallist/README.md +7 -8
- package/node_modules/yallist/package.json +12 -51
- package/package.json +131 -115
- package/manual/packages/core/src/host.js +0 -139
- package/node_modules/@mytools/core/src/host.js +0 -139
- package/node_modules/@mytools/core/src/run-logs.js +0 -63
- package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
- package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
- package/node_modules/minipass-flush/node_modules/minipass/node_modules/yallist/package.json +0 -29
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugin-installations.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/schema-path.js +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/LICENSE.md +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/package.json +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/package.json +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/LICENSE +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/iterator.js +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/yallist.js +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { readFile, access } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import { createApplication } from '@mytools/core';
|
|
7
|
+
import { verifyBundle } from '@mytools/fst-platform/bundle';
|
|
8
|
+
import { resolveProject, initializeProject } from '@mytools/fst-workspace/project';
|
|
9
|
+
import {waitForCliRun,cliRunResult} from '../src/cli-run.js';
|
|
10
|
+
import {VERSION} from '@mytools/core/version';
|
|
11
|
+
import {listDocumentation,readDocumentation,findDocumentationRoot,exportDocumentation} from '@mytools/fst-devtools/documentation';
|
|
12
|
+
import {collectInstallations} from '@mytools/fst-workspace/installations';
|
|
13
|
+
import {prepareBundleWorkspace} from '@mytools/fst-workspace/bundles';
|
|
14
|
+
import {readJson} from '@mytools/fst-platform/json';
|
|
15
|
+
|
|
16
|
+
const program=new Command().name('fst').description('Fishtools · 插件式工作流工作台').version(VERSION);
|
|
17
|
+
program.option('--maintenance','维护模式:不启动有状态服务插件');
|
|
18
|
+
program.option('--allow-unverified-privileges','本次允许权限预检结果无法确认的服务启动');
|
|
19
|
+
program.option('--watch','开发模式:监听本地 link 源码,等待业务空闲后重启');
|
|
20
|
+
program.option('--project <directory>','项目目录(默认当前项目或用户工作区)').option('--workspace <directory>','便携包的可写工作区').option('--output <directory>','日志与结果目录').option('--config <file>','插件配置覆盖文件').option('--host <host>','Web 监听地址','127.0.0.1').option('--port <port>','Web 监听端口','8765');
|
|
21
|
+
async function app({management=false,removeInstalled,allowUnknownPrivileges=false}={}){const o=program.opts();return createApplication({projectDir:process.env.FISHTOOLS_DEV_PROJECT??await resolveProject(o.project,o.workspace),outputDir:o.output,maintenance:management||!!o.maintenance,omitInstalledPlugin:removeInstalled,allowUnknownPrivileges:allowUnknownPrivileges||!!o.allowUnverifiedPrivileges,config:o.config?JSON.parse(await readFile(o.config,'utf8')):{}});}
|
|
22
|
+
const developmentChild=process.env.FISHTOOLS_DEV_CHILD==='1';
|
|
23
|
+
program.hook('preAction',(_program,command)=>{
|
|
24
|
+
if(!program.opts().watch||developmentChild)return;
|
|
25
|
+
if(![program,program.commands.find(item=>item.name()==='web')].includes(command))throw new Error('--watch 开发模式只支持交互 TUI 和 fst --watch web,不能用于一次性命令');
|
|
26
|
+
if(command===program&&(!process.stdin.isTTY||!process.stdout.isTTY))throw new Error('fst --watch 需要交互终端;Web 开发请使用 fst --watch web');
|
|
27
|
+
});
|
|
28
|
+
async function watchDevelopment(mode,settings=program.opts()){
|
|
29
|
+
const {startDevelopment}=await import('../src/development.js');
|
|
30
|
+
return startDevelopment({projectDir:await resolveProject(settings.project,settings.workspace),entry:fileURLToPath(import.meta.url),
|
|
31
|
+
args:process.argv.slice(2).filter(argument=>argument!=='--watch'),configFile:settings.config,mode,port:Number(settings.port)});
|
|
32
|
+
}
|
|
33
|
+
async function use(fn,options){const a=await app(options);try{for(const warning of a.host.warnings)console.error(`警告:${warning.message}`);return await fn(a);}finally{await a.close();}}
|
|
34
|
+
program.command('init [directory]').description('创建项目,保留已有配置与工作流').action(async directory=>console.log(`Fishtools 项目:${await initializeProject(path.resolve(directory??process.cwd()))}`));
|
|
35
|
+
program.command('docs [topic]').description('阅读或导出随安装提供的离线文档').option('--path','只打印文档路径').option('--export <directory>','导出完整资料到新目录').action(async(topic,options)=>{
|
|
36
|
+
if(options.export){if(topic||options.path)throw new Error('--export 不可与文档主题或 --path 同用');console.log(await exportDocumentation(options.export));return;}
|
|
37
|
+
if(topic){const document=await readDocumentation(topic);console.log(options.path?document.path:document.text);return;}
|
|
38
|
+
if(options.path){console.log(await findDocumentationRoot());return;}
|
|
39
|
+
console.log((await listDocumentation()).map(document=>`${document.id}\t${document.title}`).join('\n'));
|
|
40
|
+
console.log('\nfst docs <主题> 阅读正文;fst docs <主题> --path 查看路径;fst docs --export <新目录> 导出全部资料');
|
|
41
|
+
});
|
|
42
|
+
const workflow=program.command('workflow').description('查看、检查和复制工作流');
|
|
43
|
+
program.command('logs [id]').description('列出运行历史或分页读取完整日志').option('--cursor <bytes>','正文 UTF-8 字节游标','0').option('--limit <bytes>','每页字节数(4 到 262144)','65536').option('--json','输出正文与下一页游标').action((id,options)=>use(async a=>{
|
|
44
|
+
if(!id){console.log(JSON.stringify(await a.listRunHistory(),null,2));return;}
|
|
45
|
+
const page=await a.readLogPage(id,{cursor:Number(options.cursor),limit:Number(options.limit)});
|
|
46
|
+
if(options.json)console.log(JSON.stringify(page,null,2));else{process.stdout.write(page.text);if(page.hasMore)console.error(`\n下一页:fst logs ${id} --cursor ${page.nextCursor} --limit ${options.limit}`);}
|
|
47
|
+
},{management:true}));
|
|
48
|
+
workflow.command('list').action(()=>use(async a=>console.log(JSON.stringify(await a.listWorkflows(),null,2))));
|
|
49
|
+
workflow.command('check <id>').option('--json','输出包含权限要求的检查结果').action((id,options)=>use(async a=>{const report=await a.checkWorkflow(id);console.log(options.json?JSON.stringify(report,null,2):`工作流校验通过;涉及 ${report.privileges.length} 个动作的权限声明`);}));
|
|
50
|
+
workflow.command('duplicate <source>').description('复制已保存工作流,不修改原版').requiredOption('--title <name>','新工作流名称').option('--id <id>','新工作流 ID(默认自动生成)').action((source,options)=>use(async a=>{
|
|
51
|
+
const definition=await a.store.get(source);
|
|
52
|
+
const result=await a.duplicateWorkflow(definition,{id:options.id,title:options.title.trim()});
|
|
53
|
+
console.log(JSON.stringify(result,null,2));
|
|
54
|
+
}));
|
|
55
|
+
program.command('run <id>').option('--input <file>','JSON 输入文件').option('--allow-unverified-privileges','本次允许权限预检结果为待确认的动作继续运行').action((id,o)=>use(async a=>{
|
|
56
|
+
const run=await a.startRun(id,o.input?JSON.parse(await readFile(o.input,'utf8')):{},{allowUnknownPrivileges:!!o.allowUnverifiedPrivileges});
|
|
57
|
+
const result=cliRunResult(await waitForCliRun(a,run));console.log(JSON.stringify(result.value,null,2));
|
|
58
|
+
process.exitCode=result.exitCode;
|
|
59
|
+
},{allowUnknownPrivileges:!!o.allowUnverifiedPrivileges}));
|
|
60
|
+
const plugin=program.command('plugin').description('管理插件');
|
|
61
|
+
plugin.command('list').action(()=>use(a=>console.log(JSON.stringify(a.listPlugins(),null,2))));
|
|
62
|
+
plugin.command('add <source>').action(source=>use(async a=>console.log(JSON.stringify(await a.installPlugin(source),null,2)),{management:true}));
|
|
63
|
+
plugin.command('link <directory>').description('引用本地开发源码,修改后重启 fst 生效').action(directory=>use(async a=>console.log(JSON.stringify(await a.linkPlugin(path.resolve(directory)),null,2)),{management:true}));
|
|
64
|
+
plugin.command('remove <name>').option('--force','即使工作流仍引用此插件也继续卸载;不会越过插件依赖与活动 Web 检查').action((name,o)=>use(async a=>console.log(JSON.stringify(await a.removePlugin(name,{force:!!o.force}),null,2)),{management:true,removeInstalled:name}));
|
|
65
|
+
const network=program.command('network').description('查看或保存工作区、插件和动作级代理规则');
|
|
66
|
+
network.command('show').action(()=>use(a=>console.log(JSON.stringify(a.networkPolicy(),null,2))));
|
|
67
|
+
network.command('set <file>').description('从 JSON 保存代理规则').action(file=>use(async a=>{const saved=await a.configureNetworkPolicy(JSON.parse(await readFile(file,'utf8')));console.log(`代理规则已保存;原项目配置备份:${saved.backup}`);}));
|
|
68
|
+
for(const command of ['validate','lint'])plugin.command(`${command} <directory>`).description(command==='lint'?'检查可信插件声明与质量建议,不自动修改文件':'检查可信插件声明,不执行动作或运行时服务').option('--json','输出结构化报告').action(async(directory,options)=>{
|
|
69
|
+
const tools=await import('@mytools/fst-devtools');const report=await tools[command==='lint'?'lintPluginDirectory':'validatePluginDirectory'](path.resolve(directory));
|
|
70
|
+
if(options.json)console.log(JSON.stringify(report,null,2));else{
|
|
71
|
+
console.log(`${report.valid?'检查通过':'检查未通过'}:${report.plugin?.id??directory}`);
|
|
72
|
+
for(const item of report.diagnostics)console.log(`${item.severity==='error'?'错误':'警告'} [${item.code}] ${item.path??''} ${item.message}`);
|
|
73
|
+
for(const item of report.notes)console.log(`说明 [${item.code}] ${item.message}`);
|
|
74
|
+
}
|
|
75
|
+
if(!report.valid)process.exitCode=1;
|
|
76
|
+
});
|
|
77
|
+
plugin.command('create <name> [directory]').description('创建可独立安装的插件骨架').action(async(name,directory)=>{
|
|
78
|
+
const {createPlugin}=await import('@mytools/fst-devtools');const result=await createPlugin(name,path.resolve(directory??name));
|
|
79
|
+
console.log(`插件已创建:${result.directory}\n运行 fst plugin validate "${result.directory}" 检查;使用 fst plugin link "${result.directory}" 开发`);
|
|
80
|
+
});
|
|
81
|
+
plugin.command('gc').description('预览未引用安装批次;--apply 先备份再清理').option('--apply','压缩备份后清理安全批次').action(async options=>{
|
|
82
|
+
const settings=program.opts(),projectDir=await prepareBundleWorkspace(await resolveProject(settings.project,settings.workspace),settings.workspace);
|
|
83
|
+
const project=await readJson(path.join(projectDir,'flow.project.json'),{});
|
|
84
|
+
if(project.readOnly)throw new Error('只读项目不能清理插件批次');
|
|
85
|
+
console.log(JSON.stringify(await collectInstallations(projectDir,{apply:!!options.apply}),null,2));
|
|
86
|
+
});
|
|
87
|
+
const packOptions=command=>command.requiredOption('--target <target>','win32-x64 或 linux-x64-glibc').option('--out <directory>','导出目录','dist').option('--read-only','导出只读精简运行包').option('--environments <file>','环境来源覆盖 JSON').option('--plan','只查看打包资源计划');
|
|
88
|
+
async function pack(a,entries,o,portable=false){
|
|
89
|
+
const options={entries,target:o.target,outDir:o.out,includeWeb:!!o.web,readOnly:!!o.readOnly,portable,environments:o.environments?JSON.parse(await readFile(o.environments,'utf8')):{}};
|
|
90
|
+
console.log(JSON.stringify(await (o.plan?a.packPlan(options):a.pack(options)),null,2));
|
|
91
|
+
}
|
|
92
|
+
packOptions(program.command('pack <entries...>')).option('--web','包含 Web 界面').action((entries,o)=>use(a=>pack(a,entries,o)));
|
|
93
|
+
packOptions(program.command('portable').description('导出完整便携工具、当前全部流程和插件')).option('--no-web','不额外携带 Web').action(o=>use(a=>pack(a,[],o,true)));
|
|
94
|
+
program.command('command [name]').description('列出插件命令或执行动作命令').option('--input <file>','JSON 输入').option('--allow-unverified-privileges','本次允许权限预检结果为待确认的命令继续运行').action((name,o)=>use(async a=>{
|
|
95
|
+
if(!name){console.log(JSON.stringify(a.listCommands(),null,2));return;}
|
|
96
|
+
const run=await a.startCommand(name,o.input?JSON.parse(await readFile(o.input,'utf8')):{},{allowUnknownPrivileges:!!o.allowUnverifiedPrivileges});
|
|
97
|
+
const result=cliRunResult(await waitForCliRun(a,run));console.log(JSON.stringify(result.value,null,2));process.exitCode=result.exitCode;
|
|
98
|
+
},{allowUnknownPrivileges:!!o.allowUnverifiedPrivileges}));
|
|
99
|
+
program.command('configure <plugin> <file>').description('从 JSON 保存插件配置').action((id,file)=>use(async a=>{const saved=await a.configurePlugin(id,JSON.parse(await readFile(file,'utf8')));console.log(`插件配置已保存。原项目配置备份:${saved.backup}`);},{management:true}));
|
|
100
|
+
async function web(o){
|
|
101
|
+
if(program.opts().watch&&!developmentChild)return watchDevelopment('web',o);
|
|
102
|
+
if(developmentChild){const {runDevelopmentChild}=await import('../src/development.js');const a=await app();try{return await runDevelopmentChild(a,{mode:'web',webOptions:{host:o.host,port:Number(o.port)}});}catch(error){await a.close();throw error;}}
|
|
103
|
+
const a=await app();let server;
|
|
104
|
+
try{server=await a.startWeb({host:o.host,port:Number(o.port)});}catch(error){await a.close();throw error;}
|
|
105
|
+
console.log(`Fishtools:${server.url}`);
|
|
106
|
+
const stop=async()=>{await server.close();await a.close();};process.once('SIGINT',stop);process.once('SIGTERM',stop);
|
|
107
|
+
}
|
|
108
|
+
program.action(async()=>{
|
|
109
|
+
if(program.opts().watch&&!developmentChild)return watchDevelopment('terminal');
|
|
110
|
+
if(!process.stdin.isTTY||!process.stdout.isTTY){program.outputHelp();return;}
|
|
111
|
+
if(developmentChild){const {runDevelopmentChild}=await import('../src/development.js');const a=await app();try{return await runDevelopmentChild(a,{mode:'terminal',webOptions:{host:program.opts().host,port:Number(program.opts().port)}});}catch(error){await a.close();throw error;}}
|
|
112
|
+
const {startTerminal}=await import('../src/terminal/session.js');
|
|
113
|
+
await use(a=>startTerminal(a,{webOptions:{host:program.opts().host,port:Number(program.opts().port)}}));
|
|
114
|
+
});
|
|
115
|
+
program.command('web').description('启动可选的 Fishtools Web 工作台').option('--host <host>','监听地址').option('--port <port>','监听端口').action(o=>web({...program.opts(),...o}));
|
|
116
|
+
program.command('doctor').action(()=>use(async a=>{
|
|
117
|
+
let integrity;
|
|
118
|
+
const bundleRoot=a.project.baseBundle?.root??a.projectDir;
|
|
119
|
+
try{await access(path.join(bundleRoot,'manifest.json'));integrity=await verifyBundle(bundleRoot);}
|
|
120
|
+
catch(error){if(error.code!=='ENOENT')throw error;}
|
|
121
|
+
console.log(JSON.stringify({node:process.version,platform:process.platform,arch:process.arch,plugins:a.listPlugins(),integrity},null,2));
|
|
122
|
+
if(integrity&&!integrity.ok)process.exitCode=1;
|
|
123
|
+
}));
|
|
124
|
+
try {await program.parseAsync();}catch(error){console.error(error.message);process.exitCode=error.code==='CANCELLED'?130:2;if(developmentChild){process.send?.({type:'development:failed',message:error.message});process.disconnect?.();}}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mytools/fst-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/terminal/session.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/terminal/session.js",
|
|
9
|
+
"./entry": "./bin/fst.js",
|
|
10
|
+
"./run": "./src/cli-run.js",
|
|
11
|
+
"./terminal/conditions": "./src/terminal/conditions.js",
|
|
12
|
+
"./terminal/log-viewer": "./src/terminal/log-viewer.js",
|
|
13
|
+
"./terminal/model": "./src/terminal/model.js",
|
|
14
|
+
"./terminal/navigation": "./src/terminal/navigation.js",
|
|
15
|
+
"./terminal/presentation": "./src/terminal/presentation.js",
|
|
16
|
+
"./terminal/run-feed": "./src/terminal/run-feed.js",
|
|
17
|
+
"./terminal/run-presentation": "./src/terminal/run-presentation.js",
|
|
18
|
+
"./terminal/run-viewer": "./src/terminal/run-viewer.js",
|
|
19
|
+
"./terminal/schema": "./src/terminal/schema.js",
|
|
20
|
+
"./terminal/session": "./src/terminal/session.js",
|
|
21
|
+
"./terminal/theme": "./src/terminal/theme.js",
|
|
22
|
+
"./terminal/view": "./src/terminal/view.js",
|
|
23
|
+
"./terminal/workflow-menu": "./src/terminal/workflow-menu.js",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"bin",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=24.15.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@mytools/sdk": "0.1.0",
|
|
38
|
+
"@mytools/core": "0.1.0",
|
|
39
|
+
"@mytools/fst-workspace": "0.1.0",
|
|
40
|
+
"@mytools/fst-platform": "0.1.0",
|
|
41
|
+
"@mytools/fst-plugin": "0.1.0",
|
|
42
|
+
"@mytools/fst-devtools": "0.1.0",
|
|
43
|
+
"@mariozechner/pi-tui": "0.50.0",
|
|
44
|
+
"commander": "14.0.3"
|
|
45
|
+
},
|
|
46
|
+
"bin": {
|
|
47
|
+
"fst": "bin/fst.js"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// 工作流与插件命令共享取消及落盘收尾。
|
|
2
|
+
export async function waitForCliRun(app,run,signals=process){
|
|
3
|
+
const cancel=()=>app.cancelRun(run.id);signals.once('SIGINT',cancel);
|
|
4
|
+
try{return await run.done??app.getRun(run.id);}
|
|
5
|
+
finally{signals.removeListener('SIGINT',cancel);}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function cliRunResult(run){
|
|
9
|
+
const value=run.status==='stopped'?{status:'stopped',reason:run.reason}:run.error??run.output;
|
|
10
|
+
const exitCode=['succeeded','stopped'].includes(run.status)?0:run.status==='cancelled'?130:1;
|
|
11
|
+
return {value,exitCode};
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {randomBytes} from 'node:crypto';
|
|
2
|
+
import {createDevelopmentSupervisor,bindDevelopmentChild} from '@mytools/core/development';
|
|
3
|
+
|
|
4
|
+
export async function startDevelopment({projectDir,entry,args,configFile,mode,port}){
|
|
5
|
+
console.error('开发模式:源码变化后等待当前业务结束并重启;终端未保存的编辑草稿不会保留。');
|
|
6
|
+
const supervisor=await createDevelopmentSupervisor({projectDir,entry,args,configFile,
|
|
7
|
+
env:{FISHTOOLS_DEV_PROJECT:projectDir,FISHTOOLS_DEV_MODE:mode,FISHTOOLS_DEV_TOKEN:randomBytes(24).toString('hex'),FISHTOOLS_DEV_PORT:String(port)},
|
|
8
|
+
onEvent(event){
|
|
9
|
+
if(event.type==='ready')console.error(event.recovered?'开发应用已恢复,源码监听继续。':`开发应用已就绪(第 ${event.generation} 代)。`);
|
|
10
|
+
else if(event.type==='waiting')console.error('开发源码已修改,等待当前运行及管理操作结束。');
|
|
11
|
+
else if(event.type==='restarting')console.error('开发应用准备重启。');
|
|
12
|
+
else if(event.type==='failed')console.error(`开发应用启动失败:${event.message};修复源码后自动重试。`);
|
|
13
|
+
else if(['forced','blocked','watchError'].includes(event.type))console.error(`开发模式:${event.message}`);
|
|
14
|
+
if(event.type==='exited'&&mode==='terminal'&&process.stdout.isTTY){
|
|
15
|
+
process.stdin.setRawMode?.(false);process.stdout.write('\x1b[?1006l\x1b[?1000l\x1b[?25h\x1b[?1049l');
|
|
16
|
+
}
|
|
17
|
+
}});
|
|
18
|
+
const stop=()=>{void supervisor.close().catch(error=>{console.error(error.message);process.exitCode=2;}).finally(()=>{
|
|
19
|
+
process.removeListener('SIGINT',stop);process.removeListener('SIGTERM',stop);
|
|
20
|
+
// 子进程被强制回收时,监督器恢复终端的常用状态。
|
|
21
|
+
if(mode==='terminal'&&process.stdout.isTTY)process.stdout.write('\x1b[?1006l\x1b[?1000l\x1b[?25h\x1b[?1049l');
|
|
22
|
+
});};
|
|
23
|
+
process.once('SIGINT',stop);process.once('SIGTERM',stop);
|
|
24
|
+
return supervisor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runDevelopmentChild(app,{mode,webOptions}){
|
|
28
|
+
const web={...webOptions,port:Number(process.env.FISHTOOLS_DEV_PORT??webOptions.port),token:process.env.FISHTOOLS_DEV_TOKEN};
|
|
29
|
+
let binding;
|
|
30
|
+
const stop=()=>{void binding?.close();};
|
|
31
|
+
if(mode==='web'){
|
|
32
|
+
const server=await app.startWeb(web);console.log(`Fishtools:${server.url}`);
|
|
33
|
+
binding=bindDevelopmentChild(app,{details:{port:server.port},closePresentation:()=>{
|
|
34
|
+
process.removeListener('SIGINT',stop);process.removeListener('SIGTERM',stop);
|
|
35
|
+
}});
|
|
36
|
+
process.on('SIGINT',stop);process.on('SIGTERM',stop);return;
|
|
37
|
+
}
|
|
38
|
+
const [{TerminalView},{TerminalSession}]=await Promise.all([import('./terminal/view.js'),import('./terminal/session.js')]);
|
|
39
|
+
const ui=new TerminalView({baseDir:process.cwd()}),session=new TerminalSession(app,ui,{webOptions:web});
|
|
40
|
+
let presentation;
|
|
41
|
+
try{
|
|
42
|
+
ui.start();presentation=session.run();
|
|
43
|
+
binding=bindDevelopmentChild(app,{closePresentation:async()=>{
|
|
44
|
+
// 监督器已经确认业务空闲;开发重启直接结束草稿和 pending 提示。
|
|
45
|
+
session.exiting=true;session.dirty=false;
|
|
46
|
+
ui.pending?.reject(Object.assign(new Error('开发应用重启'),{code:'PROMPT_CANCELLED'}));
|
|
47
|
+
await presentation;
|
|
48
|
+
await ui.close();process.removeListener('SIGINT',stop);process.removeListener('SIGTERM',stop);
|
|
49
|
+
}});
|
|
50
|
+
process.on('SIGINT',stop);process.on('SIGTERM',stop);
|
|
51
|
+
await presentation;
|
|
52
|
+
}finally{await ui.close();await binding?.close();}
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { outputSources, precedingSteps, stepMetadata } from './model.js';
|
|
2
|
+
import { editValue } from './schema.js';
|
|
3
|
+
|
|
4
|
+
export function conditionSources(flow,index,catalog,workflows){
|
|
5
|
+
const flatten=(schema,prefix)=>Object.entries(schema?.properties??{}).flatMap(([key,value])=>[{value:`${prefix}.${key}`,label:value.title??`${prefix}.${key}`,schema:value},...flatten(value,`${prefix}.${key}`)]);
|
|
6
|
+
return [...flatten(flow.inputs,'inputs'),...outputSources(flow,index,catalog,workflows)];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 条件使用结构化表单构造;用户不需要了解底层 JSON 协议。
|
|
10
|
+
export async function editCondition(ui,flow,index,catalog,workflows,{baseDir,current}={}){
|
|
11
|
+
const presets=precedingSteps(flow,index).flatMap(step=>Object.entries(stepMetadata(step,catalog,workflows)?.conditions??{}).map(([key,preset])=>({value:`preset:steps.${step.id}.output:${key}`,label:`${step.title??step.id} · ${preset.title??key}`,condition:{left:{$ref:`steps.${step.id}.output.${preset.path}`},op:preset.op,...(preset.op==='exists'?{}:{right:preset.value})}})));
|
|
12
|
+
const mode=await ui.select('设置判断条件',[
|
|
13
|
+
...presets,{value:'compare',label:'选择字段与比较方式'},{value:'all',label:'全部条件成立(且)'},{value:'any',label:'任一条件成立(或)'},{value:'not',label:'条件取反'},...(current?[{value:'keep',label:'保留当前条件'}]:[]),
|
|
14
|
+
]);
|
|
15
|
+
if(mode==='keep')return current;
|
|
16
|
+
const preset=presets.find(p=>p.value===mode);if(preset)return structuredClone(preset.condition);
|
|
17
|
+
if(mode==='not')return {not:await editCondition(ui,flow,index,catalog,workflows,{baseDir})};
|
|
18
|
+
if(mode==='all'||mode==='any'){
|
|
19
|
+
const children=[];
|
|
20
|
+
do{children.push(await editCondition(ui,flow,index,catalog,workflows,{baseDir}));}while(await ui.confirm('继续添加判断条件?'));
|
|
21
|
+
return {[mode]:children};
|
|
22
|
+
}
|
|
23
|
+
const sources=conditionSources(flow,index,catalog,workflows);
|
|
24
|
+
if(!sources.length)throw new Error('请先添加运行参数或产生输出的前置步骤');
|
|
25
|
+
const source=await ui.select('选择要判断的字段',sources),schema=sources.find(s=>s.value===source)?.schema??{};
|
|
26
|
+
const op=await ui.select('比较方式',[['eq','等于'],['ne','不等于'],['gt','大于'],['gte','大于等于'],['lt','小于'],['lte','小于等于'],['contains','包含'],['exists','存在']].map(([value,label])=>({value,label})));
|
|
27
|
+
if(op==='exists')return {left:{$ref:source},op};
|
|
28
|
+
const type=op==='contains'&&schema.type==='array'?schema.items?.type:schema.type;
|
|
29
|
+
const chosen=['string','number','integer','boolean'].includes(type)?type:await ui.select('比较值类型',[{value:'string',label:'文本'},{value:'number',label:'数字'},{value:'boolean',label:'是 / 否'}]);
|
|
30
|
+
const right=await editValue(ui,{type:chosen},current?.right,{baseDir,required:true,label:'比较值'});
|
|
31
|
+
return {left:{$ref:source},op,right};
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Markdown, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
2
|
+
import { mouseWheel } from './run-viewer.js';
|
|
3
|
+
import {palette,markdownTheme} from './theme.js';
|
|
4
|
+
|
|
5
|
+
const theme=markdownTheme();
|
|
6
|
+
|
|
7
|
+
// 文本已由 TerminalView 的 safe 清理;每次只向 Overlay 提交当前窗口内的行。
|
|
8
|
+
export class MarkdownLogViewer{
|
|
9
|
+
constructor({title,text,terminal,onClose,requestRender}){
|
|
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;
|
|
12
|
+
}
|
|
13
|
+
invalidate(){this.markdown.invalidate();}
|
|
14
|
+
layout(width){
|
|
15
|
+
const height=Math.max(1,this.terminal.rows);
|
|
16
|
+
const pageSize=Math.max(1,height-2);
|
|
17
|
+
// Markdown 自身缓存同一宽度的完整渲染,滚动时不重新解析日志。
|
|
18
|
+
const lines=this.markdown.render(Math.max(1,width));
|
|
19
|
+
const maxOffset=Math.max(0,lines.length-pageSize);
|
|
20
|
+
this.offset=Math.min(maxOffset,Math.max(0,this.offset));
|
|
21
|
+
return {height,pageSize,lines,maxOffset};
|
|
22
|
+
}
|
|
23
|
+
render(width){
|
|
24
|
+
const {height,pageSize,lines}=this.layout(width);
|
|
25
|
+
const body=lines.slice(this.offset,this.offset+pageSize);
|
|
26
|
+
while(body.length<pageSize)body.push('');
|
|
27
|
+
const start=lines.length?this.offset+1:0,end=Math.min(this.offset+pageSize,lines.length);
|
|
28
|
+
const footer=palette.muted(`Esc/q 返回 · ↑↓/滚轮 · PgUp/PgDn · Home/End · ${start}–${end}/${lines.length}`);
|
|
29
|
+
const screen=height===1?body:height===2?[...body,footer]:[palette.brand(this.title),...body,footer];
|
|
30
|
+
return screen.map(line=>truncateToWidth(line,width));
|
|
31
|
+
}
|
|
32
|
+
handleInput(data){
|
|
33
|
+
if(['escape','enter','q'].some(key=>matchesKey(data,key))){this.onClose();return;}
|
|
34
|
+
const {pageSize,maxOffset}=this.layout(this.terminal.columns);
|
|
35
|
+
if(mouseWheel(data))this.offset+=mouseWheel(data)*3;
|
|
36
|
+
else if(matchesKey(data,'up'))this.offset--;
|
|
37
|
+
else if(matchesKey(data,'down'))this.offset++;
|
|
38
|
+
else if(matchesKey(data,'pageUp'))this.offset-=pageSize;
|
|
39
|
+
else if(matchesKey(data,'pageDown'))this.offset+=pageSize;
|
|
40
|
+
else if(matchesKey(data,'home'))this.offset=0;
|
|
41
|
+
else if(matchesKey(data,'end'))this.offset=maxOffset;
|
|
42
|
+
else return;
|
|
43
|
+
this.offset=Math.min(maxOffset,Math.max(0,this.offset));this.requestRender();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
export {builtinCommands as commands} from '@mytools/fst-plugin/contracts';
|
|
7
|
+
|
|
8
|
+
export function parseCommand(text){
|
|
9
|
+
const args=[];let token='',quote='',started=false;
|
|
10
|
+
for(const char of text.trim()){
|
|
11
|
+
if(quote){if(char===quote)quote='';else token+=char;started=true;}
|
|
12
|
+
else if(char==='"'||char==="'"){quote=char;started=true;}
|
|
13
|
+
else if(/\s/.test(char)){if(started){args.push(token);token='';started=false;}}
|
|
14
|
+
else{token+=char;started=true;}
|
|
15
|
+
}
|
|
16
|
+
if(quote)throw new Error('文件路径的引号未闭合');
|
|
17
|
+
if(started)args.push(token);
|
|
18
|
+
if(!args[0]?.startsWith('/'))throw Object.assign(new Error('这不是斜杠命令。输入 / 选择操作;执行工作流请用 /run 工作流ID。'),{code:'COMMAND_INPUT'});
|
|
19
|
+
args[0]=args[0].slice(1);return args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fileReference(text,baseDir){
|
|
23
|
+
if(text.startsWith('@@'))return text.slice(1);
|
|
24
|
+
if(!text.startsWith('@'))return text;
|
|
25
|
+
let file=text.slice(1).trim();
|
|
26
|
+
if((file.startsWith('"')&&file.endsWith('"'))||(file.startsWith("'")&&file.endsWith("'")))file=file.slice(1,-1);
|
|
27
|
+
if(!file)throw new Error('@ 后请选择文件路径');
|
|
28
|
+
if(file==='~')file=homedir();else if(file.startsWith('~/')||file.startsWith('~\\'))file=path.join(homedir(),file.slice(2));
|
|
29
|
+
return path.resolve(baseDir,file);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readRunInput(reference,baseDir){
|
|
33
|
+
if(!reference.startsWith('@')||reference.startsWith('@@'))throw new Error('运行参数文件需要使用 @路径.json');
|
|
34
|
+
const value=JSON.parse(await readFile(fileReference(reference,baseDir),'utf8'));
|
|
35
|
+
if(!value||typeof value!=='object'||Array.isArray(value))throw new Error('运行参数文件必须是 JSON 对象');
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const newWorkflow=(title,description)=>({schemaVersion:1,id:`flow-${randomUUID().slice(0,8)}`,title,description,inputs:{type:'object',properties:{}},steps:[],outputs:{}});
|
|
40
|
+
|
|
41
|
+
// 路径从根 steps 开始,例如 [1, 'branch', 'then', 0]。
|
|
42
|
+
export const stepAt=(flow,index)=>[...(Array.isArray(index)?index:[index])].reduce((node,key)=>node[key],flow.steps);
|
|
43
|
+
export const sequenceAt=(flow,parts=[])=>parts.reduce((node,key)=>node[key],flow.steps);
|
|
44
|
+
export function precedingSteps(flow,index){
|
|
45
|
+
const parts=Array.isArray(index)?index:[index];let sequence=flow.steps;const result=[];
|
|
46
|
+
for(let i=0;i<parts.length;i+=3){
|
|
47
|
+
// 并行子项同属一批,不能把前面的兄弟当成已完成步骤。
|
|
48
|
+
if(i===0||parts[i-2]!=='parallel')result.push(...sequence.slice(0,parts[i]));
|
|
49
|
+
if(i+1<parts.length)sequence=sequence[parts[i]][parts[i+1]][parts[i+2]];
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setBinding(flow,index,key,mode,schema,value){
|
|
55
|
+
const step=stepAt(flow,index),old=step.with?.[key];step.with??={};
|
|
56
|
+
if(mode==='runtime'){
|
|
57
|
+
flow.inputs??={type:'object',properties:{}};flow.inputs.properties??={};
|
|
58
|
+
const name=`fst_${step.id}_${key}`;
|
|
59
|
+
flow.inputs.properties[name]={...schema,title:schema.title??key};
|
|
60
|
+
flow.inputs.required=[...new Set([...(flow.inputs.required??[]),name])];
|
|
61
|
+
step.with[key]={$ref:`inputs.${name}`};
|
|
62
|
+
}else if(mode==='fixed'){
|
|
63
|
+
if(value===undefined)delete step.with[key];else step.with[key]=value;
|
|
64
|
+
}else step.with[key]={$ref:mode};
|
|
65
|
+
if(old?.$ref?.startsWith('inputs.fst_')){
|
|
66
|
+
const refs=[];
|
|
67
|
+
const scan=v=>{if(v&&typeof v==='object'){if(typeof v.$ref==='string')refs.push(v.$ref);for(const child of Object.values(v))scan(child);}};
|
|
68
|
+
scan(flow.steps);scan(flow.outputs);
|
|
69
|
+
if(!refs.includes(old.$ref)){
|
|
70
|
+
const name=old.$ref.slice('inputs.'.length);delete flow.inputs.properties[name];
|
|
71
|
+
flow.inputs.required=(flow.inputs.required??[]).filter(k=>k!==name);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function pruneRuntimeInputs(flow){
|
|
77
|
+
const refs=new Set();
|
|
78
|
+
const scan=value=>{if(value&&typeof value==='object'){if(typeof value.$ref==='string')refs.add(value.$ref);Object.values(value).forEach(scan);}};
|
|
79
|
+
scan(flow.steps);scan(flow.outputs);
|
|
80
|
+
for(const name of Object.keys(flow.inputs?.properties??{})){
|
|
81
|
+
if(name.startsWith('fst_')&&!refs.has(`inputs.${name}`)){
|
|
82
|
+
delete flow.inputs.properties[name];flow.inputs.required=(flow.inputs.required??[]).filter(key=>key!==name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function stepMetadata(step,catalog,workflows){
|
|
88
|
+
if(step.branch||step.parallel){
|
|
89
|
+
const children=step.parallel?.steps??[...(step.branch.then??[]),...(step.branch.else??[])];
|
|
90
|
+
return {title:step.title??(step.branch?'条件分支':'并行组'),output:{type:'object',properties:Object.fromEntries(children.map(child=>[child.id,stepMetadata(child,catalog,workflows)?.output??{}]))}};
|
|
91
|
+
}
|
|
92
|
+
if(step.stop)return {title:step.title??'提前结束',output:{type:'object',properties:{}}};
|
|
93
|
+
if(step.action)return catalog.find(a=>a.id===step.action);
|
|
94
|
+
const flow=workflows.find(w=>`${w.id}.yaml`===step.workflow?.replace(/^\.\//,''));
|
|
95
|
+
return flow?{title:flow.title??flow.id,description:flow.description,input:flow.inputs,output:{type:'object',properties:Object.fromEntries(Object.keys(flow.outputs??{}).map(k=>[k,{}]))}}:null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function outputSources(flow,index,catalog,workflows){
|
|
99
|
+
const flatten=(schema,prefix='')=>Object.entries(schema?.properties??{}).flatMap(([key,s])=>{const name=prefix?`${prefix}.${key}`:key;return [{name,schema:s},...flatten(s,name)];});
|
|
100
|
+
return precedingSteps(flow,index).flatMap((step,i)=>{
|
|
101
|
+
const meta=stepMetadata(step,catalog,workflows),base=`steps.${step.id}.output`,label=`${i+1}. ${meta?.title??step.id}`;
|
|
102
|
+
return [{value:base,label:`${label} → 完整输出`,schema:meta?.output},...flatten(meta?.output).map(f=>({value:`${base}.${f.name}`,label:`${label} → ${f.schema.title??f.name}`,schema:f.schema}))];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {builtinCommands} from '@mytools/fst-plugin/contracts';
|
|
2
|
+
|
|
3
|
+
const visible=['workflows','run','pack','logs','plugin','config','tools','help','exit'];
|
|
4
|
+
// 入口由插件 ID 推导;冲突时加前缀,不能覆盖内置或作者注册的命令。
|
|
5
|
+
export function commandMenu(plugins,commands){
|
|
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)]);
|
|
8
|
+
const groups=plugins.filter(plugin=>commands.some(c=>c.plugin===plugin.id)).sort((a,b)=>a.id.localeCompare(b.id));
|
|
9
|
+
for(const plugin of groups){
|
|
10
|
+
const leaf=plugin.id.split('/').at(-1).replace(/[^\w.-]/g,'-');
|
|
11
|
+
let name=/^[a-zA-Z]/.test(leaf)?leaf:`plugin-${leaf}`;
|
|
12
|
+
if(groups.filter(p=>p.id.split('/').at(-1)===leaf).length>1)name=plugin.id.replace(/^@/,'').replace(/[^\w.-]/g,'-');
|
|
13
|
+
while(used.has(name))name='plugin-'+name;
|
|
14
|
+
used.add(name);entries.push({name,plugin:plugin.id,description:`[插件] ${plugin.title??plugin.id} · ${plugin.id}`});
|
|
15
|
+
}
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const logo=[
|
|
2
|
+
'█▀▀ ▀█▀ █▀▀ █ █',
|
|
3
|
+
'█▀ █ ▀▀█ █▀█',
|
|
4
|
+
'▀ ▀▀▀ ▀▀▀ ▀ ▀',
|
|
5
|
+
].join('\n');
|
|
6
|
+
|
|
7
|
+
export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed:'失败',stopped:'已停止'})[plugin.runtime?.status]??'未知';
|
|
8
|
+
|
|
9
|
+
export function gettingStarted(flows,{readOnly=false}={}){
|
|
10
|
+
const sample=flows.find(w=>w.id==='hello')??flows[0];
|
|
11
|
+
return [
|
|
12
|
+
sample?`试运行 /run ${sample.id}\n${sample.description??sample.title??'按提示填写参数,执行后查看结果。'}`:'当前工作区还没有工作流。',
|
|
13
|
+
'',
|
|
14
|
+
`/workflows ${readOnly?'浏览工作流':'管理工作流'} /plugin 插件 /config 工具配置`,
|
|
15
|
+
`/logs 回看执行记录、输入输出与日志 /help 帮助`,
|
|
16
|
+
].join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fields(schema){
|
|
20
|
+
if(!schema)return '未声明';
|
|
21
|
+
if(!schema.properties)return `类型:${schema.type??'由插件约束定义'}`;
|
|
22
|
+
const entries=Object.entries(schema.properties);
|
|
23
|
+
return entries.length?entries.map(([key,s])=>`${s.title??key} (${key}, ${s.type??'任意类型'}${schema.required?.includes(key)?', 必填':''})${s.description?`:${s.description}`:''}`).join(';'):'无';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function privilege(item,plugin){
|
|
27
|
+
const declaration=item.requirements?.privilege?.[process.platform]??plugin.requirements?.privilege?.[process.platform];
|
|
28
|
+
if(!declaration)return '未声明';
|
|
29
|
+
const names={user:'普通用户',root:'root',administrator:'管理员',resource:'特定资源权限'};
|
|
30
|
+
return `${names[declaration.level]??declaration.level}${declaration.reason?`;原因:${declaration.reason}`:''}${declaration.remediation?`;处理:${declaration.remediation}`:''}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function pluginDetails(plugin,actions,commands){
|
|
34
|
+
return [
|
|
35
|
+
`# ${plugin.title??plugin.id}`,
|
|
36
|
+
plugin.description??'作者尚未提供用途说明。',
|
|
37
|
+
`标识:${plugin.id}\n\n版本:${plugin.version}\n\n来源:${plugin.removable?'自行安装(可移除)':'发行包内置 / 项目预置'}\n\n状态:${pluginStatus(plugin)}`,
|
|
38
|
+
...(plugin.runtime?.missing?.length?[`缺少服务:${plugin.runtime.missing.join(', ')}`]:[]),
|
|
39
|
+
`更新方式:${!plugin.runtime?.updatePolicy?'无运行时服务':plugin.runtime.updatePolicy==='automatic'?'可自动短暂停止并重启':'需要本次维护确认'}\n\n插件权限:${privilege(plugin,plugin)}`,
|
|
40
|
+
`声明平台:${plugin.platforms?.join(', ')??'作者未声明'}\n\n声明架构:${plugin.architectures?.join(', ')??'作者未声明'}`,
|
|
41
|
+
'## 可用于编排的能力',
|
|
42
|
+
...(actions.length?actions.map(a=>`### ${a.title??a.id}\n\n${a.description??''}\n\n能力 ID:${a.id}\n\n输入:${fields(a.input)}\n\n输出:${fields(a.output)}\n\n权限:${privilege(a,plugin)}`):['此插件没有提供编排动作,可能提供 Web、共享服务或交互命令。']),
|
|
43
|
+
'## 斜杠命令',commands.length?commands.map(c=>`- /${c.name}:${c.description??'未提供说明'}`).join('\n'):'没有注册斜杠命令。',
|
|
44
|
+
'## 插件配置',plugin.config?fields(plugin.config):'无需配置。',
|
|
45
|
+
'## 环境声明',
|
|
46
|
+
...[plugin,...actions].flatMap(item=>Object.entries(item.environments??{}).map(([name,e])=>`- ${item.id} / ${name}:${({host:'使用宿主机环境',bundled:'随包携带',tool:'使用 Fishtools 自带环境'})[e.source]??e.source}`)),
|
|
47
|
+
'环境与平台由插件作者声明;具体执行结果请在 /logs 查看。',
|
|
48
|
+
'编排用法:/workflows 新建工作流 → 添加步骤并选择上述能力 → 填写参数 → 保存 → 运行。也可在主面板用 /run 选择已保存的工作流。',
|
|
49
|
+
].join('\n\n');
|
|
50
|
+
}
|
|
@@ -3,7 +3,7 @@ import {runStatuses} from './run-viewer.js';
|
|
|
3
3
|
|
|
4
4
|
// 主面板按事件到达顺序追加;运行快照另存,配置菜单不会清掉执行记录。
|
|
5
5
|
export class RunFeed{
|
|
6
|
-
constructor(refresh){this
|
|
6
|
+
constructor(refresh,{maxEvents=1000,maxBytes=1048576,maxRuns=50}={}){Object.assign(this,{refresh,maxEvents,maxBytes,maxRuns});this.runs=new Map();this.entries=[];this.subscriptions=new Map();this.expanded=false;this.dropped=0;}
|
|
7
7
|
track({app,runId,title,input}){
|
|
8
8
|
if(this.runs.has(runId))return;
|
|
9
9
|
const record=app.getRun(runId),item={app,record,title,input,count:0,ended:false,steps:new Map(),catalog:new Map((app.catalog?.()??[]).map(action=>[action.id,action]))};
|
|
@@ -26,12 +26,15 @@ export class RunFeed{
|
|
|
26
26
|
this.refresh();
|
|
27
27
|
}
|
|
28
28
|
update(item,event){
|
|
29
|
-
|
|
29
|
+
let events;
|
|
30
|
+
if(event?.type&&!['run.succeeded','run.failed','run.cancelled','run.stopped'].includes(event.type)){item.record.events.push(event);events=[event];item.count++;}
|
|
30
31
|
else{
|
|
31
32
|
const record=item.app.getRun(item.record.id);if(!record)return;
|
|
32
33
|
item.record={...record,events:[...(record.events??[])]};
|
|
34
|
+
events=record.events.filter((e,index)=>e.seq!==undefined?e.seq>item.count:index>=item.count);
|
|
35
|
+
item.count=record.eventCount??record.events.length;
|
|
33
36
|
}
|
|
34
|
-
const {record}=item
|
|
37
|
+
const {record}=item;
|
|
35
38
|
for(const event of events)if(event.type.startsWith('step.'))item.steps.set(event.callPath,{...item.steps.get(event.callPath),...event});
|
|
36
39
|
if(events.length){
|
|
37
40
|
const last=this.entries.at(-1);
|
|
@@ -39,12 +42,34 @@ export class RunFeed{
|
|
|
39
42
|
else this.entries.push({type:'events',item,events});
|
|
40
43
|
}
|
|
41
44
|
if(record.status!=='running'&&!item.ended){item.ended=true;this.entries.push({type:'result',item});}
|
|
45
|
+
this.trim();
|
|
46
|
+
}
|
|
47
|
+
trim(){
|
|
48
|
+
let count=0,bytes=0;
|
|
49
|
+
for(let index=this.entries.length-1;index>=0;index--){
|
|
50
|
+
const entry=this.entries[index];if(!entry.events)continue;
|
|
51
|
+
let keep=entry.events.length;
|
|
52
|
+
for(let i=entry.events.length-1;i>=0;i--){
|
|
53
|
+
const size=Buffer.byteLength(JSON.stringify(entry.events[i]));
|
|
54
|
+
if(count>=this.maxEvents||bytes+size>this.maxBytes){keep=entry.events.length-i-1;this.dropped+=i+1;break;}
|
|
55
|
+
count++;bytes+=size;
|
|
56
|
+
}
|
|
57
|
+
if(keep!==entry.events.length)entry.events=keep?entry.events.slice(-keep):[];
|
|
58
|
+
}
|
|
59
|
+
this.entries=this.entries.filter(entry=>!entry.events||entry.events.length);
|
|
60
|
+
for(const item of this.runs.values()){
|
|
61
|
+
item.record.events=this.entries.filter(e=>e.item===item&&e.events).flatMap(e=>e.events);
|
|
62
|
+
while(item.steps.size>this.maxEvents)item.steps.delete(item.steps.keys().next().value);
|
|
63
|
+
}
|
|
64
|
+
const ended=[...this.runs.values()].filter(item=>item.ended);
|
|
65
|
+
for(const item of ended.slice(0,Math.max(0,ended.length-this.maxRuns))){this.runs.delete(item.record.id);this.entries=this.entries.filter(e=>e.item!==item);this.dropped++;}
|
|
66
|
+
const messages=this.entries.filter(e=>e.type==='message');for(const entry of messages.slice(0,Math.max(0,messages.length-100)))this.entries.splice(this.entries.indexOf(entry),1);
|
|
42
67
|
}
|
|
43
68
|
release(app){
|
|
44
69
|
if([...this.runs.values()].some(item=>item.app===app&&!item.ended))return;
|
|
45
70
|
this.subscriptions.get(app)?.();this.subscriptions.delete(app);
|
|
46
71
|
}
|
|
47
|
-
message(text){this.entries.push({type:'message',text});this.refresh();}
|
|
72
|
+
message(text){this.entries.push({type:'message',text:String(text).slice(-65536)});this.trim();this.refresh();}
|
|
48
73
|
compactEvents(entry,seen){
|
|
49
74
|
const {item}=entry,rows=[];
|
|
50
75
|
for(const event of entry.events){
|
|
@@ -61,7 +86,7 @@ export class RunFeed{
|
|
|
61
86
|
}
|
|
62
87
|
return rows.length?`${item.title??item.record.workflowId} · ${item.record.id.slice(0,8)}\n${rows.join('\n')}`:'';
|
|
63
88
|
}
|
|
64
|
-
text(){const seen=new Set();return this.entries.map(entry=>{
|
|
89
|
+
text(){const seen=new Set();return (this.dropped?'主面板仅保留近期摘要;完整日志可在 /logs 中按 p 分页阅读。\n\n':'')+this.entries.map(entry=>{
|
|
65
90
|
if(entry.type==='message')return entry.text;
|
|
66
91
|
const {record,title,input}=entry.item,label=`${title??record.workflowId} · ${record.id.slice(0,8)}`;
|
|
67
92
|
if(entry.type==='start')return [
|
|
@@ -71,6 +96,7 @@ export class RunFeed{
|
|
|
71
96
|
].filter(Boolean).join('\n');
|
|
72
97
|
if(entry.type==='events')return this.expanded?`${label}\n${readableTimeline(entry.events,{values:true})}`:this.compactEvents(entry,seen);
|
|
73
98
|
return [`结果 · ${label} · ${runStatuses[record.status]??record.status}`,
|
|
99
|
+
...(record.warnings??[]).map(warning=>`警告:${warning.message}`),
|
|
74
100
|
this.expanded&&record.output!==undefined?readableValue(record.output):'',record.error&&(this.expanded||![...entry.item.steps.values()].some(step=>step.type==='step.failed'))?readableError(record.error,{details:this.expanded}):'',record.reason??'',
|
|
75
101
|
].filter(Boolean).join('\n');
|
|
76
102
|
}).filter(Boolean).join('\n\n');}
|