@yanglaofish/fishtools 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) 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/examples/script-plugin/LICENSE +21 -0
  6. package/manual/docs/examples/script-plugin/README.md +64 -0
  7. package/manual/docs/examples/script-plugin/index.js +39 -0
  8. package/manual/docs/examples/script-plugin/package.json +9 -0
  9. package/manual/docs/examples/script-plugin/scripts/benchmark.py +23 -0
  10. package/manual/docs/kernel-generation.md +97 -0
  11. package/manual/docs/modules.md +57 -0
  12. package/manual/docs/plugin-development.md +163 -12
  13. package/manual/docs/releases.md +23 -0
  14. package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
  15. package/manual/docs/roadmap.md +16 -3
  16. package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
  17. package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
  18. package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
  19. package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
  20. package/manual/docs/superpowers/plans/2026-10-12-plugin-developer-experience.md +50 -0
  21. package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
  22. package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
  23. package/manual/docs/superpowers/reviews/2026-10-12-plugin-release-review.md +20 -0
  24. package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
  25. package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
  26. package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
  27. package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
  28. package/manual/docs/superpowers/specs/2026-10-12-plugin-developer-experience.md +22 -0
  29. package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
  30. package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
  31. package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
  32. package/manual/docs/verification.md +58 -1
  33. package/manual/packages/core/README.md +44 -0
  34. package/manual/packages/core/package.json +38 -0
  35. package/manual/packages/core/src/application.js +268 -87
  36. package/manual/packages/core/src/bundle.js +2 -0
  37. package/manual/packages/core/src/development-watch-worker.js +69 -0
  38. package/manual/packages/core/src/development.js +146 -0
  39. package/manual/packages/core/src/environments.js +2 -0
  40. package/manual/packages/core/src/export-snapshot.js +42 -0
  41. package/manual/packages/core/src/plugin-update.js +108 -0
  42. package/manual/packages/core/src/process.js +2 -0
  43. package/manual/packages/core/src/resources.js +31 -0
  44. package/manual/packages/core/src/version.js +2 -0
  45. package/manual/packages/fst-cli/LICENSE +21 -0
  46. package/manual/packages/fst-cli/README.md +54 -0
  47. package/manual/packages/fst-cli/bin/fst.js +124 -0
  48. package/manual/packages/fst-cli/package.json +49 -0
  49. package/manual/packages/fst-cli/src/development.js +53 -0
  50. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
  51. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
  52. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
  53. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-feed.js +31 -5
  54. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
  55. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
  56. package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
  57. package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
  58. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
  59. package/manual/packages/fst-devtools/LICENSE +21 -0
  60. package/manual/packages/fst-devtools/README.md +28 -0
  61. package/manual/packages/fst-devtools/package.json +29 -0
  62. package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
  63. package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
  64. package/manual/packages/fst-devtools/src/plugin-tools.js +152 -0
  65. package/manual/packages/fst-logs/LICENSE +21 -0
  66. package/manual/packages/fst-logs/README.md +56 -0
  67. package/manual/packages/fst-logs/package.json +24 -0
  68. package/manual/packages/fst-logs/src/log-bounds.js +91 -0
  69. package/manual/packages/fst-logs/src/run-logs.js +239 -0
  70. package/manual/packages/fst-platform/LICENSE +21 -0
  71. package/manual/packages/fst-platform/README.md +67 -0
  72. package/manual/packages/fst-platform/package.json +33 -0
  73. package/manual/packages/fst-platform/src/bundle.js +17 -0
  74. package/manual/packages/fst-platform/src/environments.js +67 -0
  75. package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
  76. package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
  77. package/manual/packages/fst-platform/src/file-probe.js +6 -0
  78. package/manual/packages/fst-platform/src/json.js +5 -0
  79. package/manual/packages/fst-platform/src/network.js +199 -0
  80. package/manual/packages/fst-platform/src/npm.js +11 -0
  81. package/manual/packages/fst-platform/src/process.js +110 -0
  82. package/manual/packages/fst-plugin/LICENSE +21 -0
  83. package/manual/packages/fst-plugin/README.md +29 -0
  84. package/manual/packages/fst-plugin/package.json +29 -0
  85. package/manual/packages/fst-plugin/src/contracts.js +86 -0
  86. package/manual/packages/fst-plugin/src/host.js +232 -0
  87. package/manual/packages/fst-plugin/src/loader.js +21 -0
  88. package/manual/packages/fst-plugin/src/privileges.js +91 -0
  89. package/manual/packages/{core → fst-plugin}/src/runtime.js +20 -4
  90. package/manual/packages/fst-workflow/LICENSE +21 -0
  91. package/manual/packages/fst-workflow/README.md +28 -0
  92. package/manual/packages/fst-workflow/package.json +24 -0
  93. package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
  94. package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
  95. package/manual/packages/fst-workspace/LICENSE +21 -0
  96. package/manual/packages/fst-workspace/README.md +32 -0
  97. package/manual/packages/fst-workspace/package.json +34 -0
  98. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
  99. package/manual/packages/fst-workspace/src/index.js +3 -0
  100. package/manual/packages/fst-workspace/src/plugin-installations.js +84 -0
  101. package/manual/packages/fst-workspace/src/plugins.js +35 -0
  102. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +41 -21
  103. package/manual/packages/pack/LICENSE +21 -0
  104. package/manual/packages/pack/README.md +31 -0
  105. package/manual/packages/pack/archive.js +15 -0
  106. package/manual/packages/pack/dependencies.js +105 -0
  107. package/manual/packages/pack/environments.js +50 -0
  108. package/manual/packages/pack/index.js +103 -0
  109. package/manual/packages/pack/package.json +29 -0
  110. package/manual/packages/pack/runtime.js +61 -0
  111. package/manual/packages/sdk/README.md +31 -0
  112. package/manual/packages/sdk/conditions.js +1 -0
  113. package/manual/packages/sdk/package.json +23 -1
  114. package/manual/packages/sdk/schema-path.js +31 -0
  115. package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +22 -9
  116. package/manual/plugins/web/README.md +29 -0
  117. package/manual/tests/application.test.js +6 -4
  118. package/node_modules/@mytools/core/README.md +44 -0
  119. package/node_modules/@mytools/core/package.json +17 -15
  120. package/node_modules/@mytools/core/src/application.js +268 -87
  121. package/node_modules/@mytools/core/src/bundle.js +2 -17
  122. package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
  123. package/node_modules/@mytools/core/src/development.js +146 -0
  124. package/node_modules/@mytools/core/src/environments.js +2 -29
  125. package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
  126. package/node_modules/@mytools/core/src/plugin-update.js +108 -0
  127. package/node_modules/@mytools/core/src/process.js +2 -71
  128. package/node_modules/@mytools/core/src/version.js +1 -1
  129. package/node_modules/@mytools/fst-cli/LICENSE +21 -0
  130. package/node_modules/@mytools/fst-cli/README.md +54 -0
  131. package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
  132. package/node_modules/@mytools/fst-cli/package.json +49 -0
  133. package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
  134. package/node_modules/@mytools/fst-cli/src/development.js +53 -0
  135. package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
  136. package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
  137. package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
  138. package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
  139. package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
  140. package/{manual/packages/core → node_modules/@mytools/fst-cli}/src/terminal/run-feed.js +31 -5
  141. package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
  142. package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
  143. package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
  144. package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
  145. package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
  146. package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
  147. package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
  148. package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
  149. package/node_modules/@mytools/fst-devtools/README.md +28 -0
  150. package/node_modules/@mytools/fst-devtools/package.json +29 -0
  151. package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
  152. package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
  153. package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
  154. package/node_modules/@mytools/fst-logs/LICENSE +21 -0
  155. package/node_modules/@mytools/fst-logs/README.md +56 -0
  156. package/node_modules/@mytools/fst-logs/package.json +24 -0
  157. package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
  158. package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
  159. package/node_modules/@mytools/fst-logs/src/run-logs.js +239 -0
  160. package/node_modules/@mytools/fst-platform/LICENSE +21 -0
  161. package/node_modules/@mytools/fst-platform/README.md +67 -0
  162. package/node_modules/@mytools/fst-platform/package.json +33 -0
  163. package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
  164. package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
  165. package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
  166. package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
  167. package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
  168. package/node_modules/@mytools/fst-platform/src/json.js +5 -0
  169. package/node_modules/@mytools/fst-platform/src/network.js +199 -0
  170. package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
  171. package/node_modules/@mytools/fst-platform/src/process.js +110 -0
  172. package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
  173. package/node_modules/@mytools/fst-plugin/README.md +29 -0
  174. package/node_modules/@mytools/fst-plugin/package.json +29 -0
  175. package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
  176. package/node_modules/@mytools/fst-plugin/src/host.js +232 -0
  177. package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
  178. package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
  179. package/node_modules/@mytools/{core → fst-plugin}/src/runtime.js +20 -4
  180. package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
  181. package/node_modules/@mytools/fst-workflow/README.md +28 -0
  182. package/node_modules/@mytools/fst-workflow/package.json +24 -0
  183. package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
  184. package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
  185. package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
  186. package/node_modules/@mytools/fst-workspace/README.md +32 -0
  187. package/node_modules/@mytools/fst-workspace/package.json +34 -0
  188. package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
  189. package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
  190. package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
  191. package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
  192. package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
  193. package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
  194. package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
  195. package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
  196. package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
  197. package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
  198. package/node_modules/@mytools/pack/README.md +31 -0
  199. package/node_modules/@mytools/pack/dependencies.js +15 -3
  200. package/node_modules/@mytools/pack/environments.js +1 -1
  201. package/node_modules/@mytools/pack/index.js +33 -25
  202. package/node_modules/@mytools/pack/package.json +29 -1
  203. package/node_modules/@mytools/sdk/README.md +31 -0
  204. package/node_modules/@mytools/sdk/conditions.js +1 -0
  205. package/node_modules/@mytools/sdk/diagnostics.js +27 -0
  206. package/node_modules/@mytools/sdk/package.json +23 -1
  207. package/node_modules/@mytools/sdk/schema-path.js +31 -0
  208. package/node_modules/@mytools/sdk/validation.js +78 -0
  209. package/node_modules/@mytools/web/README.md +29 -0
  210. package/node_modules/@mytools/web/package.json +7 -0
  211. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1275 -1076
  212. package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
  213. package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
  214. package/node_modules/@mytools/web/public/index.html +2 -2
  215. package/node_modules/@mytools/web/server.js +24 -17
  216. package/node_modules/@mytools/web/src/model.js +84 -0
  217. package/node_modules/acorn/CHANGELOG.md +1012 -0
  218. package/node_modules/acorn/LICENSE +21 -0
  219. package/node_modules/acorn/README.md +309 -0
  220. package/node_modules/acorn/bin/acorn +4 -0
  221. package/node_modules/acorn/dist/acorn.d.mts +897 -0
  222. package/node_modules/acorn/dist/acorn.d.ts +897 -0
  223. package/node_modules/acorn/dist/acorn.js +6370 -0
  224. package/node_modules/acorn/dist/acorn.mjs +6341 -0
  225. package/node_modules/acorn/dist/bin.js +90 -0
  226. package/node_modules/acorn/package.json +50 -0
  227. package/package.json +117 -101
  228. package/manual/packages/core/src/host.js +0 -124
  229. package/node_modules/@mytools/core/bin/fst.js +0 -70
  230. package/node_modules/@mytools/core/src/contracts.js +0 -41
  231. package/node_modules/@mytools/core/src/host.js +0 -124
  232. package/node_modules/@mytools/core/src/plugins.js +0 -55
  233. package/node_modules/@mytools/core/src/run-logs.js +0 -63
  234. package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
  235. package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
  236. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
  237. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
  238. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
  239. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
  240. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
  241. /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
  242. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
  243. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
  244. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
  245. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
  246. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
  247. /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
