@yanglaofish/fishtools 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -5
- package/bin/fst.js +1 -1
- package/manual/README.md +33 -5
- package/manual/docs/deployment.md +3 -3
- package/manual/docs/kernel-generation.md +97 -0
- package/manual/docs/modules.md +57 -0
- package/manual/docs/plugin-development.md +120 -10
- package/manual/docs/releases.md +12 -0
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
- package/manual/docs/roadmap.md +14 -3
- package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
- package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
- package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
- package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
- package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
- package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
- package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
- package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
- package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
- package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
- package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
- package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
- package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
- package/manual/docs/verification.md +42 -2
- package/manual/packages/core/README.md +44 -0
- package/manual/packages/core/package.json +38 -0
- package/manual/packages/core/src/application.js +242 -85
- package/manual/packages/core/src/bundle.js +2 -0
- package/manual/packages/core/src/development-watch-worker.js +69 -0
- package/manual/packages/core/src/development.js +146 -0
- package/manual/packages/core/src/environments.js +2 -0
- package/manual/packages/core/src/export-snapshot.js +42 -0
- package/manual/packages/core/src/plugin-update.js +108 -0
- package/manual/packages/core/src/process.js +2 -0
- package/manual/packages/core/src/resources.js +31 -0
- package/manual/packages/core/src/version.js +2 -0
- package/manual/packages/fst-cli/LICENSE +21 -0
- package/manual/packages/fst-cli/README.md +54 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/bin/fst.js +55 -23
- package/manual/packages/fst-cli/package.json +49 -0
- package/manual/packages/fst-cli/src/development.js +53 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-feed.js +31 -5
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
- package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
- package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
- package/manual/packages/fst-devtools/LICENSE +21 -0
- package/manual/packages/fst-devtools/README.md +28 -0
- package/manual/packages/fst-devtools/package.json +29 -0
- package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
- package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
- package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/plugin-tools.js +24 -5
- package/manual/packages/fst-logs/LICENSE +21 -0
- package/manual/packages/fst-logs/README.md +56 -0
- package/manual/packages/fst-logs/package.json +24 -0
- package/manual/packages/fst-logs/src/log-bounds.js +91 -0
- package/manual/packages/fst-logs/src/run-logs.js +239 -0
- package/manual/packages/fst-platform/LICENSE +21 -0
- package/manual/packages/fst-platform/README.md +67 -0
- package/manual/packages/fst-platform/package.json +33 -0
- package/manual/packages/fst-platform/src/bundle.js +17 -0
- package/manual/packages/fst-platform/src/environments.js +67 -0
- package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
- package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
- package/manual/packages/fst-platform/src/file-probe.js +6 -0
- package/manual/packages/fst-platform/src/json.js +5 -0
- package/manual/packages/fst-platform/src/network.js +199 -0
- package/manual/packages/fst-platform/src/npm.js +11 -0
- package/manual/packages/fst-platform/src/process.js +110 -0
- package/manual/packages/fst-plugin/LICENSE +21 -0
- package/manual/packages/fst-plugin/README.md +29 -0
- package/manual/packages/fst-plugin/package.json +29 -0
- package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/contracts.js +30 -3
- package/manual/packages/fst-plugin/src/host.js +232 -0
- package/manual/packages/fst-plugin/src/loader.js +21 -0
- package/manual/packages/fst-plugin/src/privileges.js +91 -0
- package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/runtime.js +20 -4
- package/manual/packages/fst-workflow/LICENSE +21 -0
- package/manual/packages/fst-workflow/README.md +28 -0
- package/manual/packages/fst-workflow/package.json +24 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
- package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
- package/manual/packages/fst-workspace/LICENSE +21 -0
- package/manual/packages/fst-workspace/README.md +32 -0
- package/manual/packages/fst-workspace/package.json +34 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
- package/manual/packages/fst-workspace/src/index.js +3 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugins.js +6 -36
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +40 -7
- package/manual/packages/pack/LICENSE +21 -0
- package/manual/packages/pack/README.md +31 -0
- package/manual/packages/pack/archive.js +15 -0
- package/manual/packages/pack/dependencies.js +105 -0
- package/manual/packages/pack/environments.js +50 -0
- package/manual/packages/pack/index.js +103 -0
- package/manual/packages/pack/package.json +29 -0
- package/manual/packages/pack/runtime.js +61 -0
- package/manual/packages/sdk/README.md +31 -0
- package/manual/packages/sdk/conditions.js +1 -0
- package/manual/packages/sdk/package.json +23 -1
- package/manual/plugins/web/README.md +29 -0
- package/manual/tests/application.test.js +6 -4
- package/node_modules/@mytools/core/README.md +44 -0
- package/node_modules/@mytools/core/package.json +17 -17
- package/node_modules/@mytools/core/src/application.js +242 -85
- package/node_modules/@mytools/core/src/bundle.js +2 -17
- package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
- package/node_modules/@mytools/core/src/development.js +146 -0
- package/node_modules/@mytools/core/src/environments.js +2 -67
- package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
- package/node_modules/@mytools/core/src/plugin-update.js +108 -0
- package/node_modules/@mytools/core/src/process.js +2 -80
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/LICENSE +21 -0
- package/node_modules/@mytools/fst-cli/README.md +54 -0
- package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
- package/node_modules/@mytools/fst-cli/package.json +49 -0
- package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
- package/node_modules/@mytools/fst-cli/src/development.js +53 -0
- package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
- package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
- package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
- package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
- package/{manual/packages/core → node_modules/@mytools/fst-cli}/src/terminal/run-feed.js +31 -5
- package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
- package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
- package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
- package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
- package/node_modules/@mytools/fst-devtools/README.md +28 -0
- package/node_modules/@mytools/fst-devtools/package.json +29 -0
- package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
- package/node_modules/@mytools/fst-logs/LICENSE +21 -0
- package/node_modules/@mytools/fst-logs/README.md +56 -0
- package/node_modules/@mytools/fst-logs/package.json +24 -0
- package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
- package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
- package/node_modules/@mytools/fst-logs/src/run-logs.js +239 -0
- package/node_modules/@mytools/fst-platform/LICENSE +21 -0
- package/node_modules/@mytools/fst-platform/README.md +67 -0
- package/node_modules/@mytools/fst-platform/package.json +33 -0
- package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
- package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
- package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
- package/node_modules/@mytools/fst-platform/src/json.js +5 -0
- package/node_modules/@mytools/fst-platform/src/network.js +199 -0
- package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
- package/node_modules/@mytools/fst-platform/src/process.js +110 -0
- package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
- package/node_modules/@mytools/fst-plugin/README.md +29 -0
- package/node_modules/@mytools/fst-plugin/package.json +29 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
- package/node_modules/@mytools/fst-plugin/src/host.js +232 -0
- package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
- package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
- package/{manual/packages/core → node_modules/@mytools/fst-plugin}/src/runtime.js +20 -4
- package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
- package/node_modules/@mytools/fst-workflow/README.md +28 -0
- package/node_modules/@mytools/fst-workflow/package.json +24 -0
- package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
- package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
- package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
- package/node_modules/@mytools/fst-workspace/README.md +32 -0
- package/node_modules/@mytools/fst-workspace/package.json +34 -0
- package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
- package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
- package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
- package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
- package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
- package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
- package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
- package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
- package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
- package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
- package/node_modules/@mytools/pack/README.md +31 -0
- package/node_modules/@mytools/pack/dependencies.js +7 -2
- package/node_modules/@mytools/pack/environments.js +1 -1
- package/node_modules/@mytools/pack/index.js +33 -25
- package/node_modules/@mytools/pack/package.json +29 -1
- package/node_modules/@mytools/sdk/README.md +31 -0
- package/node_modules/@mytools/sdk/conditions.js +1 -0
- package/node_modules/@mytools/sdk/diagnostics.js +27 -0
- package/node_modules/@mytools/sdk/package.json +23 -1
- package/node_modules/@mytools/sdk/schema-path.js +31 -0
- package/node_modules/@mytools/sdk/validation.js +78 -0
- package/node_modules/@mytools/web/README.md +29 -0
- package/node_modules/@mytools/web/package.json +7 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1398 -1199
- package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
- package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
- package/node_modules/@mytools/web/public/index.html +2 -2
- package/node_modules/@mytools/web/server.js +24 -17
- package/node_modules/@mytools/web/src/model.js +84 -0
- package/node_modules/acorn/CHANGELOG.md +1012 -0
- package/node_modules/acorn/LICENSE +21 -0
- package/node_modules/acorn/README.md +309 -0
- package/node_modules/acorn/bin/acorn +4 -0
- package/node_modules/acorn/dist/acorn.d.mts +897 -0
- package/node_modules/acorn/dist/acorn.d.ts +897 -0
- package/node_modules/acorn/dist/acorn.js +6370 -0
- package/node_modules/acorn/dist/acorn.mjs +6341 -0
- package/node_modules/acorn/dist/bin.js +90 -0
- package/node_modules/acorn/package.json +50 -0
- package/node_modules/{minipass-flush/node_modules/minipass → tar}/node_modules/yallist/README.md +8 -7
- package/node_modules/tar/node_modules/yallist/package.json +68 -0
- package/node_modules/yallist/README.md +7 -8
- package/node_modules/yallist/package.json +12 -51
- package/package.json +131 -115
- package/manual/packages/core/src/host.js +0 -139
- package/node_modules/@mytools/core/src/host.js +0 -139
- package/node_modules/@mytools/core/src/run-logs.js +0 -63
- package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
- package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
- package/node_modules/minipass-flush/node_modules/minipass/node_modules/yallist/package.json +0 -29
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugin-installations.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/schema-path.js +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/LICENSE.md +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/package.json +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/package.json +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/LICENSE +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/iterator.js +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/yallist.js +0 -0
|
@@ -2,9 +2,9 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { createApplication } from '
|
|
5
|
+
import { createApplication } from '@mytools/core';
|
|
6
6
|
import { temporary, testPlugin, object } from './helpers.js';
|
|
7
|
-
import {validate} from '
|
|
7
|
+
import {validate} from '@mytools/sdk/schema';
|
|
8
8
|
|
|
9
9
|
test('同 ID schema 隔离编译,保留内嵌引用且不串用旧约束',()=>{
|
|
10
10
|
const schema=type=>({$id:'https://fishtools.test/review/config',type:'object',definitions:{value:{type}},properties:{value:{$ref:'#/definitions/value'}},required:['value']});
|
|
@@ -47,7 +47,8 @@ test('运行快照独立于之后保存,状态与日志持久化',async t=>{
|
|
|
47
47
|
assert.deepEqual(app.getRun(run.id).output,{text:'HELLO'});
|
|
48
48
|
const events=(await readFile(path.join(app.outputDir,run.id,'events.jsonl'),'utf8')).trim().split('\n').map(JSON.parse);
|
|
49
49
|
assert.ok(events.some(e=>e.type==='run.succeeded'));
|
|
50
|
-
assert.
|
|
50
|
+
assert.deepEqual(events.find(e=>e.type==='run.result').output,{text:'HELLO'});
|
|
51
|
+
assert.deepEqual(events.find(e=>e.type==='step.succeeded').output,{text:'HELLO'});
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
test('仓库外本地插件可以安装、加载并执行,移除后目录同步',async t=>{
|
|
@@ -61,7 +62,8 @@ test('仓库外本地插件可以安装、加载并执行,移除后目录同
|
|
|
61
62
|
await app.saveWorkflow({id:'external',schemaVersion:1,inputs:object(),steps:[{id:'hello',action:'external-flow-test:hello',with:{}}],outputs:{answer:{$ref:'steps.hello.output.value'}}});
|
|
62
63
|
const run=await app.startRun('external',{});await run.done;
|
|
63
64
|
assert.deepEqual(app.getRun(run.id).output,{answer:42});
|
|
64
|
-
await app.removePlugin('external-flow-test');
|
|
65
|
+
await assert.rejects(app.removePlugin('external-flow-test'),error=>error.code==='PLUGIN_IN_USE'&&error.details.workflows[0].id==='external');
|
|
66
|
+
await app.removePlugin('external-flow-test',{force:true});
|
|
65
67
|
assert.ok(!app.catalog().some(a=>a.id==='external-flow-test:hello'));
|
|
66
68
|
});
|
|
67
69
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 应用内核模块
|
|
2
|
+
|
|
3
|
+
包名:`@mytools/core`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
|
|
4
|
+
|
|
5
|
+
## 职责与边界
|
|
6
|
+
|
|
7
|
+
组合各能力模块、协调运行与关闭、提供 CLI/Web 共用应用接口及导出声明快照。日志写入和缓存由 logs 负责;导出实现通过可选 pack 依赖加载。
|
|
8
|
+
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/fst-platform`、`@mytools/fst-logs`、`@mytools/fst-workflow`、`@mytools/fst-plugin`、`@mytools/fst-workspace`、`@mytools/pack`。
|
|
10
|
+
`@mytools/pack` 是可选 peer 依赖;普通发行包含它,只读发行省略它。既有 `/process`、`/bundle`、`/environments` 子入口保留兼容转发。
|
|
11
|
+
|
|
12
|
+
## 公开入口
|
|
13
|
+
|
|
14
|
+
| 导入路径 | 实现位置 |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `@mytools/core` | `./src/application.js` |
|
|
17
|
+
| `@mytools/core/version` | `./src/version.js` |
|
|
18
|
+
| `@mytools/core/process` | `./src/process.js` |
|
|
19
|
+
| `@mytools/core/environments` | `./src/environments.js` |
|
|
20
|
+
| `@mytools/core/bundle` | `./src/bundle.js` |
|
|
21
|
+
| `@mytools/core/development` | `./src/development.js` |
|
|
22
|
+
| `@mytools/core/package.json` | `./package.json` |
|
|
23
|
+
|
|
24
|
+
仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
|
|
25
|
+
|
|
26
|
+
## 维护与验证
|
|
27
|
+
|
|
28
|
+
本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
|
|
29
|
+
|
|
30
|
+
在仓库根目录执行:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
node --test tests/modules.test.js tests/application.test.js tests/lifecycle.test.js tests/module-interfaces.test.js
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 开发监督进程
|
|
37
|
+
|
|
38
|
+
`createDevelopmentSupervisor({projectDir,entry,args,env,configFile,onEvent})` 启动文件监听 worker 和应用子进程,返回 `status()`、幂等 `close()`。父进程不加载插件运行模块;每次重启使用全新的 Node 进程,完整更新 ESM 导入依赖。`bindDevelopmentChild(app,{details,closePresentation})` 通过 IPC 报告就绪,收到重启请求后等待 `app.developmentStatus()` 的运行和管理操作均为空闲,再调用 `app.close()` 及界面清理。
|
|
39
|
+
|
|
40
|
+
监听来源为项目 `.flow/plugins.json` 的 `links`,同时监听项目配置、插件状态和显式配置覆盖文件。独立 worker 跳过 `node_modules`、`.git`、`.flow`、`dist` 及符号链接;默认限制 512 个源码目录、20000 个扫描条目和 24 层深度。超出预算或扫描超时会明确报告未完整监听,独立进程扫描避免挂载目录的 I/O 阻塞监督器。只读项目拒绝启用开发模式。
|
|
41
|
+
|
|
42
|
+
源码变化默认合并 180 ms。关闭超时从应用报告空闲并开始关闭后计时,默认 5 秒;启动超时默认 15 秒。强制终止后最多等待 2 秒确认回收,未确认退出的子进程继续占用预算,不能启动重叠宿主。`onEvent` 提供 `ready/changed/waiting/restarting/failed/forced/blocked/watchError/closed` 等状态,启动失败保留源码监听,修复后可再次启动。
|
|
43
|
+
|
|
44
|
+
验证:`node --test tests/development-watch.test.js` 使用真实非交互子进程验证完整模块图、忙碌等待、连续保存、源码修复、关闭与配置刷新,并验证真实 Web 重启。
|
|
@@ -5,34 +5,34 @@
|
|
|
5
5
|
"main": "src/application.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/application.js",
|
|
8
|
+
"./version": "./src/version.js",
|
|
8
9
|
"./process": "./src/process.js",
|
|
10
|
+
"./environments": "./src/environments.js",
|
|
9
11
|
"./bundle": "./src/bundle.js",
|
|
10
|
-
"./
|
|
11
|
-
|
|
12
|
-
"bin": {
|
|
13
|
-
"fst": "bin/fst.js"
|
|
12
|
+
"./development": "./src/development.js",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"src",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
19
|
],
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=24.15.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@mariozechner/pi-tui": "0.50.0",
|
|
26
25
|
"@mytools/sdk": "0.1.0",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
"@mytools/fst-platform": "0.1.0",
|
|
27
|
+
"@mytools/fst-logs": "0.1.0",
|
|
28
|
+
"@mytools/fst-workflow": "0.1.0",
|
|
29
|
+
"@mytools/fst-plugin": "0.1.0",
|
|
30
|
+
"@mytools/fst-workspace": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@mytools/pack": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"@mytools/pack": {"optional": true}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -1,77 +1,93 @@
|
|
|
1
|
-
import { mkdir,
|
|
1
|
+
import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
6
|
+
import { createHost } from '@mytools/fst-plugin';
|
|
7
|
+
import { WorkflowStore } from '@mytools/fst-workspace/workflows';
|
|
8
|
+
import { runWorkflow } from '@mytools/fst-workflow';
|
|
9
|
+
import { runProcess } from '@mytools/fst-platform';
|
|
10
|
+
import {createFileProbe} from '@mytools/fst-platform/fs';
|
|
11
|
+
import {readJson} from '@mytools/fst-platform/json';
|
|
12
|
+
import {loadPlugin} from '@mytools/fst-plugin/loader';
|
|
13
|
+
import {installGeneration} from '@mytools/fst-workspace/plugins';
|
|
10
14
|
import { FlowError } from '@mytools/sdk';
|
|
11
|
-
import {prepareBundleWorkspace} from '
|
|
12
|
-
import {validate} from '
|
|
15
|
+
import {prepareBundleWorkspace} from '@mytools/fst-workspace/bundles';
|
|
16
|
+
import {validate} from '@mytools/sdk/schema';
|
|
13
17
|
import {createResources} from './resources.js';
|
|
14
|
-
import {errorRecord} from '
|
|
15
|
-
import {
|
|
16
|
-
import {createRunLogs,validateKeepRuns} from '
|
|
17
|
-
import {openPluginState,withPluginMutation,assertPluginState,savePluginState,leaseInstallation} from '
|
|
18
|
+
import {errorRecord} from '@mytools/sdk/diagnostics';
|
|
19
|
+
import {createExportSnapshot} from './export-snapshot.js';
|
|
20
|
+
import {createRunLogs,validateKeepRuns} from '@mytools/fst-logs';
|
|
21
|
+
import {openPluginState,withPluginMutation,assertPluginState,savePluginState,leaseInstallation} from '@mytools/fst-workspace/installations';
|
|
22
|
+
import {planPluginUpdate,changedPluginSources} from './plugin-update.js';
|
|
23
|
+
import {validateNetworkPolicy,inspectNetworkPolicy} from '@mytools/fst-platform/network';
|
|
24
|
+
import {resolvePrivilegeRequirement,checkStaticPrivilege} from '@mytools/fst-plugin/privileges';
|
|
18
25
|
|
|
19
|
-
export async function createApplication({projectDir=process.cwd(),workspace,outputDir,plugins,config={},maintenance=false,omitInstalledPlugin}={}) {
|
|
26
|
+
export async function createApplication({projectDir=process.cwd(),workspace,outputDir,plugins,config={},maintenance=false,omitInstalledPlugin,allowUnknownPrivileges=false}={}) {
|
|
20
27
|
projectDir=await prepareBundleWorkspace(path.resolve(projectDir),workspace);
|
|
21
28
|
const project=await readJson(path.join(projectDir,'flow.project.json'),{plugins:[]});
|
|
22
29
|
const output=path.resolve(outputDir??process.env.FLOW_OUTPUT_DIR??(project.readOnly?path.join(process.cwd(),'flow-results'):path.join(projectDir,'.flow','runs')));
|
|
23
30
|
const store=new WorkflowStore(path.join(projectDir,'workflows'));
|
|
24
|
-
const emitter=new EventEmitter(), runs=new Map();
|
|
31
|
+
const emitter=new EventEmitter(), runs=new Map(),journals=new WeakMap();
|
|
25
32
|
const resources=createResources(),operations=new Set();let closing,launching=0;
|
|
26
|
-
const runLogs=createRunLogs(output,{projectDir,allowLegacy:output===path.join(projectDir,'.flow','runs')}),writingRuns=new Set();
|
|
33
|
+
const runLogs=createRunLogs(output,{...project.logs,projectDir,allowLegacy:output===path.join(projectDir,'.flow','runs')}),writingRuns=new Set();
|
|
34
|
+
const maxCachedRuns=project.logs?.maxCachedRuns??50;
|
|
35
|
+
if(!Number.isSafeInteger(maxCachedRuns)||maxCachedRuns<1||maxCachedRuns>1000)throw new FlowError('maxCachedRuns 必须为 1 到 1000 的整数');
|
|
36
|
+
const fileProbe=createFileProbe(project.fileAccess);
|
|
27
37
|
validateKeepRuns(project.logs?.keepRuns??null);
|
|
28
38
|
const opened=await openPluginState(projectDir,{readOnly:project.readOnly});
|
|
29
|
-
let host,state=opened.state,
|
|
39
|
+
let host,state=opened.state,busy=false;
|
|
40
|
+
const installationLeases=new Map();
|
|
41
|
+
if(state.root)installationLeases.set(path.resolve(state.root),opened.release);
|
|
30
42
|
const app={
|
|
31
43
|
projectDir,project,outputDir:output,store,
|
|
32
44
|
get host(){return host;},
|
|
33
45
|
get maintenance(){return maintenance;},
|
|
46
|
+
developmentStatus:()=>({running:launching+writingRuns.size+operations.size,busy,closing:!!closing}),
|
|
34
47
|
catalog:()=>host.catalog(),
|
|
35
|
-
listPlugins:()=>[...host.definitions.values()].map(p=>({id:p.id,version:p.version,title:p.title,description:p.description,platforms:p.platforms,architectures:p.architectures,environments:p.environments,config:p.config,runtime:host.runtimeStatus().find(r=>r.id===p.id),removable:!!(state.dependencies?.[p.id]||state.links?.[p.id]),development:!!state.links?.[p.id],warnings:host.warnings.filter(item=>item.plugin===p.id).map(item=>item.message)})),
|
|
48
|
+
listPlugins:()=>[...host.definitions.values()].map(p=>({id:p.id,version:p.version,title:p.title,description:p.description,platforms:p.platforms,architectures:p.architectures,environments:p.environments,config:p.config,requirements:p.requirements,privileges:pluginPrivileges(p),runtime:{...host.runtimeStatus().find(r=>r.id===p.id),...(p.runtime?{updatePolicy:p.runtime.updatePolicy??'maintenance'}:{})},removable:!!(state.dependencies?.[p.id]||state.links?.[p.id]),development:!!state.links?.[p.id],warnings:host.warnings.filter(item=>item.plugin===p.id).map(item=>item.message)})),
|
|
36
49
|
listCommands:()=>host.commands(),
|
|
37
50
|
listWorkflows:()=>store.list(),
|
|
38
|
-
async saveWorkflow(w){
|
|
39
|
-
async
|
|
40
|
-
|
|
51
|
+
async saveWorkflow(w){return mutateWorkflow(()=>store.save(w));},
|
|
52
|
+
async duplicateWorkflow(definition,options){return mutateWorkflow(()=>store.duplicate(definition,options));},
|
|
53
|
+
async removeWorkflow(id){return mutateWorkflow(()=>store.remove(id));},
|
|
54
|
+
async checkWorkflow(id){
|
|
55
|
+
host.assertHealthy();
|
|
56
|
+
const closure=await store.resolve([id],host);
|
|
57
|
+
return {...closure,privileges:workflowPrivileges(closure,id)};
|
|
58
|
+
},
|
|
41
59
|
subscribe(fn){emitter.on('event',fn);return ()=>emitter.off('event',fn);},
|
|
42
|
-
async startRun(id,input={}) {
|
|
60
|
+
async startRun(id,input={},options={}) {
|
|
43
61
|
if(busy) throw new FlowError('插件更新中,请稍后运行');
|
|
44
|
-
const closure=await
|
|
62
|
+
const closure=await app.checkWorkflow(id);
|
|
63
|
+
const blocked=closure.privileges.filter(item=>item.mandatory&&item.static.status==='blocked');
|
|
45
64
|
const snapshotStore={resolve:async()=>structuredClone(closure),childId:ref=>store.childId(ref)};
|
|
46
|
-
|
|
65
|
+
const networkPolicy=structuredClone(project.network??{});
|
|
66
|
+
return app.executeTracked(id,(activeHost,context,event)=>{
|
|
67
|
+
if(blocked.length)throw new FlowError(`工作流缺少运行权限:${blocked.map(item=>item.action).join('、')}`,{code:'INSUFFICIENT_PRIVILEGE',details:{actions:blocked}});
|
|
68
|
+
return runWorkflow({store:snapshotStore,host:activeHost,id,input,signal:context.signal,onEvent:event,runId:context.runId,context:{...context,allowUnknownPrivileges:!!options.allowUnknownPrivileges,networkPolicy}});
|
|
69
|
+
});
|
|
47
70
|
},
|
|
48
71
|
async startCommand(name,input={},context={}){
|
|
49
72
|
if(busy)throw new FlowError('插件更新中,请稍后调用');
|
|
50
|
-
|
|
73
|
+
const networkPolicy=structuredClone(project.network??{});
|
|
74
|
+
return app.executeTracked(`/${name}`,(activeHost,runContext)=>activeHost.invokeCommand(name,input,{...context,...runContext,networkPolicy}));
|
|
51
75
|
},
|
|
52
76
|
async executeTracked(id,execute){
|
|
53
|
-
const runId=randomUUID(),
|
|
54
|
-
|
|
55
|
-
const
|
|
77
|
+
const runId=randomUUID(),controller=new AbortController();
|
|
78
|
+
const run={id:runId,workflowId:id,logOwner:runLogs.owner,status:'running',startedAt:new Date().toISOString(),controller};
|
|
79
|
+
const journal=await runLogs.start({id:runId,workflowId:id,status:run.status,startedAt:run.startedAt},{onEvent:event=>emitter.emit('event',event)});
|
|
80
|
+
journals.set(run,journal);
|
|
81
|
+
// 兼容运行句柄的事件读取;外部修改不影响日志模块的缓存。
|
|
82
|
+
Object.defineProperty(run,'events',{get:()=>journal.snapshot().events});
|
|
56
83
|
runs.set(runId,run);
|
|
57
84
|
writingRuns.add(runId);
|
|
58
|
-
const
|
|
59
|
-
let writes=writeFile(markdownFile,markdownHeader(run));
|
|
60
|
-
const event=e=>{
|
|
61
|
-
const full={time:new Date().toISOString(),runId,...e};run.events.push(full);
|
|
62
|
-
emitter.emit('event',full);
|
|
63
|
-
const {output:ignored,...record}=full;
|
|
64
|
-
writes=writes.then(async()=>{
|
|
65
|
-
await appendFile(path.join(dir,'events.jsonl'),JSON.stringify(record)+'\n');
|
|
66
|
-
await appendFile(markdownFile,markdownEvent(full));
|
|
67
|
-
});
|
|
68
|
-
return writes;
|
|
69
|
-
};
|
|
85
|
+
const event=e=>journal.append(e);
|
|
70
86
|
const activeHost=host;
|
|
71
87
|
run.done=(async()=>{
|
|
72
88
|
try {
|
|
73
89
|
await event({type:'run.started'});
|
|
74
|
-
run.output=await execute(activeHost,{outputDir:
|
|
90
|
+
run.output=await execute(activeHost,{outputDir:journal.directory,signal:controller.signal,runId,log:(message,data,meta={})=>event({...meta,type:'log',message,data})},event);
|
|
75
91
|
run.status='succeeded';await event({type:'run.succeeded'});
|
|
76
92
|
} catch(error) {
|
|
77
93
|
if(error?.code==='WORKFLOW_STOPPED'){
|
|
@@ -84,23 +100,48 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
84
100
|
}
|
|
85
101
|
} finally {
|
|
86
102
|
run.finishedAt=new Date().toISOString();
|
|
87
|
-
await
|
|
88
|
-
await appendFile(markdownFile,markdownResult(app.getRun(runId)));
|
|
89
|
-
await writeFile(path.join(dir,'result.json'),JSON.stringify(snapshotRun(run),null,2));
|
|
90
|
-
writingRuns.delete(runId);
|
|
103
|
+
try{await journal.finish(runState(run));}finally{writingRuns.delete(runId);}
|
|
91
104
|
try{await pruneLogs();}catch(error){
|
|
92
|
-
|
|
93
|
-
await
|
|
105
|
+
run.warnings=[{code:'LOG_PRUNE_FAILED',message:`日志自动清理失败:${error.message}`}];
|
|
106
|
+
await journal.save(runState(run));
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
|
-
|
|
109
|
+
const result=snapshotRun(run);trimRuns();return result;
|
|
97
110
|
})();
|
|
111
|
+
// Web 等调用方只保留运行 ID;立即观察拒绝,但不替换供 CLI/TUI 等待的原 Promise。
|
|
112
|
+
run.done.catch(error=>{
|
|
113
|
+
run.status='failed';
|
|
114
|
+
run.error=errorRecord(new FlowError(`运行日志保存失败:${error.message}`,{code:'LOG_WRITE_FAILED',details:errorRecord(error),cause:run.error}));
|
|
115
|
+
trimRuns();
|
|
116
|
+
});
|
|
98
117
|
return run;
|
|
99
118
|
},
|
|
100
119
|
getRun(id){const r=runs.get(id);return r?snapshotRun(r):null;},
|
|
101
|
-
async readRun(id){if(runs.has(id))return app.getRun(id);
|
|
120
|
+
async readRun(id){if(runs.has(id))return app.getRun(id);return runLogs.read(id);},
|
|
121
|
+
readLogPage:(id,options)=>runLogs.page(id,options),
|
|
102
122
|
listRuns:()=>[...runs.keys()].map(id=>{const {events,...r}=app.getRun(id);return r;}),
|
|
103
123
|
logSettings:()=>({keepRuns:project.logs?.keepRuns??null}),
|
|
124
|
+
networkPolicy:()=>structuredClone(project.network??{}),
|
|
125
|
+
networkReport:()=>inspectNetworkPolicy(project.network??{},{plugins:[...host.definitions.keys()],actions:[...host.actions.keys()]}),
|
|
126
|
+
async configureNetworkPolicy(next){
|
|
127
|
+
validateNetworkPolicy(next);
|
|
128
|
+
if(project.readOnly)throw new FlowError('只读运行包不能修改代理设置');
|
|
129
|
+
if(busy)throw new FlowError('项目正在更新,请稍后修改代理设置');
|
|
130
|
+
busy=true;let backup;
|
|
131
|
+
try{
|
|
132
|
+
await withPluginMutation(projectDir,async()=>{
|
|
133
|
+
const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
|
|
134
|
+
if(latest.readOnly)throw new FlowError('只读运行包不能修改代理设置');
|
|
135
|
+
if(!isDeepStrictEqual(latest.network??{},project.network??{}))throw new FlowError('代理设置已被另一实例修改,请重新读取后再保存',{code:'NETWORK_CONFLICT'});
|
|
136
|
+
backup=await backupProjectFile(file,latest,'network');
|
|
137
|
+
const nextProject={...latest,network:structuredClone(next)},temp=`${file}.${randomUUID()}.tmp`;
|
|
138
|
+
await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);
|
|
139
|
+
for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
|
|
140
|
+
});
|
|
141
|
+
}finally{busy=false;}
|
|
142
|
+
return {policy:app.networkPolicy(),backup};
|
|
143
|
+
},
|
|
144
|
+
logUsage:()=>runLogs.usage(),
|
|
104
145
|
async listRunHistory(){
|
|
105
146
|
const records=new Map((await runLogs.list()).map(r=>[r.id,r]));
|
|
106
147
|
for(const id of runs.keys())records.set(id,app.getRun(id));
|
|
@@ -108,52 +149,65 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
108
149
|
},
|
|
109
150
|
async readLog(id){
|
|
110
151
|
const run=runs.get(id);
|
|
111
|
-
if(run&&writingRuns.has(id))return
|
|
152
|
+
if(run&&writingRuns.has(id))return journals.get(run).markdown(runState(run));
|
|
112
153
|
return runLogs.markdown(id);
|
|
113
154
|
},
|
|
114
155
|
async configureLogs(keepRuns){
|
|
115
156
|
validateKeepRuns(keepRuns);if(project.readOnly)throw new FlowError('只读运行包不能保存日志设置');
|
|
116
157
|
if(busy)throw new FlowError('项目正在更新,请稍后修改日志设置');
|
|
117
|
-
busy=true;
|
|
158
|
+
busy=true;let backup,warning;
|
|
118
159
|
try{
|
|
119
160
|
await withPluginMutation(projectDir,async()=>{
|
|
120
161
|
const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
|
|
121
162
|
if(latest.readOnly)throw new FlowError('只读运行包不能保存日志设置');
|
|
163
|
+
backup=await backupProjectFile(file,latest,'logs');
|
|
122
164
|
const nextProject={...latest,logs:{...latest.logs,keepRuns}},temp=`${file}.${randomUUID()}.tmp`;
|
|
123
165
|
await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);
|
|
124
166
|
for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
|
|
125
|
-
await pruneLogs();
|
|
167
|
+
try{await pruneLogs();}
|
|
168
|
+
catch(error){warning={code:'LOG_PRUNE_FAILED',message:`日志保留设置已保存,但旧日志清理失败:${error.message}`};emitter.emit('event',{type:'logs.prune.warning',...warning});}
|
|
126
169
|
});
|
|
127
170
|
}finally{busy=false;}
|
|
171
|
+
return {keepRuns,backup,...(warning?{warning}:{})};
|
|
128
172
|
},
|
|
129
173
|
cancelRun(id){const r=runs.get(id);if(r?.status==='running')r.controller.abort();return !!r;},
|
|
130
|
-
async installPlugin(source){return change({add:source});},
|
|
131
|
-
async linkPlugin(source){return change({link:path.resolve(projectDir,source)});},
|
|
132
|
-
async removePlugin(name
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
174
|
+
async installPlugin(source,options){return change({add:source},options);},
|
|
175
|
+
async linkPlugin(source,options){return change({link:path.resolve(projectDir,source)},options);},
|
|
176
|
+
async removePlugin(name,options={}){
|
|
177
|
+
return change({remove:name},options);
|
|
178
|
+
},
|
|
179
|
+
exportSnapshot:options=>createExportSnapshot({store,host,project},options),
|
|
180
|
+
async pack(options){if(project.readOnly)throw new FlowError('只读运行包不能重新打包');const {packApplication}=await import('@mytools/pack');return packApplication(await app.exportSnapshot(options),options);},
|
|
181
|
+
async packPlan(options){const {planApplication}=await import('@mytools/pack');return planApplication(await app.exportSnapshot(options),options);},
|
|
182
|
+
async configurePlugin(id,value,options={}){
|
|
136
183
|
if(project.readOnly)throw new FlowError('只读项目不能保存配置');
|
|
137
|
-
assertMutable();
|
|
138
|
-
if(resources.active(id))throw new FlowError('请先停止此插件提供的 Web,再修改配置');
|
|
139
184
|
if(busy||launching||app.listRuns().some(r=>r.status==='running'))throw new FlowError('请等待运行或安装结束');
|
|
140
185
|
if(!host.definitions.has(id))throw new FlowError(`插件不存在:${id}`);
|
|
141
186
|
value=validate(host.definitions.get(id).config,value,`${id} 配置`);
|
|
142
|
-
busy=true;let
|
|
187
|
+
busy=true;let backup;
|
|
143
188
|
try{
|
|
144
189
|
await withPluginMutation(projectDir,async()=>{
|
|
145
190
|
await assertPluginState(projectDir,state);
|
|
146
191
|
const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
|
|
147
192
|
if(latest.readOnly)throw new FlowError('只读项目不能保存配置');
|
|
148
193
|
const nextConfig={...latest.config,[id]:value};
|
|
149
|
-
|
|
194
|
+
const oldConfig={...project.config,...config},effectiveNext={...nextConfig,...config};
|
|
195
|
+
const entries=[...host.definitions.values()].map(p=>({plugin:p,root:p.root}));
|
|
196
|
+
const plan=planPluginUpdate(host.definitions,host.definitions,oldConfig,effectiveNext,new Set([id]));
|
|
197
|
+
assertMaintenance(plan,options);
|
|
198
|
+
backup=await backupProjectFile(file,latest,'config');
|
|
199
|
+
if(plan.affected.length)await host.replace({plugins:entries,config:effectiveNext,affected:plan.affected});
|
|
150
200
|
const nextProject={...latest,config:nextConfig},temp=`${file}.${randomUUID()}.tmp`;
|
|
151
|
-
await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);
|
|
152
|
-
|
|
201
|
+
try{await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);}
|
|
202
|
+
catch(error){
|
|
203
|
+
if(plan.affected.length)try{await host.replace({plugins:entries,config:oldConfig,affected:plan.affected});}
|
|
204
|
+
catch(recovery){host.invalidate();throw new AggregateError([error,recovery],`配置保存失败且插件恢复失败:${error.message};${recovery.message}`);}
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
153
207
|
for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
|
|
154
|
-
await old.close();
|
|
155
208
|
});
|
|
156
|
-
}finally{
|
|
209
|
+
}finally{busy=false;}
|
|
210
|
+
return {backup};
|
|
157
211
|
},
|
|
158
212
|
async setMaintenance(enabled){
|
|
159
213
|
if(typeof enabled!=='boolean')throw new FlowError('维护模式必须为布尔值');
|
|
@@ -164,7 +218,7 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
164
218
|
try{
|
|
165
219
|
const entries=[...host.definitions.values()].map(p=>({plugin:p,root:p.root}));
|
|
166
220
|
if(enabled){maintenance=true;await host.close();}
|
|
167
|
-
const next=await createHost({plugins:entries,config:{...project.config,...config},services,environmentOverrides:project.environments,runtimeEnabled:!enabled});
|
|
221
|
+
const next=await createHost({plugins:entries,config:{...project.config,...config},services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:!enabled,allowUnknownPrivileges});
|
|
168
222
|
const old=host;host=next;maintenance=enabled;if(!enabled)await old.close();
|
|
169
223
|
}finally{busy=false;}
|
|
170
224
|
},
|
|
@@ -182,22 +236,24 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
182
236
|
const errors=[];
|
|
183
237
|
try{await resources.close();}catch(error){errors.push(error);}
|
|
184
238
|
try{await host.close();}catch(error){errors.push(error);}
|
|
185
|
-
try{await
|
|
239
|
+
try{const report=await fileProbe.close();if(report.incomplete)errors.push(new FlowError('文件探测进程尚未全部回收',{code:'FS_CLEANUP_INCOMPLETE',details:report}));}catch(error){errors.push(error);}
|
|
240
|
+
for(const release of installationLeases.values())try{await release();}catch(error){errors.push(error);}
|
|
186
241
|
if(errors.length)throw new AggregateError(errors,`应用关闭失败:${errors.map(e=>e.message).join(';')}`);
|
|
187
242
|
});return closing;
|
|
188
243
|
},
|
|
189
244
|
};
|
|
190
245
|
let initial;
|
|
191
|
-
const services={application:app,runProcess};
|
|
192
|
-
const
|
|
246
|
+
const services={application:app,runProcess,fs:fileProbe};
|
|
247
|
+
const hostProject=()=>({directory:projectDir,readOnly:!!project.readOnly});
|
|
248
|
+
const build=extra=>createHost({plugins:[...initial,...extra],config:{...project.config,...config},services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:!maintenance,allowUnknownPrivileges});
|
|
193
249
|
try{
|
|
194
250
|
initial=plugins??await Promise.all((project.plugins??[]).map(spec=>loadPlugin(spec,project.baseBundle?.root??projectDir)));
|
|
195
251
|
const external=[];
|
|
196
252
|
for(const name of Object.keys(state.dependencies??{}))if(!maintenance||name!==omitInstalledPlugin)external.push(await loadPlugin(name,state.root));
|
|
197
253
|
for(const [name,source] of Object.entries(state.links??{}))if(!maintenance||name!==omitInstalledPlugin)external.push(await loadPlugin(source,projectDir));
|
|
198
254
|
host=await build(external);
|
|
199
|
-
}catch(error){await
|
|
200
|
-
for(const name of ['startRun','startCommand','startWeb','saveWorkflow','removeWorkflow','installPlugin','linkPlugin','removePlugin','configurePlugin','configureLogs','setMaintenance','pack']){
|
|
255
|
+
}catch(error){await fileProbe.close();await opened.release();throw error;}
|
|
256
|
+
for(const name of ['startRun','startCommand','startWeb','saveWorkflow','duplicateWorkflow','removeWorkflow','installPlugin','linkPlugin','removePlugin','configurePlugin','configureNetworkPolicy','configureLogs','setMaintenance','pack']){
|
|
201
257
|
const original=app[name],isRun=['startRun','startCommand'].includes(name);
|
|
202
258
|
app[name]=async(...args)=>{
|
|
203
259
|
if(closing)throw new FlowError('应用已关闭');
|
|
@@ -206,30 +262,131 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
206
262
|
try{return await task;}finally{operations.delete(task);if(isRun)launching--;}
|
|
207
263
|
};
|
|
208
264
|
}
|
|
209
|
-
function
|
|
210
|
-
|
|
265
|
+
async function mutateWorkflow(operation){
|
|
266
|
+
if(project.readOnly)throw new FlowError('只读运行包不能修改工作流');
|
|
267
|
+
if(busy)throw new FlowError('插件变更期间暂不能修改工作流',{code:'PLUGIN_BUSY'});
|
|
268
|
+
return withPluginMutation(projectDir,async()=>{
|
|
269
|
+
await assertPluginState(projectDir,state);
|
|
270
|
+
return operation();
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
async function backupProjectFile(file,latest,label){
|
|
274
|
+
const directory=path.join(projectDir,'.flow','backups');
|
|
275
|
+
await mkdir(directory,{recursive:true,mode:0o700});
|
|
276
|
+
const backup=path.join(directory,`flow.project.${label}-${randomUUID()}.json`);
|
|
277
|
+
let original;
|
|
278
|
+
try{original=await readFile(file,'utf8');}
|
|
279
|
+
catch(error){if(error.code!=='ENOENT')throw error;original=JSON.stringify(latest,null,2);}
|
|
280
|
+
await writeFile(backup,original,{flag:'wx',mode:0o600});
|
|
281
|
+
return backup;
|
|
282
|
+
}
|
|
283
|
+
function pluginPrivileges(plugin){
|
|
284
|
+
const report=action=>{
|
|
285
|
+
const requirement=resolvePrivilegeRequirement(plugin,action);
|
|
286
|
+
return {action:action===undefined?null:`${plugin.id}:${action}`,requirement,static:checkStaticPrivilege(requirement)};
|
|
287
|
+
};
|
|
288
|
+
return [report(),...Object.keys(plugin.actions??{}).map(report)];
|
|
289
|
+
}
|
|
290
|
+
function assertMaintenance(plan,options={}){
|
|
291
|
+
if(!maintenance&&plan.maintenance.length&&!options.allowMaintenance)throw new FlowError(`更新将重启插件 ${plan.maintenance.join('、')},请确认本次定向维护`,{code:'MAINTENANCE_REQUIRED',details:{plugins:plan.maintenance,affected:plan.affected}});
|
|
292
|
+
const active=plan.affected.filter(id=>resources.active(id));
|
|
293
|
+
if(active.length)throw new FlowError(`请先停止受影响插件提供的 Web:${active.join('、')}`,{code:'WEB_ACTIVE',details:{plugins:active}});
|
|
294
|
+
}
|
|
295
|
+
function workflowPrivileges(closure,entryId){
|
|
296
|
+
const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[];
|
|
297
|
+
const visit=(id,mandatory)=>{
|
|
298
|
+
const workflow=flows.get(id);
|
|
299
|
+
if(!workflow)return;
|
|
300
|
+
const steps=(list,required)=>{
|
|
301
|
+
let available=required;
|
|
302
|
+
for(const step of list){
|
|
303
|
+
const needed=available&&!step.when;
|
|
304
|
+
if(step.action){
|
|
305
|
+
const pluginId=step.action.slice(0,step.action.lastIndexOf(':')),plugin=host.definitions.get(pluginId);
|
|
306
|
+
const requirement=resolvePrivilegeRequirement(plugin,step.action.slice(pluginId.length+1));
|
|
307
|
+
report.push({workflowId:id,stepId:step.id,action:step.action,mandatory:needed,requirement,static:checkStaticPrivilege(requirement)});
|
|
308
|
+
}
|
|
309
|
+
if(step.workflow)visit(store.childId(step.workflow),needed);
|
|
310
|
+
if(step.branch){steps(step.branch.then,false);steps(step.branch.else??[],false);}
|
|
311
|
+
if(step.parallel)steps(step.parallel.steps,needed);
|
|
312
|
+
if(step.stop)available=false;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
steps(workflow.steps,mandatory);
|
|
316
|
+
};
|
|
317
|
+
visit(entryId,true);return report;
|
|
318
|
+
}
|
|
319
|
+
async function findPluginWorkflowReferences(id){
|
|
320
|
+
const refs=[];
|
|
321
|
+
const collect=(steps,found)=>{for(const step of steps??[]){
|
|
322
|
+
if(typeof step.action==='string'&&step.action.startsWith(`${id}:`))found.add(step.action);
|
|
323
|
+
if(step.branch){collect(step.branch.then,found);collect(step.branch.else,found);}
|
|
324
|
+
if(step.parallel)collect(step.parallel.steps,found);
|
|
325
|
+
}};
|
|
326
|
+
for(const workflow of await store.list()){
|
|
327
|
+
const found=new Set();collect(workflow.steps,found);
|
|
328
|
+
if(found.size)refs.push({id:workflow.id,title:workflow.title??workflow.id,actions:[...found]});
|
|
329
|
+
}
|
|
330
|
+
return refs;
|
|
331
|
+
}
|
|
332
|
+
function runState(run){const {done,controller,...result}=run;return result;}
|
|
333
|
+
function snapshotRun(run){return journals.get(run).snapshot(runState(run));}
|
|
334
|
+
function trimRuns(){
|
|
335
|
+
const completed=[...runs.values()].filter(run=>!writingRuns.has(run.id)&&run.status!=='running');
|
|
336
|
+
for(const run of completed.slice(0,Math.max(0,completed.length-maxCachedRuns)))runs.delete(run.id);
|
|
337
|
+
// 超大业务结果交给 done 的调用方;宿主历史改读磁盘摘要,避免长期持有大对象。
|
|
338
|
+
for(const run of completed){
|
|
339
|
+
try{if(run.output!==undefined&&Buffer.byteLength(JSON.stringify(run.output))>(project.logs?.maxMemoryBytes??1048576)){
|
|
340
|
+
if(run.error?.code==='LOG_WRITE_FAILED'){delete run.output;run.outputTruncated=true;}else runs.delete(run.id);
|
|
341
|
+
}}
|
|
342
|
+
catch{delete run.output;run.outputTruncated=true;}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
211
345
|
async function pruneLogs(){for(const id of await runLogs.prune(project.logs?.keepRuns??null,writingRuns))runs.delete(id);}
|
|
212
|
-
async function change(operation){
|
|
346
|
+
async function change(operation,options={}){
|
|
213
347
|
if(project.readOnly)throw new FlowError('运行包不支持安装或移除插件');
|
|
214
|
-
assertMutable();
|
|
215
348
|
if(busy || launching || [...runs.values()].some(r=>r.status==='running'))throw new FlowError('请等待当前运行或安装结束');
|
|
216
|
-
busy=true;let
|
|
349
|
+
busy=true;let candidate,nextRelease;
|
|
217
350
|
try {
|
|
218
351
|
return await withPluginMutation(projectDir,async()=>{
|
|
219
352
|
await assertPluginState(projectDir,state);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
353
|
+
const latest=await readJson(path.join(projectDir,'flow.project.json'),project);
|
|
354
|
+
if(latest.readOnly)throw new FlowError('运行包不支持安装或移除插件');
|
|
355
|
+
if(operation.remove){
|
|
356
|
+
const name=operation.remove;
|
|
357
|
+
if(resources.active(name))throw new FlowError('请先停止此插件提供的 Web');
|
|
358
|
+
if(!state.dependencies?.[name]&&!state.links?.[name])throw new FlowError('此插件来自发行配置,不能在此移除');
|
|
359
|
+
const dependents=[...host.definitions.values()].filter(p=>p.id!==name&&Object.hasOwn(p.dependencies??{},name)).map(p=>p.id);
|
|
360
|
+
if(dependents.length)throw new FlowError(`插件仍被其他插件依赖:${dependents.join('、')}`,{code:'PLUGIN_IN_USE',details:{dependencies:dependents}});
|
|
361
|
+
const workflows=await findPluginWorkflowReferences(name);
|
|
362
|
+
if(workflows.length&&!options.force)throw new FlowError(`插件 ${name} 仍被工作流引用:${workflows.map(w=>w.id).join('、')}`,{code:'PLUGIN_IN_USE',details:{workflows}});
|
|
224
363
|
}
|
|
225
|
-
|
|
364
|
+
const next=await installGeneration(projectDir,{...operation,state});
|
|
365
|
+
const nextEntries=[...initial,...next.loaded],activeConfig={...project.config,...config},effectiveConfig={...latest.config,...config};
|
|
366
|
+
candidate=await createHost({plugins:nextEntries,config:effectiveConfig,services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:false,allowUnknownPrivileges});
|
|
367
|
+
const sourceChanges=await changedPluginSources(host.definitions,candidate.definitions,{oldInstallationRoot:state.root,newInstallationRoot:next.state.root});
|
|
368
|
+
const plan=planPluginUpdate(host.definitions,candidate.definitions,activeConfig,effectiveConfig,sourceChanges);
|
|
369
|
+
assertMaintenance(plan,options);
|
|
370
|
+
await candidate.close();candidate=null;
|
|
226
371
|
nextRelease=await leaseInstallation(projectDir,next.state.root);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
try{await
|
|
372
|
+
const oldEntries=[...host.definitions.values()].map(p=>({plugin:p,root:p.root}));
|
|
373
|
+
if(plan.affected.length)await host.replace({plugins:nextEntries,config:effectiveConfig,affected:plan.affected});
|
|
374
|
+
try{await savePluginState(projectDir,next.state,state);}
|
|
375
|
+
catch(error){
|
|
376
|
+
if(plan.affected.length)try{await host.replace({plugins:oldEntries,config:activeConfig,affected:plan.affected});}
|
|
377
|
+
catch(recovery){host.invalidate();throw new AggregateError([error,recovery],`插件状态保存失败且恢复失败:${error.message};${recovery.message}`);}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
state=next.state;
|
|
381
|
+
for(const key of Object.keys(project))delete project[key];Object.assign(project,latest);
|
|
382
|
+
installationLeases.set(path.resolve(state.root),nextRelease);nextRelease=null;
|
|
383
|
+
for(const [root,release] of installationLeases)if(root!==path.resolve(state.root)&&![...host.definitions.values()].some(p=>p.root&&path.resolve(p.root).startsWith(root+path.sep))){
|
|
384
|
+
try{await release();installationLeases.delete(root);}
|
|
385
|
+
catch(error){emitter.emit('event',{type:'plugin.cleanup.warning',message:`旧安装批次租约暂未清理:${error.message}`,root});}
|
|
386
|
+
}
|
|
230
387
|
return app.listPlugins();
|
|
231
388
|
});
|
|
232
|
-
} finally {try{if(
|
|
389
|
+
} finally {try{if(candidate)await candidate.close();}finally{try{if(nextRelease)await nextRelease();}finally{busy=false;}}}
|
|
233
390
|
}
|
|
234
391
|
return app;
|
|
235
392
|
}
|
|
@@ -1,17 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
export async function verifyBundle(root){
|
|
6
|
-
root=await realpath(root);
|
|
7
|
-
const manifest=JSON.parse(await readFile(path.join(root,'manifest.json'),'utf8'));
|
|
8
|
-
const errors=[];
|
|
9
|
-
for(const [relative,expected] of Object.entries(manifest.files??{})){
|
|
10
|
-
const file=path.resolve(root,relative);
|
|
11
|
-
try{
|
|
12
|
-
if(!file.startsWith(root+path.sep)||!(await realpath(file)).startsWith(root+path.sep))throw new Error('路径越界');
|
|
13
|
-
if(createHash('sha256').update(await readFile(file)).digest('hex')!==expected)errors.push(relative);
|
|
14
|
-
}catch{errors.push(relative);}
|
|
15
|
-
}
|
|
16
|
-
return {ok:errors.length===0,errors};
|
|
17
|
-
}
|
|
1
|
+
// 兼容既有 core 公开入口,实际能力由 platform 模块维护。
|
|
2
|
+
export * from '@mytools/fst-platform/bundle';
|