@yanglaofish/fishtools 0.4.0 → 0.5.1
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 +24 -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-0.5.0-followup-review.md +67 -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 +55 -2
- package/manual/packages/core/README.md +44 -0
- package/manual/packages/core/package.json +38 -0
- package/manual/packages/core/src/application.js +251 -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 +59 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/bin/fst.js +55 -23
- package/manual/packages/fst-cli/package.json +50 -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/manual/packages/fst-cli/src/terminal/run-feed.js +119 -0
- 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 +67 -0
- package/manual/packages/fst-logs/package.json +26 -0
- package/manual/packages/fst-logs/src/log-bounds.js +91 -0
- package/manual/packages/fst-logs/src/run-logs.js +224 -0
- 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/LICENSE +21 -0
- package/manual/packages/fst-platform/README.md +73 -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 +235 -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 +27 -8
- 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 +251 -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 +59 -0
- package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
- package/node_modules/@mytools/fst-cli/package.json +50 -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/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +119 -0
- 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 +67 -0
- package/node_modules/@mytools/fst-logs/package.json +26 -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 +224 -0
- 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/LICENSE +21 -0
- package/node_modules/@mytools/fst-platform/README.md +73 -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 +235 -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 +27 -8
- 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 +1440 -1240
- 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/manual/packages/core/src/terminal/run-feed.js +0 -78
- 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/core/src/terminal/run-feed.js +0 -78
- 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,146 @@
|
|
|
1
|
+
import {fork} from 'node:child_process';
|
|
2
|
+
import {fileURLToPath} from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const worker=fileURLToPath(new URL('./development-watch-worker.js',import.meta.url));
|
|
6
|
+
const positive=(value,fallback,name)=>{const number=value??fallback;if(!Number.isInteger(number)||number<1)throw new TypeError(`${name} 必须是正整数`);return number;};
|
|
7
|
+
const send=(child,message)=>{if(child?.connected)child.send(message,()=>{});};
|
|
8
|
+
|
|
9
|
+
/** 监督进程只传递配置和 IPC;每一代应用拥有独立的完整 ESM 模块图。 */
|
|
10
|
+
export async function createDevelopmentSupervisor(options={}){
|
|
11
|
+
const projectDir=path.resolve(options.projectDir??process.cwd());
|
|
12
|
+
if(!options.entry)throw new TypeError('开发模式需要应用子进程入口');
|
|
13
|
+
const debounce=positive(options.debounce,180,'debounce'),closeTimeout=positive(options.closeTimeout,5000,'closeTimeout');
|
|
14
|
+
const killTimeout=positive(options.killTimeout,2000,'killTimeout'),startupTimeout=positive(options.startupTimeout,15000,'startupTimeout');
|
|
15
|
+
const watchTimeout=positive(options.watchTimeout,5000,'watchTimeout');
|
|
16
|
+
let phase='starting',child=null,watcher=null,generation=0,pending=false,draining=false,stopping=false,failed=false,closing;
|
|
17
|
+
let debounceTimer,startTimer,closeTimer,scanTimer;
|
|
18
|
+
const environment={...process.env,...options.env,FISHTOOLS_DEV_CHILD:'1'};
|
|
19
|
+
const emit=(type,details={})=>options.onEvent?.({generation,...details,type});
|
|
20
|
+
const active=()=>child&&child.exitCode===null&&child.signalCode===null;
|
|
21
|
+
const clearChildTimers=()=>{clearTimeout(startTimer);clearTimeout(closeTimer);};
|
|
22
|
+
async function terminate(target,kind){
|
|
23
|
+
if(!target||target.exitCode!==null||target.signalCode!==null)return true;
|
|
24
|
+
emit('forced',{pid:target.pid,kind,message:`${kind==='application'?'应用关闭':'文件监听'}超时,正在强制回收`});
|
|
25
|
+
target.kill('SIGKILL');
|
|
26
|
+
return new Promise(resolve=>{
|
|
27
|
+
const timer=setTimeout(()=>{target.removeListener('exit',exited);emit('blocked',{pid:target.pid,kind,message:'子进程尚未回收,保留占用预算并停止启动新宿主'});phase='blocked';resolve(false);},killTimeout);
|
|
28
|
+
const exited=()=>{clearTimeout(timer);resolve(true);};target.once('exit',exited);
|
|
29
|
+
if(target.exitCode!==null||target.signalCode!==null)exited();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function launch(){
|
|
33
|
+
if(stopping||active()||phase==='blocked')return;
|
|
34
|
+
clearTimeout(debounceTimer);debounceTimer=null;pending=false;draining=false;phase='starting';generation++;
|
|
35
|
+
const next=fork(path.resolve(options.entry),options.args??[],{cwd:options.cwd??process.cwd(),env:environment,execArgv:[],windowsHide:true,stdio:options.stdio??['inherit','inherit','inherit','ipc']});
|
|
36
|
+
child=next;let ready=false,reported=false;
|
|
37
|
+
startTimer=setTimeout(()=>{reported=true;failed=true;emit('failed',{pid:next.pid,message:'开发应用启动超时,继续等待源码修复'});void terminate(next,'application');},startupTimeout);
|
|
38
|
+
next.on('message',message=>{
|
|
39
|
+
if(next!==child)return;
|
|
40
|
+
if(message?.type==='development:ready'){
|
|
41
|
+
clearTimeout(startTimer);ready=true;phase='running';
|
|
42
|
+
if(message.details?.port!==undefined)environment.FISHTOOLS_DEV_PORT=String(message.details.port);
|
|
43
|
+
emit('ready',{pid:next.pid,details:message.details??{},recovered:failed});failed=false;
|
|
44
|
+
if(stopping||pending)drain();
|
|
45
|
+
}else if(message?.type==='development:waiting')emit('waiting',{pid:next.pid,status:message.status});
|
|
46
|
+
else if(message?.type==='development:closing'){
|
|
47
|
+
clearTimeout(startTimer);phase='closing';
|
|
48
|
+
closeTimer=setTimeout(()=>{void terminate(next,'application');},closeTimeout);
|
|
49
|
+
}else if(message?.type==='development:failed'){
|
|
50
|
+
reported=true;failed=true;emit('failed',{pid:next.pid,message:message.message});
|
|
51
|
+
}else if(message?.type==='development:closed')emit('applicationClosed',{pid:next.pid});
|
|
52
|
+
});
|
|
53
|
+
next.on('error',error=>{reported=true;failed=true;emit('failed',{pid:next.pid,message:error.message});});
|
|
54
|
+
next.once('exit',(code,signal)=>{
|
|
55
|
+
clearChildTimers();emit('exited',{pid:next.pid,code,signal});if(child===next)child=null;
|
|
56
|
+
if(stopping)return;
|
|
57
|
+
if(draining){draining=false;phase='waiting';if(!debounceTimer)launch();}
|
|
58
|
+
else if(pending){phase='waiting';if(!debounceTimer)launch();}
|
|
59
|
+
else if(ready&&!reported){void supervisor.close();}
|
|
60
|
+
else{phase='failed';failed=true;if(!reported)emit('failed',{pid:next.pid,message:`开发应用启动失败(${code??signal}),修复源码后自动重试`});}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function drain(){
|
|
64
|
+
if(draining||!active())return;
|
|
65
|
+
draining=true;phase='waiting';emit('restarting',{pid:child.pid});send(child,{type:'development:drain'});
|
|
66
|
+
}
|
|
67
|
+
function changed(message){
|
|
68
|
+
if(stopping)return;pending=true;emit('changed',{file:message.file});clearTimeout(debounceTimer);
|
|
69
|
+
debounceTimer=setTimeout(()=>{debounceTimer=null;if(active()){if(phase!=='starting')drain();}else launch();},debounce);
|
|
70
|
+
}
|
|
71
|
+
const supervisor={
|
|
72
|
+
status:()=>({phase,generation,pending,pid:active()?child.pid:null,watcherPid:watcher?.exitCode===null&&watcher.signalCode===null?watcher.pid:null}),
|
|
73
|
+
close(){
|
|
74
|
+
if(closing)return closing;stopping=true;clearTimeout(debounceTimer);debounceTimer=null;clearTimeout(scanTimer);
|
|
75
|
+
closing=(async()=>{
|
|
76
|
+
const watched=watcher;
|
|
77
|
+
const stopWatch=watched&&watched.exitCode===null&&watched.signalCode===null?new Promise(resolve=>{
|
|
78
|
+
const timer=setTimeout(async()=>resolve(await terminate(watched,'watcher')),closeTimeout);
|
|
79
|
+
watched.once('exit',()=>{clearTimeout(timer);resolve(true);});send(watched,{type:'close'});
|
|
80
|
+
}):Promise.resolve(true);
|
|
81
|
+
const running=child;
|
|
82
|
+
const stopApp=active()?new Promise(resolve=>{
|
|
83
|
+
// 活跃业务无强制截止时间;收到 idle/closing 才开始关闭超时预算。
|
|
84
|
+
running.once('exit',()=>resolve(true));drain();
|
|
85
|
+
const blocked=()=>{if(phase==='blocked'){clearInterval(check);resolve(false);}};
|
|
86
|
+
const check=setInterval(blocked,Math.min(killTimeout,100));running.once('exit',()=>clearInterval(check));
|
|
87
|
+
}):Promise.resolve(true);
|
|
88
|
+
const results=await Promise.all([stopWatch,stopApp]);clearChildTimers();
|
|
89
|
+
if(results.some(result=>!result)){phase='blocked';throw new Error('开发子进程未回收,不能启动重叠宿主');}
|
|
90
|
+
phase='closed';emit('closed');
|
|
91
|
+
})();return closing;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const watchOptions={projectDir,configFile:options.configFile?path.resolve(options.configFile):undefined,
|
|
95
|
+
maxDirectories:positive(options.maxDirectories,512,'maxDirectories'),maxEntries:positive(options.maxEntries,20000,'maxEntries'),maxDepth:positive(options.maxDepth,24,'maxDepth')};
|
|
96
|
+
try{
|
|
97
|
+
await new Promise((resolve,reject)=>{
|
|
98
|
+
let initialized=false;
|
|
99
|
+
watcher=fork(worker,[],{env:{...process.env,FISHTOOLS_WATCH_OPTIONS:JSON.stringify(watchOptions)},execArgv:[],windowsHide:true,stdio:['ignore','ignore','pipe','ipc']});
|
|
100
|
+
const deadline=()=>{clearTimeout(scanTimer);scanTimer=setTimeout(async()=>{
|
|
101
|
+
emit('watchError',{message:'开发目录扫描超时,停止监听并回收隔离进程'});
|
|
102
|
+
await terminate(watcher,'watcher');if(!initialized)reject(new Error('开发目录扫描超时'));
|
|
103
|
+
},watchTimeout);};deadline();
|
|
104
|
+
watcher.on('message',message=>{
|
|
105
|
+
if(message?.type==='scanning')deadline();
|
|
106
|
+
else if(message?.type==='ready'||message?.type==='snapshot'){
|
|
107
|
+
clearTimeout(scanTimer);emit('watching',message);
|
|
108
|
+
if(message.readOnly){if(!initialized)reject(new Error('只读运行包不能启用开发源码监听'));else{emit('watchError',{message:'项目已变为只读,开发监听正在关闭'});void supervisor.close();}return;}
|
|
109
|
+
if(!initialized){initialized=true;resolve();}
|
|
110
|
+
}else if(message?.type==='changed')changed(message);
|
|
111
|
+
else if(message?.type==='warning')emit('watchError',{message:message.message});
|
|
112
|
+
else if(message?.type==='fatal'){clearTimeout(scanTimer);if(!initialized)reject(new Error(message.message));else emit('watchError',{message:message.message});}
|
|
113
|
+
});
|
|
114
|
+
watcher.on('error',error=>{if(!initialized)reject(error);else emit('watchError',{message:error.message});});
|
|
115
|
+
watcher.once('exit',()=>{clearTimeout(scanTimer);if(!initialized)reject(new Error('开发文件监听进程提前退出'));else if(!stopping)emit('watchError',{message:'开发文件监听进程已停止'});});
|
|
116
|
+
watcher.stderr.on('data',data=>emit('watchError',{message:String(data).trim()}));
|
|
117
|
+
});launch();return supervisor;
|
|
118
|
+
}catch(error){await supervisor.close();throw error;}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 子进程必须把应用空闲状态与关闭动作放在同一事件循环中判断。 */
|
|
122
|
+
export function bindDevelopmentChild(app,{details={},closePresentation,pollInterval=100}={}){
|
|
123
|
+
let requested=false,checking=false,closed=false,waiting=false;
|
|
124
|
+
let finish;const done=new Promise(resolve=>{finish=resolve;});
|
|
125
|
+
const sendMessage=message=>{if(process.connected)process.send(message);};
|
|
126
|
+
async function check(){
|
|
127
|
+
if(!requested||checking||closed)return;
|
|
128
|
+
const status=app.developmentStatus();
|
|
129
|
+
if(status.running||status.busy||status.closing){if(!waiting){waiting=true;sendMessage({type:'development:waiting',status});}return;}
|
|
130
|
+
checking=true;sendMessage({type:'development:closing'});
|
|
131
|
+
try{
|
|
132
|
+
const errors=[];
|
|
133
|
+
try{await app.close();}catch(error){errors.push(error);}
|
|
134
|
+
try{await closePresentation?.();}catch(error){errors.push(error);}
|
|
135
|
+
if(errors.length)throw new AggregateError(errors,errors.map(error=>error.message).join(';'));
|
|
136
|
+
sendMessage({type:'development:closed'});
|
|
137
|
+
}
|
|
138
|
+
catch(error){sendMessage({type:'development:failed',message:`开发应用关闭失败:${error.message}`});process.exitCode=1;}
|
|
139
|
+
finally{closed=true;clearInterval(timer);process.removeListener('message',message);process.removeListener('disconnect',disconnected);process.disconnect?.();finish();}
|
|
140
|
+
}
|
|
141
|
+
const message=message=>{if(message?.type==='development:drain'){requested=true;void check();}};
|
|
142
|
+
const disconnected=()=>{requested=true;void check();};
|
|
143
|
+
const timer=setInterval(()=>{void check();},positive(pollInterval,100,'pollInterval'));
|
|
144
|
+
process.on('message',message);process.on('disconnect',disconnected);sendMessage({type:'development:ready',details});
|
|
145
|
+
return {close:()=>{requested=true;void check();return done;}};
|
|
146
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {fileURLToPath} from 'node:url';
|
|
2
|
+
import {FlowError} from '@mytools/sdk';
|
|
3
|
+
|
|
4
|
+
function selectedNetwork(network,ids){
|
|
5
|
+
if(!network)return undefined;
|
|
6
|
+
const plugins=Object.fromEntries(Object.entries(network.plugins??{}).filter(([id])=>ids.has(id)));
|
|
7
|
+
const used=new Set();
|
|
8
|
+
const add=rule=>{if(rule?.mode==='profile')used.add(rule.profile);};
|
|
9
|
+
add(network.default);
|
|
10
|
+
for(const policy of Object.values(plugins)){
|
|
11
|
+
add(policy.default);
|
|
12
|
+
for(const rule of Object.values(policy.actions??{}))add(rule);
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
...(network.default?{default:network.default}:{}),
|
|
16
|
+
profiles:Object.fromEntries(Object.entries(network.profiles??{}).filter(([name])=>used.has(name))),
|
|
17
|
+
plugins,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 只向打包器交付声明快照和发行入口位置,避免其直接访问活跃宿主。
|
|
22
|
+
export async function createExportSnapshot({store,host,project},{entries,portable=false,includeWeb=false}={}){
|
|
23
|
+
if(portable)entries=(await store.list()).map(w=>w.id);
|
|
24
|
+
if(!portable&&!entries?.length)throw new FlowError('请至少选择一个工作流');
|
|
25
|
+
const closure=await store.resolve(entries,host);
|
|
26
|
+
const web=[...host.definitions.values()].find(p=>typeof p.services?.web==='function');
|
|
27
|
+
const ids=new Set(portable?[...host.definitions.keys()].filter(id=>id!==web?.id||includeWeb):closure.plugins);
|
|
28
|
+
for(const id of closure.plugins)ids.add(id);
|
|
29
|
+
if(includeWeb){if(!web)throw new FlowError('未安装提供 Web 服务的插件');ids.add(web.id);}
|
|
30
|
+
for(const id of ids)for(const dependency of Object.keys(host.definitions.get(id)?.dependencies??{}))ids.add(dependency);
|
|
31
|
+
const plugins=[...ids].map(id=>{
|
|
32
|
+
const p=host.definitions.get(id);
|
|
33
|
+
if(!p?.root)throw new FlowError('内存中临时定义的插件不能打包,请先安装为标准包');
|
|
34
|
+
return Object.fromEntries([
|
|
35
|
+
...['id','version','root','platforms','architectures','environments','dependencies','defaultConfig'].map(key=>[key,p[key]]),
|
|
36
|
+
['requirements',p.requirements],
|
|
37
|
+
['actions',Object.fromEntries(Object.entries(p.actions??{}).map(([name,action])=>[name,{environments:action.environments,requirements:action.requirements}]))],
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
return structuredClone({entries,closure,plugins,project:{environments:project.environments,network:selectedNetwork(project.network,ids),logs:project.logs,baseBundle:project.baseBundle},
|
|
41
|
+
toolRoot:fileURLToPath(new URL('../../fst-cli',import.meta.url))});
|
|
42
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {createReadStream} from 'node:fs';
|
|
4
|
+
import {readFile,readdir,readlink} from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
function comparable(value){
|
|
8
|
+
if(typeof value==='function')return {functionSource:Function.prototype.toString.call(value)};
|
|
9
|
+
if(Array.isArray(value))return value.map(comparable);
|
|
10
|
+
if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).filter(([key])=>key!=='root').map(([key,item])=>[key,comparable(item)]));
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function dependencies(definitions){
|
|
15
|
+
const consumers=new Map(),providers=new Map();
|
|
16
|
+
for(const [id,p] of definitions)for(const name of p.runtime?.provides??[])providers.set(name,id);
|
|
17
|
+
for(const [id,p] of definitions){
|
|
18
|
+
const used=new Set(Object.keys(p.dependencies??{}));
|
|
19
|
+
for(const name of p.runtime?.inject??[])if(providers.has(name))used.add(providers.get(name));
|
|
20
|
+
for(const owner of used){
|
|
21
|
+
if(!consumers.has(owner))consumers.set(owner,new Set());
|
|
22
|
+
consumers.get(owner).add(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return consumers;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function planPluginUpdate(current,next,currentConfig={},nextConfig={},sourceChanged=new Set()){
|
|
29
|
+
const all=[...new Set([...current.keys(),...next.keys()])],changed=[];
|
|
30
|
+
for(const id of all){
|
|
31
|
+
const before=current.get(id),after=next.get(id);
|
|
32
|
+
if(!before||!after||sourceChanged.has(id)||!isDeepStrictEqual(comparable(before),comparable(after))||!isDeepStrictEqual(currentConfig[id]??{},nextConfig[id]??{}))changed.push(id);
|
|
33
|
+
}
|
|
34
|
+
const oldConsumers=dependencies(current),newConsumers=dependencies(next),affected=new Set(changed),queue=[...changed];
|
|
35
|
+
while(queue.length){
|
|
36
|
+
const owner=queue.shift();
|
|
37
|
+
for(const id of new Set([...(oldConsumers.get(owner)??[]),...(newConsumers.get(owner)??[])]))if(!affected.has(id)){
|
|
38
|
+
affected.add(id);queue.push(id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const maintenance=[...affected].filter(id=>[current.get(id),next.get(id)].some(p=>p?.runtime&&p.runtime.updatePolicy!=='automatic'));
|
|
42
|
+
return {changed,affected:[...affected],maintenance};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function directoryDigest(root){
|
|
46
|
+
const hash=createHash('sha256');
|
|
47
|
+
const visit=async(directory,relative='')=>{
|
|
48
|
+
const entries=(await readdir(directory,{withFileTypes:true})).sort((a,b)=>a.name.localeCompare(b.name));
|
|
49
|
+
for(const entry of entries){
|
|
50
|
+
if(entry.name==='node_modules'||entry.name==='.git')continue;
|
|
51
|
+
const name=path.posix.join(relative,entry.name),file=path.join(directory,entry.name);
|
|
52
|
+
hash.update(name);hash.update('\0');
|
|
53
|
+
if(entry.isDirectory())await visit(file,name);
|
|
54
|
+
else if(entry.isFile())for await(const chunk of createReadStream(file))hash.update(chunk);
|
|
55
|
+
else if(entry.isSymbolicLink())hash.update(await readlink(file));
|
|
56
|
+
else hash.update(entry.name);
|
|
57
|
+
hash.update('\0');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
await visit(root);return hash.digest('hex');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function resolutionDigest(installationRoot,pluginRoot){
|
|
64
|
+
if(!installationRoot)return null;
|
|
65
|
+
let lock;
|
|
66
|
+
try{lock=JSON.parse(await readFile(path.join(installationRoot,'package-lock.json'),'utf8'));}
|
|
67
|
+
catch(error){if(error.code==='ENOENT')return null;throw error;}
|
|
68
|
+
const packages=lock.packages??{},start=path.relative(installationRoot,pluginRoot).split(path.sep).join('/');
|
|
69
|
+
if(!Object.hasOwn(packages,start))return null;
|
|
70
|
+
const visited=new Set(),items=[];
|
|
71
|
+
const resolve=(from,name)=>{
|
|
72
|
+
let prefix=from;
|
|
73
|
+
while(true){
|
|
74
|
+
const key=`${prefix?`${prefix}/`:''}node_modules/${name}`;
|
|
75
|
+
if(Object.hasOwn(packages,key))return key;
|
|
76
|
+
if(!prefix)return null;
|
|
77
|
+
const index=prefix.lastIndexOf('/node_modules/');
|
|
78
|
+
prefix=index<0?'':prefix.slice(0,index);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const visit=key=>{
|
|
82
|
+
if(visited.has(key))return;visited.add(key);
|
|
83
|
+
const entry=packages[key];items.push([key,entry]);
|
|
84
|
+
for(const name of Object.keys({...entry.dependencies,...entry.optionalDependencies,...entry.peerDependencies}).sort()){
|
|
85
|
+
const dependency=resolve(key,name);
|
|
86
|
+
if(dependency)visit(dependency);
|
|
87
|
+
else items.push([`${key} -> ${name}`,null]);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
visit(start);
|
|
91
|
+
items.sort(([a],[b])=>a.localeCompare(b));
|
|
92
|
+
return createHash('sha256').update(JSON.stringify(items)).digest('hex');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function changedPluginSources(current,next,{oldInstallationRoot,newInstallationRoot}={}){
|
|
96
|
+
const changed=new Set();
|
|
97
|
+
for(const [id,before] of current){
|
|
98
|
+
const after=next.get(id);
|
|
99
|
+
if(!after||before.root===after.root)continue;
|
|
100
|
+
if(!before.root||!after.root){changed.add(id);continue;}
|
|
101
|
+
const [beforeFiles,afterFiles,beforeLock,afterLock]=await Promise.all([
|
|
102
|
+
directoryDigest(before.root),directoryDigest(after.root),
|
|
103
|
+
resolutionDigest(oldInstallationRoot,before.root),resolutionDigest(newInstallationRoot,after.root),
|
|
104
|
+
]);
|
|
105
|
+
if(beforeFiles!==afterFiles||beforeLock!==afterLock)changed.add(id);
|
|
106
|
+
}
|
|
107
|
+
return changed;
|
|
108
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {FlowError} from '@mytools/sdk';
|
|
2
|
+
|
|
3
|
+
// 应用持有长驻资源;业务插件宿主替换时,这些句柄仍属于同一个应用。
|
|
4
|
+
export function createResources(){
|
|
5
|
+
const pending=new Map(),handles=new Map();let closing;
|
|
6
|
+
return {
|
|
7
|
+
active:owner=>[...pending.values(),...handles.values()].includes(owner),
|
|
8
|
+
async own(factory,owner){
|
|
9
|
+
if(closing)throw new FlowError('应用已关闭');
|
|
10
|
+
const task=Promise.resolve().then(factory).then(raw=>{
|
|
11
|
+
if(typeof raw?.close!=='function')throw new FlowError('长期资源必须提供 close()');
|
|
12
|
+
let closed;
|
|
13
|
+
const handle={...raw,close(){
|
|
14
|
+
closed??=Promise.resolve().then(()=>raw.close()).then(()=>{handles.delete(handle);});
|
|
15
|
+
return closed;
|
|
16
|
+
}};
|
|
17
|
+
handles.set(handle,owner);return handle;
|
|
18
|
+
});
|
|
19
|
+
pending.set(task,owner);
|
|
20
|
+
try{return await task;}finally{pending.delete(task);}
|
|
21
|
+
},
|
|
22
|
+
close(){
|
|
23
|
+
closing??=(async()=>{
|
|
24
|
+
await Promise.allSettled([...pending.keys()]);
|
|
25
|
+
const results=await Promise.allSettled([...handles.keys()].map(h=>h.close()));
|
|
26
|
+
const errors=results.filter(r=>r.status==='rejected').map(r=>r.reason);
|
|
27
|
+
if(errors.length)throw new AggregateError(errors,`资源关闭失败:${errors.map(e=>e.message).join(';')}`);
|
|
28
|
+
})();return closing;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mytools contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 命令行与终端模块
|
|
2
|
+
|
|
3
|
+
包名:`@mytools/fst-cli`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
|
|
4
|
+
|
|
5
|
+
## 职责与边界
|
|
6
|
+
|
|
7
|
+
fst 命令入口、交互终端、输入呈现和运行观察。通过公开应用和模块接口工作,业务执行不在界面中重复实现。
|
|
8
|
+
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/core`、`@mytools/fst-workspace`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-devtools`、`@mytools/fst-logs`。
|
|
10
|
+
## 公开入口
|
|
11
|
+
|
|
12
|
+
| 导入路径 | 实现位置 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `@mytools/fst-cli` | `./src/terminal/session.js` |
|
|
15
|
+
| `@mytools/fst-cli/entry` | `./bin/fst.js` |
|
|
16
|
+
| `@mytools/fst-cli/run` | `./src/cli-run.js` |
|
|
17
|
+
| `@mytools/fst-cli/terminal/conditions` | `./src/terminal/conditions.js` |
|
|
18
|
+
| `@mytools/fst-cli/terminal/log-viewer` | `./src/terminal/log-viewer.js` |
|
|
19
|
+
| `@mytools/fst-cli/terminal/model` | `./src/terminal/model.js` |
|
|
20
|
+
| `@mytools/fst-cli/terminal/navigation` | `./src/terminal/navigation.js` |
|
|
21
|
+
| `@mytools/fst-cli/terminal/presentation` | `./src/terminal/presentation.js` |
|
|
22
|
+
| `@mytools/fst-cli/terminal/run-feed` | `./src/terminal/run-feed.js` |
|
|
23
|
+
| `@mytools/fst-cli/terminal/run-presentation` | `./src/terminal/run-presentation.js` |
|
|
24
|
+
| `@mytools/fst-cli/terminal/run-viewer` | `./src/terminal/run-viewer.js` |
|
|
25
|
+
| `@mytools/fst-cli/terminal/schema` | `./src/terminal/schema.js` |
|
|
26
|
+
| `@mytools/fst-cli/terminal/session` | `./src/terminal/session.js` |
|
|
27
|
+
| `@mytools/fst-cli/terminal/theme` | `./src/terminal/theme.js` |
|
|
28
|
+
| `@mytools/fst-cli/terminal/view` | `./src/terminal/view.js` |
|
|
29
|
+
| `@mytools/fst-cli/terminal/workflow-menu` | `./src/terminal/workflow-menu.js` |
|
|
30
|
+
| `@mytools/fst-cli/package.json` | `./package.json` |
|
|
31
|
+
|
|
32
|
+
仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
|
|
33
|
+
|
|
34
|
+
## 维护与验证
|
|
35
|
+
|
|
36
|
+
本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
|
|
37
|
+
|
|
38
|
+
在仓库根目录执行:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
node --test tests/modules.test.js tests/cli.test.js tests/terminal.test.js
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 开发源码自动重载
|
|
45
|
+
|
|
46
|
+
`fst --watch` 只在交互终端启动开发 TUI;`fst --watch web` 启动开发 Web。先使用 `fst plugin link <目录>` 引用开发源码,修改源码后监督进程等待当前业务运行和管理操作结束,再关闭旧应用并启动新进程。`run`、`plugin`、`pack` 等一次性命令不能使用 `--watch`;只读运行包也不启用源码监听。
|
|
47
|
+
|
|
48
|
+
连续保存会合并为一次重启,静态导入的子模块随整个进程更新。启动错误在终端明确显示,源码修复后自动恢复。开发 Web 保持同一端口和令牌,`--port 0` 首次分配后的实际端口也会保留。终端重启会恢复旧终端并启动新会话,未保存的编辑草稿不保证保留。关闭卡住时会报告强制回收,确认旧进程退出前不启动下一代。
|
|
49
|
+
|
|
50
|
+
启动监督器的进程只解析参数及监听源码,不创建插件宿主。实际应用子进程由 `src/development.js` 绑定核心开发生命周期。验证:`node --test tests/development-watch.test.js tests/cli.test.js tests/terminal.test.js`。
|
|
51
|
+
|
|
52
|
+
## 完整日志分页
|
|
53
|
+
|
|
54
|
+
`fst logs` 列出历史,`fst logs <id> --cursor 0 --limit 65536 --json` 返回正文和下一页字节游标。TUI 运行详情按 `p` 打开完整日志,每页读取后可前后翻页或刷新。主面板保留有界近期摘要;日志清理失败显示维护警告。细节见 `fst docs generation`。
|
|
55
|
+
|
|
56
|
+
主面板通过 `fst-logs/steps` 共用步骤摘要缓存,按最近更新顺序淘汰,同时限制条数和字节量。
|
|
57
|
+
摘要不保存业务 input/output;缺失的状态显示“摘要已省略”,已有错误仍可查看。
|
|
58
|
+
运行输入、最终结果与错误通过 `fst-logs/preview` 保存有界预览,避免业务完成后又缓存完整大结果。
|
|
59
|
+
展开时明确提示结果为摘要,完整结果仍在日志中;不会替换业务调用方取得的完整输出。
|
|
@@ -2,20 +2,34 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { readFile, access } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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';
|
|
8
9
|
import {waitForCliRun,cliRunResult} from '../src/cli-run.js';
|
|
9
|
-
import {VERSION} from '
|
|
10
|
-
import {listDocumentation,readDocumentation,findDocumentationRoot,exportDocumentation} from '
|
|
11
|
-
import {collectInstallations} from '
|
|
12
|
-
import {prepareBundleWorkspace} from '
|
|
13
|
-
import {readJson} from '
|
|
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';
|
|
14
15
|
|
|
15
16
|
const program=new Command().name('fst').description('Fishtools · 插件式工作流工作台').version(VERSION);
|
|
16
17
|
program.option('--maintenance','维护模式:不启动有状态服务插件');
|
|
18
|
+
program.option('--allow-unverified-privileges','本次允许权限预检结果无法确认的服务启动');
|
|
19
|
+
program.option('--watch','开发模式:监听本地 link 源码,等待业务空闲后重启');
|
|
17
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');
|
|
18
|
-
async function app({management=false,removeInstalled}={}){const o=program.opts();return createApplication({projectDir:await resolveProject(o.project,o.workspace),outputDir:o.output,maintenance:management||!!o.maintenance,omitInstalledPlugin:removeInstalled,config:o.config?JSON.parse(await readFile(o.config,'utf8')):{}});}
|
|
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
|
+
}
|
|
19
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();}}
|
|
20
34
|
program.command('init [directory]').description('创建项目,保留已有配置与工作流').action(async directory=>console.log(`Fishtools 项目:${await initializeProject(path.resolve(directory??process.cwd()))}`));
|
|
21
35
|
program.command('docs [topic]').description('阅读或导出随安装提供的离线文档').option('--path','只打印文档路径').option('--export <directory>','导出完整资料到新目录').action(async(topic,options)=>{
|
|
@@ -25,29 +39,43 @@ program.command('docs [topic]').description('阅读或导出随安装提供的
|
|
|
25
39
|
console.log((await listDocumentation()).map(document=>`${document.id}\t${document.title}`).join('\n'));
|
|
26
40
|
console.log('\nfst docs <主题> 阅读正文;fst docs <主题> --path 查看路径;fst docs --export <新目录> 导出全部资料');
|
|
27
41
|
});
|
|
28
|
-
const workflow=program.command('workflow').description('
|
|
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}));
|
|
29
48
|
workflow.command('list').action(()=>use(async a=>console.log(JSON.stringify(await a.listWorkflows(),null,2))));
|
|
30
|
-
workflow.command('check <id>').action(id=>use(async a=>{await a.checkWorkflow(id);console.log(
|
|
31
|
-
|
|
32
|
-
const
|
|
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});
|
|
33
57
|
const result=cliRunResult(await waitForCliRun(a,run));console.log(JSON.stringify(result.value,null,2));
|
|
34
58
|
process.exitCode=result.exitCode;
|
|
35
|
-
}));
|
|
59
|
+
},{allowUnknownPrivileges:!!o.allowUnverifiedPrivileges}));
|
|
36
60
|
const plugin=program.command('plugin').description('管理插件');
|
|
37
61
|
plugin.command('list').action(()=>use(a=>console.log(JSON.stringify(a.listPlugins(),null,2))));
|
|
38
62
|
plugin.command('add <source>').action(source=>use(async a=>console.log(JSON.stringify(await a.installPlugin(source),null,2)),{management:true}));
|
|
39
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}));
|
|
40
|
-
plugin.command('remove <name>').action(name=>use(async a=>console.log(JSON.stringify(await a.removePlugin(name),null,2)),{management:true,removeInstalled:name}));
|
|
41
|
-
|
|
42
|
-
|
|
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));
|
|
43
70
|
if(options.json)console.log(JSON.stringify(report,null,2));else{
|
|
44
71
|
console.log(`${report.valid?'检查通过':'检查未通过'}:${report.plugin?.id??directory}`);
|
|
45
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}`);
|
|
46
74
|
}
|
|
47
75
|
if(!report.valid)process.exitCode=1;
|
|
48
76
|
});
|
|
49
77
|
plugin.command('create <name> [directory]').description('创建可独立安装的插件骨架').action(async(name,directory)=>{
|
|
50
|
-
const {createPlugin}=await import('
|
|
78
|
+
const {createPlugin}=await import('@mytools/fst-devtools');const result=await createPlugin(name,path.resolve(directory??name));
|
|
51
79
|
console.log(`插件已创建:${result.directory}\n运行 fst plugin validate "${result.directory}" 检查;使用 fst plugin link "${result.directory}" 开发`);
|
|
52
80
|
});
|
|
53
81
|
plugin.command('gc').description('预览未引用安装批次;--apply 先备份再清理').option('--apply','压缩备份后清理安全批次').action(async options=>{
|
|
@@ -63,20 +91,24 @@ async function pack(a,entries,o,portable=false){
|
|
|
63
91
|
}
|
|
64
92
|
packOptions(program.command('pack <entries...>')).option('--web','包含 Web 界面').action((entries,o)=>use(a=>pack(a,entries,o)));
|
|
65
93
|
packOptions(program.command('portable').description('导出完整便携工具、当前全部流程和插件')).option('--no-web','不额外携带 Web').action(o=>use(a=>pack(a,[],o,true)));
|
|
66
|
-
program.command('command [name]').description('列出插件命令或执行动作命令').option('--input <file>','JSON 输入').action((name,o)=>use(async a=>{
|
|
94
|
+
program.command('command [name]').description('列出插件命令或执行动作命令').option('--input <file>','JSON 输入').option('--allow-unverified-privileges','本次允许权限预检结果为待确认的命令继续运行').action((name,o)=>use(async a=>{
|
|
67
95
|
if(!name){console.log(JSON.stringify(a.listCommands(),null,2));return;}
|
|
68
|
-
const run=await a.startCommand(name,o.input?JSON.parse(await readFile(o.input,'utf8')):{});
|
|
96
|
+
const run=await a.startCommand(name,o.input?JSON.parse(await readFile(o.input,'utf8')):{},{allowUnknownPrivileges:!!o.allowUnverifiedPrivileges});
|
|
69
97
|
const result=cliRunResult(await waitForCliRun(a,run));console.log(JSON.stringify(result.value,null,2));process.exitCode=result.exitCode;
|
|
70
|
-
}));
|
|
71
|
-
program.command('configure <plugin> <file>').description('从 JSON 保存插件配置').action((id,file)=>use(async a=>{await a.configurePlugin(id,JSON.parse(await readFile(file,'utf8')));console.log(
|
|
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}));
|
|
72
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;}}
|
|
73
103
|
const a=await app();let server;
|
|
74
104
|
try{server=await a.startWeb({host:o.host,port:Number(o.port)});}catch(error){await a.close();throw error;}
|
|
75
105
|
console.log(`Fishtools:${server.url}`);
|
|
76
106
|
const stop=async()=>{await server.close();await a.close();};process.once('SIGINT',stop);process.once('SIGTERM',stop);
|
|
77
107
|
}
|
|
78
108
|
program.action(async()=>{
|
|
109
|
+
if(program.opts().watch&&!developmentChild)return watchDevelopment('terminal');
|
|
79
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;}}
|
|
80
112
|
const {startTerminal}=await import('../src/terminal/session.js');
|
|
81
113
|
await use(a=>startTerminal(a,{webOptions:{host:program.opts().host,port:Number(program.opts().port)}}));
|
|
82
114
|
});
|
|
@@ -89,4 +121,4 @@ program.command('doctor').action(()=>use(async a=>{
|
|
|
89
121
|
console.log(JSON.stringify({node:process.version,platform:process.platform,arch:process.arch,plugins:a.listPlugins(),integrity},null,2));
|
|
90
122
|
if(integrity&&!integrity.ok)process.exitCode=1;
|
|
91
123
|
}));
|
|
92
|
-
try {await program.parseAsync();}catch(error){console.error(error.message);process.exitCode=error.code==='CANCELLED'?130:2;}
|
|
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,50 @@
|
|
|
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-logs": "0.1.0",
|
|
40
|
+
"@mytools/fst-workspace": "0.1.0",
|
|
41
|
+
"@mytools/fst-platform": "0.1.0",
|
|
42
|
+
"@mytools/fst-plugin": "0.1.0",
|
|
43
|
+
"@mytools/fst-devtools": "0.1.0",
|
|
44
|
+
"@mariozechner/pi-tui": "0.50.0",
|
|
45
|
+
"commander": "14.0.3"
|
|
46
|
+
},
|
|
47
|
+
"bin": {
|
|
48
|
+
"fst": "bin/fst.js"
|
|
49
|
+
}
|
|
50
|
+
}
|