@@ -0,0 +1,84 @@
1
+ import path from 'node:path';
2
+ import {mkdir,readFile,writeFile,rename,readdir,realpath,lstat,rm,unlink,rmdir} from 'node:fs/promises';
3
+ import {randomUUID} from 'node:crypto';
4
+ import {isDeepStrictEqual} from 'node:util';
5
+ import {create as archive} from 'tar';
6
+ import {FlowError} from '@mytools/sdk';
7
+
8
+ async function json(file,fallback){try{return JSON.parse(await readFile(file,'utf8'));}catch(error){if(error.code==='ENOENT')return fallback;throw error;}}
9
+ const flow=project=>path.join(project,'.flow');
10
+ const stateFile=project=>path.join(flow(project),'plugins.json');
11
+ export const readPluginState=project=>json(stateFile(project),{});
12
+
13
+ // 所有安装/引用/回收操作共用工作区锁,防止其他 fst 进程同时切换批次。
14
+ export async function withPluginMutation(project,callback){
15
+ await mkdir(flow(project),{recursive:true});const lock=path.join(flow(project),'plugin-mutation.lock');
16
+ try{await mkdir(lock);}catch(error){if(error.code==='EEXIST')throw new FlowError('插件状态正在被其他进程修改;若上次异常退出,请确认没有活动 fst 后移除 .flow/plugin-mutation.lock',{code:'PLUGIN_BUSY'});throw error;}
17
+ try{return await callback();}finally{await rmdir(lock);}
18
+ }
19
+
20
+ export async function leaseInstallation(project,root){
21
+ if(!root)return async()=>{};
22
+ const directory=path.join(flow(project),'installation-leases');await mkdir(directory,{recursive:true});
23
+ const file=path.join(directory,`${randomUUID()}.json`);
24
+ await writeFile(file,JSON.stringify({pid:process.pid,root:path.resolve(root)}));
25
+ return async()=>{try{await unlink(file);}catch(error){if(error.code!=='ENOENT')throw error;}};
26
+ }
27
+
28
+ export async function openPluginState(project,{readOnly=false}={}){
29
+ const existing=await readPluginState(project);
30
+ if(readOnly||!existing.root)return {state:existing,release:async()=>{}};
31
+ return withPluginMutation(project,async()=>{const state=await readPluginState(project);return {state,release:await leaseInstallation(project,state.root)};});
32
+ }
33
+
34
+ export async function assertPluginState(project,expected){
35
+ if(!isDeepStrictEqual(await readPluginState(project),expected))throw new FlowError('插件状态已被其他进程修改,请重启当前宿主后再操作',{code:'PLUGIN_STATE_CHANGED'});
36
+ }
37
+
38
+ export async function savePluginState(project,next,previous){
39
+ const backup=path.join(flow(project),'plugins.previous.json'),id=randomUUID();
40
+ await writeFile(`${backup}.${id}.tmp`,JSON.stringify(previous,null,2));await rename(`${backup}.${id}.tmp`,backup);
41
+ const file=stateFile(project);await writeFile(`${file}.${id}.tmp`,JSON.stringify(next,null,2));await rename(`${file}.${id}.tmp`,file);
42
+ }
43
+
44
+ export async function collectInstallations(project,{apply=false}={}){
45
+ // 预览仅读取,不创建锁或备份目录;执行时在锁内重新计算,不能复用旧预览。
46
+ const collect=async()=>{
47
+ const base=path.join(flow(project),'installations'),state=await readPluginState(project),previous=await json(path.join(flow(project),'plugins.previous.json'),{});
48
+ const protectedRoots=new Set([state.root,state.previousRoot,previous.root].filter(Boolean).map(root=>path.resolve(root)));
49
+ let leases=[];try{leases=await readdir(path.join(flow(project),'installation-leases'));}catch(error){if(error.code!=='ENOENT')throw error;}
50
+ for(const file of leases){
51
+ const lease=await json(path.join(flow(project),'installation-leases',file),null);if(!lease)continue;
52
+ // 无法确认进程已退出时宁可保留;PID 重用只会保守地多保留一个批次。
53
+ try{process.kill(lease.pid,0);protectedRoots.add(path.resolve(lease.root));}catch(error){if(error.code!=='ESRCH')protectedRoots.add(path.resolve(lease.root));}
54
+ }
55
+ let entries=[];try{entries=await readdir(base,{withFileTypes:true});}catch(error){if(error.code!=='ENOENT')throw error;}
56
+ const candidates=[],kept=[],removed=[];
57
+ const canonicalBase=entries.length?await realpath(base):base;
58
+ if(entries.length){
59
+ const projectRoot=await realpath(project),relative=path.relative(projectRoot,canonicalBase);
60
+ if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative)||(await lstat(base)).isSymbolicLink())throw new FlowError('安装目录越界或是目录链接,拒绝清理');
61
+ for(const root of [...protectedRoots]){try{protectedRoots.add(await realpath(root));}catch(error){if(error.code!=='ENOENT')throw error;}}
62
+ }
63
+ for(const entry of entries){
64
+ const directory=path.join(base,entry.name);
65
+ if(!entry.isDirectory()||!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(entry.name)){kept.push({directory,reason:'非受管批次'});continue;}
66
+ const physical=await realpath(directory);
67
+ if(path.dirname(physical)!==canonicalBase||(await lstat(directory)).isSymbolicLink()||protectedRoots.has(physical)){kept.push({directory,reason:'当前、恢复、活动批次或链接'});continue;}
68
+ const pkg=await json(path.join(directory,'package.json'),{});
69
+ if(pkg.name!=='flow-project-plugins'){kept.push({directory,reason:'无法确认批次归属'});continue;}
70
+ candidates.push({directory});
71
+ if(apply){
72
+ const backups=path.join(flow(project),'installation-backups');await mkdir(backups,{recursive:true});
73
+ const backup=path.join(backups,`${entry.name}-${randomUUID()}.tgz`),temporary=`${backup}.tmp`;
74
+ await archive({cwd:canonicalBase,file:temporary,gzip:true,follow:false,portable:true},[entry.name]);
75
+ await rename(temporary,backup);
76
+ // 备份完成后再次确认真实绝对路径仍是本项目的直接子目录,才允许递归删除。
77
+ if(await realpath(directory)!==physical||path.dirname(physical)!==canonicalBase)throw new FlowError('安装批次路径在清理期间改变,已停止');
78
+ await rm(physical,{recursive:true,maxRetries:5,retryDelay:200});removed.push({directory,backup});
79
+ }
80
+ }
81
+ return {candidates,kept,removed};
82
+ };
83
+ return apply?withPluginMutation(project,collect):collect();
84
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import {mkdir,writeFile,access} from 'node:fs/promises';
3
+ import {randomUUID} from 'node:crypto';
4
+ import {FlowError} from '@mytools/sdk';
5
+ import {readJson} from '@mytools/fst-platform/json';
6
+ import {npm} from '@mytools/fst-platform/npm';
7
+ import {loadPlugin} from '@mytools/fst-plugin/loader';
8
+ export async function installGeneration(projectDir,{add,remove,link,state}) {
9
+ const root=path.join(projectDir,'.flow','installations',randomUUID());
10
+ await mkdir(root,{recursive:true});
11
+ const dependencies={...(state?.dependencies??{})};
12
+ const links={...(state?.links??{})};
13
+ if(remove){delete dependencies[remove];delete links[remove];}
14
+ if(link){
15
+ const source=path.resolve(link),pkg=await readJson(path.join(source,'package.json'));
16
+ const loaded=await loadPlugin(source,projectDir);
17
+ if(!pkg.name||pkg.name!==loaded.plugin?.id||pkg.version!==loaded.plugin?.version)throw new FlowError('开发插件的包名/版本必须与插件 id/version 一致',{code:'VALIDATION'});
18
+ links[pkg.name]=source;delete dependencies[pkg.name];
19
+ }
20
+ await writeFile(path.join(root,'package.json'),JSON.stringify({name:'flow-project-plugins',private:true,dependencies},null,2));
21
+ let source=add;
22
+ if(add) {
23
+ try {await access(path.resolve(projectDir,add));source=path.resolve(projectDir,add);} catch {}
24
+ if(source.startsWith('-')) throw new FlowError('插件来源不能以选项符号开头',{code:'VALIDATION'});
25
+ }
26
+ if(source||Object.keys(dependencies).length){
27
+ const result=await npm(['install',...(source?[source]:[]),'--save-exact','--ignore-scripts','--install-links','--no-audit','--no-fund'],{cwd:root});
28
+ if(result.exitCode!==0) throw new FlowError(`插件安装失败:${result.stderr}`,{code:'INSTALL_FAILED'});
29
+ }
30
+ const pkg=await readJson(path.join(root,'package.json'));
31
+ const loaded=[];
32
+ for(const name of Object.keys(pkg.dependencies??{})){delete links[name];loaded.push(await loadPlugin(name,root));}
33
+ for(const source of Object.values(links))loaded.push(await loadPlugin(source,projectDir));
34
+ return {state:{root,dependencies:pkg.dependencies??{},links,...(state?.root?{previousRoot:state.root}:{})},loaded};
35
+ }
@@ -1,27 +1,14 @@
1
- import { mkdir, readFile, readdir, realpath, writeFile, rename } from 'node:fs/promises';
1
+ import { mkdir, readFile, readdir, realpath, writeFile, rename, open, unlink } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import YAML from 'yaml';
5
5
  import { FlowError } from '@mytools/sdk';
