@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.
Files changed (255) hide show
  1. package/README.md +33 -5
  2. package/bin/fst.js +1 -1
  3. package/manual/README.md +33 -5
  4. package/manual/docs/deployment.md +3 -3
  5. package/manual/docs/kernel-generation.md +97 -0
  6. package/manual/docs/modules.md +57 -0
  7. package/manual/docs/plugin-development.md +120 -10
  8. package/manual/docs/releases.md +12 -0
  9. package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
  10. package/manual/docs/roadmap.md +14 -3
  11. package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
  12. package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
  13. package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
  14. package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
  15. package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
  16. package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
  17. package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
  18. package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
  19. package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
  20. package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
  21. package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
  22. package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
  23. package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
  24. package/manual/docs/verification.md +42 -2
  25. package/manual/packages/core/README.md +44 -0
  26. package/manual/packages/core/package.json +38 -0
  27. package/manual/packages/core/src/application.js +242 -85
  28. package/manual/packages/core/src/bundle.js +2 -0
  29. package/manual/packages/core/src/development-watch-worker.js +69 -0
  30. package/manual/packages/core/src/development.js +146 -0
  31. package/manual/packages/core/src/environments.js +2 -0
  32. package/manual/packages/core/src/export-snapshot.js +42 -0
  33. package/manual/packages/core/src/plugin-update.js +108 -0
  34. package/manual/packages/core/src/process.js +2 -0
  35. package/manual/packages/core/src/resources.js +31 -0
  36. package/manual/packages/core/src/version.js +2 -0
  37. package/manual/packages/fst-cli/LICENSE +21 -0
  38. package/manual/packages/fst-cli/README.md +54 -0
  39. package/{node_modules/@mytools/core → manual/packages/fst-cli}/bin/fst.js +55 -23
  40. package/manual/packages/fst-cli/package.json +49 -0
  41. package/manual/packages/fst-cli/src/development.js +53 -0
  42. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
  43. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
  44. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
  45. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-feed.js +31 -5
  46. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
  47. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
  48. package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
  49. package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
  50. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
  51. package/manual/packages/fst-devtools/LICENSE +21 -0
  52. package/manual/packages/fst-devtools/README.md +28 -0
  53. package/manual/packages/fst-devtools/package.json +29 -0
  54. package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
  55. package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
  56. package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/plugin-tools.js +24 -5
  57. package/manual/packages/fst-logs/LICENSE +21 -0
  58. package/manual/packages/fst-logs/README.md +56 -0
  59. package/manual/packages/fst-logs/package.json +24 -0
  60. package/manual/packages/fst-logs/src/log-bounds.js +91 -0
  61. package/manual/packages/fst-logs/src/run-logs.js +239 -0
  62. package/manual/packages/fst-platform/LICENSE +21 -0
  63. package/manual/packages/fst-platform/README.md +67 -0
  64. package/manual/packages/fst-platform/package.json +33 -0
  65. package/manual/packages/fst-platform/src/bundle.js +17 -0
  66. package/manual/packages/fst-platform/src/environments.js +67 -0
  67. package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
  68. package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
  69. package/manual/packages/fst-platform/src/file-probe.js +6 -0
  70. package/manual/packages/fst-platform/src/json.js +5 -0
  71. package/manual/packages/fst-platform/src/network.js +199 -0
  72. package/manual/packages/fst-platform/src/npm.js +11 -0
  73. package/manual/packages/fst-platform/src/process.js +110 -0
  74. package/manual/packages/fst-plugin/LICENSE +21 -0
  75. package/manual/packages/fst-plugin/README.md +29 -0
  76. package/manual/packages/fst-plugin/package.json +29 -0
  77. package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/contracts.js +30 -3
  78. package/manual/packages/fst-plugin/src/host.js +232 -0
  79. package/manual/packages/fst-plugin/src/loader.js +21 -0
  80. package/manual/packages/fst-plugin/src/privileges.js +91 -0
  81. package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/runtime.js +20 -4
  82. package/manual/packages/fst-workflow/LICENSE +21 -0
  83. package/manual/packages/fst-workflow/README.md +28 -0
  84. package/manual/packages/fst-workflow/package.json +24 -0
  85. package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
  86. package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
  87. package/manual/packages/fst-workspace/LICENSE +21 -0
  88. package/manual/packages/fst-workspace/README.md +32 -0
  89. package/manual/packages/fst-workspace/package.json +34 -0
  90. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
  91. package/manual/packages/fst-workspace/src/index.js +3 -0
  92. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugins.js +6 -36
  93. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +40 -7
  94. package/manual/packages/pack/LICENSE +21 -0
  95. package/manual/packages/pack/README.md +31 -0
  96. package/manual/packages/pack/archive.js +15 -0
  97. package/manual/packages/pack/dependencies.js +105 -0
  98. package/manual/packages/pack/environments.js +50 -0
  99. package/manual/packages/pack/index.js +103 -0
  100. package/manual/packages/pack/package.json +29 -0
  101. package/manual/packages/pack/runtime.js +61 -0
  102. package/manual/packages/sdk/README.md +31 -0
  103. package/manual/packages/sdk/conditions.js +1 -0
  104. package/manual/packages/sdk/package.json +23 -1
  105. package/manual/plugins/web/README.md +29 -0
  106. package/manual/tests/application.test.js +6 -4
  107. package/node_modules/@mytools/core/README.md +44 -0
  108. package/node_modules/@mytools/core/package.json +17 -17
  109. package/node_modules/@mytools/core/src/application.js +242 -85
  110. package/node_modules/@mytools/core/src/bundle.js +2 -17
  111. package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
  112. package/node_modules/@mytools/core/src/development.js +146 -0
  113. package/node_modules/@mytools/core/src/environments.js +2 -67
  114. package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
  115. package/node_modules/@mytools/core/src/plugin-update.js +108 -0
  116. package/node_modules/@mytools/core/src/process.js +2 -80
  117. package/node_modules/@mytools/core/src/version.js +1 -1
  118. package/node_modules/@mytools/fst-cli/LICENSE +21 -0
  119. package/node_modules/@mytools/fst-cli/README.md +54 -0
  120. package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
  121. package/node_modules/@mytools/fst-cli/package.json +49 -0
  122. package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
  123. package/node_modules/@mytools/fst-cli/src/development.js +53 -0
  124. package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
  125. package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
  126. package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
  127. package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
  128. package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
  129. package/{manual/packages/core → node_modules/@mytools/fst-cli}/src/terminal/run-feed.js +31 -5
  130. package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
  131. package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
  132. package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
  133. package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
  134. package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
  135. package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
  136. package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
  137. package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
  138. package/node_modules/@mytools/fst-devtools/README.md +28 -0
  139. package/node_modules/@mytools/fst-devtools/package.json +29 -0
  140. package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
  141. package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
  142. package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
  143. package/node_modules/@mytools/fst-logs/LICENSE +21 -0
  144. package/node_modules/@mytools/fst-logs/README.md +56 -0
  145. package/node_modules/@mytools/fst-logs/package.json +24 -0
  146. package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
  147. package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
  148. package/node_modules/@mytools/fst-logs/src/run-logs.js +239 -0
  149. package/node_modules/@mytools/fst-platform/LICENSE +21 -0
  150. package/node_modules/@mytools/fst-platform/README.md +67 -0
  151. package/node_modules/@mytools/fst-platform/package.json +33 -0
  152. package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
  153. package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
  154. package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
  155. package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
  156. package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
  157. package/node_modules/@mytools/fst-platform/src/json.js +5 -0
  158. package/node_modules/@mytools/fst-platform/src/network.js +199 -0
  159. package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
  160. package/node_modules/@mytools/fst-platform/src/process.js +110 -0
  161. package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
  162. package/node_modules/@mytools/fst-plugin/README.md +29 -0
  163. package/node_modules/@mytools/fst-plugin/package.json +29 -0
  164. package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
  165. package/node_modules/@mytools/fst-plugin/src/host.js +232 -0
  166. package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
  167. package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
  168. package/{manual/packages/core → node_modules/@mytools/fst-plugin}/src/runtime.js +20 -4
  169. package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
  170. package/node_modules/@mytools/fst-workflow/README.md +28 -0
  171. package/node_modules/@mytools/fst-workflow/package.json +24 -0
  172. package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
  173. package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
  174. package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
  175. package/node_modules/@mytools/fst-workspace/README.md +32 -0
  176. package/node_modules/@mytools/fst-workspace/package.json +34 -0
  177. package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
  178. package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
  179. package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
  180. package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
  181. package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
  182. package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
  183. package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
  184. package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
  185. package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
  186. package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
  187. package/node_modules/@mytools/pack/README.md +31 -0
  188. package/node_modules/@mytools/pack/dependencies.js +7 -2
  189. package/node_modules/@mytools/pack/environments.js +1 -1
  190. package/node_modules/@mytools/pack/index.js +33 -25
  191. package/node_modules/@mytools/pack/package.json +29 -1
  192. package/node_modules/@mytools/sdk/README.md +31 -0
  193. package/node_modules/@mytools/sdk/conditions.js +1 -0
  194. package/node_modules/@mytools/sdk/diagnostics.js +27 -0
  195. package/node_modules/@mytools/sdk/package.json +23 -1
  196. package/node_modules/@mytools/sdk/schema-path.js +31 -0
  197. package/node_modules/@mytools/sdk/validation.js +78 -0
  198. package/node_modules/@mytools/web/README.md +29 -0
  199. package/node_modules/@mytools/web/package.json +7 -0
  200. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1398 -1199
  201. package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
  202. package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
  203. package/node_modules/@mytools/web/public/index.html +2 -2
  204. package/node_modules/@mytools/web/server.js +24 -17
  205. package/node_modules/@mytools/web/src/model.js +84 -0
  206. package/node_modules/acorn/CHANGELOG.md +1012 -0
  207. package/node_modules/acorn/LICENSE +21 -0
  208. package/node_modules/acorn/README.md +309 -0
  209. package/node_modules/acorn/bin/acorn +4 -0
  210. package/node_modules/acorn/dist/acorn.d.mts +897 -0
  211. package/node_modules/acorn/dist/acorn.d.ts +897 -0
  212. package/node_modules/acorn/dist/acorn.js +6370 -0
  213. package/node_modules/acorn/dist/acorn.mjs +6341 -0
  214. package/node_modules/acorn/dist/bin.js +90 -0
  215. package/node_modules/acorn/package.json +50 -0
  216. package/node_modules/{minipass-flush/node_modules/minipass → tar}/node_modules/yallist/README.md +8 -7
  217. package/node_modules/tar/node_modules/yallist/package.json +68 -0
  218. package/node_modules/yallist/README.md +7 -8
  219. package/node_modules/yallist/package.json +12 -51
  220. package/package.json +131 -115
  221. package/manual/packages/core/src/host.js +0 -139
  222. package/node_modules/@mytools/core/src/host.js +0 -139
  223. package/node_modules/@mytools/core/src/run-logs.js +0 -63
  224. package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
  225. package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
  226. package/node_modules/minipass-flush/node_modules/minipass/node_modules/yallist/package.json +0 -29
  227. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
  228. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
  229. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
  230. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
  231. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
  232. /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
  233. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugin-installations.js +0 -0
  234. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
  235. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
  236. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
  237. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
  238. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
  239. /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
  240. /package/{node_modules/@mytools/core/src → manual/packages/sdk}/schema-path.js +0 -0
  241. /package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +0 -0
  242. /package/node_modules/{yallist → tar/node_modules/yallist}/LICENSE.md +0 -0
  243. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts +0 -0
  244. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts.map +0 -0
  245. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js +0 -0
  246. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js.map +0 -0
  247. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/package.json +0 -0
  248. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts +0 -0
  249. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts.map +0 -0
  250. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js +0 -0
  251. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js.map +0 -0
  252. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/package.json +0 -0
  253. /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/LICENSE +0 -0
  254. /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/iterator.js +0 -0
  255. /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 '../packages/core/src/application.js';
