@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,69 @@
|
|
|
1
|
+
import {watch} from 'node:fs';
|
|
2
|
+
import {readFile,opendir,lstat} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// 所有目录 I/O 都留在可回收进程中;扫描限制同时约束监听句柄和遍历工作量。
|
|
6
|
+
const options=JSON.parse(process.env.FISHTOOLS_WATCH_OPTIONS),handles=new Map();
|
|
7
|
+
const ignored=new Set(['node_modules','.git','.flow','dist']);
|
|
8
|
+
let stopped=false,scanning=false,again=false,first=true,links=[];
|
|
9
|
+
const send=message=>{if(process.connected)process.send(message);};
|
|
10
|
+
const remote=value=>/^\\\\|^\/\//.test(value);
|
|
11
|
+
async function json(file,fallback){try{return JSON.parse(await readFile(file,'utf8'));}catch(error){if(error.code==='ENOENT')return fallback;throw error;}}
|
|
12
|
+
function change(file){send({type:'changed',file});}
|
|
13
|
+
function addWatch(key,directory,callback,wanted){
|
|
14
|
+
wanted.add(key);if(handles.has(key))return;
|
|
15
|
+
try{const handle=watch(directory,callback);handle.on('error',error=>send({type:'warning',message:`监听 ${directory} 失败:${error.message}`}));handles.set(key,handle);}
|
|
16
|
+
catch(error){send({type:'warning',message:`无法监听 ${directory}:${error.message}`});}
|
|
17
|
+
}
|
|
18
|
+
async function scan(){
|
|
19
|
+
if(stopped)return;if(scanning){again=true;return;}scanning=true;
|
|
20
|
+
send({type:'scanning'});const wanted=new Set();let directories=0,entries=0,incomplete=false;
|
|
21
|
+
try{
|
|
22
|
+
addWatch('project',options.projectDir,(_event,name)=>{
|
|
23
|
+
if(name===null||['flow.project.json','.flow'].includes(String(name))){
|
|
24
|
+
if(name===null||String(name)==='flow.project.json')change(path.join(options.projectDir,String(name??'')));
|
|
25
|
+
void scan();
|
|
26
|
+
}
|
|
27
|
+
},wanted);
|
|
28
|
+
const stateDirectory=path.join(options.projectDir,'.flow');
|
|
29
|
+
addWatch('state',stateDirectory,(_event,name)=>{if(name===null||String(name)==='plugins.json'){change(path.join(stateDirectory,'plugins.json'));void scan();}},wanted);
|
|
30
|
+
if(options.configFile)addWatch('config',path.dirname(options.configFile),(_event,name)=>{
|
|
31
|
+
if(name===null||String(name)===path.basename(options.configFile)){change(options.configFile);void scan();}
|
|
32
|
+
},wanted);
|
|
33
|
+
let project={};
|
|
34
|
+
try{
|
|
35
|
+
project=await json(path.join(options.projectDir,'flow.project.json'),{});
|
|
36
|
+
const state=await json(path.join(stateDirectory,'plugins.json'),{});
|
|
37
|
+
links=Object.values(state.links??{}).filter(value=>typeof value==='string').map(value=>path.resolve(options.projectDir,value));
|
|
38
|
+
}catch(error){send({type:'warning',message:`开发配置暂不可读,保留已有源码监听:${error.message}`});}
|
|
39
|
+
incomplete=links.length>options.maxDirectories;
|
|
40
|
+
const queue=links.slice(0,options.maxDirectories).map(directory=>({directory,depth:0})),seen=new Set();
|
|
41
|
+
while(queue.length&&!stopped){
|
|
42
|
+
const {directory,depth}=queue.shift();if(seen.has(directory))continue;seen.add(directory);
|
|
43
|
+
if(remote(directory)){send({type:'warning',message:`跳过远程开发目录:${directory}`});continue;}
|
|
44
|
+
if(directories>=options.maxDirectories||entries>=options.maxEntries){incomplete=true;break;}
|
|
45
|
+
let stat;try{stat=await lstat(directory);}catch{continue;}
|
|
46
|
+
if(!stat.isDirectory()||stat.isSymbolicLink())continue;directories++;
|
|
47
|
+
addWatch(`source:${directory}`,directory,(event,name)=>{
|
|
48
|
+
const relative=String(name??'');if(relative.split(/[\\/]/).some(part=>ignored.has(part)))return;
|
|
49
|
+
change(path.join(directory,relative));if(event==='rename')void scan();
|
|
50
|
+
},wanted);
|
|
51
|
+
if(depth>=options.maxDepth){incomplete=true;continue;}
|
|
52
|
+
let listing;
|
|
53
|
+
try{
|
|
54
|
+
listing=await opendir(directory);
|
|
55
|
+
for await(const entry of listing){
|
|
56
|
+
entries++;if(entries>=options.maxEntries){incomplete=true;break;}
|
|
57
|
+
if(entry.isDirectory()&&!ignored.has(entry.name))queue.push({directory:path.join(directory,entry.name),depth:depth+1});
|
|
58
|
+
}
|
|
59
|
+
}catch(error){send({type:'warning',message:`扫描 ${directory} 失败:${error.message}`});}
|
|
60
|
+
}
|
|
61
|
+
for(const [key,handle] of handles)if(!wanted.has(key)){handle.close();handles.delete(key);}
|
|
62
|
+
if(incomplete)send({type:'warning',message:'开发源码扫描达到目录、深度或条目预算,超出部分未监听'});
|
|
63
|
+
send({type:first?'ready':'snapshot',readOnly:!!project.readOnly,directories,watchers:handles.size,incomplete});first=false;
|
|
64
|
+
}finally{scanning=false;if(again){again=false;void scan();}}
|
|
65
|
+
}
|
|
66
|
+
function close(){stopped=true;for(const handle of handles.values())handle.close();handles.clear();process.disconnect?.();}
|
|
67
|
+
process.on('message',message=>{if(message?.type==='close')close();});
|
|
68
|
+
process.on('disconnect',close);
|
|
69
|
+
void scan().catch(error=>{send({type:'fatal',message:error.message});close();});
|
|
@@ -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
|
+
}
|
|
@@ -1,67 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import which from 'which';
|
|
4
|
-
|
|
5
|
-
export const currentTarget=()=>`${process.platform}-${process.arch}${process.platform==='linux'?'-glibc':''}`;
|
|
6
|
-
export function inside(root,relative){
|
|
7
|
-
if(typeof relative!=='string'||!relative||path.isAbsolute(relative)||/^[A-Za-z]:|\\/.test(relative))throw new FlowError(`资源路径必须是包内相对路径:${relative}`);
|
|
8
|
-
const absolute=path.resolve(root,relative),rel=path.relative(root,absolute);
|
|
9
|
-
if(rel==='..'||rel.startsWith('..'+path.sep)||path.isAbsolute(rel))throw new FlowError(`资源路径越界:${relative}`);
|
|
10
|
-
return absolute;
|
|
11
|
-
}
|
|
12
|
-
export function environmentDeclarations(plugin,action){return {...plugin.environments,...action?.environments};}
|
|
13
|
-
export function resolveEnvironments(plugin,action,overrides={},target=currentTarget()){
|
|
14
|
-
return Object.fromEntries(Object.entries(environmentDeclarations(plugin,action)).map(([id,declaration])=>{
|
|
15
|
-
const selected=overrides[id];
|
|
16
|
-
if(selected!==undefined&&(!selected||typeof selected!=='object'||Array.isArray(selected)||Object.keys(selected).some(k=>!['source','executable'].includes(k))||selected.source!==undefined&&typeof selected.source!=='string'||selected.executable!==undefined&&(typeof selected.executable!=='string'||!selected.executable.trim())))throw new FlowError(`环境来源覆盖无效:${plugin.id}/${id}`);
|
|
17
|
-
const source=selected?.source??declaration.source;
|
|
18
|
-
if(!(declaration.allowedSources??[declaration.source]).includes(source))throw new FlowError(`环境 ${plugin.id}/${id} 不允许来源 ${source}`);
|
|
19
|
-
const e={...declaration,source};delete e.artifacts;
|
|
20
|
-
if(selected?.executable!==undefined){
|
|
21
|
-
if(source!=='host')throw new FlowError(`只有 host 环境允许覆盖解释器:${plugin.id}/${id}`);
|
|
22
|
-
e.executable=selected.executable;
|
|
23
|
-
}
|
|
24
|
-
if(source==='tool'){
|
|
25
|
-
if(id!=='node')throw new FlowError(`工具尚未提供环境:${id}`);
|
|
26
|
-
e.executable=process.execPath;
|
|
27
|
-
}else if(source==='bundled'){
|
|
28
|
-
const artifact=declaration.artifacts?.[target];
|
|
29
|
-
if(!artifact)throw new FlowError(`环境 ${plugin.id}/${id} 未提供 ${target} 的随包资源`);
|
|
30
|
-
e.root=inside(plugin.root,artifact.directory);e.executable=inside(e.root,artifact.executable);
|
|
31
|
-
}
|
|
32
|
-
return [id,e];
|
|
33
|
-
}));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 打包只解析声明;真正调用时才在当前工作区选择候选,避免将开发机路径写入跨平台分发包。
|
|
37
|
-
export async function prepareEnvironments(plugin,action,overrides={}, {cwd=process.cwd()}={}){
|
|
38
|
-
const resolved=resolveEnvironments(plugin,action,overrides);
|
|
39
|
-
for(const [id,environment] of Object.entries(resolved)){
|
|
40
|
-
if(environment.source!=='host'||!Array.isArray(environment.executable)&&overrides[id]?.executable===undefined)continue;
|
|
41
|
-
const candidates=Array.isArray(environment.executable)?environment.executable:[environment.executable];
|
|
42
|
-
const windows=process.platform==='win32',directory=path.resolve(cwd);
|
|
43
|
-
const searchDirectories=[...(windows?[directory]:[]),...(process.env.PATH??'').split(path.delimiter)]
|
|
44
|
-
.map(entry=>path.resolve(directory,entry.replace(/^"(.*)"$/,'$1')));
|
|
45
|
-
let found;
|
|
46
|
-
for(const candidate of candidates){
|
|
47
|
-
const direct=path.isAbsolute(candidate)||candidate.includes('/')||windows&&candidate.includes('\\');
|
|
48
|
-
// 使用绝对候选,防止 which 在 Windows 擅自优先搜索宿主启动目录。
|
|
49
|
-
const paths=direct?[path.resolve(directory,candidate)]:searchDirectories.map(base=>path.resolve(base,candidate));
|
|
50
|
-
for(const executable of paths){
|
|
51
|
-
found=await which(executable,{nothrow:true});
|
|
52
|
-
if(found)break;
|
|
53
|
-
}
|
|
54
|
-
if(found)break;
|
|
55
|
-
}
|
|
56
|
-
if(!found)throw new FlowError(`环境 ${plugin.id}/${id} 未找到程序:${candidates.join('、')}`,{code:'ENOENT',details:{plugin:plugin.id,environment:id,candidates}});
|
|
57
|
-
environment.executable=path.resolve(found);
|
|
58
|
-
}
|
|
59
|
-
return resolved;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function platformWarnings(plugin,platform=process.platform,architecture=process.arch){
|
|
63
|
-
const warnings=[];
|
|
64
|
-
if(plugin.platforms&&!plugin.platforms.includes(platform))warnings.push(`插件 ${plugin.id} 的平台声明不包含当前 ${platform},继续执行;请核实插件支持范围`);
|
|
65
|
-
if(plugin.architectures&&!plugin.architectures.includes(architecture))warnings.push(`插件 ${plugin.id} 的架构声明不包含当前 ${architecture},继续执行;请核实插件支持范围`);
|
|
66
|
-
return warnings;
|
|
67
|
-
}
|
|
1
|
+
// 兼容既有 core 公开入口,实际能力由 platform 模块维护。
|
|
2
|
+
export * from '@mytools/fst-platform/environments';
|
|
@@ -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
|
+
}
|
|
@@ -1,80 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import which from 'which';
|
|
4
|
-
import { FlowError } from '@mytools/sdk';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
|
|
7
|
-
const outputText=bytes=>bytes??new Uint8Array();
|
|
8
|
-
const stripFinalNewline=text=>text.endsWith('\r\n')?text.slice(0,-2):text.endsWith('\n')?text.slice(0,-1):text;
|
|
9
|
-
|
|
10
|
-
export async function runProcess(file,args=[],{cwd,env,signal,timeout=30000,maxBuffer=4*1024*1024,encoding='utf8',onOutput}={}) {
|
|
11
|
-
if(typeof encoding!=='string')throw new TypeError(`不支持的程序输出编码:${String(encoding)}`);
|
|
12
|
-
const label=/^(utf16le|ucs2|ucs-2)$/i.test(encoding)?'utf-16le':encoding;
|
|
13
|
-
let decoderEncoding;
|
|
14
|
-
try {decoderEncoding=new TextDecoder(label).encoding;}
|
|
15
|
-
catch {throw new TypeError(`不支持的程序输出编码:${encoding}`);}
|
|
16
|
-
if(signal?.aborted) throw new FlowError('操作已取消',{code:'CANCELLED'});
|
|
17
|
-
let executable;
|
|
18
|
-
try {
|
|
19
|
-
// 不改变宿主 cwd;程序路径和 PATH 中相对目录都以子进程 cwd 为基准。
|
|
20
|
-
const directory=path.resolve(cwd??process.cwd()),windows=process.platform==='win32';
|
|
21
|
-
const hasPath=path.isAbsolute(file)||file.includes('/')||(windows&&file.includes('\\'));
|
|
22
|
-
const search=hasPath?[path.resolve(directory,file)]:[
|
|
23
|
-
...(windows?[directory]:[]),...(env?.PATH??(windows?env?.Path:undefined)??process.env.PATH??'').split(path.delimiter),
|
|
24
|
-
].map(entry=>path.resolve(directory,entry.replace(/^"(.*)"$/,'$1'),file));
|
|
25
|
-
for(const candidate of search){
|
|
26
|
-
executable=await which(candidate,{nothrow:true,pathExt:env?.PATHEXT??process.env.PATHEXT});
|
|
27
|
-
if(executable)break;
|
|
28
|
-
}
|
|
29
|
-
if(!executable)throw Object.assign(new Error(`not found: ${file}`),{code:'ENOENT'});
|
|
30
|
-
}
|
|
31
|
-
catch(cause){throw new FlowError(`未找到程序:${file}`,{code:'ENOENT',details:{executable:file},cause});}
|
|
32
|
-
// Execa 收集原始字节;文本只在这里按同一编码解码,避免返回值被默认 UTF-8 解码。
|
|
33
|
-
const child=execa(executable,args,{cwd,env,reject:false,windowsHide:true,maxBuffer,timeout:0,shell:false,encoding:'buffer',stripFinalNewline:false});
|
|
34
|
-
let reason,killing,outputError,outputFailed=false,outputWrites=Promise.resolve();
|
|
35
|
-
const stop=kind=>{
|
|
36
|
-
if(reason) return; reason=kind;
|
|
37
|
-
killing=new Promise(resolve=>child.pid ? treeKill(child.pid,'SIGKILL',()=>resolve()) : resolve());
|
|
38
|
-
};
|
|
39
|
-
const cancel=()=>stop('cancelled');
|
|
40
|
-
signal?.addEventListener('abort',cancel,{once:true});
|
|
41
|
-
if(signal?.aborted)cancel();
|
|
42
|
-
const emit=(stream,message)=>{
|
|
43
|
-
if(!onOutput||!message)return;
|
|
44
|
-
// 立即挂接拒绝处理,不能让事件回调中的异步落盘变成未处理拒绝。
|
|
45
|
-
outputWrites=outputWrites.then(async()=>{if(!outputFailed)await onOutput({stream,message});}).catch(error=>{
|
|
46
|
-
if(!outputFailed){outputFailed=true;outputError=error;stop('output-callback');}
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
const flush=[];
|
|
50
|
-
for(const [name,stream] of [['stdout',child.stdout],['stderr',child.stderr]]){
|
|
51
|
-
let bytes=0,ended=false;
|
|
52
|
-
const decoder=new TextDecoder(decoderEncoding);
|
|
53
|
-
const finish=()=>{if(!ended){ended=true;emit(name,decoder.decode());}};
|
|
54
|
-
flush.push(finish);stream?.once('end',finish);
|
|
55
|
-
stream?.on('data',chunk=>{
|
|
56
|
-
bytes+=Buffer.byteLength(chunk);
|
|
57
|
-
// 流量超限时立即回收整棵进程树,不继续扩张待落盘的日志队列。
|
|
58
|
-
if(bytes>maxBuffer){stop('output-limit');return;}
|
|
59
|
-
if(onOutput)emit(name,decoder.decode(chunk,{stream:true}));
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
const timer=timeout>0 ? setTimeout(()=>stop('timeout'),timeout) : null;
|
|
63
|
-
let result;
|
|
64
|
-
try {
|
|
65
|
-
result=await child;
|
|
66
|
-
} finally {
|
|
67
|
-
clearTimeout(timer);
|
|
68
|
-
for(const finish of flush)finish();
|
|
69
|
-
await outputWrites;
|
|
70
|
-
if(killing)await killing;
|
|
71
|
-
signal?.removeEventListener('abort',cancel);
|
|
72
|
-
}
|
|
73
|
-
if(reason==='output-callback')throw outputError;
|
|
74
|
-
if(reason==='output-limit')throw new FlowError('程序输出超过上限',{code:'OUTPUT_LIMIT'});
|
|
75
|
-
if(reason)throw new FlowError(reason==='cancelled'?'操作已取消':'程序运行超时',{code:reason==='cancelled'?'CANCELLED':'TIMEOUT'});
|
|
76
|
-
if(result.isMaxBuffer)throw new FlowError('程序输出超过上限',{code:'OUTPUT_LIMIT'});
|
|
77
|
-
if(result.exitCode===undefined&&result.failed)throw new FlowError(`无法启动程序 ${file}:${result.shortMessage??result.message}`,{code:result.code??'SPAWN_FAILED'});
|
|
78
|
-
const decode=bytes=>stripFinalNewline(new TextDecoder(decoderEncoding).decode(outputText(bytes)));
|
|
79
|
-
return {exitCode:result.exitCode,stdout:decode(result.stdout),stderr:decode(result.stderr)};
|
|
80
|
-
}
|
|
1
|
+
// 兼容既有 core 公开入口,实际能力由 platform 模块维护。
|
|
2
|
+
export * from '@mytools/fst-platform';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// 由 npm version 的生命周期脚本同步,内部插件 API 版本独立维护。
|
|
2
|
-
export const VERSION="0.
|
|
2
|
+
export const VERSION="0.5.0";
|
|
@@ -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,54 @@
|
|
|
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`。
|
|
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`。
|