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