@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,43 @@
|
|
|
1
|
+
import { lstat, opendir } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
function type(value) {
|
|
4
|
+
return value.isSymbolicLink() ? 'symlink' : value.isDirectory() ? 'directory' : value.isFile() ? 'file' : 'other';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function probe(request) {
|
|
8
|
+
if (request.operation === 'stat') {
|
|
9
|
+
const value = await lstat(request.path);
|
|
10
|
+
return { status: 'ok', incomplete: false, value: {
|
|
11
|
+
type: type(value), size: value.size, mtimeMs: value.mtimeMs,
|
|
12
|
+
ctimeMs: value.ctimeMs, birthtimeMs: value.birthtimeMs, mode: value.mode,
|
|
13
|
+
} };
|
|
14
|
+
}
|
|
15
|
+
if (request.operation !== 'readDirectory') throw new TypeError('未知文件探测操作');
|
|
16
|
+
const directory = await opendir(request.path, { bufferSize: Math.min(32, request.limit) });
|
|
17
|
+
const value = [];
|
|
18
|
+
let incomplete = false;
|
|
19
|
+
try {
|
|
20
|
+
// 不递归,也不读取文件正文;多读一个条目确认是否被上限截断。
|
|
21
|
+
for (let index = 0; index <= request.limit; index++) {
|
|
22
|
+
const entry = await directory.read();
|
|
23
|
+
if (!entry) break;
|
|
24
|
+
if (index === request.limit) { incomplete = true; break; }
|
|
25
|
+
value.push({ name: entry.name, type: type(entry) });
|
|
26
|
+
}
|
|
27
|
+
} finally {
|
|
28
|
+
await directory.close();
|
|
29
|
+
}
|
|
30
|
+
return { status: 'ok', incomplete, value, limit: request.limit, ...(incomplete ? { reason: 'entryLimit' } : {}) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
process.once('message', async request => {
|
|
34
|
+
let result;
|
|
35
|
+
try { result = await probe(request); }
|
|
36
|
+
catch (error) {
|
|
37
|
+
result = { status: error.code === 'ENOENT' ? 'missing' : 'error', incomplete: true,
|
|
38
|
+
reason: error.code === 'ENOENT' ? 'notFound' : 'filesystemError',
|
|
39
|
+
error: { code: error.code || 'PROBE_ERROR', message: error.message } };
|
|
40
|
+
}
|
|
41
|
+
if (!process.connected) return;
|
|
42
|
+
process.send(result, () => { process.disconnect(); });
|
|
43
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {Agent,EnvHttpProxyAgent,fetch as undiciFetch} from 'undici';
|
|
2
|
+
|
|
3
|
+
const privateProxy=new WeakMap();
|
|
4
|
+
const profileNamePattern=/^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
5
|
+
const pluginIdPattern=/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
6
|
+
const actionIdPattern=/^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
7
|
+
const envNamePattern=/^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
|
+
|
|
9
|
+
function object(value,label){
|
|
10
|
+
if(value===null||typeof value!=='object'||Array.isArray(value))throw new TypeError(`${label} 必须是对象`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function keys(value,allowed,label){
|
|
14
|
+
for(const key of Object.keys(value))if(!allowed.includes(key))throw new TypeError(`${label} 包含无效字段;请检查字段名称`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function proxyUrl(value,label,{allowCredentials=false}={}){
|
|
18
|
+
let url;
|
|
19
|
+
try{url=new URL(value);}catch{throw new TypeError(`${label} 无效;请提供 http 或 https 代理地址`);}
|
|
20
|
+
if(typeof value!=='string'||!['http:','https:'].includes(url.protocol)||!url.hostname)throw new TypeError(`${label} 无效;请提供 http 或 https 代理地址`);
|
|
21
|
+
if(!allowCredentials&&(url.username||url.password))throw new TypeError(`${label} 无效;含凭据的代理地址请改用环境变量引用`);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function profileConfig(name,profile){
|
|
26
|
+
const label=`代理配置 ${name}`;
|
|
27
|
+
object(profile,label);
|
|
28
|
+
keys(profile,['httpUrl','httpEnv','httpsUrl','httpsEnv','noProxy','noProxyEnv'],label);
|
|
29
|
+
for(const [direct,reference] of [['httpUrl','httpEnv'],['httpsUrl','httpsEnv'],['noProxy','noProxyEnv']]){
|
|
30
|
+
if(profile[direct]!==undefined&&profile[reference]!==undefined)throw new TypeError(`${label} 的 ${direct} 和 ${reference} 不能同时设置;请只选一种来源`);
|
|
31
|
+
if(profile[reference]!==undefined&&(!envNamePattern.test(profile[reference])||typeof profile[reference]!=='string'))throw new TypeError(`${label} 的 ${reference} 无效;请使用环境变量名称`);
|
|
32
|
+
if(profile[direct]!==undefined){
|
|
33
|
+
if(typeof profile[direct]!=='string'||!profile[direct])throw new TypeError(`${label} 的 ${direct} 无效;请提供非空值`);
|
|
34
|
+
if(direct==='noProxy')continue;
|
|
35
|
+
proxyUrl(profile[direct],`${label} 的 ${direct}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if(profile.httpUrl===undefined&&profile.httpEnv===undefined&&profile.httpsUrl===undefined&&profile.httpsEnv===undefined)throw new TypeError(`${label} 缺少 HTTP 或 HTTPS 代理;请配置至少一个地址或环境变量引用`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ruleConfig(rule,label,profiles){
|
|
42
|
+
object(rule,label);
|
|
43
|
+
keys(rule,['mode','profile'],label);
|
|
44
|
+
if(!['inherit','direct','profile'].includes(rule.mode))throw new TypeError(`${label} 模式无效;请选择 inherit、direct 或 profile`);
|
|
45
|
+
if(rule.mode==='profile'){
|
|
46
|
+
if(typeof rule.profile!=='string'||!Object.hasOwn(profiles,rule.profile))throw new TypeError(`${label} 引用的代理配置不存在;请先创建配置`);
|
|
47
|
+
}else if(rule.profile!==undefined)throw new TypeError(`${label} 的配置引用无效;仅 profile 模式可引用配置`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateNetworkPolicy(network){
|
|
51
|
+
if(network===undefined)return {};
|
|
52
|
+
object(network,'network');
|
|
53
|
+
keys(network,['default','profiles','plugins'],'network');
|
|
54
|
+
const profiles=network.profiles??{};
|
|
55
|
+
object(profiles,'profiles');
|
|
56
|
+
for(const [name,profile] of Object.entries(profiles)){
|
|
57
|
+
if(!profileNamePattern.test(name))throw new TypeError('代理配置名称无效;请使用字母开头的标识');
|
|
58
|
+
profileConfig(name,profile);
|
|
59
|
+
}
|
|
60
|
+
if(network.default!==undefined)ruleConfig(network.default,'工作区默认规则',profiles);
|
|
61
|
+
const plugins=network.plugins??{};
|
|
62
|
+
object(plugins,'plugins');
|
|
63
|
+
for(const [pluginId,plugin] of Object.entries(plugins)){
|
|
64
|
+
if(!pluginIdPattern.test(pluginId))throw new TypeError('插件标识无效;请使用合法包名');
|
|
65
|
+
object(plugin,`插件 ${pluginId}`);
|
|
66
|
+
keys(plugin,['default','actions'],`插件 ${pluginId}`);
|
|
67
|
+
if(plugin.default!==undefined)ruleConfig(plugin.default,`插件 ${pluginId} 默认规则`,profiles);
|
|
68
|
+
const actions=plugin.actions??{};
|
|
69
|
+
object(actions,`插件 ${pluginId} 动作规则`);
|
|
70
|
+
for(const [actionId,rule] of Object.entries(actions)){
|
|
71
|
+
if(!actionIdPattern.test(actionId))throw new TypeError(`插件 ${pluginId} 的动作标识无效;请使用合法标识`);
|
|
72
|
+
ruleConfig(rule,`动作 ${pluginId}:${actionId}`,profiles);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return network;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function source(profile,profileName,field,env){
|
|
79
|
+
const label=`代理配置 ${profileName}`;
|
|
80
|
+
const direct=field==='noProxy'?'noProxy':`${field}Url`;
|
|
81
|
+
const reference=`${field}Env`;
|
|
82
|
+
if(profile[direct]!==undefined)return profile[direct];
|
|
83
|
+
if(profile[reference]===undefined)return undefined;
|
|
84
|
+
const value=env?.[profile[reference]];
|
|
85
|
+
if(typeof value!=='string'||(field!=='noProxy'&&!value))throw new TypeError(`${label} 缺少环境变量 ${profile[reference]};请在运行环境中设置它`);
|
|
86
|
+
if(field!=='noProxy')proxyUrl(value,`${label} 的环境变量 ${profile[reference]}`,{allowCredentials:true});
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function inheritedProxy(env){
|
|
91
|
+
const get=name=>env?.[name.toLowerCase()]??env?.[name];
|
|
92
|
+
const allProxy=get('ALL_PROXY');
|
|
93
|
+
const httpProxy=get('HTTP_PROXY')??allProxy??'';
|
|
94
|
+
const httpsProxy=get('HTTPS_PROXY')??get('HTTP_PROXY')??allProxy??'';
|
|
95
|
+
return {httpProxy,httpsProxy,noProxy:get('NO_PROXY')??''};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function selectRule(selected,pluginId,actionId){
|
|
99
|
+
const plugin=selected.plugins?.[pluginId];
|
|
100
|
+
if(actionId!==undefined&&plugin?.actions?.[actionId])return {rule:plugin.actions[actionId],source:'action'};
|
|
101
|
+
if(plugin?.default)return {rule:plugin.default,source:'plugin'};
|
|
102
|
+
if(selected.default)return {rule:selected.default,source:'workspace'};
|
|
103
|
+
return {rule:{mode:'inherit'},source:'system'};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function inspectNetworkPolicy(network,{plugins=[],actions=[]}={}){
|
|
107
|
+
const selected=validateNetworkPolicy(network);
|
|
108
|
+
const installed=new Set(plugins),available=new Set(actions);
|
|
109
|
+
const entry=(id,pluginId,actionId)=>{
|
|
110
|
+
const {rule,source}=selectRule(selected,pluginId,actionId);
|
|
111
|
+
return {id,mode:rule.mode,source,...(rule.profile?{profile:rule.profile}:{})};
|
|
112
|
+
};
|
|
113
|
+
const entries=[];
|
|
114
|
+
for(const pluginId of [...installed].sort()){
|
|
115
|
+
entries.push(entry(pluginId,pluginId));
|
|
116
|
+
for(const actionId of [...available].filter(id=>id.startsWith(`${pluginId}:`)).sort())entries.push(entry(actionId,pluginId,actionId.slice(pluginId.length+1)));
|
|
117
|
+
}
|
|
118
|
+
const stalePlugins=Object.keys(selected.plugins??{}).filter(id=>!installed.has(id)).sort();
|
|
119
|
+
const staleActions=[];
|
|
120
|
+
for(const [pluginId,rule] of Object.entries(selected.plugins??{}))if(installed.has(pluginId)){
|
|
121
|
+
for(const actionId of Object.keys(rule.actions??{}))if(!available.has(`${pluginId}:${actionId}`))staleActions.push(`${pluginId}:${actionId}`);
|
|
122
|
+
}
|
|
123
|
+
staleActions.sort();
|
|
124
|
+
return {entries,stalePlugins,staleActions};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveNetworkPolicy(network,{pluginId,actionId,env=process.env}={}){
|
|
128
|
+
const selected=validateNetworkPolicy(network);
|
|
129
|
+
const {rule}=selectRule(selected,pluginId,actionId);
|
|
130
|
+
const resolved=rule.mode==='profile'?{mode:'profile',profile:rule.profile}:{mode:rule.mode};
|
|
131
|
+
if(rule.mode==='profile'){
|
|
132
|
+
const profile=selected.profiles[rule.profile];
|
|
133
|
+
const httpProxy=source(profile,rule.profile,'http',env)??'';
|
|
134
|
+
privateProxy.set(resolved,{
|
|
135
|
+
httpProxy,
|
|
136
|
+
httpsProxy:source(profile,rule.profile,'https',env)??httpProxy,
|
|
137
|
+
noProxy:source(profile,rule.profile,'noProxy',env)??'',
|
|
138
|
+
});
|
|
139
|
+
}else if(rule.mode==='inherit')privateProxy.set(resolved,inheritedProxy(env));
|
|
140
|
+
else privateProxy.set(resolved,{});
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const proxyVariableNames=new Set(['http_proxy','https_proxy','all_proxy','no_proxy']);
|
|
145
|
+
|
|
146
|
+
export function buildProcessEnv(resolved,{baseEnv=process.env,overrides={}}={}){
|
|
147
|
+
if(!privateProxy.has(resolved))throw new TypeError('网络策略尚未解析;请先调用 resolveNetworkPolicy');
|
|
148
|
+
const result={...baseEnv,...overrides};
|
|
149
|
+
if(resolved.mode==='inherit')return result;
|
|
150
|
+
for(const name of Object.keys(result))if(proxyVariableNames.has(name.toLowerCase()))delete result[name];
|
|
151
|
+
if(resolved.mode==='direct')return result;
|
|
152
|
+
const {httpProxy,httpsProxy,noProxy}=privateProxy.get(resolved);
|
|
153
|
+
if(httpProxy)result.HTTP_PROXY=httpProxy;
|
|
154
|
+
if(httpsProxy)result.HTTPS_PROXY=httpsProxy;
|
|
155
|
+
result.NO_PROXY=noProxy;
|
|
156
|
+
result.NODE_USE_ENV_PROXY='1';
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createHttpClient(resolved,{signal,timeoutMs=30000}={}){
|
|
161
|
+
if(!privateProxy.has(resolved))throw new TypeError('网络策略尚未解析;请先调用 resolveNetworkPolicy');
|
|
162
|
+
const proxy=privateProxy.get(resolved);
|
|
163
|
+
let dispatcher;
|
|
164
|
+
const getDispatcher=()=>{
|
|
165
|
+
if(dispatcher)return dispatcher;
|
|
166
|
+
try{
|
|
167
|
+
dispatcher=resolved.mode==='direct'?new Agent():new EnvHttpProxyAgent({
|
|
168
|
+
httpProxy:proxy.httpProxy??'',
|
|
169
|
+
httpsProxy:proxy.httpsProxy??'',
|
|
170
|
+
noProxy:proxy.noProxy??'',
|
|
171
|
+
});
|
|
172
|
+
return dispatcher;
|
|
173
|
+
}catch{
|
|
174
|
+
const label=resolved.mode==='profile'?`配置 ${resolved.profile}`:'继承的代理环境';
|
|
175
|
+
throw new TypeError(`${label} 无效;请检查 HTTP_PROXY 和 HTTPS_PROXY`);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
let closed=false;
|
|
179
|
+
return {
|
|
180
|
+
async fetch(input,options={}){
|
|
181
|
+
if(closed)throw new TypeError('HTTP 客户端已关闭');
|
|
182
|
+
const {timeout,timeoutMs:requestTimeoutMs,signal:requestSignal,dispatcher:ignored,...init}=options;
|
|
183
|
+
const requestTimeout=timeout??requestTimeoutMs??timeoutMs;
|
|
184
|
+
if(!Number.isSafeInteger(requestTimeout)||requestTimeout<=0)throw new TypeError('HTTP 请求超时必须是正整数毫秒数');
|
|
185
|
+
const combined=AbortSignal.any([signal,requestSignal,AbortSignal.timeout(requestTimeout)].filter(Boolean));
|
|
186
|
+
try{return await undiciFetch(input,{...init,signal:combined,dispatcher:getDispatcher()});}
|
|
187
|
+
catch(error){
|
|
188
|
+
if(combined.aborted)throw error;
|
|
189
|
+
const label=resolved.mode==='profile'?`(配置 ${resolved.profile})`:'';
|
|
190
|
+
throw new TypeError(`HTTP 请求失败${label};请检查目标地址或代理连接`);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
async close(){
|
|
194
|
+
if(closed)return;
|
|
195
|
+
closed=true;
|
|
196
|
+
if(dispatcher)await dispatcher.destroy();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {access} from 'node:fs/promises';
|
|
3
|
+
import {runProcess} from './process.js';
|
|
4
|
+
export async function npm(args,options={}) {
|
|
5
|
+
const candidates=[path.join(path.dirname(process.execPath),'node_modules/npm/bin/npm-cli.js'),process.env.npm_execpath].filter(Boolean);
|
|
6
|
+
for(const cli of candidates) {
|
|
7
|
+
try {await access(cli);} catch {continue;}
|
|
8
|
+
return runProcess(process.execPath,[cli,...args],{timeout:180000,...options});
|
|
9
|
+
}
|
|
10
|
+
return runProcess('npm',args,{timeout:180000,...options});
|
|
11
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import treeKill from 'tree-kill';
|
|
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,outputMode='buffer',summaryBytes=65536}={}) {
|
|
11
|
+
if(!['buffer','stream'].includes(outputMode))throw new TypeError('outputMode 必须为 buffer 或 stream');
|
|
12
|
+
const streaming=outputMode==='stream';
|
|
13
|
+
if(streaming&&typeof onOutput!=='function')throw new TypeError('stream 模式必须提供 onOutput 消费输出');
|
|
14
|
+
if(!Number.isSafeInteger(summaryBytes)||summaryBytes<0||summaryBytes>1024*1024)throw new TypeError('summaryBytes 必须为 0 到 1048576 的整数');
|
|
15
|
+
if(typeof encoding!=='string')throw new TypeError(`不支持的程序输出编码:${String(encoding)}`);
|
|
16
|
+
const label=/^(utf16le|ucs2|ucs-2)$/i.test(encoding)?'utf-16le':encoding;
|
|
17
|
+
let decoderEncoding;
|
|
18
|
+
try {decoderEncoding=new TextDecoder(label).encoding;}
|
|
19
|
+
catch {throw new TypeError(`不支持的程序输出编码:${encoding}`);}
|
|
20
|
+
if(signal?.aborted) throw new FlowError('操作已取消',{code:'CANCELLED'});
|
|
21
|
+
let executable;
|
|
22
|
+
try {
|
|
23
|
+
// 不改变宿主 cwd;程序路径和 PATH 中相对目录都以子进程 cwd 为基准。
|
|
24
|
+
const directory=path.resolve(cwd??process.cwd()),windows=process.platform==='win32';
|
|
25
|
+
const hasPath=path.isAbsolute(file)||file.includes('/')||(windows&&file.includes('\\'));
|
|
26
|
+
const search=hasPath?[path.resolve(directory,file)]:[
|
|
27
|
+
...(windows?[directory]:[]),...(env?.PATH??(windows?env?.Path:undefined)??process.env.PATH??'').split(path.delimiter),
|
|
28
|
+
].map(entry=>path.resolve(directory,entry.replace(/^"(.*)"$/,'$1'),file));
|
|
29
|
+
for(const candidate of search){
|
|
30
|
+
executable=await which(candidate,{nothrow:true,pathExt:env?.PATHEXT??process.env.PATHEXT});
|
|
31
|
+
if(executable)break;
|
|
32
|
+
}
|
|
33
|
+
if(!executable)throw Object.assign(new Error(`not found: ${file}`),{code:'ENOENT'});
|
|
34
|
+
}
|
|
35
|
+
catch(cause){throw new FlowError(`未找到程序:${file}`,{code:'ENOENT',details:{executable:file},cause});}
|
|
36
|
+
// Execa 收集原始字节;文本只在这里按同一编码解码,避免返回值被默认 UTF-8 解码。
|
|
37
|
+
const child=execa(executable,args,{cwd,env,reject:false,windowsHide:true,maxBuffer,buffer:!streaming,timeout:0,shell:false,encoding:'buffer',stripFinalNewline:false});
|
|
38
|
+
let reason,killing,outputError,outputFailed=false,outputWrites=Promise.resolve();
|
|
39
|
+
let markStopped;const stopped=new Promise(resolve=>{markStopped=resolve;});
|
|
40
|
+
const outputBytes={stdout:0,stderr:0},tails={stdout:'',stderr:''};let outputTruncated=false;
|
|
41
|
+
const remember=(name,message)=>{
|
|
42
|
+
const bytes=Buffer.from(tails[name]+message);let start=Math.max(0,bytes.length-summaryBytes);
|
|
43
|
+
if(start){outputTruncated=true;while(start<bytes.length&&(bytes[start]&0xc0)===0x80)start++;}
|
|
44
|
+
tails[name]=bytes.subarray(start).toString('utf8');
|
|
45
|
+
};
|
|
46
|
+
const stop=kind=>{
|
|
47
|
+
if(reason) return; reason=kind;
|
|
48
|
+
markStopped();
|
|
49
|
+
killing=new Promise(resolve=>child.pid&&child.exitCode==null&&child.signalCode==null ? treeKill(child.pid,'SIGKILL',()=>resolve()) : resolve());
|
|
50
|
+
};
|
|
51
|
+
const cancel=()=>stop('cancelled');
|
|
52
|
+
signal?.addEventListener('abort',cancel,{once:true});
|
|
53
|
+
if(signal?.aborted)cancel();
|
|
54
|
+
const emit=(stream,message)=>{
|
|
55
|
+
if(!onOutput||!message)return;
|
|
56
|
+
// 立即挂接拒绝处理,不能让事件回调中的异步落盘变成未处理拒绝。
|
|
57
|
+
outputWrites=outputWrites.then(async()=>{if(!outputFailed)await onOutput({stream,message});}).catch(error=>{
|
|
58
|
+
if(!outputFailed){outputFailed=true;outputError=error;stop('output-callback');}
|
|
59
|
+
});
|
|
60
|
+
return outputWrites;
|
|
61
|
+
};
|
|
62
|
+
const flush=[],readers=[];
|
|
63
|
+
for(const [name,stream] of [['stdout',child.stdout],['stderr',child.stderr]]){
|
|
64
|
+
if(streaming){
|
|
65
|
+
// 每条流最多挂起一个块,两条流共用消费顺序;慢消费者让管道自然背压。
|
|
66
|
+
readers.push((async()=>{
|
|
67
|
+
const decoder=new TextDecoder(decoderEncoding);
|
|
68
|
+
try{
|
|
69
|
+
for await(const chunk of stream){
|
|
70
|
+
if(reason)break;outputBytes[name]+=chunk.byteLength;
|
|
71
|
+
const message=decoder.decode(chunk,{stream:true});remember(name,message);
|
|
72
|
+
await Promise.race([emit(name,message),stopped]);
|
|
73
|
+
}
|
|
74
|
+
if(!reason){const message=decoder.decode();remember(name,message);await Promise.race([emit(name,message),stopped]);}
|
|
75
|
+
}catch(error){if(!reason){outputError=error;stop('output-callback');}}
|
|
76
|
+
})());
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let bytes=0,ended=false;
|
|
80
|
+
const decoder=new TextDecoder(decoderEncoding);
|
|
81
|
+
const finish=()=>{if(!ended){ended=true;emit(name,decoder.decode());}};
|
|
82
|
+
flush.push(finish);stream?.once('end',finish);
|
|
83
|
+
stream?.on('data',chunk=>{
|
|
84
|
+
bytes+=Buffer.byteLength(chunk);
|
|
85
|
+
// 流量超限时立即回收整棵进程树,不继续扩张待落盘的日志队列。
|
|
86
|
+
if(bytes>maxBuffer){stop('output-limit');return;}
|
|
87
|
+
if(onOutput)emit(name,decoder.decode(chunk,{stream:true}));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const timer=timeout>0 ? setTimeout(()=>stop('timeout'),timeout) : null;
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result=await child;
|
|
94
|
+
} finally {
|
|
95
|
+
for(const finish of flush)finish();
|
|
96
|
+
await Promise.all(readers);
|
|
97
|
+
await (streaming?Promise.race([outputWrites,stopped]):outputWrites);
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
if(killing)await killing;
|
|
100
|
+
signal?.removeEventListener('abort',cancel);
|
|
101
|
+
}
|
|
102
|
+
if(reason==='output-callback')throw outputError;
|
|
103
|
+
if(reason==='output-limit')throw new FlowError('程序输出超过上限',{code:'OUTPUT_LIMIT'});
|
|
104
|
+
if(reason)throw new FlowError(reason==='cancelled'?'操作已取消':'程序运行超时',{code:reason==='cancelled'?'CANCELLED':'TIMEOUT'});
|
|
105
|
+
if(result.isMaxBuffer)throw new FlowError('程序输出超过上限',{code:'OUTPUT_LIMIT'});
|
|
106
|
+
if(result.exitCode===undefined&&result.failed)throw new FlowError(`无法启动程序 ${file}:${result.shortMessage??result.message}`,{code:result.code??'SPAWN_FAILED'});
|
|
107
|
+
const decode=bytes=>stripFinalNewline(new TextDecoder(decoderEncoding).decode(outputText(bytes)));
|
|
108
|
+
if(streaming)return {exitCode:result.exitCode,stdout:stripFinalNewline(tails.stdout),stderr:stripFinalNewline(tails.stderr),outputMode,outputTruncated,outputBytes};
|
|
109
|
+
return {exitCode:result.exitCode,stdout:decode(result.stdout),stderr:decode(result.stderr)};
|
|
110
|
+
}
|
|
@@ -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,29 @@
|
|
|
1
|
+
# 插件模块
|
|
2
|
+
|
|
3
|
+
包名:`@mytools/fst-plugin`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
|
|
4
|
+
|
|
5
|
+
## 职责与边界
|
|
6
|
+
|
|
7
|
+
插件装载、契约校验、动作宿主和服务生命周期。安装批次由 workspace 负责,开发检查由 devtools 负责。
|
|
8
|
+
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/fst-platform`。
|
|
10
|
+
## 公开入口
|
|
11
|
+
|
|
12
|
+
| 导入路径 | 实现位置 |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `@mytools/fst-plugin` | `./src/host.js` |
|
|
15
|
+
| `@mytools/fst-plugin/loader` | `./src/loader.js` |
|
|
16
|
+
| `@mytools/fst-plugin/contracts` | `./src/contracts.js` |
|
|
17
|
+
| `@mytools/fst-plugin/package.json` | `./package.json` |
|
|
18
|
+
|
|
19
|
+
仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
|
|
20
|
+
|
|
21
|
+
## 维护与验证
|
|
22
|
+
|
|
23
|
+
本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
|
|
24
|
+
|
|
25
|
+
在仓库根目录执行:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
node --test tests/modules.test.js tests/plugin-contract.test.js tests/runtime.test.js tests/plugin-host-context.test.js
|
|
29
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mytools/fst-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/host.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/host.js",
|
|
9
|
+
"./loader": "./src/loader.js",
|
|
10
|
+
"./contracts": "./src/contracts.js",
|
|
11
|
+
"./privileges": "./src/privileges.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24.15.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@mytools/sdk": "0.1.0",
|
|
25
|
+
"@mytools/fst-platform": "0.1.0",
|
|
26
|
+
"cordis": "3.18.1",
|
|
27
|
+
"semver": "7.7.4"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {FlowError} from '@mytools/sdk';
|
|
2
|
+
import {conditionOperators} from '@mytools/sdk/conditions';
|
|
3
|
+
import {resolveSchemaPath} from '@mytools/sdk/schema-path';
|
|
4
|
+
import {inside} from '@mytools/fst-platform/environments';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const builtinCommands=[
|
|
8
|
+
['workflows','选择已有工作流'],['new','新建工作流'],['add','添加插件能力或子工作流'],
|
|
9
|
+
['steps','设置步骤参数、排序或删除'],['outputs','设置最终输出'],['save','保存工作流'],
|
|
10
|
+
['run','运行工作流,可附带 @参数.json'],['runs','运行记录:选择任意一次执行,查看进度、结果和日志'],['logs','查看运行日志'],['plugin','管理插件'],['plugins','管理插件(兼容旧入口)'],['config','配置 Fishtools'],['tools','Web 与离线文档'],
|
|
11
|
+
['pack','选择工作流打包'],['web','启动 Web 工作台'],['docs','阅读离线开发与使用文档'],['help','查看操作说明'],['exit','退出终端工作台'],
|
|
12
|
+
].map(([name,description])=>({name,description}));
|
|
13
|
+
|
|
14
|
+
function validatePrivilegeDeclarations(plugin){
|
|
15
|
+
const validObject=value=>Boolean(value)&&typeof value==='object'&&!Array.isArray(value);
|
|
16
|
+
const invalid=(path,message)=>{throw new FlowError(message,{code:'VALIDATION',details:{path}});};
|
|
17
|
+
const entries=[['plugin',plugin,undefined],...Object.entries(plugin.actions??{}).map(([name,action])=>[`plugin.actions.${name}`,action,plugin])];
|
|
18
|
+
for(const [location,owner,parent] of entries){
|
|
19
|
+
if(owner.preflight!==undefined&&typeof owner.preflight!=='function')invalid(`${location}.preflight`,`${location} 的 preflight 必须为函数`);
|
|
20
|
+
if(owner.requirements===undefined)continue;
|
|
21
|
+
if(!validObject(owner.requirements))invalid(`${location}.requirements`,`${location} 的 requirements 必须为对象`);
|
|
22
|
+
const privilege=owner.requirements.privilege;
|
|
23
|
+
if(privilege===undefined)continue;
|
|
24
|
+
if(!validObject(privilege))invalid(`${location}.requirements.privilege`,`${location} 的 privilege 必须为对象`);
|
|
25
|
+
for(const [platform,requirement] of Object.entries(privilege)){
|
|
26
|
+
const at=`${location}.requirements.privilege.${platform}`;
|
|
27
|
+
if(!['linux','win32'].includes(platform))invalid(at,`不支持的权限声明平台:${platform}`);
|
|
28
|
+
if(!validObject(requirement))invalid(at,`${at} 必须为对象`);
|
|
29
|
+
const levels=platform==='linux'?['user','root','resource']:['user','administrator','resource'];
|
|
30
|
+
if(!levels.includes(requirement.level))invalid(`${at}.level`,`${at}.level 无效`);
|
|
31
|
+
for(const field of ['reason','remediation']){
|
|
32
|
+
if((requirement.level!=='user'||requirement[field]!==undefined)&&(typeof requirement[field]!=='string'||!requirement[field].trim()))invalid(`${at}.${field}`,`${at}.${field} 必须为非空字符串`);
|
|
33
|
+
}
|
|
34
|
+
if((requirement.level==='resource'||requirement.level==='administrator')&&typeof (owner.preflight??parent?.preflight)!=='function')invalid(at,`${at} 必须配套 preflight`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validateDeclarations(plugin){
|
|
40
|
+
for(const [name,action] of Object.entries(plugin.actions??{})){
|
|
41
|
+
if(!action||typeof action!=='object'||Array.isArray(action))throw new FlowError(`动作 ${name} 的声明必须为对象`,{code:'VALIDATION'});
|
|
42
|
+
if(action.conditions!==undefined&&(!action.conditions||typeof action.conditions!=='object'||Array.isArray(action.conditions)))throw new FlowError(`动作 ${name} 的 conditions 必须为对象`,{code:'VALIDATION'});
|
|
43
|
+
for(const [key,preset] of Object.entries(action.conditions??{})){
|
|
44
|
+
if(!preset||typeof preset.title!=='string'||!preset.title.trim()||typeof preset.path!=='string'||!preset.path||preset.path.split('.').some(p=>!p||['__proto__','constructor','prototype'].includes(p))||!conditionOperators.includes(preset.op)||(preset.op!=='exists'&&!Object.hasOwn(preset,'value')))throw new FlowError(`动作 ${name} 的判定预设 ${key} 无效`,{code:'VALIDATION'});
|
|
45
|
+
try{resolveSchemaPath(action.output,preset.path);}
|
|
46
|
+
catch{throw new FlowError(`判定预设 ${key} 引用了未声明的输出字段 ${preset.path}`,{code:'VALIDATION'});}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for(const key of ['platforms','architectures'])if(plugin[key]!==undefined&&(!Array.isArray(plugin[key])||!plugin[key].length||!plugin[key].every(x=>typeof x==='string'&&x.trim())||new Set(plugin[key]).size!==plugin[key].length))throw new FlowError(`${plugin.id} 的 ${key} 必须是非空且不重复的字符串数组`,{code:'VALIDATION'});
|
|
50
|
+
validatePrivilegeDeclarations(plugin);
|
|
51
|
+
if(plugin.runtime?.updatePolicy!==undefined&&!['automatic','maintenance'].includes(plugin.runtime.updatePolicy))throw new FlowError('运行时 updatePolicy 必须为 automatic 或 maintenance',{code:'VALIDATION',details:{path:'plugin.runtime.updatePolicy'}});
|
|
52
|
+
for(const [owner,location,declarations] of [['插件','plugin.environments',plugin.environments],...Object.entries(plugin.actions??{}).map(([name,action])=>[`动作 ${name}`,`plugin.actions.${name}.environments`,action.environments])]){
|
|
53
|
+
if(declarations!==undefined&&(!declarations||typeof declarations!=='object'||Array.isArray(declarations)))throw new FlowError(`${owner} 的 environments 必须为对象`,{code:'VALIDATION'});
|
|
54
|
+
for(const [id,e] of Object.entries(declarations??{})){
|
|
55
|
+
const at=`${location}.${id}`;
|
|
56
|
+
const invalid=(field,message)=>{throw new FlowError(message,{code:'VALIDATION',details:{path:`${at}.${field}`}});};
|
|
57
|
+
if(!/^[\w-]+$/.test(id)||!e||!['host','bundled','tool'].includes(e.source))throw new FlowError(`环境声明无效:${plugin.id}/${id}`);
|
|
58
|
+
const validString=value=>typeof value==='string'&&Boolean(value.trim());
|
|
59
|
+
if((e.source==='host'||e.executable!==undefined)&&!(e.source==='host'?(validString(e.executable)||Array.isArray(e.executable)&&e.executable.length>0&&e.executable.every(validString)&&new Set(e.executable).size===e.executable.length):validString(e.executable)))throw new FlowError(`环境 executable 声明无效:${plugin.id}/${id}`,{code:'VALIDATION'});
|
|
60
|
+
if(e.allowedSources&&(!Array.isArray(e.allowedSources)||!e.allowedSources.includes(e.source)||!e.allowedSources.every(x=>['host','bundled','tool'].includes(x))))throw new FlowError(`环境来源范围无效:${id}`);
|
|
61
|
+
if(e.source==='tool'&&id!=='node')throw new FlowError(`工具尚未提供环境:${id}`);
|
|
62
|
+
if(e.source==='bundled'||e.artifacts!==undefined){
|
|
63
|
+
const artifacts=e.artifacts;
|
|
64
|
+
if(!artifacts||typeof artifacts!=='object'||Array.isArray(artifacts)||e.source==='bundled'&&!Object.keys(artifacts).length)invalid('artifacts',`环境 ${plugin.id}/${id} 的 artifacts 必须为非空目标字典`);
|
|
65
|
+
for(const [target,artifact] of Object.entries(artifacts)){
|
|
66
|
+
if(!target.trim()||!artifact||typeof artifact!=='object'||Array.isArray(artifact))invalid(`artifacts.${target}`,`环境 ${plugin.id}/${id} 的目标资源无效:${target}`);
|
|
67
|
+
// 与运行时使用同一包内路径规则;此处只查声明,不读取目标平台文件。
|
|
68
|
+
let directory;
|
|
69
|
+
try{if(typeof artifact.directory!=='string'||!artifact.directory.trim())throw new Error();directory=inside(process.cwd(),artifact.directory);}
|
|
70
|
+
catch{invalid(`artifacts.${target}.directory`,`环境 ${plugin.id}/${id} 的资源目录必须是包内相对路径`);}
|
|
71
|
+
try{if(typeof artifact.executable!=='string'||!artifact.executable.trim()||path.normalize(artifact.executable)==='.')throw new Error();inside(directory,artifact.executable);}
|
|
72
|
+
catch{invalid(`artifacts.${target}.executable`,`环境 ${plugin.id}/${id} 的程序路径必须是包内相对路径`);}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateCommands(plugin,registered){
|
|
80
|
+
for(const [name,c] of Object.entries(plugin.commands??{})){
|
|
81
|
+
if(!/^[a-zA-Z][\w.-]*$/.test(name)||builtinCommands.some(x=>x.name===name)||registered.has(name))throw new FlowError(`命令名称无效或冲突:/${name}`);
|
|
82
|
+
if(!c||Boolean(c.action)===Boolean(c.run)||c.run&&typeof c.run!=='function')throw new FlowError(`命令 /${name} 必须声明 action 或 run`);
|
|
83
|
+
if(c.action&&!plugin.actions?.[c.action])throw new FlowError(`命令 /${name} 指向不存在的动作:${c.action}`);
|
|
84
|
+
registered.add(name);
|
|
85
|
+
}
|
|
86
|
+
}
|