@yanglaofish/fishtools 0.3.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/examples/script-plugin/LICENSE +21 -0
- package/manual/docs/examples/script-plugin/README.md +64 -0
- package/manual/docs/examples/script-plugin/index.js +39 -0
- package/manual/docs/examples/script-plugin/package.json +9 -0
- package/manual/docs/examples/script-plugin/scripts/benchmark.py +23 -0
- package/manual/docs/kernel-generation.md +97 -0
- package/manual/docs/modules.md +57 -0
- package/manual/docs/plugin-development.md +163 -12
- package/manual/docs/releases.md +23 -0
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
- package/manual/docs/roadmap.md +16 -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/plans/2026-10-12-plugin-developer-experience.md +50 -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-12-plugin-release-review.md +20 -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-developer-experience.md +22 -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 +58 -1
- package/manual/packages/core/README.md +44 -0
- package/manual/packages/core/package.json +38 -0
- package/manual/packages/core/src/application.js +268 -87
- 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/manual/packages/fst-cli/bin/fst.js +124 -0
- 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/manual/packages/fst-devtools/src/plugin-tools.js +152 -0
- 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/manual/packages/fst-plugin/src/contracts.js +86 -0
- 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/manual/packages/{core → 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/manual/packages/fst-workspace/src/plugin-installations.js +84 -0
- package/manual/packages/fst-workspace/src/plugins.js +35 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +41 -21
- 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/packages/sdk/schema-path.js +31 -0
- package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +22 -9
- 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 -15
- package/node_modules/@mytools/core/src/application.js +268 -87
- 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 -29
- 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 -71
- 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/node_modules/@mytools/{core → 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 +15 -3
- 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 +1275 -1076
- 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/package.json +117 -101
- package/manual/packages/core/src/host.js +0 -124
- package/node_modules/@mytools/core/bin/fst.js +0 -70
- package/node_modules/@mytools/core/src/contracts.js +0 -41
- package/node_modules/@mytools/core/src/host.js +0 -124
- package/node_modules/@mytools/core/src/plugins.js +0 -55
- 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/@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/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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import {mkdir,readFile,readdir,writeFile,copyFile,rename,rmdir} from 'node:fs/promises';
|
|
4
|
+
import {constants} from 'node:fs';
|
|
5
|
+
import {randomUUID} from 'node:crypto';
|
|
6
|
+
import {setTimeout} from 'node:timers/promises';
|
|
7
|
+
import {FlowError} from '@mytools/sdk';
|
|
8
|
+
import {readJson} from '@mytools/fst-platform/json';
|
|
9
|
+
|
|
10
|
+
export async function prepareBundleWorkspace(bundleRoot,requested){
|
|
11
|
+
bundleRoot=path.resolve(bundleRoot);
|
|
12
|
+
const bundle=await readJson(path.join(bundleRoot,'bundle.json'),null);
|
|
13
|
+
if(!bundle?.editable)return bundleRoot;
|
|
14
|
+
if(!/^[\w-]+$/.test(bundle.id))throw new FlowError('运行包 ID 无效');
|
|
15
|
+
const workspace=path.resolve(requested??process.env.FISHTOOLS_WORKSPACE??path.join(process.env.FISHTOOLS_HOME??path.join(os.homedir(),'.fishtools'),'workspaces',bundle.id));
|
|
16
|
+
if(workspace===bundleRoot||workspace.startsWith(bundleRoot+path.sep))throw new FlowError('可写工作区必须位于分发包目录之外');
|
|
17
|
+
await mkdir(workspace,{recursive:true});
|
|
18
|
+
const lock=path.join(workspace,'.fst-init-lock');let locked=false;
|
|
19
|
+
for(let attempt=0;attempt<100;attempt++){
|
|
20
|
+
try{await mkdir(lock);locked=true;break;}catch(e){if(e.code!=='EEXIST')throw e;await setTimeout(100);}
|
|
21
|
+
}
|
|
22
|
+
if(!locked)throw new FlowError('工作区正在初始化,请稍后重试;若进程意外终止,请检查 .fst-init-lock');
|
|
23
|
+
try{
|
|
24
|
+
const file=path.join(workspace,'flow.project.json');
|
|
25
|
+
let project=await readJson(file,null);
|
|
26
|
+
if(project&&project.baseBundle?.id!==bundle.id)throw new FlowError('指定工作区已属于其他项目或分发包');
|
|
27
|
+
if(!project){
|
|
28
|
+
project=JSON.parse(await readFile(path.join(bundleRoot,'flow.project.json'),'utf8'));
|
|
29
|
+
await mkdir(path.join(workspace,'workflows'),{recursive:true});
|
|
30
|
+
for(const name of await readdir(path.join(bundleRoot,'workflows'))){
|
|
31
|
+
if(!name.endsWith('.yaml'))continue;
|
|
32
|
+
try{await copyFile(path.join(bundleRoot,'workflows',name),path.join(workspace,'workflows',name),constants.COPYFILE_EXCL);}catch(e){if(e.code!=='EEXIST')throw e;}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
project.readOnly=false;project.baseBundle={id:bundle.id,root:bundleRoot};
|
|
36
|
+
const temp=`${file}.${randomUUID()}.tmp`;await writeFile(temp,JSON.stringify(project,null,2));await rename(temp,file);
|
|
37
|
+
return workspace;
|
|
38
|
+
}finally{await rmdir(lock);}
|
|
39
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {mkdir,readFile,writeFile,rename,readdir,realpath,lstat,rm,unlink,rmdir} from 'node:fs/promises';
|
|
3
|
+
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
5
|
+
import {create as archive} from 'tar';
|
|
6
|
+
import {FlowError} from '@mytools/sdk';
|
|
7
|
+
|
|
8
|
+
async function json(file,fallback){try{return JSON.parse(await readFile(file,'utf8'));}catch(error){if(error.code==='ENOENT')return fallback;throw error;}}
|
|
9
|
+
const flow=project=>path.join(project,'.flow');
|
|
10
|
+
const stateFile=project=>path.join(flow(project),'plugins.json');
|
|
11
|
+
export const readPluginState=project=>json(stateFile(project),{});
|
|
12
|
+
|
|
13
|
+
// 所有安装/引用/回收操作共用工作区锁,防止其他 fst 进程同时切换批次。
|
|
14
|
+
export async function withPluginMutation(project,callback){
|
|
15
|
+
await mkdir(flow(project),{recursive:true});const lock=path.join(flow(project),'plugin-mutation.lock');
|
|
16
|
+
try{await mkdir(lock);}catch(error){if(error.code==='EEXIST')throw new FlowError('插件状态正在被其他进程修改;若上次异常退出,请确认没有活动 fst 后移除 .flow/plugin-mutation.lock',{code:'PLUGIN_BUSY'});throw error;}
|
|
17
|
+
try{return await callback();}finally{await rmdir(lock);}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function leaseInstallation(project,root){
|
|
21
|
+
if(!root)return async()=>{};
|
|
22
|
+
const directory=path.join(flow(project),'installation-leases');await mkdir(directory,{recursive:true});
|
|
23
|
+
const file=path.join(directory,`${randomUUID()}.json`);
|
|
24
|
+
await writeFile(file,JSON.stringify({pid:process.pid,root:path.resolve(root)}));
|
|
25
|
+
return async()=>{try{await unlink(file);}catch(error){if(error.code!=='ENOENT')throw error;}};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function openPluginState(project,{readOnly=false}={}){
|
|
29
|
+
const existing=await readPluginState(project);
|
|
30
|
+
if(readOnly||!existing.root)return {state:existing,release:async()=>{}};
|
|
31
|
+
return withPluginMutation(project,async()=>{const state=await readPluginState(project);return {state,release:await leaseInstallation(project,state.root)};});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function assertPluginState(project,expected){
|
|
35
|
+
if(!isDeepStrictEqual(await readPluginState(project),expected))throw new FlowError('插件状态已被其他进程修改,请重启当前宿主后再操作',{code:'PLUGIN_STATE_CHANGED'});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function savePluginState(project,next,previous){
|
|
39
|
+
const backup=path.join(flow(project),'plugins.previous.json'),id=randomUUID();
|
|
40
|
+
await writeFile(`${backup}.${id}.tmp`,JSON.stringify(previous,null,2));await rename(`${backup}.${id}.tmp`,backup);
|
|
41
|
+
const file=stateFile(project);await writeFile(`${file}.${id}.tmp`,JSON.stringify(next,null,2));await rename(`${file}.${id}.tmp`,file);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function collectInstallations(project,{apply=false}={}){
|
|
45
|
+
// 预览仅读取,不创建锁或备份目录;执行时在锁内重新计算,不能复用旧预览。
|
|
46
|
+
const collect=async()=>{
|
|
47
|
+
const base=path.join(flow(project),'installations'),state=await readPluginState(project),previous=await json(path.join(flow(project),'plugins.previous.json'),{});
|
|
48
|
+
const protectedRoots=new Set([state.root,state.previousRoot,previous.root].filter(Boolean).map(root=>path.resolve(root)));
|
|
49
|
+
let leases=[];try{leases=await readdir(path.join(flow(project),'installation-leases'));}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
50
|
+
for(const file of leases){
|
|
51
|
+
const lease=await json(path.join(flow(project),'installation-leases',file),null);if(!lease)continue;
|
|
52
|
+
// 无法确认进程已退出时宁可保留;PID 重用只会保守地多保留一个批次。
|
|
53
|
+
try{process.kill(lease.pid,0);protectedRoots.add(path.resolve(lease.root));}catch(error){if(error.code!=='ESRCH')protectedRoots.add(path.resolve(lease.root));}
|
|
54
|
+
}
|
|
55
|
+
let entries=[];try{entries=await readdir(base,{withFileTypes:true});}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
56
|
+
const candidates=[],kept=[],removed=[];
|
|
57
|
+
const canonicalBase=entries.length?await realpath(base):base;
|
|
58
|
+
if(entries.length){
|
|
59
|
+
const projectRoot=await realpath(project),relative=path.relative(projectRoot,canonicalBase);
|
|
60
|
+
if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative)||(await lstat(base)).isSymbolicLink())throw new FlowError('安装目录越界或是目录链接,拒绝清理');
|
|
61
|
+
for(const root of [...protectedRoots]){try{protectedRoots.add(await realpath(root));}catch(error){if(error.code!=='ENOENT')throw error;}}
|
|
62
|
+
}
|
|
63
|
+
for(const entry of entries){
|
|
64
|
+
const directory=path.join(base,entry.name);
|
|
65
|
+
if(!entry.isDirectory()||!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(entry.name)){kept.push({directory,reason:'非受管批次'});continue;}
|
|
66
|
+
const physical=await realpath(directory);
|
|
67
|
+
if(path.dirname(physical)!==canonicalBase||(await lstat(directory)).isSymbolicLink()||protectedRoots.has(physical)){kept.push({directory,reason:'当前、恢复、活动批次或链接'});continue;}
|
|
68
|
+
const pkg=await json(path.join(directory,'package.json'),{});
|
|
69
|
+
if(pkg.name!=='flow-project-plugins'){kept.push({directory,reason:'无法确认批次归属'});continue;}
|
|
70
|
+
candidates.push({directory});
|
|
71
|
+
if(apply){
|
|
72
|
+
const backups=path.join(flow(project),'installation-backups');await mkdir(backups,{recursive:true});
|
|
73
|
+
const backup=path.join(backups,`${entry.name}-${randomUUID()}.tgz`),temporary=`${backup}.tmp`;
|
|
74
|
+
await archive({cwd:canonicalBase,file:temporary,gzip:true,follow:false,portable:true},[entry.name]);
|
|
75
|
+
await rename(temporary,backup);
|
|
76
|
+
// 备份完成后再次确认真实绝对路径仍是本项目的直接子目录,才允许递归删除。
|
|
77
|
+
if(await realpath(directory)!==physical||path.dirname(physical)!==canonicalBase)throw new FlowError('安装批次路径在清理期间改变,已停止');
|
|
78
|
+
await rm(physical,{recursive:true,maxRetries:5,retryDelay:200});removed.push({directory,backup});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {candidates,kept,removed};
|
|
82
|
+
};
|
|
83
|
+
return apply?withPluginMutation(project,collect):collect();
|
|
84
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {mkdir,writeFile,access} from 'node:fs/promises';
|
|
3
|
+
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import {FlowError} from '@mytools/sdk';
|
|
5
|
+
import {readJson} from '@mytools/fst-platform/json';
|
|
6
|
+
import {npm} from '@mytools/fst-platform/npm';
|
|
7
|
+
import {loadPlugin} from '@mytools/fst-plugin/loader';
|
|
8
|
+
export async function installGeneration(projectDir,{add,remove,link,state}) {
|
|
9
|
+
const root=path.join(projectDir,'.flow','installations',randomUUID());
|
|
10
|
+
await mkdir(root,{recursive:true});
|
|
11
|
+
const dependencies={...(state?.dependencies??{})};
|
|
12
|
+
const links={...(state?.links??{})};
|
|
13
|
+
if(remove){delete dependencies[remove];delete links[remove];}
|
|
14
|
+
if(link){
|
|
15
|
+
const source=path.resolve(link),pkg=await readJson(path.join(source,'package.json'));
|
|
16
|
+
const loaded=await loadPlugin(source,projectDir);
|
|
17
|
+
if(!pkg.name||pkg.name!==loaded.plugin?.id||pkg.version!==loaded.plugin?.version)throw new FlowError('开发插件的包名/版本必须与插件 id/version 一致',{code:'VALIDATION'});
|
|
18
|
+
links[pkg.name]=source;delete dependencies[pkg.name];
|
|
19
|
+
}
|
|
20
|
+
await writeFile(path.join(root,'package.json'),JSON.stringify({name:'flow-project-plugins',private:true,dependencies},null,2));
|
|
21
|
+
let source=add;
|
|
22
|
+
if(add) {
|
|
23
|
+
try {await access(path.resolve(projectDir,add));source=path.resolve(projectDir,add);} catch {}
|
|
24
|
+
if(source.startsWith('-')) throw new FlowError('插件来源不能以选项符号开头',{code:'VALIDATION'});
|
|
25
|
+
}
|
|
26
|
+
if(source||Object.keys(dependencies).length){
|
|
27
|
+
const result=await npm(['install',...(source?[source]:[]),'--save-exact','--ignore-scripts','--install-links','--no-audit','--no-fund'],{cwd:root});
|
|
28
|
+
if(result.exitCode!==0) throw new FlowError(`插件安装失败:${result.stderr}`,{code:'INSTALL_FAILED'});
|
|
29
|
+
}
|
|
30
|
+
const pkg=await readJson(path.join(root,'package.json'));
|
|
31
|
+
const loaded=[];
|
|
32
|
+
for(const name of Object.keys(pkg.dependencies??{})){delete links[name];loaded.push(await loadPlugin(name,root));}
|
|
33
|
+
for(const source of Object.values(links))loaded.push(await loadPlugin(source,projectDir));
|
|
34
|
+
return {state:{root,dependencies:pkg.dependencies??{},links,...(state?.root?{previousRoot:state.root}:{})},loaded};
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdir, copyFile, writeFile, access } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import {prepareBundleWorkspace} from './bundle-workspace.js';
|
|
7
|
+
|
|
8
|
+
const templates=fileURLToPath(new URL('../templates/',import.meta.url));
|
|
9
|
+
async function exists(file){try{await access(file);return true;}catch(error){if(error.code==='ENOENT')return false;throw error;}}
|
|
10
|
+
|
|
11
|
+
export async function initializeProject(directory){
|
|
12
|
+
directory=path.resolve(directory);
|
|
13
|
+
if(await exists(path.join(directory,'flow.project.json')))return directory;
|
|
14
|
+
await mkdir(path.join(directory,'workflows'),{recursive:true});
|
|
15
|
+
for(const name of ['hello','system-check','python-check','control-demo']){
|
|
16
|
+
try{await copyFile(path.join(templates,`${name}.yaml`),path.join(directory,'workflows',`${name}.yaml`),constants.COPYFILE_EXCL);}
|
|
17
|
+
catch(error){if(error.code!=='EEXIST')throw error;}
|
|
18
|
+
}
|
|
19
|
+
// npm 全局安装目录只保存程序,用户数据始终写入独立项目。
|
|
20
|
+
try{await writeFile(path.join(directory,'flow.project.json'),JSON.stringify({plugins:['@mytools/environment','@mytools/examples','@mytools/web'],config:{}},null,2),{flag:'wx'});}
|
|
21
|
+
catch(error){if(error.code!=='EEXIST')throw error;}
|
|
22
|
+
return directory;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function resolveProject(explicit,workspace){
|
|
26
|
+
if(explicit)return prepareBundleWorkspace(path.resolve(explicit),workspace);
|
|
27
|
+
if(await exists(path.join(process.cwd(),'flow.project.json')))return prepareBundleWorkspace(process.cwd(),workspace);
|
|
28
|
+
return initializeProject(process.env.FISHTOOLS_HOME??path.join(os.homedir(),'.fishtools'));
|
|
29
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, realpath, writeFile, rename, open, unlink } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { FlowError } from '@mytools/sdk';
|
|
6
|
+
import { assertId, assertSchema, references, referenceParts, emptyObject } from '@mytools/sdk/schema';
|
|
7
|
+
import {resolveSchemaPath} from '@mytools/sdk/schema-path';
|
|
8
|
+
import {validateCondition,validateStepList} from '@mytools/fst-workflow/control';
|
|
9
|
+
|
|
10
|
+
export class WorkflowStore {
|
|
11
|
+
constructor(root,{openFile=open}={}) { this.root = path.resolve(root);this.openFile=openFile; }
|
|
12
|
+
file(id) { assertId(id); return path.join(this.root,`${id}.yaml`); }
|
|
13
|
+
async get(id) {
|
|
14
|
+
const file=this.file(id);
|
|
15
|
+
const physical=await realpath(file);
|
|
16
|
+
const root=await realpath(this.root);
|
|
17
|
+
if (!physical.startsWith(root+path.sep)) throw new FlowError('工作流越界,必须位于工作流目录内',{code:'VALIDATION'});
|
|
18
|
+
const definition=YAML.parse(await readFile(file,'utf8'));
|
|
19
|
+
if (definition?.id !== id) throw new FlowError(`工作流 ID 与文件名不一致:${id}`,{code:'VALIDATION'});
|
|
20
|
+
return this.validateShape(definition);
|
|
21
|
+
}
|
|
22
|
+
validateShape(w) {
|
|
23
|
+
assertId(w?.id);
|
|
24
|
+
if (w.schemaVersion!==1 || !Array.isArray(w.steps)) throw new FlowError('工作流格式错误:需要 schemaVersion: 1 与 steps 列表',{code:'VALIDATION'});
|
|
25
|
+
validateStepList(w.steps,assertId);
|
|
26
|
+
return structuredClone(w);
|
|
27
|
+
}
|
|
28
|
+
async save(definition) {
|
|
29
|
+
const w=this.validateShape(definition);
|
|
30
|
+
await mkdir(this.root,{recursive:true});
|
|
31
|
+
const file=this.file(w.id), temporary=`${file}.${randomUUID()}.tmp`;
|
|
32
|
+
let previous;
|
|
33
|
+
try{
|
|
34
|
+
const physical=await realpath(file),root=await realpath(this.root);
|
|
35
|
+
if(!physical.startsWith(root+path.sep))throw new FlowError('工作流越界,不能覆盖工作流目录之外的文件',{code:'VALIDATION'});
|
|
36
|
+
previous=await readFile(file,'utf8');
|
|
37
|
+
}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
38
|
+
if(previous!==undefined){
|
|
39
|
+
const directory=path.join(path.dirname(this.root),'.flow','workflow-backups');
|
|
40
|
+
await mkdir(directory,{recursive:true,mode:0o700});
|
|
41
|
+
await writeFile(path.join(directory,`${w.id}-${randomUUID()}.yaml`),previous,{flag:'wx',mode:0o600});
|
|
42
|
+
}
|
|
43
|
+
try{await writeFile(temporary,YAML.stringify(w),'utf8');await rename(temporary,file);}
|
|
44
|
+
catch(error){try{await unlink(temporary);}catch(cleanup){if(cleanup.code!=='ENOENT')throw new AggregateError([error,cleanup],`工作流保存失败且临时文件无法清理:${temporary}`);}throw error;}
|
|
45
|
+
return w;
|
|
46
|
+
}
|
|
47
|
+
async duplicate(definition,{id,title}={}) {
|
|
48
|
+
const source=this.validateShape(definition);
|
|
49
|
+
const copy=this.validateShape({...source,id:id??`flow-${randomUUID()}`,title:title??`${source.title??source.id} 副本`});
|
|
50
|
+
const file=this.file(copy.id),content=YAML.stringify(copy);
|
|
51
|
+
if(typeof copy.title!=='string'||!copy.title.trim())throw new FlowError('工作流名称不能为空',{code:'VALIDATION'});
|
|
52
|
+
await mkdir(this.root,{recursive:true});
|
|
53
|
+
let handle;
|
|
54
|
+
try{
|
|
55
|
+
// 排他创建保证并发请求也不能覆盖同名工作流。
|
|
56
|
+
handle=await this.openFile(file,'wx');
|
|
57
|
+
await handle.writeFile(content,'utf8');
|
|
58
|
+
await handle.close();handle=null;
|
|
59
|
+
return {definition:copy,path:file};
|
|
60
|
+
}catch(error){
|
|
61
|
+
if(handle){
|
|
62
|
+
try{await handle.close();}catch{}
|
|
63
|
+
try{await unlink(file);}catch(cleanupError){if(cleanupError.code!=='ENOENT')throw new AggregateError([error,cleanupError],`复制失败且无法清理新文件:${file}`);}
|
|
64
|
+
}
|
|
65
|
+
if(error.code==='EEXIST')throw new FlowError(`工作流已存在:${copy.id}`,{code:'VALIDATION',cause:error});
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async list() {
|
|
70
|
+
await mkdir(this.root,{recursive:true});
|
|
71
|
+
const files=await readdir(this.root);
|
|
72
|
+
return Promise.all(files.filter(f=>f.endsWith('.yaml')).sort().map(f=>this.get(f.slice(0,-5))));
|
|
73
|
+
}
|
|
74
|
+
async remove(id){
|
|
75
|
+
await this.get(id);
|
|
76
|
+
const refers=steps=>steps.some(step=>step.workflow&&this.childId(step.workflow)===id||step.branch&&(refers(step.branch.then)||refers(step.branch.else??[]))||step.parallel&&refers(step.parallel.steps));
|
|
77
|
+
const dependents=(await this.list()).filter(flow=>flow.id!==id&&refers(flow.steps));
|
|
78
|
+
if(dependents.length)throw new FlowError(`工作流仍被引用:${dependents.map(flow=>flow.title??flow.id).join('、')}`);
|
|
79
|
+
const directory=path.join(path.dirname(this.root),'.flow','deleted-workflows');
|
|
80
|
+
await mkdir(directory,{recursive:true});
|
|
81
|
+
const backup=path.join(directory,`${id}-${randomUUID()}.yaml`);
|
|
82
|
+
// 同项目内原子移动,备份保留完整原文;复制回原文件名即可恢复。
|
|
83
|
+
await rename(this.file(id),backup);return backup;
|
|
84
|
+
}
|
|
85
|
+
childId(reference) {
|
|
86
|
+
if(typeof reference!=='string' || !/^(\.\/)?[A-Za-z0-9][A-Za-z0-9_-]*\.yaml$/.test(reference)) throw new FlowError(`子流程必须位于工作流目录内:${reference}`,{code:'VALIDATION'});
|
|
87
|
+
return reference.replace(/^\.\//,'').slice(0,-5);
|
|
88
|
+
}
|
|
89
|
+
async resolve(entries,host) {
|
|
90
|
+
const workflows=new Map(), outputs=new Map(), plugins=new Set(), visiting=new Set();
|
|
91
|
+
const visit=async id=>{
|
|
92
|
+
if(visiting.has(id)) throw new FlowError(`工作流循环引用:${[...visiting,id].join(' → ')}`,{code:'VALIDATION'});
|
|
93
|
+
if(workflows.has(id)) return;
|
|
94
|
+
visiting.add(id);
|
|
95
|
+
const w=await this.get(id);
|
|
96
|
+
const checker=previous=>ref=>{
|
|
97
|
+
const p=referenceParts(ref);
|
|
98
|
+
if(p[0]==='steps' && !previous.has(p[1])) throw new FlowError(`${id} 引用必须指向前序步骤:${ref}`,{code:'VALIDATION'});
|
|
99
|
+
return resolveSchemaPath(p[0]==='inputs'?(w.inputs??emptyObject):previous.get(p[1]),p.slice(p[0]==='inputs'?1:3),ref);
|
|
100
|
+
};
|
|
101
|
+
const resolveList=async(steps,inherited=new Map(),parallel=false)=>{
|
|
102
|
+
const previous=new Map(inherited),own={};
|
|
103
|
+
for(const s of steps){
|
|
104
|
+
const check=checker(parallel?inherited:previous);
|
|
105
|
+
if(s.when)validateCondition(s.when,check);
|
|
106
|
+
let output;
|
|
107
|
+
if(s.action){
|
|
108
|
+
references(s.with??{},check);
|
|
109
|
+
const action=host.actions.get(s.action);
|
|
110
|
+
if(!action)throw new FlowError(`${id} → ${s.id} 缺少插件动作 ${s.action}`,{code:'VALIDATION'});
|
|
111
|
+
if(action.input===undefined||action.output===undefined)throw new FlowError(`可编排动作 ${s.action} 必须声明 input 和 output schema`,{code:'VALIDATION'});
|
|
112
|
+
assertSchema(action.input,`${s.action} input schema`);assertSchema(action.output,`${s.action} output schema`);
|
|
113
|
+
plugins.add(action.plugin);output=action.output;
|
|
114
|
+
}else if(s.workflow){references(s.with??{},check);const child=this.childId(s.workflow);await visit(child);output=outputs.get(child);}
|
|
115
|
+
else if(s.branch){
|
|
116
|
+
validateCondition(s.branch.condition,check);
|
|
117
|
+
const left=await resolveList(s.branch.then,parallel?inherited:previous),right=await resolveList(s.branch.else??[],parallel?inherited:previous);
|
|
118
|
+
output={type:'object',additionalProperties:false,properties:{...left.own,...right.own}};
|
|
119
|
+
}else if(s.parallel){const group=await resolveList(s.parallel.steps,parallel?inherited:previous,true);output={type:'object',additionalProperties:false,properties:group.own};}
|
|
120
|
+
else output=emptyObject;
|
|
121
|
+
previous.set(s.id,output);own[s.id]=output;
|
|
122
|
+
}
|
|
123
|
+
return {previous,own};
|
|
124
|
+
};
|
|
125
|
+
const {previous}=await resolveList(w.steps),check=checker(previous);
|
|
126
|
+
references(w.outputs??{},check);
|
|
127
|
+
const infer=value=>{
|
|
128
|
+
if(value?.$ref)return check(value.$ref);
|
|
129
|
+
if(Array.isArray(value))return {type:'array'};
|
|
130
|
+
if(value&&typeof value==='object')return {type:'object',additionalProperties:false,properties:Object.fromEntries(Object.entries(value).map(([key,v])=>[key,infer(v)]))};
|
|
131
|
+
return {type:value===null?'null':typeof value};
|
|
132
|
+
};
|
|
133
|
+
outputs.set(id,infer(w.outputs??{}));
|
|
134
|
+
workflows.set(id,w); visiting.delete(id);
|
|
135
|
+
};
|
|
136
|
+
for(const id of entries) await visit(id);
|
|
137
|
+
const resolved=new Set(), visitingPlugins=new Set();
|
|
138
|
+
const addPlugin=id=>{
|
|
139
|
+
if(visitingPlugins.has(id)) throw new FlowError(`插件循环依赖:${id}`,{code:'VALIDATION'});
|
|
140
|
+
if(resolved.has(id)) return;
|
|
141
|
+
const p=host.definitions.get(id);
|
|
142
|
+
if(!p) throw new FlowError(`插件依赖缺失:${id}`,{code:'VALIDATION'});
|
|
143
|
+
visitingPlugins.add(id);
|
|
144
|
+
Object.keys(p.dependencies??{}).forEach(addPlugin);
|
|
145
|
+
visitingPlugins.delete(id); resolved.add(id);
|
|
146
|
+
};
|
|
147
|
+
plugins.forEach(addPlugin);
|
|
148
|
+
return {workflows:[...workflows.values()],plugins:[...resolved]};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
id: control-demo
|
|
3
|
+
title: 条件与并行入门
|
|
4
|
+
description: 检查系统后,根据你的选择继续或提前结束;继续时并行处理两段文本。运行中可切换步骤和查看日志。
|
|
5
|
+
inputs:
|
|
6
|
+
type: object
|
|
7
|
+
properties:
|
|
8
|
+
proceed:
|
|
9
|
+
type: boolean
|
|
10
|
+
title: 继续执行文本处理
|
|
11
|
+
default: true
|
|
12
|
+
required: [proceed]
|
|
13
|
+
steps:
|
|
14
|
+
- id: environment
|
|
15
|
+
action: "@mytools/environment:report"
|
|
16
|
+
with: {}
|
|
17
|
+
- id: guard
|
|
18
|
+
when:
|
|
19
|
+
left: {$ref: steps.environment.output.ok}
|
|
20
|
+
op: eq
|
|
21
|
+
right: false
|
|
22
|
+
stop:
|
|
23
|
+
reason: 环境检查未通过,已停止后续步骤。
|
|
24
|
+
- id: choose
|
|
25
|
+
branch:
|
|
26
|
+
condition:
|
|
27
|
+
left: {$ref: inputs.proceed}
|
|
28
|
+
op: eq
|
|
29
|
+
right: true
|
|
30
|
+
then:
|
|
31
|
+
- id: batch
|
|
32
|
+
parallel:
|
|
33
|
+
concurrency: 2
|
|
34
|
+
steps:
|
|
35
|
+
- id: first
|
|
36
|
+
action: "@mytools/examples:uppercase"
|
|
37
|
+
with: {text: "Hello, Fishtools!"}
|
|
38
|
+
- id: second
|
|
39
|
+
action: "@mytools/examples:uppercase"
|
|
40
|
+
with: {text: Parallel workflow}
|
|
41
|
+
else:
|
|
42
|
+
- id: finish
|
|
43
|
+
stop:
|
|
44
|
+
reason: 本次选择提前结束,没有执行文本处理。
|
|
45
|
+
outputs:
|
|
46
|
+
first: {$ref: steps.choose.output.batch.first.text}
|
|
47
|
+
second: {$ref: steps.choose.output.batch.second.text}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
id: hello
|
|
3
|
+
title: 我的第一个工作流
|
|
4
|
+
description: 将输入文本转为大写,追加完成标记,再保存到文件。所有步骤都可在界面中调整。
|
|
5
|
+
inputs:
|
|
6
|
+
type: object
|
|
7
|
+
required:
|
|
8
|
+
- text
|
|
9
|
+
properties:
|
|
10
|
+
text:
|
|
11
|
+
type: string
|
|
12
|
+
title: 输入文本
|
|
13
|
+
default: Hello, workflow!
|
|
14
|
+
steps:
|
|
15
|
+
- id: uppercase
|
|
16
|
+
action: "@mytools/examples:uppercase"
|
|
17
|
+
with:
|
|
18
|
+
text:
|
|
19
|
+
$ref: inputs.text
|
|
20
|
+
- id: append
|
|
21
|
+
action: "@mytools/examples:append"
|
|
22
|
+
with:
|
|
23
|
+
text:
|
|
24
|
+
$ref: steps.uppercase.output.text
|
|
25
|
+
suffix: " ✓"
|
|
26
|
+
- id: save
|
|
27
|
+
action: "@mytools/examples:save"
|
|
28
|
+
with:
|
|
29
|
+
text:
|
|
30
|
+
$ref: steps.append.output.text
|
|
31
|
+
outputs:
|
|
32
|
+
text:
|
|
33
|
+
$ref: steps.append.output.text
|
|
34
|
+
file:
|
|
35
|
+
$ref: steps.save.output.path
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
id: python-check
|
|
3
|
+
title: 检查 Python 并执行脚本
|
|
4
|
+
description: 检查指定的目标 Python,再用同一个解释器运行插件提供的脚本。检查失败会停止。
|
|
5
|
+
inputs:
|
|
6
|
+
type: object
|
|
7
|
+
required: [executable, text]
|
|
8
|
+
properties:
|
|
9
|
+
executable: { type: string, title: 目标 Python 解释器路径 }
|
|
10
|
+
text: { type: string, title: 输入文本, default: Hello Python }
|
|
11
|
+
steps:
|
|
12
|
+
- id: check
|
|
13
|
+
action: '@mytools/environment:check'
|
|
14
|
+
with:
|
|
15
|
+
python:
|
|
16
|
+
executable: { $ref: inputs.executable }
|
|
17
|
+
version: '>=3.9'
|
|
18
|
+
pip: false
|
|
19
|
+
- id: python
|
|
20
|
+
action: '@mytools/examples:pythonEcho'
|
|
21
|
+
with:
|
|
22
|
+
executable: { $ref: steps.check.output.python.executable }
|
|
23
|
+
text: { $ref: inputs.text }
|
|
24
|
+
outputs:
|
|
25
|
+
text: { $ref: steps.python.output.text }
|
|
26
|
+
executable: { $ref: steps.python.output.executable }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
id: system-check
|
|
3
|
+
title: 查看系统环境
|
|
4
|
+
description: 通过官方环境检查插件查看目标机器的操作系统、架构和内核,无需安装 Python。
|
|
5
|
+
inputs: { type: object, properties: {} }
|
|
6
|
+
steps:
|
|
7
|
+
- id: check
|
|
8
|
+
action: '@mytools/environment:check'
|
|
9
|
+
with: {}
|
|
10
|
+
outputs:
|
|
11
|
+
system: { $ref: steps.check.output.system }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 分发模块
|
|
2
|
+
|
|
3
|
+
包名:`@mytools/pack`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
|
|
4
|
+
|
|
5
|
+
## 职责与边界
|
|
6
|
+
|
|
7
|
+
根据声明快照规划环境、收集生产依赖、复制运行时、生成清单和归档。不读取活动宿主或工作流存储;CLI 根目录作为发行载荷路径传入。
|
|
8
|
+
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/fst-platform`、`@mytools/fst-devtools`。
|
|
10
|
+
## 公开入口
|
|
11
|
+
|
|
12
|
+
| 导入路径 | 实现位置 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `@mytools/pack` | `./index.js` |
|
|
15
|
+
| `@mytools/pack/dependencies` | `./dependencies.js` |
|
|
16
|
+
| `@mytools/pack/runtime` | `./runtime.js` |
|
|
17
|
+
| `@mytools/pack/environments` | `./environments.js` |
|
|
18
|
+
| `@mytools/pack/archive` | `./archive.js` |
|
|
19
|
+
| `@mytools/pack/package.json` | `./package.json` |
|
|
20
|
+
|
|
21
|
+
仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
|
|
22
|
+
|
|
23
|
+
## 维护与验证
|
|
24
|
+
|
|
25
|
+
本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
|
|
26
|
+
|
|
27
|
+
在仓库根目录执行:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
node --test tests/modules.test.js tests/pack.test.js tests/dependencies.test.js tests/npm-dist.test.js
|
|
31
|
+
```
|
|
@@ -4,7 +4,7 @@ import { access, copyFile, mkdir, writeFile, realpath,stat,chmod } from 'node:fs
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { FlowError } from '@mytools/sdk';
|
|
6
6
|
|
|
7
|
-
export async function collectPackages(roots) {
|
|
7
|
+
export async function collectPackages(roots,{omitOptionalPeers=[]}={}) {
|
|
8
8
|
const trees=new Map(), nodes=new Map();
|
|
9
9
|
async function locate(root){
|
|
10
10
|
root=await realpath(root);let base=root,installedRoot;
|
|
@@ -24,7 +24,12 @@ export async function collectPackages(roots) {
|
|
|
24
24
|
}
|
|
25
25
|
if(!trees.has(base))trees.set(base,await new Arborist({path:base}).loadActual());
|
|
26
26
|
const tree=trees.get(base);
|
|
27
|
-
|
|
27
|
+
let found=[...tree.inventory.values()].find(n=>path.relative(n.realpath,root)==='' && !n.isLink);
|
|
28
|
+
// 开发引用可能嵌套在无关项目下,其上层 lock 不代表当前插件属于该依赖树。
|
|
29
|
+
if(!found&&base!==root){
|
|
30
|
+
if(!trees.has(root))trees.set(root,await new Arborist({path:root}).loadActual());
|
|
31
|
+
found=[...trees.get(root).inventory.values()].find(n=>path.relative(n.realpath,root)===''&&!n.isLink);
|
|
32
|
+
}
|
|
28
33
|
if(!found)throw new FlowError(`无法解析包的生产依赖:${root}`);
|
|
29
34
|
return found;
|
|
30
35
|
}
|
|
@@ -34,6 +39,7 @@ export async function collectPackages(roots) {
|
|
|
34
39
|
nodes.set(node.realpath,node);
|
|
35
40
|
for(const edge of node.edgesOut.values()){
|
|
36
41
|
if(edge.type==='dev')continue;
|
|
42
|
+
if(edge.type==='peerOptional'&&omitOptionalPeers.includes(edge.name))continue;
|
|
37
43
|
if(!edge.to){if(edge.optional)continue;throw new FlowError(`${node.name} 缺少依赖 ${edge.name}`);}
|
|
38
44
|
if(!edge.valid)throw new FlowError(`${node.name} 的依赖不兼容:${edge.name}`);
|
|
39
45
|
visit(edge.to);
|
|
@@ -44,9 +50,12 @@ export async function collectPackages(roots) {
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export async function copyPackages(nodes,destination,target,{filters=new Map()}={}){
|
|
53
|
+
const included=new Set(nodes.map(n=>n.realpath));
|
|
47
54
|
const top=new Map();
|
|
48
55
|
for(const n of nodes)if(!top.has(n.package.name))top.set(n.package.name,n);
|
|
49
|
-
for(const n of nodes)for(const edge of n.edgesOut.values())if(edge.type!=='dev'&&edge.to&&!top.has(edge.name))
|
|
56
|
+
for(const n of nodes)for(const edge of n.edgesOut.values())if(edge.type!=='dev'&&edge.to&&!top.has(edge.name)){
|
|
57
|
+
const child=edge.to.isLink?edge.to.target:edge.to;if(included.has(child.realpath))top.set(edge.name,child);
|
|
58
|
+
}
|
|
50
59
|
const copied=new Set(),summary=[];
|
|
51
60
|
const placements=new Map([...top].map(([name,node])=>[path.join(destination,'node_modules',name),node]));
|
|
52
61
|
function resolvedAt(from,name){
|
|
@@ -66,6 +75,8 @@ export async function copyPackages(nodes,destination,target,{filters=new Map()}=
|
|
|
66
75
|
const filter=filters.get(packageRoot);
|
|
67
76
|
const files=[...new Set([...await packlist(await new Arborist({path:packageRoot}).loadActual()),...(filter?.extraFiles??[])])];
|
|
68
77
|
for(const file of files){
|
|
78
|
+
// 项目可以与开发插件共用根目录;宿主私有状态和安装批次永远不进入插件快照。
|
|
79
|
+
if(file.split(/[\\/]/)[0]==='.flow')continue;
|
|
69
80
|
if(filter&&!filter.include(file))continue;
|
|
70
81
|
const source=path.resolve(packageRoot,file),physical=await realpath(source);
|
|
71
82
|
// 两端先解析实际路径,再按系统规则比较,兼容 Windows 大小写、短路径和目录联接。
|
|
@@ -80,6 +91,7 @@ export async function copyPackages(nodes,destination,target,{filters=new Map()}=
|
|
|
80
91
|
for(const edge of node.edgesOut.values()){
|
|
81
92
|
if(edge.type==='dev'||!edge.to)continue;
|
|
82
93
|
const child=edge.to.isLink?edge.to.target:edge.to;
|
|
94
|
+
if(!included.has(child.realpath))continue;
|
|
83
95
|
if(resolvedAt(dest,edge.name)?.realpath===child.realpath)continue;
|
|
84
96
|
const childDest=path.join(dest,'node_modules',edge.name);
|
|
85
97
|
placements.set(childDest,child);nested.push([child,childDest]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import {realpath,readdir,stat} from 'node:fs/promises';
|
|
3
|
-
import {inside,resolveEnvironments,environmentDeclarations} from '@mytools/
|
|
3
|
+
import {inside,resolveEnvironments,environmentDeclarations} from '@mytools/fst-platform/environments';
|
|
4
4
|
import {FlowError} from '@mytools/sdk';
|
|
5
5
|
|
|
6
6
|
// 只核对被声明的资源能否完整收集;版本和业务兼容性由作者负责。
|