5
+ import { createApplication } from '@mytools/core';
6
6
  import { temporary, testPlugin, object } from './helpers.js';
7
- import {validate} from '../packages/core/src/validation.js';
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.ok(!events.some(e=>Object.hasOwn(e,'output')));
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
- "./environments": "./src/environments.js"
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
- "bin",
18
- "templates"
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
- "ajv": "8.20.0",
28
- "commander": "14.0.3",
29
- "cordis": "3.18.1",
30
- "execa": "10.1.0",
31
- "semver": "7.7.4",
32
- "tar": "7.5.22",
33
- "tree-kill": "1.2.2",
34
- "validate-npm-package-name": "8.0.0",
35
- "which": "7.0.0",
36
- "yaml": "2.9.1"
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, appendFile, writeFile, rename, readFile } from 'node:fs/promises';
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 { createHost } from './host.js';
6
- import { WorkflowStore } from './workflows.js';
7
- import { runWorkflow } from './engine.js';
8
- import { runProcess } from './process.js';
9
- import { readJson, loadPlugin, installGeneration } from './plugins.js';
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 './bundle-workspace.js';
12
- import {validate} from './validation.js';
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 './diagnostics.js';
15
- import {markdownHeader,markdownEvent,markdownResult} from './markdown-log.js';
16
- import {createRunLogs,validateKeepRuns} from './run-logs.js';
17
- import {openPluginState,withPluginMutation,assertPluginState,savePluginState,leaseInstallation} from './plugin-installations.js';
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,releaseInstallation=opened.release,busy=false;
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){if(project.readOnly)throw new FlowError('此分发包仅用于运行;请在开发项目中编辑');return store.save(w);},
39
- async removeWorkflow(id){if(project.readOnly)throw new FlowError('只读项目不能删除工作流');return store.remove(id);},
40
- checkWorkflow:id=>store.resolve([id],host),
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 store.resolve([id],host);
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
- return app.executeTracked(id,(activeHost,context,event)=>runWorkflow({store:snapshotStore,host:activeHost,id,input,signal:context.signal,onEvent:event,runId:context.runId,context}));
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
- return app.executeTracked(`/${name}`,(activeHost,runContext)=>activeHost.invokeCommand(name,input,{...context,...runContext}));
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(),dir=path.join(output,runId),controller=new AbortController();
54
- await mkdir(dir,{recursive:true});
55
- const run={id:runId,workflowId:id,logOwner:runLogs.owner,status:'running',startedAt:new Date().toISOString(),events:[],controller};
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 markdownFile=path.join(dir,'run.md');
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:dir,signal:controller.signal,runId,log:(message,data,meta={})=>event({...meta,type:'log',message,data})},event);
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 writes;
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
- await event({type:'log',message:`日志自动清理失败:${error.message}`});
93
- await writeFile(path.join(dir,'result.json'),JSON.stringify(snapshotRun(run),null,2));
105
+ run.warnings=[{code:'LOG_PRUNE_FAILED',message:`日志自动清理失败:${error.message}`}];
106
+ await journal.save(runState(run));
94
107
  }