6
- import { assertId, assertSchema, references, referenceParts, emptyObject } from './validation.js';
7
- import {validateCondition,validateStepList} from './control.js';
8
-
9
- function fieldSchema(schema,fields,ref){
10
- let current=schema;
11
- for(const field of fields){
12
- if(!current||current===true)return {};
13
- if(current.properties&&Object.hasOwn(current.properties,field)){current=current.properties[field];continue;}
14
- if(current.type==='array'&&/^\d+$/.test(field)){current=current.items;continue;}
15
- if(current.additionalProperties===false||current===false||(current.type&&!['object','array'].includes(current.type))){
16
- throw new FlowError(`引用字段不存在:${ref}`,{code:'VALIDATION'});
17
- }
18
- return {};
19
- }
20
- return current??{};
21
- }
6
+ import { assertId, assertSchema, references, referenceParts, emptyObject } from '@mytools/sdk/schema';
7
+ import {resolveSchemaPath} from '@mytools/sdk/schema-path';
8
+ import {validateCondition,validateStepList} from '@mytools/fst-workflow/control';
22
9
 
23
10
  export class WorkflowStore {
24
- constructor(root) { this.root = path.resolve(root); }
11
+ constructor(root,{openFile=open}={}) { this.root = path.resolve(root);this.openFile=openFile; }
25
12
  file(id) { assertId(id); return path.join(this.root,`${id}.yaml`); }
26
13
  async get(id) {
27
14
  const file=this.file(id);
@@ -42,10 +29,43 @@ export class WorkflowStore {
42
29
  const w=this.validateShape(definition);
43
30
  await mkdir(this.root,{recursive:true});
44
31
  const file=this.file(w.id), temporary=`${file}.${randomUUID()}.tmp`;
45
- await writeFile(temporary,YAML.stringify(w),'utf8');
46
- await rename(temporary,file);
32
+ let previous;
33
+ try{
34
+ const physical=await realpath(file),root=await realpath(this.root);
35
+ if(!physical.startsWith(root+path.sep))throw new FlowError('工作流越界,不能覆盖工作流目录之外的文件',{code:'VALIDATION'});
36
+ previous=await readFile(file,'utf8');
37
+ }catch(error){if(error.code!=='ENOENT')throw error;}
38
+ if(previous!==undefined){
39
+ const directory=path.join(path.dirname(this.root),'.flow','workflow-backups');
40
+ await mkdir(directory,{recursive:true,mode:0o700});
41
+ await writeFile(path.join(directory,`${w.id}-${randomUUID()}.yaml`),previous,{flag:'wx',mode:0o600});
42
+ }
43
+ try{await writeFile(temporary,YAML.stringify(w),'utf8');await rename(temporary,file);}
44
+ catch(error){try{await unlink(temporary);}catch(cleanup){if(cleanup.code!=='ENOENT')throw new AggregateError([error,cleanup],`工作流保存失败且临时文件无法清理:${temporary}`);}throw error;}
47
45
  return w;
48
46
  }
47
+ async duplicate(definition,{id,title}={}) {
48
+ const source=this.validateShape(definition);
49
+ const copy=this.validateShape({...source,id:id??`flow-${randomUUID()}`,title:title??`${source.title??source.id} 副本`});
50
+ const file=this.file(copy.id),content=YAML.stringify(copy);
51
+ if(typeof copy.title!=='string'||!copy.title.trim())throw new FlowError('工作流名称不能为空',{code:'VALIDATION'});
52
+ await mkdir(this.root,{recursive:true});
53
+ let handle;
54
+ try{
55
+ // 排他创建保证并发请求也不能覆盖同名工作流。
56
+ handle=await this.openFile(file,'wx');
57
+ await handle.writeFile(content,'utf8');
58
+ await handle.close();handle=null;
59
+ return {definition:copy,path:file};
60
+ }catch(error){
61
+ if(handle){
62
+ try{await handle.close();}catch{}
63
+ try{await unlink(file);}catch(cleanupError){if(cleanupError.code!=='ENOENT')throw new AggregateError([error,cleanupError],`复制失败且无法清理新文件:${file}`);}
64
+ }
65
+ if(error.code==='EEXIST')throw new FlowError(`工作流已存在:${copy.id}`,{code:'VALIDATION',cause:error});
66
+ throw error;
67
+ }
68
+ }
49
69
  async list() {
50
70
  await mkdir(this.root,{recursive:true});
51
71
  const files=await readdir(this.root);
@@ -76,7 +96,7 @@ export class WorkflowStore {
76
96
  const checker=previous=>ref=>{
77
97
  const p=referenceParts(ref);
78
98
  if(p[0]==='steps' && !previous.has(p[1])) throw new FlowError(`${id} 引用必须指向前序步骤:${ref}`,{code:'VALIDATION'});
79
- return fieldSchema(p[0]==='inputs'?(w.inputs??emptyObject):previous.get(p[1]),p.slice(p[0]==='inputs'?1:3),ref);
99
+ return resolveSchemaPath(p[0]==='inputs'?(w.inputs??emptyObject):previous.get(p[1]),p.slice(p[0]==='inputs'?1:3),ref);
80
100
  };
81
101
  const resolveList=async(steps,inherited=new Map(),parallel=false)=>{
82
102
  const previous=new Map(inherited),own={};
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mytools contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,31 @@
1
+ # 分发模块
2
+
3
+ 包名:`@mytools/pack`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
4
+
5
+ ## 职责与边界
6
+
7
+ 根据声明快照规划环境、收集生产依赖、复制运行时、生成清单和归档。不读取活动宿主或工作流存储;CLI 根目录作为发行载荷路径传入。
8
+
9
+ 内部依赖:`@mytools/sdk`、`@mytools/fst-platform`、`@mytools/fst-devtools`。
10
+ ## 公开入口
11
+
12
+ | 导入路径 | 实现位置 |
13
+ | --- | --- |
14
+ | `@mytools/pack` | `./index.js` |
15
+ | `@mytools/pack/dependencies` | `./dependencies.js` |
16
+ | `@mytools/pack/runtime` | `./runtime.js` |
17
+ | `@mytools/pack/environments` | `./environments.js` |
18
+ | `@mytools/pack/archive` | `./archive.js` |
19
+ | `@mytools/pack/package.json` | `./package.json` |
20
+
21
+ 仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
22
+
23
+ ## 维护与验证
24
+
25
+ 本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
26
+
27
+ 在仓库根目录执行:
28
+
29
+ ```sh
30
+ node --test tests/modules.test.js tests/pack.test.js tests/dependencies.test.js tests/npm-dist.test.js
31
+ ```
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+ import {createWriteStream} from 'node:fs';
3
+ import {ZipArchive,TarArchive} from 'archiver';
4
+
5
+ export async function archiveDirectory(directory,target,executablePaths=new Set()){
6
+ const name=path.basename(directory),archive=`${directory}.${target==='win32-x64'?'zip':'tar.gz'}`;
7
+ const stream=createWriteStream(archive),zip=target==='win32-x64'?new ZipArchive({zlib:{level:6}}):new TarArchive({gzip:true,gzipOptions:{level:6}});
8
+ await new Promise((resolve,reject)=>{
9
+ stream.on('close',resolve);stream.on('error',reject);zip.on('error',reject);zip.pipe(stream);
10
+ // Archiver 的回调给出源相对路径,顶层归档目录由库随后添加。
11
+ zip.directory(directory,name,data=>/\.sh$/.test(data.name)||executablePaths.has(data.name)?{...data,mode:0o755}:data);
12
+ zip.finalize().catch(reject);
13
+ });
14
+ return archive;
15
+ }
@@ -0,0 +1,105 @@
1
+ import Arborist from '@npmcli/arborist';
2
+ import packlist from 'npm-packlist';
3
+ import { access, copyFile, mkdir, writeFile, realpath,stat,chmod } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { FlowError } from '@mytools/sdk';
6
+
7
+ export async function collectPackages(roots,{omitOptionalPeers=[]}={}) {
8
+ const trees=new Map(), nodes=new Map();
9
+ async function locate(root){
10
+ root=await realpath(root);let base=root,installedRoot;
11
+ for(let current=root;current!==path.dirname(current);current=path.dirname(current)){
12
+ if(path.basename(current)==='node_modules'){
13
+ const owner=path.dirname(current);
14
+ try{await access(path.join(owner,'package.json'));installedRoot=owner;}catch{}
15
+ }
16
+ }
17
+ // 使用拥有这条 node_modules 链的最外层包,既保留提升依赖,也避开无关用户目录的锁文件。
18
+ if(installedRoot)base=installedRoot;
19
+ else{
20
+ while(base!==path.dirname(base)){
21
+ try{await access(path.join(base,'package-lock.json'));break;}catch{base=path.dirname(base);}
22
+ }
23
+ if(base===path.dirname(base))base=root;
24
+ }
25
+ if(!trees.has(base))trees.set(base,await new Arborist({path:base}).loadActual());
26
+ const tree=trees.get(base);
27
+ let found=[...tree.inventory.values()].find(n=>path.relative(n.realpath,root)==='' && !n.isLink);
28
+ // 开发引用可能嵌套在无关项目下,其上层 lock 不代表当前插件属于该依赖树。
29
+ if(!found&&base!==root){
30
+ if(!trees.has(root))trees.set(root,await new Arborist({path:root}).loadActual());
31
+ found=[...trees.get(root).inventory.values()].find(n=>path.relative(n.realpath,root)===''&&!n.isLink);
32
+ }
33
+ if(!found)throw new FlowError(`无法解析包的生产依赖:${root}`);
34
+ return found;
35
+ }
36
+ function visit(raw){
37
+ const node=raw.isLink?raw.target:raw;
38
+ if(nodes.has(node.realpath))return;
39
+ nodes.set(node.realpath,node);
40
+ for(const edge of node.edgesOut.values()){
41
+ if(edge.type==='dev')continue;
42
+ if(edge.type==='peerOptional'&&omitOptionalPeers.includes(edge.name))continue;
43
+ if(!edge.to){if(edge.optional)continue;throw new FlowError(`${node.name} 缺少依赖 ${edge.name}`);}
44
+ if(!edge.valid)throw new FlowError(`${node.name} 的依赖不兼容:${edge.name}`);
45
+ visit(edge.to);
46
+ }
47
+ }
48
+ for(const root of roots)visit(await locate(root));
49
+ return [...nodes.values()];
50
+ }
51
+
52
+ export async function copyPackages(nodes,destination,target,{filters=new Map()}={}){
53
+ const included=new Set(nodes.map(n=>n.realpath));
54
+ const top=new Map();
55
+ for(const n of nodes)if(!top.has(n.package.name))top.set(n.package.name,n);
56
+ for(const n of nodes)for(const edge of n.edgesOut.values())if(edge.type!=='dev'&&edge.to&&!top.has(edge.name)){
57
+ const child=edge.to.isLink?edge.to.target:edge.to;if(included.has(child.realpath))top.set(edge.name,child);
58
+ }
59
+ const copied=new Set(),summary=[];
60
+ const placements=new Map([...top].map(([name,node])=>[path.join(destination,'node_modules',name),node]));
61
+ function resolvedAt(from,name){
62
+ let current=from;
63
+ while(current.startsWith(destination+path.sep)||current===destination){
64
+ const placed=placements.get(path.join(current,'node_modules',name));
65
+ if(placed)return placed;
66
+ if(current===destination)break;
67
+ current=path.dirname(current);
68
+ }
69
+ }
70
+ async function copy(node,dest){
71
+ if(copied.has(dest))return;
72
+ copied.add(dest);await mkdir(dest,{recursive:true});
73
+ // packlist 对项目根只应用发布规则;依赖节点会额外收集依赖,所以单独读取包根。
74
+ const packageRoot=await realpath(node.realpath);
75
+ const filter=filters.get(packageRoot);
76
+ const files=[...new Set([...await packlist(await new Arborist({path:packageRoot}).loadActual()),...(filter?.extraFiles??[])])];
77
+ for(const file of files){
78
+ // 项目可以与开发插件共用根目录;宿主私有状态和安装批次永远不进入插件快照。
79
+ if(file.split(/[\\/]/)[0]==='.flow')continue;
80
+ if(filter&&!filter.include(file))continue;
81
+ const source=path.resolve(packageRoot,file),physical=await realpath(source);
82
+ // 两端先解析实际路径,再按系统规则比较,兼容 Windows 大小写、短路径和目录联接。
83
+ const relative=path.relative(packageRoot,physical);
84
+ if(!relative||relative==='..'||relative.startsWith(`..${path.sep}`)||path.isAbsolute(relative))throw new FlowError(`包包含外部资源链接:${node.name}/${file}`);
85
+ const output=path.join(dest,file);await mkdir(path.dirname(output),{recursive:true});await copyFile(source,output);
86
+ await chmod(output,(await stat(source)).mode);
87
+ }
88
+ summary.push({name:node.package.name,version:node.version,path:path.relative(destination,dest).split(path.sep).join('/'),license:node.package.license??'UNSPECIFIED'});
89
+ const nested=[];
90
+ // 先预留全部直接依赖的位置,再递归;兄弟依赖也会遮蔽其子包的查找路径。
91
+ for(const edge of node.edgesOut.values()){
92
+ if(edge.type==='dev'||!edge.to)continue;
93
+ const child=edge.to.isLink?edge.to.target:edge.to;
94
+ if(!included.has(child.realpath))continue;
95
+ if(resolvedAt(dest,edge.name)?.realpath===child.realpath)continue;
96
+ const childDest=path.join(dest,'node_modules',edge.name);
97
+ placements.set(childDest,child);nested.push([child,childDest]);
98
+ }
99
+ for(const [child,childDest] of nested)await copy(child,childDest);
100
+ }
101
+ for(const [name,node] of top)await copy(node,path.join(destination,'node_modules',name));
102
+ // 明确应用层安装根,让便携包在离线再次导出时可重建生产依赖树。
103
+ await writeFile(path.join(destination,'package.json'),JSON.stringify({name:'fishtools-bundle-app',private:true,dependencies:Object.fromEntries([...top].map(([name,node])=>[name,node.version]))},null,2));
104
+ return summary;
105
+ }
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+ import {realpath,readdir,stat} from 'node:fs/promises';
3
+ import {inside,resolveEnvironments,environmentDeclarations} from '@mytools/fst-platform/environments';
4
+ import {FlowError} from '@mytools/sdk';
5
+
6
+ // 只核对被声明的资源能否完整收集;版本和业务兼容性由作者负责。
7
+ export async function planEnvironments(plugins,target,overrides={}){
8
+ const entries=[],filters=new Map(),warnings=[],allFiles=new Map();
9
+ const platform=target.split('-')[0],architecture=target.split('-')[1];
10
+ for(const plugin of plugins){
11
+ if(plugin.platforms&&!plugin.platforms.includes(platform)||plugin.architectures&&!plugin.architectures.includes(architecture))warnings.push(`${plugin.id} 的作者声明未包含 ${target},仍按所选目标打包`);
12
+ const root=await realpath(plugin.root),excluded=new Set(),selected=new Set(),extraFiles=new Set();
13
+ for(const [actionName,action] of [['',undefined],...Object.entries(plugin.actions??{})]){
14
+ const declarations=environmentDeclarations(plugin,action);
15
+ const resolved=resolveEnvironments({...plugin,root},action,overrides[plugin.id],target);
16
+ for(const [id,declaration] of Object.entries(declarations)){
17
+ const e=resolved[id];
18
+ for(const artifact of Object.values(declaration.artifacts??{})){
19
+ const directory=inside(root,artifact.directory);if(directory===root)throw new FlowError('环境资源目录不能是插件根目录');
20
+ excluded.add(directory);
21
+ }
22
+ let bytes=0;
23
+ if(e.source==='bundled'){
24
+ selected.add(e.root);
25
+ const seen=new Set();
26
+ const scan=async dir=>{
27
+ const physical=await realpath(dir),relative=path.relative(root,physical);
28
+ if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))throw new FlowError(`环境资源越界:${dir}`);
29
+ const info=await stat(dir);
30
+ if(info.isDirectory()){
31
+ if(seen.has(physical))throw new FlowError(`环境目录循环:${dir}`);seen.add(physical);
32
+ for(const name of await readdir(dir))await scan(path.join(dir,name));
33
+ seen.delete(physical);
34
+ }
35
+ else if(info.isFile()){bytes+=info.size;extraFiles.add(path.relative(root,dir));allFiles.set(dir,info.size);}
36
+ else throw new FlowError(`环境资源不是普通文件:${dir}`);
37
+ };
38
+ await scan(e.root);
39
+ if(!(await stat(e.executable)).isFile())throw new FlowError(`环境入口不是文件:${e.executable}`);
40
+ }
41
+ const key=`${plugin.id}/${actionName||'*'}/${id}`;
42
+ entries.push({key,plugin:plugin.id,action:actionName||undefined,id,source:e.source,version:e.version,packages:e.packages,allowedSources:declaration.allowedSources??[declaration.source],bytes,executable:e.source==='bundled'?path.relative(root,e.executable).split(path.sep).join('/'):e.executable});
43
+ }
44
+ }
45
+ const within=(file,dir)=>file===dir||file.startsWith(dir+path.sep);
46
+ filters.set(root,{extraFiles:[...extraFiles],include:file=>{const full=path.resolve(root,file);return [...selected].some(dir=>within(full,dir))||![...excluded].some(dir=>within(full,dir));}});
47
+ }
48
+ for(const [plugin,values] of Object.entries(overrides))for(const id of Object.keys(values))if(!entries.some(e=>e.plugin===plugin&&e.id===id))throw new FlowError(`环境覆盖未匹配声明:${plugin}/${id}`);
49
+ return {entries,filters,warnings,bytes:[...allFiles.values()].reduce((sum,bytes)=>sum+bytes,0)};
50
+ }
@@ -0,0 +1,103 @@
1
+ import { mkdir, writeFile, readFile, readdir, chmod,cp } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { collectPackages, copyPackages } from './dependencies.js';
5
+ import { copyRuntime, digest } from './runtime.js';
6
+ import { FlowError } from '@mytools/sdk';
7
+ import YAML from 'yaml';
8
+ import { verifyBundle } from '@mytools/fst-platform/bundle';
9
+ import {planEnvironments} from './environments.js';
10
+ import {archiveDirectory} from './archive.js';
11
+ import {copyDocumentation} from '@mytools/fst-devtools/documentation';
12
+ export {verifyBundle};
13
+
14
+ async function hashes(root,dir=root,result={}){
15
+ for(const entry of await readdir(dir,{withFileTypes:true})){
16
+ const file=path.join(dir,entry.name);
17
+ if(entry.isDirectory())await hashes(root,file,result);
18
+ else if(entry.isFile())result[path.relative(root,file).split(path.sep).join('/')]=digest(await readFile(file));
19
+ else throw new FlowError(`分发目录包含未处理链接:${file}`);
20
+ }
21
+ return result;
22
+ }
23
+ function declaredPrivileges(closure,plugins){
24
+ const used=new Set();
25
+ const scan=steps=>{for(const step of steps??[]){
26
+ if(step.action)used.add(step.action);
27
+ if(step.branch){scan(step.branch.then);scan(step.branch.else);}
28
+ if(step.parallel)scan(step.parallel.steps);
29
+ }};
30
+ for(const workflow of closure.workflows)scan(workflow.steps);
31
+ const report=[];
32
+ for(const plugin of plugins){
33
+ if(plugin.requirements?.privilege)report.push({plugin:plugin.id,scope:'plugin',privilege:plugin.requirements.privilege});
34
+ for(const [name,action] of Object.entries(plugin.actions??{}))if(used.has(`${plugin.id}:${name}`)&&action.requirements?.privilege)report.push({plugin:plugin.id,action:name,scope:'action',privilege:action.requirements.privilege});
35
+ }
36
+ return report;
37
+ }
38
+ async function prepare(source,{entries,target,includeWeb=false,portable=false,environments={}}){
39
+ // 兼容既有 packApplication(app) 调用,但只使用明确的快照 Interface。
40
+ const snapshot=typeof source.exportSnapshot==='function'?await source.exportSnapshot({entries,includeWeb,portable}):source;
41
+ const {closure,plugins}=snapshot;entries=snapshot.entries;
42
+ if(!portable&&!entries?.length)throw new FlowError('请至少选择一个工作流');
43
+ if(!['win32-x64','linux-x64-glibc'].includes(target))throw new FlowError('请选择支持的目标平台');
44
+ const ids=new Set(plugins.map(p=>p.id));
45
+ if(plugins.some(p=>!p.root))throw new FlowError('内存中临时定义的插件不能打包,请先安装为标准包');
46
+ const overrides=structuredClone(snapshot.project.environments??{});
47
+ for(const [id,value] of Object.entries(environments))overrides[id]={...overrides[id],...value};
48
+ // 未选中的插件配置不进入导出,但显式拼错的覆盖项应报错。
49
+ for(const id of Object.keys(overrides))if(!ids.has(id)&&!Object.hasOwn(environments,id))delete overrides[id];
50
+ const environmentPlan=await planEnvironments(plugins,target,overrides);
51
+ return {closure,plugins,environmentPlan,overrides,entries,snapshot};
52
+ }
53
+ export async function planApplication(app,options={}){
54
+ const {plugins,closure,environmentPlan,entries,snapshot}=await prepare(app,options);
55
+ return {target:options.target,entries,plugins:plugins.map(p=>({id:p.id,platforms:p.platforms,architectures:p.architectures})),network:snapshot.project.network,privileges:declaredPrivileges(closure,plugins),environments:environmentPlan.entries,environmentBytes:environmentPlan.bytes,warnings:environmentPlan.warnings};
56
+ }
57
+ export async function packApplication(app,{entries,target,outDir='dist',includeWeb=false,runtimePath,cacheDir=path.resolve('.cache','runtimes'),readOnly=false,portable=false,environments={}}={}){
58
+ const prepared=await prepare(app,{entries,target,includeWeb,portable,environments});
59
+ const {closure,plugins,environmentPlan,overrides,snapshot}=prepared;entries=prepared.entries;
60
+ const nodes=await collectPackages([snapshot.toolRoot,...plugins.map(p=>p.root)],{omitOptionalPeers:readOnly?['@mytools/pack']:[]});
61
+ const name=`fishtools-${target}-${Date.now()}-${randomUUID().slice(0,6)}`;
62
+ const directory=path.resolve(outDir,name);await mkdir(directory,{recursive:true});
63
+ const packages=await copyPackages(nodes,path.join(directory,'app'),target,{filters:environmentPlan.filters});
64
+ let runtime;
65
+ const base=snapshot.project.baseBundle?.root;
66
+ if(base){
67
+ const previous=JSON.parse(await readFile(path.join(base,'manifest.json'),'utf8'));
68
+ if(previous.target===target&&(readOnly||previous.runtime.npm)){
69
+ if(!(await verifyBundle(base)).ok)throw new FlowError('原分发包完整性校验失败,不能复用运行时');
70
+ await cp(path.join(base,'runtime'),path.join(directory,'runtime'),{recursive:true,dereference:true});
71
+ await cp(path.join(base,'licenses'),path.join(directory,'licenses'),{recursive:true});runtime=previous.runtime;
72
+ }
73
+ }
74
+ runtime??=await copyRuntime({target,directory,cacheDir,runtimePath,includeNpm:!readOnly});
75
+ runtime={...runtime,npm:!readOnly||!!runtime.npm};
76
+ const workflowDir=path.join(directory,'workflows');await mkdir(workflowDir,{recursive:true});
77
+ // 保存来源文件,保持子流程相对引用;不打包未选中的流程。
78
+ for(const w of closure.workflows)await writeFile(path.join(workflowDir,`${w.id}.yaml`),YAML.stringify(w));
79
+ const config={};
80
+ for(const p of plugins)if(p.defaultConfig)config[p.id]=p.defaultConfig;
81
+ await writeFile(path.join(directory,'flow.project.json'),JSON.stringify({readOnly,plugins:plugins.map(p=>`./app/node_modules/${p.id}`),config,entries,environments:overrides,network:snapshot.project.network,logs:snapshot.project.logs},null,2));
82
+ const bundleId=randomUUID();
83
+ await writeFile(path.join(directory,'bundle.json'),JSON.stringify({format:2,id:bundleId,editable:!readOnly}));
84
+ await writeFile(path.join(directory,'package.json'),JSON.stringify({private:true,type:'module'},null,2));
85
+ const launch=target==='win32-x64'
86
+ ? '@echo off\r\n"%~dp0runtime\\node.exe" "%~dp0app\\node_modules\\@mytools\\fst-cli\\bin\\fst.js" --project "%~dp0." %*\r\n'
87
+ : '#!/bin/sh\nROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)\nexec "$ROOT/runtime/node" "$ROOT/app/node_modules/@mytools/fst-cli/bin/fst.js" --project "$ROOT" "$@"\n';
88
+ const launcher=path.join(directory,target==='win32-x64'?'run.cmd':'run.sh');await writeFile(launcher,launch);await chmod(launcher,0o755);
89
+ const fst=path.join(directory,target==='win32-x64'?'fst.cmd':'fst');await writeFile(fst,launch);await chmod(fst,0o755);
90
+ await mkdir(path.join(directory,'bin'),{recursive:true});
91
+ const binLaunch=target==='win32-x64'?'@echo off\r\ncall "%~dp0..\\fst.cmd" %*\r\n':'#!/bin/sh\nexec "$(dirname -- "$0")/../fst" "$@"\n';
92
+ await writeFile(path.join(directory,'bin',target==='win32-x64'?'fst.cmd':'fst'),binLaunch);
93
+ if(includeWeb){const web=target==='win32-x64'?'@echo off\r\ncall "%~dp0run.cmd" web %*\r\n':'#!/bin/sh\nexec "$(dirname -- "$0")/run.sh" web "$@"\n';const f=path.join(directory,target==='win32-x64'?'web.cmd':'web.sh');await writeFile(f,web);await chmod(f,0o755);}
94
+ const executablePaths=new Set(['runtime/node','fst','bin/fst']);
95
+ for(const e of environmentPlan.entries.filter(e=>e.source==='bundled'))for(const p of packages.filter(p=>p.name===e.plugin)){
96
+ const file=['app',p.path,e.executable].join('/');executablePaths.add(file);await chmod(path.join(directory,file),0o755);
97
+ }
98
+ await copyDocumentation(path.join(directory,'manual'));
99
+ const manifest={format:2,bundleId,editable:!readOnly,createdAt:new Date().toISOString(),target,runtime,entries,workflows:closure.workflows.map(w=>w.id),plugins:plugins.map(p=>({id:p.id,version:p.version,platforms:p.platforms,architectures:p.architectures})),network:snapshot.project.network,privileges:declaredPrivileges(closure,plugins),environments:environmentPlan.entries,warnings:environmentPlan.warnings,packages,files:await hashes(directory)};
100
+ await writeFile(path.join(directory,'manifest.json'),JSON.stringify(manifest,null,2));
101
+ const archive=await archiveDirectory(directory,target,executablePaths);
102
+ return {directory,archive,manifest};
103
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@mytools/pack",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "files": [
7
+ "*.js"
8
+ ],
9
+ "license": "MIT",
10
+ "dependencies": {
11
+ "@mytools/sdk": "0.1.0",
12
+ "@npmcli/arborist": "10.0.3",
13
+ "archiver": "8.0.0",
14
+ "make-fetch-happen": "16.0.1",
15
+ "npm-packlist": "11.3.0",
16
+ "tar": "7.5.22",
17
+ "yaml": "2.9.1",
18
+ "@mytools/fst-platform": "0.1.0",
19
+ "@mytools/fst-devtools": "0.1.0"
20
+ },
21
+ "exports": {
22
+ ".": "./index.js",
23
+ "./dependencies": "./dependencies.js",
24
+ "./runtime": "./runtime.js",
25
+ "./environments": "./environments.js",
26
+ "./archive": "./archive.js",
27
+ "./package.json": "./package.json"
28
+ }
29
+ }
@@ -0,0 +1,61 @@
1
+ import { mkdir, readFile, writeFile, copyFile, chmod,cp } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import * as tar from 'tar';
5
+ import fetch from 'make-fetch-happen';
6
+ import { FlowError } from '@mytools/sdk';
7
+
8
+ export const NODE_VERSION='24.15.0';
9
+ const hashes={
10
+ 'win32-x64':'3331e1ffe19874215472217c5e94f5a0c6d8e18c4ac7111d3937aa0ad5e9b4a5',
11
+ 'linux-x64-glibc':'44836872d9aec49f1e6b52a9a922872db9a2b02d235a616a5681b6a85fec8d89',
12
+ };
13
+ export const digest=data=>createHash('sha256').update(data).digest('hex');
14
+ async function download(url){const response=await fetch(url,{timeout:120000,retry:{retries:2}});if(!response.ok)throw new FlowError(`下载失败 ${response.status}:${url}`);return Buffer.from(await response.arrayBuffer());}
15
+
16
+ export async function copyRuntime({target,directory,cacheDir,runtimePath,includeNpm=false}){
17
+ if(!hashes[target])throw new FlowError(`尚不支持目标:${target}`);
18
+ await mkdir(path.join(directory,'runtime'),{recursive:true});
19
+ await mkdir(cacheDir,{recursive:true});
20
+ const win=target==='win32-x64';
21
+ const file=win?'win-x64/node.exe':`node-v${NODE_VERSION}-linux-x64.tar.gz`;
22
+ const cache=path.join(cacheDir,file.replaceAll('/','-'));
23
+ let bytes;
24
+ if(runtimePath){
25
+ if(!win)throw new FlowError('自定义 runtimePath 首版只接受已验证的 Windows Node.js 文件');
26
+ bytes=await readFile(runtimePath);
27
+ }else{
28
+ try{bytes=await readFile(cache);}catch{
29
+ bytes=await download(`https://nodejs.org/download/release/v${NODE_VERSION}/${file}`);
30
+ }
31
+ }
32
+ if(digest(bytes)!==hashes[target])throw new FlowError('Node.js 运行时校验失败,请检查缓存或指定文件');
33
+ await writeFile(cache,bytes);
34
+ if(win)await writeFile(path.join(directory,'runtime','node.exe'),bytes);
35
+ else{
36
+ const extraction=path.join(cacheDir,'linux-extracted');await mkdir(extraction,{recursive:true});
37
+ await tar.x({file:cache,cwd:extraction,filter:name=>name.endsWith('/bin/node')||name.endsWith('/LICENSE')});
38
+ await copyFile(path.join(extraction,`node-v${NODE_VERSION}-linux-x64/bin/node`),path.join(directory,'runtime','node'));
39
+ await chmod(path.join(directory,'runtime','node'),0o755);
40
+ }
41
+ const licenseCache=path.join(cacheDir,'NODE_LICENSE');
42
+ let license;
43
+ try{license=await readFile(licenseCache);}catch{
44
+ try{if(process.versions.node!==NODE_VERSION)throw new Error('版本不同');license=await readFile(path.join(path.dirname(process.execPath),'LICENSE'));}
45
+ catch{license=await download(`https://raw.githubusercontent.com/nodejs/node/v${NODE_VERSION}/LICENSE`);}
46
+ await writeFile(licenseCache,license);
47
+ }
48
+ await mkdir(path.join(directory,'licenses'),{recursive:true});await writeFile(path.join(directory,'licenses','NODE_LICENSE'),license);
49
+ if(includeNpm){
50
+ // npm 是平台无关的 JS,直接复用已固定校验的官方 Linux 分发中的完整 npm 目录。
51
+ const npmArchive=path.join(cacheDir,`node-v${NODE_VERSION}-linux-x64.tar.gz`);
52
+ let npmBytes;
53
+ try{npmBytes=await readFile(npmArchive);}catch{npmBytes=await download(`https://nodejs.org/download/release/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz`);}
54
+ if(digest(npmBytes)!==hashes['linux-x64-glibc'])throw new FlowError('npm 来源的 Node.js 归档校验失败');
55
+ await writeFile(npmArchive,npmBytes);
56
+ const extraction=path.join(cacheDir,'npm-extracted');await mkdir(extraction,{recursive:true});
57
+ await tar.x({file:npmArchive,cwd:extraction,filter:name=>name.includes('/lib/node_modules/npm/')});
58
+ await cp(path.join(extraction,`node-v${NODE_VERSION}-linux-x64/lib/node_modules/npm`),path.join(directory,'runtime/node_modules/npm'),{recursive:true,dereference:true});
59
+ }
60
+ return {version:NODE_VERSION,source:`https://nodejs.org/download/release/v${NODE_VERSION}/${file}`,sha256:hashes[target]};
61
+ }