@yanglaofish/fishtools 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -5
- package/bin/fst.js +1 -1
- package/manual/README.md +33 -5
- package/manual/docs/deployment.md +3 -3
- package/manual/docs/examples/script-plugin/LICENSE +21 -0
- package/manual/docs/examples/script-plugin/README.md +64 -0
- package/manual/docs/examples/script-plugin/index.js +39 -0
- package/manual/docs/examples/script-plugin/package.json +9 -0
- package/manual/docs/examples/script-plugin/scripts/benchmark.py +23 -0
- package/manual/docs/kernel-generation.md +97 -0
- package/manual/docs/modules.md +57 -0
- package/manual/docs/plugin-development.md +163 -12
- package/manual/docs/releases.md +23 -0
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
- package/manual/docs/roadmap.md +16 -3
- package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
- package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
- package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
- package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
- package/manual/docs/superpowers/plans/2026-10-12-plugin-developer-experience.md +50 -0
- package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
- package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
- package/manual/docs/superpowers/reviews/2026-10-12-plugin-release-review.md +20 -0
- package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
- package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
- package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
- package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
- package/manual/docs/superpowers/specs/2026-10-12-plugin-developer-experience.md +22 -0
- package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
- package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
- package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
- package/manual/docs/verification.md +58 -1
- package/manual/packages/core/README.md +44 -0
- package/manual/packages/core/package.json +38 -0
- package/manual/packages/core/src/application.js +268 -87
- package/manual/packages/core/src/bundle.js +2 -0
- package/manual/packages/core/src/development-watch-worker.js +69 -0
- package/manual/packages/core/src/development.js +146 -0
- package/manual/packages/core/src/environments.js +2 -0
- package/manual/packages/core/src/export-snapshot.js +42 -0
- package/manual/packages/core/src/plugin-update.js +108 -0
- package/manual/packages/core/src/process.js +2 -0
- package/manual/packages/core/src/resources.js +31 -0
- package/manual/packages/core/src/version.js +2 -0
- package/manual/packages/fst-cli/LICENSE +21 -0
- package/manual/packages/fst-cli/README.md +54 -0
- package/manual/packages/fst-cli/bin/fst.js +124 -0
- package/manual/packages/fst-cli/package.json +49 -0
- package/manual/packages/fst-cli/src/development.js +53 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-feed.js +31 -5
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
- package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
- package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
- package/manual/packages/fst-devtools/LICENSE +21 -0
- package/manual/packages/fst-devtools/README.md +28 -0
- package/manual/packages/fst-devtools/package.json +29 -0
- package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
- package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
- package/manual/packages/fst-devtools/src/plugin-tools.js +152 -0
- package/manual/packages/fst-logs/LICENSE +21 -0
- package/manual/packages/fst-logs/README.md +56 -0
- package/manual/packages/fst-logs/package.json +24 -0
- package/manual/packages/fst-logs/src/log-bounds.js +91 -0
- package/manual/packages/fst-logs/src/run-logs.js +239 -0
- package/manual/packages/fst-platform/LICENSE +21 -0
- package/manual/packages/fst-platform/README.md +67 -0
- package/manual/packages/fst-platform/package.json +33 -0
- package/manual/packages/fst-platform/src/bundle.js +17 -0
- package/manual/packages/fst-platform/src/environments.js +67 -0
- package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
- package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
- package/manual/packages/fst-platform/src/file-probe.js +6 -0
- package/manual/packages/fst-platform/src/json.js +5 -0
- package/manual/packages/fst-platform/src/network.js +199 -0
- package/manual/packages/fst-platform/src/npm.js +11 -0
- package/manual/packages/fst-platform/src/process.js +110 -0
- package/manual/packages/fst-plugin/LICENSE +21 -0
- package/manual/packages/fst-plugin/README.md +29 -0
- package/manual/packages/fst-plugin/package.json +29 -0
- package/manual/packages/fst-plugin/src/contracts.js +86 -0
- package/manual/packages/fst-plugin/src/host.js +232 -0
- package/manual/packages/fst-plugin/src/loader.js +21 -0
- package/manual/packages/fst-plugin/src/privileges.js +91 -0
- package/manual/packages/{core → fst-plugin}/src/runtime.js +20 -4
- package/manual/packages/fst-workflow/LICENSE +21 -0
- package/manual/packages/fst-workflow/README.md +28 -0
- package/manual/packages/fst-workflow/package.json +24 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
- package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
- package/manual/packages/fst-workspace/LICENSE +21 -0
- package/manual/packages/fst-workspace/README.md +32 -0
- package/manual/packages/fst-workspace/package.json +34 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
- package/manual/packages/fst-workspace/src/index.js +3 -0
- package/manual/packages/fst-workspace/src/plugin-installations.js +84 -0
- package/manual/packages/fst-workspace/src/plugins.js +35 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +41 -21
- package/manual/packages/pack/LICENSE +21 -0
- package/manual/packages/pack/README.md +31 -0
- package/manual/packages/pack/archive.js +15 -0
- package/manual/packages/pack/dependencies.js +105 -0
- package/manual/packages/pack/environments.js +50 -0
- package/manual/packages/pack/index.js +103 -0
- package/manual/packages/pack/package.json +29 -0
- package/manual/packages/pack/runtime.js +61 -0
- package/manual/packages/sdk/README.md +31 -0
- package/manual/packages/sdk/conditions.js +1 -0
- package/manual/packages/sdk/package.json +23 -1
- package/manual/packages/sdk/schema-path.js +31 -0
- package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +22 -9
- package/manual/plugins/web/README.md +29 -0
- package/manual/tests/application.test.js +6 -4
- package/node_modules/@mytools/core/README.md +44 -0
- package/node_modules/@mytools/core/package.json +17 -15
- package/node_modules/@mytools/core/src/application.js +268 -87
- package/node_modules/@mytools/core/src/bundle.js +2 -17
- package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
- package/node_modules/@mytools/core/src/development.js +146 -0
- package/node_modules/@mytools/core/src/environments.js +2 -29
- package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
- package/node_modules/@mytools/core/src/plugin-update.js +108 -0
- package/node_modules/@mytools/core/src/process.js +2 -71
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/LICENSE +21 -0
- package/node_modules/@mytools/fst-cli/README.md +54 -0
- package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
- package/node_modules/@mytools/fst-cli/package.json +49 -0
- package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
- package/node_modules/@mytools/fst-cli/src/development.js +53 -0
- package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
- package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
- package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
- package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
- package/{manual/packages/core → node_modules/@mytools/fst-cli}/src/terminal/run-feed.js +31 -5
- package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
- package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
- package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
- package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
- package/node_modules/@mytools/fst-devtools/README.md +28 -0
- package/node_modules/@mytools/fst-devtools/package.json +29 -0
- package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
- package/node_modules/@mytools/fst-logs/LICENSE +21 -0
- package/node_modules/@mytools/fst-logs/README.md +56 -0
- package/node_modules/@mytools/fst-logs/package.json +24 -0
- package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
- package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
- package/node_modules/@mytools/fst-logs/src/run-logs.js +239 -0
- package/node_modules/@mytools/fst-platform/LICENSE +21 -0
- package/node_modules/@mytools/fst-platform/README.md +67 -0
- package/node_modules/@mytools/fst-platform/package.json +33 -0
- package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
- package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
- package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
- package/node_modules/@mytools/fst-platform/src/json.js +5 -0
- package/node_modules/@mytools/fst-platform/src/network.js +199 -0
- package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
- package/node_modules/@mytools/fst-platform/src/process.js +110 -0
- package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
- package/node_modules/@mytools/fst-plugin/README.md +29 -0
- package/node_modules/@mytools/fst-plugin/package.json +29 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
- package/node_modules/@mytools/fst-plugin/src/host.js +232 -0
- package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
- package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
- package/node_modules/@mytools/{core → fst-plugin}/src/runtime.js +20 -4
- package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
- package/node_modules/@mytools/fst-workflow/README.md +28 -0
- package/node_modules/@mytools/fst-workflow/package.json +24 -0
- package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
- package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
- package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
- package/node_modules/@mytools/fst-workspace/README.md +32 -0
- package/node_modules/@mytools/fst-workspace/package.json +34 -0
- package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
- package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
- package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
- package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
- package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
- package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
- package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
- package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
- package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
- package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
- package/node_modules/@mytools/pack/README.md +31 -0
- package/node_modules/@mytools/pack/dependencies.js +15 -3
- package/node_modules/@mytools/pack/environments.js +1 -1
- package/node_modules/@mytools/pack/index.js +33 -25
- package/node_modules/@mytools/pack/package.json +29 -1
- package/node_modules/@mytools/sdk/README.md +31 -0
- package/node_modules/@mytools/sdk/conditions.js +1 -0
- package/node_modules/@mytools/sdk/diagnostics.js +27 -0
- package/node_modules/@mytools/sdk/package.json +23 -1
- package/node_modules/@mytools/sdk/schema-path.js +31 -0
- package/node_modules/@mytools/sdk/validation.js +78 -0
- package/node_modules/@mytools/web/README.md +29 -0
- package/node_modules/@mytools/web/package.json +7 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1275 -1076
- package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
- package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
- package/node_modules/@mytools/web/public/index.html +2 -2
- package/node_modules/@mytools/web/server.js +24 -17
- package/node_modules/@mytools/web/src/model.js +84 -0
- package/node_modules/acorn/CHANGELOG.md +1012 -0
- package/node_modules/acorn/LICENSE +21 -0
- package/node_modules/acorn/README.md +309 -0
- package/node_modules/acorn/bin/acorn +4 -0
- package/node_modules/acorn/dist/acorn.d.mts +897 -0
- package/node_modules/acorn/dist/acorn.d.ts +897 -0
- package/node_modules/acorn/dist/acorn.js +6370 -0
- package/node_modules/acorn/dist/acorn.mjs +6341 -0
- package/node_modules/acorn/dist/bin.js +90 -0
- package/node_modules/acorn/package.json +50 -0
- package/package.json +117 -101
- package/manual/packages/core/src/host.js +0 -124
- package/node_modules/@mytools/core/bin/fst.js +0 -70
- package/node_modules/@mytools/core/src/contracts.js +0 -41
- package/node_modules/@mytools/core/src/host.js +0 -124
- package/node_modules/@mytools/core/src/plugins.js +0 -55
- package/node_modules/@mytools/core/src/run-logs.js +0 -63
- package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
- package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
|
@@ -1,75 +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 '
|
|
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';
|
|
17
25
|
|
|
18
|
-
export async function createApplication({projectDir=process.cwd(),workspace,outputDir,plugins,config={},maintenance=false}={}) {
|
|
26
|
+
export async function createApplication({projectDir=process.cwd(),workspace,outputDir,plugins,config={},maintenance=false,omitInstalledPlugin,allowUnknownPrivileges=false}={}) {
|
|
19
27
|
projectDir=await prepareBundleWorkspace(path.resolve(projectDir),workspace);
|
|
20
28
|
const project=await readJson(path.join(projectDir,'flow.project.json'),{plugins:[]});
|
|
21
29
|
const output=path.resolve(outputDir??process.env.FLOW_OUTPUT_DIR??(project.readOnly?path.join(process.cwd(),'flow-results'):path.join(projectDir,'.flow','runs')));
|
|
22
30
|
const store=new WorkflowStore(path.join(projectDir,'workflows'));
|
|
23
|
-
const emitter=new EventEmitter(), runs=new Map();
|
|
31
|
+
const emitter=new EventEmitter(), runs=new Map(),journals=new WeakMap();
|
|
24
32
|
const resources=createResources(),operations=new Set();let closing,launching=0;
|
|
25
|
-
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);
|
|
26
37
|
validateKeepRuns(project.logs?.keepRuns??null);
|
|
27
|
-
|
|
38
|
+
const opened=await openPluginState(projectDir,{readOnly:project.readOnly});
|
|
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);
|
|
28
42
|
const app={
|
|
29
43
|
projectDir,project,outputDir:output,store,
|
|
30
44
|
get host(){return host;},
|
|
31
45
|
get maintenance(){return maintenance;},
|
|
46
|
+
developmentStatus:()=>({running:launching+writingRuns.size+operations.size,busy,closing:!!closing}),
|
|
32
47
|
catalog:()=>host.catalog(),
|
|
33
|
-
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]})),
|
|
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)})),
|
|
34
49
|
listCommands:()=>host.commands(),
|
|
35
50
|
listWorkflows:()=>store.list(),
|
|
36
|
-
async saveWorkflow(w){
|
|
37
|
-
async
|
|
38
|
-
|
|
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
|
+
},
|
|
39
59
|
subscribe(fn){emitter.on('event',fn);return ()=>emitter.off('event',fn);},
|
|
40
|
-
async startRun(id,input={}) {
|
|
60
|
+
async startRun(id,input={},options={}) {
|
|
41
61
|
if(busy) throw new FlowError('插件更新中,请稍后运行');
|
|
42
|
-
const closure=await
|
|
62
|
+
const closure=await app.checkWorkflow(id);
|
|
63
|
+
const blocked=closure.privileges.filter(item=>item.mandatory&&item.static.status==='blocked');
|
|
43
64
|
const snapshotStore={resolve:async()=>structuredClone(closure),childId:ref=>store.childId(ref)};
|
|
44
|
-
|
|
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
|
+
});
|
|
45
70
|
},
|
|
46
71
|
async startCommand(name,input={},context={}){
|
|
47
72
|
if(busy)throw new FlowError('插件更新中,请稍后调用');
|
|
48
|
-
|
|
73
|
+
const networkPolicy=structuredClone(project.network??{});
|
|
74
|
+
return app.executeTracked(`/${name}`,(activeHost,runContext)=>activeHost.invokeCommand(name,input,{...context,...runContext,networkPolicy}));
|
|
49
75
|
},
|
|
50
76
|
async executeTracked(id,execute){
|
|
51
|
-
const runId=randomUUID(),
|
|
52
|
-
|
|
53
|
-
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});
|
|
54
83
|
runs.set(runId,run);
|
|
55
84
|
writingRuns.add(runId);
|
|
56
|
-
const
|
|
57
|
-
let writes=writeFile(markdownFile,markdownHeader(run));
|
|
58
|
-
const event=e=>{
|
|
59
|
-
const full={time:new Date().toISOString(),runId,...e};run.events.push(full);
|
|
60
|
-
emitter.emit('event',full);
|
|
61
|
-
const {output:ignored,...record}=full;
|
|
62
|
-
writes=writes.then(async()=>{
|
|
63
|
-
await appendFile(path.join(dir,'events.jsonl'),JSON.stringify(record)+'\n');
|
|
64
|
-
await appendFile(markdownFile,markdownEvent(full));
|
|
65
|
-
});
|
|
66
|
-
return writes;
|
|
67
|
-
};
|
|
85
|
+
const event=e=>journal.append(e);
|
|
68
86
|
const activeHost=host;
|
|
69
87
|
run.done=(async()=>{
|
|
70
88
|
try {
|
|
71
89
|
await event({type:'run.started'});
|
|
72
|
-
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);
|
|
73
91
|
run.status='succeeded';await event({type:'run.succeeded'});
|
|
74
92
|
} catch(error) {
|
|
75
93
|
if(error?.code==='WORKFLOW_STOPPED'){
|
|
@@ -82,23 +100,48 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
82
100
|
}
|
|
83
101
|
} finally {
|
|
84
102
|
run.finishedAt=new Date().toISOString();
|
|
85
|
-
await
|
|
86
|
-
await appendFile(markdownFile,markdownResult(app.getRun(runId)));
|
|
87
|
-
await writeFile(path.join(dir,'result.json'),JSON.stringify(snapshotRun(run),null,2));
|
|
88
|
-
writingRuns.delete(runId);
|
|
103
|
+
try{await journal.finish(runState(run));}finally{writingRuns.delete(runId);}
|
|
89
104
|
try{await pruneLogs();}catch(error){
|
|
90
|
-
|
|
91
|
-
await
|
|
105
|
+
run.warnings=[{code:'LOG_PRUNE_FAILED',message:`日志自动清理失败:${error.message}`}];
|
|
106
|
+
await journal.save(runState(run));
|
|
92
107
|
}
|
|
93
108
|
}
|
|
94
|
-
|
|
109
|
+
const result=snapshotRun(run);trimRuns();return result;
|
|
95
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
|
+
});
|
|
96
117
|
return run;
|
|
97
118
|
},
|
|
98
119
|
getRun(id){const r=runs.get(id);return r?snapshotRun(r):null;},
|
|
99
|
-
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),
|
|
100
122
|
listRuns:()=>[...runs.keys()].map(id=>{const {events,...r}=app.getRun(id);return r;}),
|
|
101
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(),
|
|
102
145
|
async listRunHistory(){
|
|
103
146
|
const records=new Map((await runLogs.list()).map(r=>[r.id,r]));
|
|
104
147
|
for(const id of runs.keys())records.set(id,app.getRun(id));
|
|
@@ -106,39 +149,65 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
106
149
|
},
|
|
107
150
|
async readLog(id){
|
|
108
151
|
const run=runs.get(id);
|
|
109
|
-
if(run&&writingRuns.has(id))return
|
|
152
|
+
if(run&&writingRuns.has(id))return journals.get(run).markdown(runState(run));
|
|
110
153
|
return runLogs.markdown(id);
|
|
111
154
|
},
|
|
112
155
|
async configureLogs(keepRuns){
|
|
113
156
|
validateKeepRuns(keepRuns);if(project.readOnly)throw new FlowError('只读运行包不能保存日志设置');
|
|
114
157
|
if(busy)throw new FlowError('项目正在更新,请稍后修改日志设置');
|
|
115
|
-
busy=true;
|
|
158
|
+
busy=true;let backup,warning;
|
|
116
159
|
try{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
await withPluginMutation(projectDir,async()=>{
|
|
161
|
+
const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
|
|
162
|
+
if(latest.readOnly)throw new FlowError('只读运行包不能保存日志设置');
|
|
163
|
+
backup=await backupProjectFile(file,latest,'logs');
|
|
164
|
+
const nextProject={...latest,logs:{...latest.logs,keepRuns}},temp=`${file}.${randomUUID()}.tmp`;
|
|
165
|
+
await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);
|
|
166
|
+
for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
|
|
167
|
+
try{await pruneLogs();}
|
|
168
|
+
catch(error){warning={code:'LOG_PRUNE_FAILED',message:`日志保留设置已保存,但旧日志清理失败:${error.message}`};emitter.emit('event',{type:'logs.prune.warning',...warning});}
|
|
169
|
+
});
|
|
120
170
|
}finally{busy=false;}
|
|
171
|
+
return {keepRuns,backup,...(warning?{warning}:{})};
|
|
121
172
|
},
|
|
122
173
|
cancelRun(id){const r=runs.get(id);if(r?.status==='running')r.controller.abort();return !!r;},
|
|
123
|
-
async installPlugin(source){return change({add:source});},
|
|
124
|
-
async
|
|
125
|
-
async
|
|
126
|
-
|
|
127
|
-
|
|
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={}){
|
|
128
183
|
if(project.readOnly)throw new FlowError('只读项目不能保存配置');
|
|
129
|
-
assertMutable();
|
|
130
|
-
if(resources.active(id))throw new FlowError('请先停止此插件提供的 Web,再修改配置');
|
|
131
184
|
if(busy||launching||app.listRuns().some(r=>r.status==='running'))throw new FlowError('请等待运行或安装结束');
|
|
132
185
|
if(!host.definitions.has(id))throw new FlowError(`插件不存在:${id}`);
|
|
133
186
|
value=validate(host.definitions.get(id).config,value,`${id} 配置`);
|
|
134
|
-
busy=true;let
|
|
187
|
+
busy=true;let backup;
|
|
135
188
|
try{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
189
|
+
await withPluginMutation(projectDir,async()=>{
|
|
190
|
+
await assertPluginState(projectDir,state);
|
|
191
|
+
const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
|
|
192
|
+
if(latest.readOnly)throw new FlowError('只读项目不能保存配置');
|
|
193
|
+
const nextConfig={...latest.config,[id]:value};
|
|
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});
|
|
200
|
+
const nextProject={...latest,config:nextConfig},temp=`${file}.${randomUUID()}.tmp`;
|
|
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
|
+
}
|
|
207
|
+
for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
|
|
208
|
+
});
|
|
209
|
+
}finally{busy=false;}
|
|
210
|
+
return {backup};
|
|
142
211
|
},
|
|
143
212
|
async setMaintenance(enabled){
|
|
144
213
|
if(typeof enabled!=='boolean')throw new FlowError('维护模式必须为布尔值');
|
|
@@ -149,7 +218,7 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
149
218
|
try{
|
|
150
219
|
const entries=[...host.definitions.values()].map(p=>({plugin:p,root:p.root}));
|
|
151
220
|
if(enabled){maintenance=true;await host.close();}
|
|
152
|
-
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});
|
|
153
222
|
const old=host;host=next;maintenance=enabled;if(!enabled)await old.close();
|
|
154
223
|
}finally{busy=false;}
|
|
155
224
|
},
|
|
@@ -167,17 +236,24 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
167
236
|
const errors=[];
|
|
168
237
|
try{await resources.close();}catch(error){errors.push(error);}
|
|
169
238
|
try{await host.close();}catch(error){errors.push(error);}
|
|
239
|
+
try{const report=await fileProbe.close();if(report.incomplete)errors.push(new FlowError('文件探测进程尚未全部回收',{code:'FS_CLEANUP_INCOMPLETE',details:report}));}catch(error){errors.push(error);}
|
|
240
|
+
for(const release of installationLeases.values())try{await release();}catch(error){errors.push(error);}
|
|
170
241
|
if(errors.length)throw new AggregateError(errors,`应用关闭失败:${errors.map(e=>e.message).join(';')}`);
|
|
171
242
|
});return closing;
|
|
172
243
|
},
|
|
173
244
|
};
|
|
174
|
-
|
|
175
|
-
const services={application:app,runProcess};
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
245
|
+
let initial;
|
|
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});
|
|
249
|
+
try{
|
|
250
|
+
initial=plugins??await Promise.all((project.plugins??[]).map(spec=>loadPlugin(spec,project.baseBundle?.root??projectDir)));
|
|
251
|
+
const external=[];
|
|
252
|
+
for(const name of Object.keys(state.dependencies??{}))if(!maintenance||name!==omitInstalledPlugin)external.push(await loadPlugin(name,state.root));
|
|
253
|
+
for(const [name,source] of Object.entries(state.links??{}))if(!maintenance||name!==omitInstalledPlugin)external.push(await loadPlugin(source,projectDir));
|
|
254
|
+
host=await build(external);
|
|
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']){
|
|
181
257
|
const original=app[name],isRun=['startRun','startCommand'].includes(name);
|
|
182
258
|
app[name]=async(...args)=>{
|
|
183
259
|
if(closing)throw new FlowError('应用已关闭');
|
|
@@ -186,26 +262,131 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
186
262
|
try{return await task;}finally{operations.delete(task);if(isRun)launching--;}
|
|
187
263
|
};
|
|
188
264
|
}
|
|
189
|
-
function
|
|
190
|
-
|
|
265
|
+
async function mutateWorkflow(operation){
|
|
266
|
+
if(project.readOnly)throw new FlowError('只读运行包不能修改工作流');
|
|
267
|
+
if(busy)throw new FlowError('插件变更期间暂不能修改工作流',{code:'PLUGIN_BUSY'});
|
|
268
|
+
return withPluginMutation(projectDir,async()=>{
|
|
269
|
+
await assertPluginState(projectDir,state);
|
|
270
|
+
return operation();
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
async function backupProjectFile(file,latest,label){
|
|
274
|
+
const directory=path.join(projectDir,'.flow','backups');
|
|
275
|
+
await mkdir(directory,{recursive:true,mode:0o700});
|
|
276
|
+
const backup=path.join(directory,`flow.project.${label}-${randomUUID()}.json`);
|
|
277
|
+
let original;
|
|
278
|
+
try{original=await readFile(file,'utf8');}
|
|
279
|
+
catch(error){if(error.code!=='ENOENT')throw error;original=JSON.stringify(latest,null,2);}
|
|
280
|
+
await writeFile(backup,original,{flag:'wx',mode:0o600});
|
|
281
|
+
return backup;
|
|
282
|
+
}
|
|
283
|
+
function pluginPrivileges(plugin){
|
|
284
|
+
const report=action=>{
|
|
285
|
+
const requirement=resolvePrivilegeRequirement(plugin,action);
|
|
286
|
+
return {action:action===undefined?null:`${plugin.id}:${action}`,requirement,static:checkStaticPrivilege(requirement)};
|
|
287
|
+
};
|
|
288
|
+
return [report(),...Object.keys(plugin.actions??{}).map(report)];
|
|
289
|
+
}
|
|
290
|
+
function assertMaintenance(plan,options={}){
|
|
291
|
+
if(!maintenance&&plan.maintenance.length&&!options.allowMaintenance)throw new FlowError(`更新将重启插件 ${plan.maintenance.join('、')},请确认本次定向维护`,{code:'MAINTENANCE_REQUIRED',details:{plugins:plan.maintenance,affected:plan.affected}});
|
|
292
|
+
const active=plan.affected.filter(id=>resources.active(id));
|
|
293
|
+
if(active.length)throw new FlowError(`请先停止受影响插件提供的 Web:${active.join('、')}`,{code:'WEB_ACTIVE',details:{plugins:active}});
|
|
294
|
+
}
|
|
295
|
+
function workflowPrivileges(closure,entryId){
|
|
296
|
+
const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[];
|
|
297
|
+
const visit=(id,mandatory)=>{
|
|
298
|
+
const workflow=flows.get(id);
|
|
299
|
+
if(!workflow)return;
|
|
300
|
+
const steps=(list,required)=>{
|
|
301
|
+
let available=required;
|
|
302
|
+
for(const step of list){
|
|
303
|
+
const needed=available&&!step.when;
|
|
304
|
+
if(step.action){
|
|
305
|
+
const pluginId=step.action.slice(0,step.action.lastIndexOf(':')),plugin=host.definitions.get(pluginId);
|
|
306
|
+
const requirement=resolvePrivilegeRequirement(plugin,step.action.slice(pluginId.length+1));
|
|
307
|
+
report.push({workflowId:id,stepId:step.id,action:step.action,mandatory:needed,requirement,static:checkStaticPrivilege(requirement)});
|
|
308
|
+
}
|
|
309
|
+
if(step.workflow)visit(store.childId(step.workflow),needed);
|
|
310
|
+
if(step.branch){steps(step.branch.then,false);steps(step.branch.else??[],false);}
|
|
311
|
+
if(step.parallel)steps(step.parallel.steps,needed);
|
|
312
|
+
if(step.stop)available=false;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
steps(workflow.steps,mandatory);
|
|
316
|
+
};
|
|
317
|
+
visit(entryId,true);return report;
|
|
318
|
+
}
|
|
319
|
+
async function findPluginWorkflowReferences(id){
|
|
320
|
+
const refs=[];
|
|
321
|
+
const collect=(steps,found)=>{for(const step of steps??[]){
|
|
322
|
+
if(typeof step.action==='string'&&step.action.startsWith(`${id}:`))found.add(step.action);
|
|
323
|
+
if(step.branch){collect(step.branch.then,found);collect(step.branch.else,found);}
|
|
324
|
+
if(step.parallel)collect(step.parallel.steps,found);
|
|
325
|
+
}};
|
|
326
|
+
for(const workflow of await store.list()){
|
|
327
|
+
const found=new Set();collect(workflow.steps,found);
|
|
328
|
+
if(found.size)refs.push({id:workflow.id,title:workflow.title??workflow.id,actions:[...found]});
|
|
329
|
+
}
|
|
330
|
+
return refs;
|
|
331
|
+
}
|
|
332
|
+
function runState(run){const {done,controller,...result}=run;return result;}
|
|
333
|
+
function snapshotRun(run){return journals.get(run).snapshot(runState(run));}
|
|
334
|
+
function trimRuns(){
|
|
335
|
+
const completed=[...runs.values()].filter(run=>!writingRuns.has(run.id)&&run.status!=='running');
|
|
336
|
+
for(const run of completed.slice(0,Math.max(0,completed.length-maxCachedRuns)))runs.delete(run.id);
|
|
337
|
+
// 超大业务结果交给 done 的调用方;宿主历史改读磁盘摘要,避免长期持有大对象。
|
|
338
|
+
for(const run of completed){
|
|
339
|
+
try{if(run.output!==undefined&&Buffer.byteLength(JSON.stringify(run.output))>(project.logs?.maxMemoryBytes??1048576)){
|
|
340
|
+
if(run.error?.code==='LOG_WRITE_FAILED'){delete run.output;run.outputTruncated=true;}else runs.delete(run.id);
|
|
341
|
+
}}
|
|
342
|
+
catch{delete run.output;run.outputTruncated=true;}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
191
345
|
async function pruneLogs(){for(const id of await runLogs.prune(project.logs?.keepRuns??null,writingRuns))runs.delete(id);}
|
|
192
|
-
async function change(operation){
|
|
346
|
+
async function change(operation,options={}){
|
|
193
347
|
if(project.readOnly)throw new FlowError('运行包不支持安装或移除插件');
|
|
194
|
-
assertMutable();
|
|
195
348
|
if(busy || launching || [...runs.values()].some(r=>r.status==='running'))throw new FlowError('请等待当前运行或安装结束');
|
|
196
|
-
busy=true;let
|
|
349
|
+
busy=true;let candidate,nextRelease;
|
|
197
350
|
try {
|
|
351
|
+
return await withPluginMutation(projectDir,async()=>{
|
|
352
|
+
await assertPluginState(projectDir,state);
|
|
353
|
+
const latest=await readJson(path.join(projectDir,'flow.project.json'),project);
|
|
354
|
+
if(latest.readOnly)throw new FlowError('运行包不支持安装或移除插件');
|
|
355
|
+
if(operation.remove){
|
|
356
|
+
const name=operation.remove;
|
|
357
|
+
if(resources.active(name))throw new FlowError('请先停止此插件提供的 Web');
|
|
358
|
+
if(!state.dependencies?.[name]&&!state.links?.[name])throw new FlowError('此插件来自发行配置,不能在此移除');
|
|
359
|
+
const dependents=[...host.definitions.values()].filter(p=>p.id!==name&&Object.hasOwn(p.dependencies??{},name)).map(p=>p.id);
|
|
360
|
+
if(dependents.length)throw new FlowError(`插件仍被其他插件依赖:${dependents.join('、')}`,{code:'PLUGIN_IN_USE',details:{dependencies:dependents}});
|
|
361
|
+
const workflows=await findPluginWorkflowReferences(name);
|
|
362
|
+
if(workflows.length&&!options.force)throw new FlowError(`插件 ${name} 仍被工作流引用:${workflows.map(w=>w.id).join('、')}`,{code:'PLUGIN_IN_USE',details:{workflows}});
|
|
363
|
+
}
|
|
198
364
|
const next=await installGeneration(projectDir,{...operation,state});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
365
|
+
const nextEntries=[...initial,...next.loaded],activeConfig={...project.config,...config},effectiveConfig={...latest.config,...config};
|
|
366
|
+
candidate=await createHost({plugins:nextEntries,config:effectiveConfig,services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:false,allowUnknownPrivileges});
|
|
367
|
+
const sourceChanges=await changedPluginSources(host.definitions,candidate.definitions,{oldInstallationRoot:state.root,newInstallationRoot:next.state.root});
|
|
368
|
+
const plan=planPluginUpdate(host.definitions,candidate.definitions,activeConfig,effectiveConfig,sourceChanges);
|
|
369
|
+
assertMaintenance(plan,options);
|
|
370
|
+
await candidate.close();candidate=null;
|
|
371
|
+
nextRelease=await leaseInstallation(projectDir,next.state.root);
|
|
372
|
+
const oldEntries=[...host.definitions.values()].map(p=>({plugin:p,root:p.root}));
|
|
373
|
+
if(plan.affected.length)await host.replace({plugins:nextEntries,config:effectiveConfig,affected:plan.affected});
|
|
374
|
+
try{await savePluginState(projectDir,next.state,state);}
|
|
375
|
+
catch(error){
|
|
376
|
+
if(plan.affected.length)try{await host.replace({plugins:oldEntries,config:activeConfig,affected:plan.affected});}
|
|
377
|
+
catch(recovery){host.invalidate();throw new AggregateError([error,recovery],`插件状态保存失败且恢复失败:${error.message};${recovery.message}`);}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
state=next.state;
|
|
381
|
+
for(const key of Object.keys(project))delete project[key];Object.assign(project,latest);
|
|
382
|
+
installationLeases.set(path.resolve(state.root),nextRelease);nextRelease=null;
|
|
383
|
+
for(const [root,release] of installationLeases)if(root!==path.resolve(state.root)&&![...host.definitions.values()].some(p=>p.root&&path.resolve(p.root).startsWith(root+path.sep))){
|
|
384
|
+
try{await release();installationLeases.delete(root);}
|
|
385
|
+
catch(error){emitter.emit('event',{type:'plugin.cleanup.warning',message:`旧安装批次租约暂未清理:${error.message}`,root});}
|
|
202
386
|
}
|
|
203
|
-
nextHost=await build(next.loaded);
|
|
204
|
-
const file=path.join(projectDir,'.flow','plugins.json'),temp=`${file}.tmp`;
|
|
205
|
-
await writeFile(temp,JSON.stringify(next.state,null,2));await rename(temp,file);
|
|
206
|
-
const old=host;host=nextHost;nextHost=null;state=next.state;await old.close();
|
|
207
387
|
return app.listPlugins();
|
|
208
|
-
|
|
388
|
+
});
|
|
389
|
+
} finally {try{if(candidate)await candidate.close();}finally{try{if(nextRelease)await nextRelease();}finally{busy=false;}}}
|
|
209
390
|
}
|
|
210
391
|
return app;
|
|
211
392
|
}
|
|
@@ -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();});
|