95
108
  }
96
- return snapshotRun(run);
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);if(!/^[a-f0-9-]{36}$/.test(id))throw new FlowError('运行 ID 无效');return JSON.parse(await readFile(path.join(output,id,'result.json'),'utf8'));},
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 markdownHeader(run)+run.events.map(markdownEvent).join('');
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){if(resources.active(name))throw new FlowError('请先停止此插件提供的 Web');if(!state.dependencies?.[name]&&!state.links?.[name])throw new FlowError('此插件来自发行配置,不能在此移除');return change({remove:name});},
133
- async pack(options){const {packApplication}=await import('@mytools/pack');return packApplication(app,options);},
134
- async packPlan(options){const {planApplication}=await import('@mytools/pack');return planApplication(app,options);},
135
- async configurePlugin(id,value){
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 nextHost;
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
- nextHost=await createHost({plugins:[...host.definitions.values()].map(p=>({plugin:p,root:p.root})),config:{...nextConfig,...config},services,environmentOverrides:latest.environments,runtimeEnabled:!maintenance});
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
- const old=host;host=nextHost;nextHost=null;
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{try{if(nextHost)await nextHost.close();}finally{busy=false;}}
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 releaseInstallation();}catch(error){errors.push(error);}
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 build=extra=>createHost({plugins:[...initial,...extra],config:{...project.config,...config},services,environmentOverrides:project.environments,runtimeEnabled:!maintenance});
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 releaseInstallation();throw error;}
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 assertMutable(){if(!maintenance&&host.hasRuntime)throw new FlowError('运行时插件已启动,请先进入维护模式再修改插件或配置');}
210
- function snapshotRun(run){const {done,controller,...result}=run;return structuredClone(result);}
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 nextHost,nextRelease;
349
+ busy=true;let candidate,nextRelease;
217
350
  try {
218
351
  return await withPluginMutation(projectDir,async()=>{
219
352
  await assertPluginState(projectDir,state);
220
- const next=await installGeneration(projectDir,{...operation,state});
221
- for(const p of host.definitions.values())if(resources.active(p.id)){
222
- const candidate=[...initial,...next.loaded].find(e=>(e.plugin??e).id===p.id);
223
- if(!candidate||candidate.root!==p.root||(candidate.plugin??candidate).version!==p.version)throw new FlowError('请先停止活动 Web,再替换其提供者');
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
- nextHost=await build(next.loaded);
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
- await savePluginState(projectDir,next.state,state);
228
- const old=host,oldRelease=releaseInstallation;host=nextHost;nextHost=null;state=next.state;releaseInstallation=nextRelease;nextRelease=null;
229
- try{await old.close();}finally{await oldRelease();}
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(nextHost)await nextHost.close();}finally{try{if(nextRelease)await nextRelease();}finally{busy=false;}}}
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
- import { createHash } from 'node:crypto';
2
- import { readFile, realpath } from 'node:fs/promises';
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';