@llm4ts/shell 2.45.0 → 2.47.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/dist/Cli.d.ts +3 -7
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +20 -4
- package/dist/Cli.js.map +1 -1
- package/dist/FlowLaunch.d.ts +2 -16
- package/dist/FlowLaunch.d.ts.map +1 -1
- package/dist/FlowLaunch.js +20 -66
- package/dist/FlowLaunch.js.map +1 -1
- package/dist/FlowProcess.d.ts +67 -0
- package/dist/FlowProcess.d.ts.map +1 -0
- package/dist/FlowProcess.js +175 -0
- package/dist/FlowProcess.js.map +1 -0
- package/dist/Menu.d.ts +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/OperatorActTools.d.ts +170 -0
- package/dist/OperatorActTools.d.ts.map +1 -0
- package/dist/OperatorActTools.js +332 -0
- package/dist/OperatorActTools.js.map +1 -0
- package/dist/OperatorConfig.d.ts +44 -0
- package/dist/OperatorConfig.d.ts.map +1 -0
- package/dist/OperatorConfig.js +88 -0
- package/dist/OperatorConfig.js.map +1 -0
- package/dist/OperatorNode.d.ts +45 -0
- package/dist/OperatorNode.d.ts.map +1 -0
- package/dist/OperatorNode.js +103 -0
- package/dist/OperatorNode.js.map +1 -0
- package/dist/OperatorRuns.d.ts +76 -0
- package/dist/OperatorRuns.d.ts.map +1 -0
- package/dist/OperatorRuns.js +146 -0
- package/dist/OperatorRuns.js.map +1 -0
- package/dist/OperatorServer.d.ts +160 -0
- package/dist/OperatorServer.d.ts.map +1 -0
- package/dist/OperatorServer.js +123 -0
- package/dist/OperatorServer.js.map +1 -0
- package/dist/OperatorTools.d.ts +558 -0
- package/dist/OperatorTools.d.ts.map +1 -0
- package/dist/OperatorTools.js +398 -0
- package/dist/OperatorTools.js.map +1 -0
- package/dist/Refine.d.ts +2 -2
- package/dist/Refine.d.ts.map +1 -1
- package/dist/Refine.js +3 -5
- package/dist/Refine.js.map +1 -1
- package/dist/RunSupervisor.d.ts +51 -0
- package/dist/RunSupervisor.d.ts.map +1 -0
- package/dist/RunSupervisor.js +282 -0
- package/dist/RunSupervisor.js.map +1 -0
- package/dist/ShellErrors.d.ts +9 -0
- package/dist/ShellErrors.d.ts.map +1 -0
- package/dist/ShellErrors.js +7 -0
- package/dist/ShellErrors.js.map +1 -0
- package/flows/epic-mvp.js +21 -0
- package/flows/lib/epic-mvp.js +451 -0
- package/flows/lib/epic-stories.js +88 -48
- package/flows/port-guide.js +4 -1
- package/package.json +13 -4
- package/src/Cli.ts +64 -3
- package/src/FlowLaunch.ts +20 -66
- package/src/FlowProcess.ts +239 -0
- package/src/OperatorActTools.ts +407 -0
- package/src/OperatorConfig.ts +148 -0
- package/src/OperatorNode.ts +147 -0
- package/src/OperatorRuns.ts +270 -0
- package/src/OperatorServer.ts +192 -0
- package/src/OperatorTools.ts +610 -0
- package/src/Refine.ts +4 -7
- package/src/RunSupervisor.ts +403 -0
- package/src/ShellErrors.ts +6 -0
package/flows/port-guide.js
CHANGED
|
@@ -85,7 +85,10 @@ const program = Effect.gen(function* () {
|
|
|
85
85
|
coder,
|
|
86
86
|
reasoning: asReadOnly(coder),
|
|
87
87
|
reviewers: [asReadOnly(coder)],
|
|
88
|
-
environment: process.env
|
|
88
|
+
environment: process.env,
|
|
89
|
+
// The trial ports are written under `.llm4ts/port/trial/` by the
|
|
90
|
+
// coder itself, so this run lifts the default deny of the run state.
|
|
91
|
+
deniedPaths: []
|
|
89
92
|
}, (context) => Effect.gen(function* () {
|
|
90
93
|
const events = context.events;
|
|
91
94
|
const repo = yield* makeNodeWorkspace(input.workDir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llm4ts/shell",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.47.0",
|
|
4
4
|
"description": "Interactive shell and CLI for llm4ts: flow discovery, run-a-flow, and view",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,9 +22,18 @@
|
|
|
22
22
|
"./CoderChoice": "./dist/CoderChoice.js",
|
|
23
23
|
"./FlowCatalog": "./dist/FlowCatalog.js",
|
|
24
24
|
"./FlowLaunch": "./dist/FlowLaunch.js",
|
|
25
|
+
"./FlowProcess": "./dist/FlowProcess.js",
|
|
25
26
|
"./Menu": "./dist/Menu.js",
|
|
27
|
+
"./OperatorActTools": "./dist/OperatorActTools.js",
|
|
28
|
+
"./OperatorConfig": "./dist/OperatorConfig.js",
|
|
29
|
+
"./OperatorNode": "./dist/OperatorNode.js",
|
|
30
|
+
"./OperatorRuns": "./dist/OperatorRuns.js",
|
|
31
|
+
"./OperatorServer": "./dist/OperatorServer.js",
|
|
32
|
+
"./OperatorTools": "./dist/OperatorTools.js",
|
|
26
33
|
"./Refine": "./dist/Refine.js",
|
|
34
|
+
"./RunSupervisor": "./dist/RunSupervisor.js",
|
|
27
35
|
"./Package": "./dist/Package.js",
|
|
36
|
+
"./ShellErrors": "./dist/ShellErrors.js",
|
|
28
37
|
"./package.json": "./package.json"
|
|
29
38
|
},
|
|
30
39
|
"publishConfig": {
|
|
@@ -52,9 +61,9 @@
|
|
|
52
61
|
"dependencies": {
|
|
53
62
|
"@effect/platform-node": "4.0.0",
|
|
54
63
|
"@effect/platform-node-shared": "4.0.0",
|
|
55
|
-
"@llm4ts/
|
|
56
|
-
"@llm4ts/
|
|
57
|
-
"@llm4ts/runner": "2.
|
|
64
|
+
"@llm4ts/core": "2.47.0",
|
|
65
|
+
"@llm4ts/flow": "2.47.0",
|
|
66
|
+
"@llm4ts/runner": "2.47.0"
|
|
58
67
|
},
|
|
59
68
|
"peerDependencies": {
|
|
60
69
|
"effect": "4.0.0"
|
package/src/Cli.ts
CHANGED
|
@@ -50,13 +50,14 @@ import {
|
|
|
50
50
|
type FlowTierPaths
|
|
51
51
|
} from "./FlowCatalog.ts"
|
|
52
52
|
import { launchFlow } from "./FlowLaunch.ts"
|
|
53
|
+
import { mcpOptionsFrom } from "./OperatorConfig.ts"
|
|
54
|
+
import { nodeOperatorRuntime, nodeRead, runMcp } from "./OperatorNode.ts"
|
|
53
55
|
import { mainMenu } from "./Menu.ts"
|
|
54
56
|
import { ModDir, refineSession } from "./Refine.ts"
|
|
55
57
|
import { packageVersion } from "./Package.ts"
|
|
58
|
+
import { ShellUsageError } from "./ShellErrors.ts"
|
|
56
59
|
|
|
57
|
-
export
|
|
58
|
-
message: Schema.String
|
|
59
|
-
}) {}
|
|
60
|
+
export { ShellUsageError }
|
|
60
61
|
|
|
61
62
|
export class ShellActionError extends Schema.TaggedError<ShellActionError>()("ShellAction", {
|
|
62
63
|
message: Schema.String
|
|
@@ -981,6 +982,65 @@ const geminiBridgeCommand = Command.make(
|
|
|
981
982
|
)
|
|
982
983
|
)
|
|
983
984
|
|
|
985
|
+
const mcpCommand = Command.make(
|
|
986
|
+
"mcp",
|
|
987
|
+
{
|
|
988
|
+
stdio: Flag.Boolean("stdio").pipe(
|
|
989
|
+
Flag.withDefault(false),
|
|
990
|
+
Flag.withDescription("Speak MCP on stdin/stdout (the default transport)")
|
|
991
|
+
),
|
|
992
|
+
http: Flag.Boolean("http").pipe(
|
|
993
|
+
Flag.withDefault(false),
|
|
994
|
+
Flag.withDescription(
|
|
995
|
+
"Serve MCP over HTTP at /mcp; needs a bearer token from LLM4TS_MCP_TOKEN or --token-file"
|
|
996
|
+
)
|
|
997
|
+
),
|
|
998
|
+
host: Flag.String("host").pipe(
|
|
999
|
+
Flag.optional,
|
|
1000
|
+
Flag.withDescription("HTTP host to bind (default 127.0.0.1)")
|
|
1001
|
+
),
|
|
1002
|
+
port: Flag.Int("port").pipe(Flag.optional, Flag.withDescription("HTTP port (default 4321)")),
|
|
1003
|
+
tokenFile: Flag.String("token-file").pipe(
|
|
1004
|
+
Flag.optional,
|
|
1005
|
+
Flag.withDescription("File holding the HTTP bearer token (when LLM4TS_MCP_TOKEN is unset)")
|
|
1006
|
+
),
|
|
1007
|
+
repo: Flag.String("repo").pipe(
|
|
1008
|
+
Flag.atLeast(0),
|
|
1009
|
+
Flag.withDescription(
|
|
1010
|
+
"Repository to serve, repeatable (default: the repos in ~/.config/llm4ts/mcp.json)"
|
|
1011
|
+
)
|
|
1012
|
+
),
|
|
1013
|
+
allowRun: Flag.Boolean("allow-run").pipe(
|
|
1014
|
+
Flag.withDefault(false),
|
|
1015
|
+
Flag.withDescription("Offer the tools that start, stop and resume flows")
|
|
1016
|
+
),
|
|
1017
|
+
allowApprove: Flag.Boolean("allow-approve").pipe(
|
|
1018
|
+
Flag.withDefault(false),
|
|
1019
|
+
Flag.withDescription("Offer the approve tool, which ticks a retro report's approval marker")
|
|
1020
|
+
),
|
|
1021
|
+
parallelRuns: Flag.Int("parallel-runs").pipe(
|
|
1022
|
+
Flag.optional,
|
|
1023
|
+
Flag.withDescription(
|
|
1024
|
+
"Flows that write a checkout and may run at once per repository (default 1)"
|
|
1025
|
+
)
|
|
1026
|
+
)
|
|
1027
|
+
},
|
|
1028
|
+
(flags) =>
|
|
1029
|
+
Effect.gen(function* () {
|
|
1030
|
+
const config = yield* mcpOptionsFrom(flags, process.env, process.cwd(), nodeRead)
|
|
1031
|
+
// SIGINT/SIGTERM interrupt the server; runMcp then stops the flows it started.
|
|
1032
|
+
yield* runMcp(
|
|
1033
|
+
config,
|
|
1034
|
+
nodeOperatorRuntime(config, (cwd) => shellTierPaths({ cwd })),
|
|
1035
|
+
process
|
|
1036
|
+
)
|
|
1037
|
+
})
|
|
1038
|
+
).pipe(
|
|
1039
|
+
Command.withDescription(
|
|
1040
|
+
"Serve llm4ts to an MCP client (Grok Bot, Claude Code, Cursor): status, diagnosis and, when allowed, runs (ADR 0041)"
|
|
1041
|
+
)
|
|
1042
|
+
)
|
|
1043
|
+
|
|
984
1044
|
const doctorCommand = Command.make("doctor", {}, () =>
|
|
985
1045
|
Effect.gen(function* () {
|
|
986
1046
|
const report = yield* makeDoctorProgram()
|
|
@@ -1014,6 +1074,7 @@ export const shellCommand = Command.make("llm4ts", {}, () =>
|
|
|
1014
1074
|
graphCommand,
|
|
1015
1075
|
rosterCommand,
|
|
1016
1076
|
geminiBridgeCommand,
|
|
1077
|
+
mcpCommand,
|
|
1017
1078
|
doctorCommand
|
|
1018
1079
|
])
|
|
1019
1080
|
)
|
package/src/FlowLaunch.ts
CHANGED
|
@@ -1,46 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
3
2
|
import { constants } from "node:os"
|
|
4
|
-
import { dirname, join, resolve } from "node:path"
|
|
5
|
-
import { fileURLToPath } from "node:url"
|
|
6
3
|
import * as Effect from "effect/Effect"
|
|
7
|
-
import
|
|
4
|
+
import { FlowLaunchError, needsResolveFallback, nodeFlowProcess } from "./FlowProcess.ts"
|
|
8
5
|
|
|
9
|
-
export
|
|
10
|
-
message: Schema.String
|
|
11
|
-
}) {}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* True when the flow script cannot resolve `@llm4ts/runner` from its own
|
|
15
|
-
* location — a global flow, a built-in copy, or a bare repository. Project
|
|
16
|
-
* flows inside a repository that installs `@llm4ts/*` resolve their own pin
|
|
17
|
-
* and need no help (the project-wins policy of ADR 0006). Implemented as the
|
|
18
|
-
* same `node_modules` directory walk ESM resolution performs, so the check
|
|
19
|
-
* stays honest under test runners that patch the module system.
|
|
20
|
-
*/
|
|
21
|
-
export const needsResolveFallback = (flowPath: string): boolean => {
|
|
22
|
-
let directory = dirname(resolve(flowPath))
|
|
23
|
-
while (true) {
|
|
24
|
-
if (existsSync(join(directory, "node_modules", "@llm4ts", "runner", "package.json"))) {
|
|
25
|
-
return false
|
|
26
|
-
}
|
|
27
|
-
const parent = dirname(directory)
|
|
28
|
-
if (parent === directory) {
|
|
29
|
-
return true
|
|
30
|
-
}
|
|
31
|
-
directory = parent
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// In the published package the hook is the compiled sibling; when running
|
|
36
|
-
// from src (tests), it is the TypeScript source, which the child can load
|
|
37
|
-
// because it always runs with type stripping enabled.
|
|
38
|
-
const resolveFallbackUrl = (() => {
|
|
39
|
-
const compiled = new URL("./ResolveFallback.js", import.meta.url)
|
|
40
|
-
return existsSync(fileURLToPath(compiled))
|
|
41
|
-
? compiled.href
|
|
42
|
-
: new URL("./ResolveFallback.ts", import.meta.url).href
|
|
43
|
-
})()
|
|
6
|
+
export { FlowLaunchError, needsResolveFallback }
|
|
44
7
|
|
|
45
8
|
export interface LaunchFlowOptions {
|
|
46
9
|
readonly flowPath: string
|
|
@@ -76,40 +39,31 @@ const restoreTerminal = (stdio: "inherit" | "ignore"): void => {
|
|
|
76
39
|
* the child's exit code (`128 + signal` when terminated by a signal).
|
|
77
40
|
*/
|
|
78
41
|
export const launchFlow = (options: LaunchFlowOptions): Effect.Effect<number, FlowLaunchError> =>
|
|
79
|
-
Effect.
|
|
42
|
+
Effect.gen(function* () {
|
|
80
43
|
const environment = { ...(options.environment ?? process.env) }
|
|
81
44
|
if (options.coder !== undefined) {
|
|
82
45
|
environment.LLM4TS_CODER = options.coder
|
|
83
46
|
}
|
|
84
|
-
const
|
|
85
|
-
if (needsResolveFallback(options.flowPath)) {
|
|
86
|
-
nodeArgs.push("--import", resolveFallbackUrl)
|
|
87
|
-
}
|
|
88
|
-
const child = spawn(process.execPath, [...nodeArgs, options.flowPath, ...options.taskArgs], {
|
|
89
|
-
stdio: options.stdio ?? "inherit",
|
|
90
|
-
env: environment,
|
|
91
|
-
...(options.cwd === undefined ? {} : { cwd: options.cwd })
|
|
92
|
-
})
|
|
47
|
+
const stdio = options.stdio ?? "inherit"
|
|
93
48
|
const ignoreSigint = (): void => {}
|
|
94
49
|
process.on("SIGINT", ignoreSigint)
|
|
95
50
|
const restoreSigint = (): void => {
|
|
96
51
|
process.removeListener("SIGINT", ignoreSigint)
|
|
97
52
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
53
|
+
return yield* Effect.gen(function* () {
|
|
54
|
+
const handle = yield* nodeFlowProcess.start({
|
|
55
|
+
flowPath: options.flowPath,
|
|
56
|
+
args: options.taskArgs,
|
|
57
|
+
environment,
|
|
58
|
+
stdio,
|
|
59
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd })
|
|
60
|
+
})
|
|
61
|
+
const exit = yield* handle.exited.pipe(Effect.onInterrupt(() => handle.signal("SIGINT")))
|
|
62
|
+
restoreTerminal(stdio)
|
|
63
|
+
if (exit.signal !== undefined) {
|
|
64
|
+
const number = Object.entries(constants.signals).find(([name]) => name === exit.signal)
|
|
65
|
+
return 128 + (number?.[1] ?? 15)
|
|
109
66
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
restoreSigint()
|
|
113
|
-
child.kill("SIGINT")
|
|
114
|
-
})
|
|
67
|
+
return exit.code ?? 1
|
|
68
|
+
}).pipe(Effect.ensuring(Effect.sync(restoreSigint)))
|
|
115
69
|
})
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync } from "node:fs"
|
|
3
|
+
import { dirname, join, resolve } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import * as Deferred from "effect/Deferred"
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Ref from "effect/Ref"
|
|
8
|
+
import * as Schema from "effect/Schema"
|
|
9
|
+
|
|
10
|
+
export class FlowLaunchError extends Schema.TaggedError<FlowLaunchError>()("FlowLaunch", {
|
|
11
|
+
message: Schema.String
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* True when the flow script cannot resolve `@llm4ts/runner` from its own
|
|
16
|
+
* location — a global flow, a built-in copy, or a bare repository. Project
|
|
17
|
+
* flows inside a repository that installs `@llm4ts/*` resolve their own pin
|
|
18
|
+
* and need no help (the project-wins policy of ADR 0006). Implemented as the
|
|
19
|
+
* same `node_modules` directory walk ESM resolution performs, so the check
|
|
20
|
+
* stays honest under test runners that patch the module system.
|
|
21
|
+
*/
|
|
22
|
+
export const needsResolveFallback = (flowPath: string): boolean => {
|
|
23
|
+
let directory = dirname(resolve(flowPath))
|
|
24
|
+
while (true) {
|
|
25
|
+
if (existsSync(join(directory, "node_modules", "@llm4ts", "runner", "package.json"))) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
const parent = dirname(directory)
|
|
29
|
+
if (parent === directory) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
directory = parent
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// In the published package the hook is the compiled sibling; when running
|
|
37
|
+
// from src (tests), it is the TypeScript source, which the child can load
|
|
38
|
+
// because it always runs with type stripping enabled.
|
|
39
|
+
const resolveFallbackUrl = (() => {
|
|
40
|
+
const compiled = new URL("./ResolveFallback.js", import.meta.url)
|
|
41
|
+
return existsSync(fileURLToPath(compiled))
|
|
42
|
+
? compiled.href
|
|
43
|
+
: new URL("./ResolveFallback.ts", import.meta.url).href
|
|
44
|
+
})()
|
|
45
|
+
|
|
46
|
+
export interface FlowProcessStart {
|
|
47
|
+
readonly flowPath: string
|
|
48
|
+
readonly args: ReadonlyArray<string>
|
|
49
|
+
readonly environment: Readonly<Record<string, string | undefined>>
|
|
50
|
+
readonly cwd?: string
|
|
51
|
+
/**
|
|
52
|
+
* File paths are opened for append by the Node implementation. `ignore`
|
|
53
|
+
* discards both streams (used by the terminal launcher's tests).
|
|
54
|
+
*/
|
|
55
|
+
readonly stdio: "inherit" | "ignore" | { readonly stdout: string; readonly stderr: string }
|
|
56
|
+
/**
|
|
57
|
+
* Runs the child in its own process group, so a Ctrl-C on the parent's
|
|
58
|
+
* terminal or a whole-group kill reaches only the parent, which then stops
|
|
59
|
+
* the child once. The terminal launcher leaves it off: there the
|
|
60
|
+
* terminal's SIGINT must keep reaching the child.
|
|
61
|
+
*/
|
|
62
|
+
readonly detached?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FlowProcessExit {
|
|
66
|
+
readonly code: number | undefined
|
|
67
|
+
readonly signal: string | undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface FlowProcessHandle {
|
|
71
|
+
readonly pid: number
|
|
72
|
+
readonly exited: Effect.Effect<FlowProcessExit>
|
|
73
|
+
readonly signal: (signal: "SIGINT" | "SIGTERM") => Effect.Effect<void>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FlowProcessShape {
|
|
77
|
+
readonly start: (options: FlowProcessStart) => Effect.Effect<FlowProcessHandle, FlowLaunchError>
|
|
78
|
+
readonly isAlive: (pid: number) => boolean
|
|
79
|
+
/** Signals a process this shape did not start (a run left by an earlier server). */
|
|
80
|
+
readonly signal: (pid: number, signal: "SIGINT" | "SIGTERM") => Effect.Effect<void>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const flowNodeArgs = (flowPath: string): ReadonlyArray<string> => [
|
|
84
|
+
"--experimental-strip-types",
|
|
85
|
+
...(needsResolveFallback(flowPath) ? ["--import", resolveFallbackUrl] : [])
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
const openAppend = (path: string): number => {
|
|
89
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
90
|
+
return openSync(path, "a")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const nodeFlowProcess: FlowProcessShape = {
|
|
94
|
+
start: (options) =>
|
|
95
|
+
Effect.callback<FlowProcessHandle, FlowLaunchError>((resume) => {
|
|
96
|
+
const exited = Deferred.makeUnsafe<FlowProcessExit>()
|
|
97
|
+
const fds: Array<number> = []
|
|
98
|
+
const closeAll = (): void => {
|
|
99
|
+
for (const fd of fds.splice(0)) {
|
|
100
|
+
try {
|
|
101
|
+
closeSync(fd)
|
|
102
|
+
} catch {
|
|
103
|
+
// already closed
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let stdio: "inherit" | "ignore" | Array<"ignore" | number>
|
|
108
|
+
try {
|
|
109
|
+
if (typeof options.stdio === "string") {
|
|
110
|
+
stdio = options.stdio
|
|
111
|
+
} else {
|
|
112
|
+
const out = openAppend(options.stdio.stdout)
|
|
113
|
+
fds.push(out)
|
|
114
|
+
const err = openAppend(options.stdio.stderr)
|
|
115
|
+
fds.push(err)
|
|
116
|
+
stdio = ["ignore", out, err]
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
closeAll()
|
|
120
|
+
resume(
|
|
121
|
+
Effect.fail(
|
|
122
|
+
new FlowLaunchError({ message: error instanceof Error ? error.message : String(error) })
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
return Effect.void
|
|
126
|
+
}
|
|
127
|
+
const child = spawn(
|
|
128
|
+
process.execPath,
|
|
129
|
+
[...flowNodeArgs(options.flowPath), options.flowPath, ...options.args],
|
|
130
|
+
{
|
|
131
|
+
stdio,
|
|
132
|
+
env: { ...options.environment },
|
|
133
|
+
...(options.detached === true ? { detached: true } : {}),
|
|
134
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd })
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
closeAll()
|
|
138
|
+
let started = false
|
|
139
|
+
child.on("error", (error) => {
|
|
140
|
+
if (!started) {
|
|
141
|
+
resume(Effect.fail(new FlowLaunchError({ message: error.message })))
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
child.on("spawn", () => {
|
|
145
|
+
started = true
|
|
146
|
+
resume(
|
|
147
|
+
Effect.succeed({
|
|
148
|
+
pid: child.pid ?? 0,
|
|
149
|
+
exited: Deferred.await(exited),
|
|
150
|
+
signal: (signal) =>
|
|
151
|
+
Effect.sync(() => {
|
|
152
|
+
child.kill(signal)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
child.on("exit", (code, signal) => {
|
|
158
|
+
Deferred.doneUnsafe(
|
|
159
|
+
exited,
|
|
160
|
+
Effect.succeed({ code: code ?? undefined, signal: signal ?? undefined })
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
return Effect.void
|
|
164
|
+
}),
|
|
165
|
+
isAlive: (pid) => {
|
|
166
|
+
try {
|
|
167
|
+
process.kill(pid, 0)
|
|
168
|
+
return true
|
|
169
|
+
} catch {
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
signal: (pid, signal) =>
|
|
174
|
+
Effect.sync(() => {
|
|
175
|
+
try {
|
|
176
|
+
process.kill(pid, signal)
|
|
177
|
+
} catch {
|
|
178
|
+
// already gone (ESRCH) or not ours to signal
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface FakeFlowProcess {
|
|
184
|
+
readonly process: FlowProcessShape
|
|
185
|
+
readonly started: Effect.Effect<ReadonlyArray<FlowProcessStart>>
|
|
186
|
+
readonly exit: (pid: number, exit: FlowProcessExit) => Effect.Effect<void>
|
|
187
|
+
readonly signals: Effect.Effect<ReadonlyArray<{ pid: number; signal: string }>>
|
|
188
|
+
readonly setAlive: (pid: number, alive: boolean) => Effect.Effect<void>
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const makeFakeFlowProcess = (): Effect.Effect<FakeFlowProcess> =>
|
|
192
|
+
Effect.gen(function* () {
|
|
193
|
+
const starts = yield* Ref.make<ReadonlyArray<FlowProcessStart>>([])
|
|
194
|
+
const signals = yield* Ref.make<ReadonlyArray<{ pid: number; signal: string }>>([])
|
|
195
|
+
const alive = yield* Ref.make<ReadonlySet<number>>(new Set())
|
|
196
|
+
const exits = new Map<number, Deferred.Deferred<FlowProcessExit>>()
|
|
197
|
+
let nextPid = 1000
|
|
198
|
+
const setMember = (pid: number, member: boolean) =>
|
|
199
|
+
Ref.update(alive, (set) => {
|
|
200
|
+
const next = new Set(set)
|
|
201
|
+
if (member) {
|
|
202
|
+
next.add(pid)
|
|
203
|
+
} else {
|
|
204
|
+
next.delete(pid)
|
|
205
|
+
}
|
|
206
|
+
return next
|
|
207
|
+
})
|
|
208
|
+
const process: FlowProcessShape = {
|
|
209
|
+
start: (options) =>
|
|
210
|
+
Effect.gen(function* () {
|
|
211
|
+
const pid = nextPid++
|
|
212
|
+
const exited = yield* Deferred.make<FlowProcessExit>()
|
|
213
|
+
exits.set(pid, exited)
|
|
214
|
+
yield* Ref.update(starts, (all) => [...all, options])
|
|
215
|
+
yield* setMember(pid, true)
|
|
216
|
+
return {
|
|
217
|
+
pid,
|
|
218
|
+
exited: Deferred.await(exited),
|
|
219
|
+
signal: (signal) => Ref.update(signals, (all) => [...all, { pid, signal }])
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
isAlive: (pid) => Ref.getUnsafe(alive).has(pid),
|
|
223
|
+
signal: (pid, signal) => Ref.update(signals, (all) => [...all, { pid, signal }])
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
process,
|
|
227
|
+
started: Ref.get(starts),
|
|
228
|
+
signals: Ref.get(signals),
|
|
229
|
+
exit: (pid, exit) =>
|
|
230
|
+
Effect.gen(function* () {
|
|
231
|
+
yield* setMember(pid, false)
|
|
232
|
+
const deferred = exits.get(pid)
|
|
233
|
+
if (deferred !== undefined) {
|
|
234
|
+
yield* Deferred.succeed(deferred, exit)
|
|
235
|
+
}
|
|
236
|
+
}),
|
|
237
|
+
setAlive: (pid, value) => setMember(pid, value)
|
|
238
|
+
}
|
|
239
|
+
})
|