@tui-sandbox/library 7.2.1 → 7.4.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/CHANGELOG.md +14 -0
- package/dist/src/scripts/tui.js +107 -10
- package/dist/src/server/cypress-support/contents.js +3 -0
- package/dist/src/server/cypress-support/contents.test.js +3 -0
- package/dist/src/server/neovim/NeovimApplication.d.ts +3 -1
- package/dist/src/server/neovim/NeovimApplication.js +6 -0
- package/dist/src/server/neovim/NeovimJavascriptApiClient.js +2 -2
- package/dist/src/server/neovim/environment/createTempDir.d.ts +2 -2
- package/dist/src/server/neovim/index.d.ts +6 -3
- package/dist/src/server/neovim/index.js +13 -2
- package/dist/src/server/server.d.ts +2 -2
- package/dist/src/server/server.js +3 -3
- package/dist/src/server/updateTestdirectorySchemaFile.d.ts +6 -2
- package/dist/src/server/utilities/DisposableSingleApplication.d.ts +3 -2
- package/dist/src/server/utilities/DisposableSingleApplication.js +4 -0
- package/dist/src/server/utilities/DisposableSingleApplication.test.js +18 -0
- package/dist/src/server/utilities/TerminalApplication.d.ts +5 -0
- package/dist/src/server/utilities/TerminalApplication.js +13 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/scripts/tui.ts +58 -11
- package/src/server/cypress-support/contents.test.ts +3 -0
- package/src/server/cypress-support/contents.ts +3 -0
- package/src/server/neovim/NeovimApplication.ts +12 -2
- package/src/server/neovim/NeovimJavascriptApiClient.ts +2 -2
- package/src/server/neovim/environment/createTempDir.ts +2 -2
- package/src/server/neovim/index.ts +17 -5
- package/src/server/server.ts +5 -5
- package/src/server/updateTestdirectorySchemaFile.ts +7 -2
- package/src/server/utilities/DisposableSingleApplication.test.ts +23 -2
- package/src/server/utilities/DisposableSingleApplication.ts +10 -2
- package/src/server/utilities/TerminalApplication.ts +14 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tui-sandbox/library",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"node-pty": "1.0.0",
|
|
23
23
|
"prettier": "3.4.1",
|
|
24
24
|
"tsx": "4.19.2",
|
|
25
|
-
"type-fest": "4.
|
|
25
|
+
"type-fest": "4.30.0",
|
|
26
26
|
"winston": "3.17.0",
|
|
27
27
|
"zod": "3.23.8"
|
|
28
28
|
},
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"@types/command-exists": "1.2.3",
|
|
32
32
|
"@types/cors": "2.8.17",
|
|
33
33
|
"@types/express": "5.0.0",
|
|
34
|
-
"@types/node": "22.10.
|
|
34
|
+
"@types/node": "22.10.1",
|
|
35
35
|
"nodemon": "3.1.7",
|
|
36
|
-
"vite": "6.0.
|
|
37
|
-
"vitest": "2.1.
|
|
36
|
+
"vite": "6.0.2",
|
|
37
|
+
"vitest": "2.1.8"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"cypress": "^13",
|
package/src/scripts/tui.ts
CHANGED
|
@@ -1,38 +1,85 @@
|
|
|
1
|
+
import assert from "node:assert"
|
|
1
2
|
import { stat } from "node:fs/promises"
|
|
2
3
|
import path from "node:path"
|
|
3
4
|
import { createCypressSupportFile } from "../server/cypress-support/createCypressSupportFile.js"
|
|
4
5
|
import type { TestServerConfig } from "../server/index.js"
|
|
5
6
|
import { startTestServer, updateTestdirectorySchemaFile } from "../server/index.js"
|
|
7
|
+
import type { StdoutOrStderrMessage } from "../server/neovim/NeovimApplication.js"
|
|
8
|
+
import { NeovimApplication } from "../server/neovim/NeovimApplication.js"
|
|
9
|
+
import { prepareNewTestDirectory } from "../server/neovim/index.js"
|
|
6
10
|
|
|
7
11
|
//
|
|
8
12
|
// This is the main entrypoint to tui-sandbox
|
|
9
13
|
//
|
|
10
14
|
|
|
11
|
-
// the arguments passed to this script start at index 2
|
|
12
|
-
const args = process.argv.slice(2)
|
|
13
|
-
|
|
14
|
-
if (args[0] !== "start") {
|
|
15
|
-
throw new Error(`Usage: tui start`)
|
|
16
|
-
}
|
|
17
15
|
const outputFileName = "MyTestDirectory.ts"
|
|
18
16
|
|
|
19
17
|
/** The cwd in the user's directory when they are running this script. Not the
|
|
20
18
|
* cwd of the script itself. */
|
|
21
19
|
const cwd = process.cwd()
|
|
20
|
+
const config = {
|
|
21
|
+
directories: {
|
|
22
|
+
testEnvironmentPath: path.join(cwd, "test-environment/"),
|
|
23
|
+
outputFilePath: path.join(cwd, outputFileName),
|
|
24
|
+
},
|
|
25
|
+
port: process.env["PORT"] ? parseInt(process.env["PORT"]) : 3000,
|
|
26
|
+
} satisfies TestServerConfig
|
|
27
|
+
|
|
28
|
+
// the arguments passed to this script start at index 2
|
|
29
|
+
const args = process.argv.slice(2)
|
|
30
|
+
|
|
31
|
+
if (args[0] === "neovim") {
|
|
32
|
+
if (!(args[1] === "exec" && args.length === 3)) {
|
|
33
|
+
showUsageAndExit()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const command = args[2]
|
|
37
|
+
assert(command, "No command provided")
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
// automatically dispose of the neovim instance when done
|
|
41
|
+
await using app = new NeovimApplication(config.directories.testEnvironmentPath)
|
|
42
|
+
app.events.on("stdout" satisfies StdoutOrStderrMessage, data => {
|
|
43
|
+
console.log(` neovim output: ${data}`)
|
|
44
|
+
})
|
|
45
|
+
const testDirectory = await prepareNewTestDirectory(config.directories)
|
|
46
|
+
await app.startNextAndKillCurrent(
|
|
47
|
+
testDirectory,
|
|
48
|
+
{ filename: "empty.txt", headlessCmd: command },
|
|
49
|
+
{ cols: 80, rows: 24 }
|
|
50
|
+
)
|
|
51
|
+
await app.application.untilExit()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
process.exit(0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (args[0] !== "start") {
|
|
58
|
+
showUsageAndExit()
|
|
59
|
+
}
|
|
22
60
|
console.log(`🚀 Starting test server in ${cwd} - this should be the root of your integration-tests directory 🤞🏻`)
|
|
23
61
|
await stat(path.join(cwd, outputFileName))
|
|
24
62
|
|
|
25
63
|
try {
|
|
26
|
-
const config = {
|
|
27
|
-
testEnvironmentPath: path.join(cwd, "test-environment/"),
|
|
28
|
-
outputFilePath: path.join(cwd, outputFileName),
|
|
29
|
-
} satisfies TestServerConfig
|
|
30
64
|
await createCypressSupportFile({
|
|
31
65
|
cypressSupportDirectoryPath: path.join(cwd, "cypress", "support"),
|
|
32
66
|
supportFileName: "tui-sandbox.ts",
|
|
33
67
|
})
|
|
34
|
-
await updateTestdirectorySchemaFile(config)
|
|
68
|
+
await updateTestdirectorySchemaFile(config.directories)
|
|
35
69
|
await startTestServer(config)
|
|
36
70
|
} catch (e) {
|
|
37
71
|
console.error(e)
|
|
38
72
|
}
|
|
73
|
+
|
|
74
|
+
function showUsageAndExit() {
|
|
75
|
+
console.log(
|
|
76
|
+
[
|
|
77
|
+
//
|
|
78
|
+
`Usage (pick one):`,
|
|
79
|
+
` tui start`,
|
|
80
|
+
` tui neovim exec '<ex-command>'`,
|
|
81
|
+
].join("\n")
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
@@ -96,6 +96,9 @@ it("should return the expected contents", async () => {
|
|
|
96
96
|
|
|
97
97
|
runLuaCode(input: LuaCodeClientInput): Chainable<RunLuaCodeOutput>
|
|
98
98
|
|
|
99
|
+
/** Run an ex command in neovim.
|
|
100
|
+
* @example "echo expand('%:.')" current file, relative to the cwd
|
|
101
|
+
*/
|
|
99
102
|
runExCommand(input: ExCommandClientInput): Chainable<RunExCommandOutput>
|
|
100
103
|
}
|
|
101
104
|
}
|
|
@@ -99,6 +99,9 @@ declare global {
|
|
|
99
99
|
|
|
100
100
|
runLuaCode(input: LuaCodeClientInput): Chainable<RunLuaCodeOutput>
|
|
101
101
|
|
|
102
|
+
/** Run an ex command in neovim.
|
|
103
|
+
* @example "echo expand('%:.')" current file, relative to the cwd
|
|
104
|
+
*/
|
|
102
105
|
runExCommand(input: ExCommandClientInput): Chainable<RunExCommandOutput>
|
|
103
106
|
}
|
|
104
107
|
}
|
|
@@ -56,12 +56,15 @@ Run "nvim -V1 -v" for more info
|
|
|
56
56
|
|
|
57
57
|
*/
|
|
58
58
|
|
|
59
|
-
export type
|
|
59
|
+
export type StdoutOrStderrMessage = "stdout"
|
|
60
60
|
|
|
61
61
|
export type StartNeovimGenericArguments = {
|
|
62
62
|
filename: string | { openInVerticalSplits: string[] }
|
|
63
63
|
startupScriptModifications?: string[]
|
|
64
64
|
|
|
65
|
+
/** Executes the given command with --headless -c <command> -c qa */
|
|
66
|
+
headlessCmd?: string
|
|
67
|
+
|
|
65
68
|
/** Additions to the environment variables for the Neovim process. These
|
|
66
69
|
* override any already existing environment variables. */
|
|
67
70
|
additionalEnvironmentVariables?: Record<string, string> | undefined
|
|
@@ -128,6 +131,13 @@ export class NeovimApplication {
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
if (startArgs.headlessCmd) {
|
|
135
|
+
// NOTE: update the doc comment above if this changes
|
|
136
|
+
neovimArguments.push("--headless")
|
|
137
|
+
neovimArguments.push("-c", startArgs.headlessCmd)
|
|
138
|
+
neovimArguments.push("-c", "qa")
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
const id = Math.random().toString().slice(2, 8)
|
|
132
142
|
const socketPath = `${tmpdir()}/tui-sandbox-nvim-socket-${id}`
|
|
133
143
|
neovimArguments.push("--listen", socketPath)
|
|
@@ -146,7 +156,7 @@ export class NeovimApplication {
|
|
|
146
156
|
|
|
147
157
|
onStdoutOrStderr(data) {
|
|
148
158
|
data satisfies string
|
|
149
|
-
stdout.emit("stdout" satisfies
|
|
159
|
+
stdout.emit("stdout" satisfies StdoutOrStderrMessage, data)
|
|
150
160
|
},
|
|
151
161
|
})
|
|
152
162
|
})
|
|
@@ -14,10 +14,10 @@ export function connectNeovimApi(socketPath: string): Lazy<Promise<NeovimJavascr
|
|
|
14
14
|
for (let i = 0; i < 100; i++) {
|
|
15
15
|
try {
|
|
16
16
|
await access(socketPath)
|
|
17
|
-
console.log(`socket file ${socketPath} created after at attempt ${i + 1}`)
|
|
17
|
+
// console.log(`socket file ${socketPath} created after at attempt ${i + 1}`)
|
|
18
18
|
break
|
|
19
19
|
} catch (e) {
|
|
20
|
-
console.log(`polling for socket file ${socketPath} to be created (attempt ${i + 1})`)
|
|
20
|
+
// console.log(`polling for socket file ${socketPath} to be created (attempt ${i + 1})`)
|
|
21
21
|
await new Promise(resolve => setTimeout(resolve, 100 satisfies PollingInterval))
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -6,10 +6,10 @@ import { access, mkdir, mkdtemp } from "fs/promises"
|
|
|
6
6
|
import path from "path"
|
|
7
7
|
import { convertDree, getDirectoryTree } from "../../dirtree/index.js"
|
|
8
8
|
import type { TestDirectory } from "../../types.js"
|
|
9
|
-
import type {
|
|
9
|
+
import type { DirectoriesConfig } from "../../updateTestdirectorySchemaFile.js"
|
|
10
10
|
import { updateTestdirectorySchemaFile } from "../../updateTestdirectorySchemaFile.js"
|
|
11
11
|
|
|
12
|
-
export async function createTempDir(config:
|
|
12
|
+
export async function createTempDir(config: DirectoriesConfig): Promise<TestDirectory> {
|
|
13
13
|
try {
|
|
14
14
|
const dir = await createUniqueDirectory(config.testEnvironmentPath)
|
|
15
15
|
|
|
@@ -10,16 +10,20 @@ import type {
|
|
|
10
10
|
StartNeovimGenericArguments,
|
|
11
11
|
TestDirectory,
|
|
12
12
|
} from "../types.js"
|
|
13
|
-
import type {
|
|
13
|
+
import type { DirectoriesConfig } from "../updateTestdirectorySchemaFile.js"
|
|
14
14
|
import { convertEventEmitterToAsyncGenerator } from "../utilities/generator.js"
|
|
15
|
+
import { Lazy } from "../utilities/Lazy.js"
|
|
15
16
|
import type { TabId } from "../utilities/tabId.js"
|
|
16
17
|
import { createTempDir, removeTestDirectories } from "./environment/createTempDir.js"
|
|
17
18
|
import type { TerminalDimensions } from "./NeovimApplication.js"
|
|
18
19
|
import { NeovimApplication } from "./NeovimApplication.js"
|
|
19
20
|
|
|
20
21
|
const neovims = new Map<TabId["tabId"], NeovimApplication>()
|
|
22
|
+
export const resources: Lazy<AsyncDisposableStack> = new Lazy(() => {
|
|
23
|
+
return new AsyncDisposableStack()
|
|
24
|
+
})
|
|
21
25
|
|
|
22
|
-
export async function
|
|
26
|
+
export async function initializeStdout(
|
|
23
27
|
options: { client: TabId },
|
|
24
28
|
signal: AbortSignal | undefined,
|
|
25
29
|
testEnvironmentPath: string
|
|
@@ -28,6 +32,9 @@ export async function onStdout(
|
|
|
28
32
|
const neovim = neovims.get(tabId) ?? new NeovimApplication(testEnvironmentPath)
|
|
29
33
|
if (neovims.get(tabId) === undefined) {
|
|
30
34
|
neovims.set(tabId, neovim)
|
|
35
|
+
resources.get().adopt(neovim, async n => {
|
|
36
|
+
await n[Symbol.asyncDispose]()
|
|
37
|
+
})
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
const stdout = convertEventEmitterToAsyncGenerator(neovim.events, "stdout")
|
|
@@ -46,18 +53,23 @@ export async function start(
|
|
|
46
53
|
options: StartNeovimGenericArguments,
|
|
47
54
|
terminalDimensions: TerminalDimensions,
|
|
48
55
|
tabId: TabId,
|
|
49
|
-
config:
|
|
56
|
+
config: DirectoriesConfig
|
|
50
57
|
): Promise<TestDirectory> {
|
|
51
58
|
const neovim = neovims.get(tabId.tabId)
|
|
52
59
|
assert(neovim, `Neovim instance not found for client id ${tabId.tabId}`)
|
|
53
60
|
|
|
54
|
-
await
|
|
55
|
-
const testDirectory = await createTempDir(config)
|
|
61
|
+
const testDirectory = await prepareNewTestDirectory(config)
|
|
56
62
|
await neovim.startNextAndKillCurrent(testDirectory, options, terminalDimensions)
|
|
57
63
|
|
|
58
64
|
return testDirectory
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
export async function prepareNewTestDirectory(config: DirectoriesConfig): Promise<TestDirectory> {
|
|
68
|
+
await removeTestDirectories(config.testEnvironmentPath)
|
|
69
|
+
const testDirectory = await createTempDir(config)
|
|
70
|
+
return testDirectory
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export async function sendStdin(options: { tabId: TabId; data: string }): Promise<void> {
|
|
62
74
|
const neovim = neovims.get(options.tabId.tabId)
|
|
63
75
|
assert(
|
package/src/server/server.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod"
|
|
|
5
5
|
import { trpc } from "./connection/trpc.js"
|
|
6
6
|
import * as neovim from "./neovim/index.js"
|
|
7
7
|
import { TestServer } from "./TestServer.js"
|
|
8
|
-
import type { TestServerConfig } from "./updateTestdirectorySchemaFile.js"
|
|
8
|
+
import type { DirectoriesConfig, TestServerConfig } from "./updateTestdirectorySchemaFile.js"
|
|
9
9
|
import { applicationAvailable } from "./utilities/applicationAvailable.js"
|
|
10
10
|
import { tabIdSchema } from "./utilities/tabId.js"
|
|
11
11
|
|
|
@@ -35,7 +35,7 @@ export type ExCommandInput = z.infer<typeof exCommandInputSchema>
|
|
|
35
35
|
|
|
36
36
|
/** @private */
|
|
37
37
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
38
|
-
export async function createAppRouter(config:
|
|
38
|
+
export async function createAppRouter(config: DirectoriesConfig) {
|
|
39
39
|
if (!(await applicationAvailable("nvim"))) {
|
|
40
40
|
throw new Error("Neovim is not installed. Please install Neovim (nvim).")
|
|
41
41
|
}
|
|
@@ -71,7 +71,7 @@ export async function createAppRouter(config: TestServerConfig) {
|
|
|
71
71
|
)
|
|
72
72
|
}),
|
|
73
73
|
onStdout: trpc.procedure.input(z.object({ client: tabIdSchema })).subscription(options => {
|
|
74
|
-
return neovim.
|
|
74
|
+
return neovim.initializeStdout(options.input, options.signal, config.testEnvironmentPath)
|
|
75
75
|
}),
|
|
76
76
|
sendStdin: trpc.procedure.input(z.object({ tabId: tabIdSchema, data: z.string() })).mutation(options => {
|
|
77
77
|
return neovim.sendStdin(options.input)
|
|
@@ -99,9 +99,9 @@ export type RouterInput = inferRouterInputs<AppRouter>
|
|
|
99
99
|
|
|
100
100
|
export async function startTestServer(config: TestServerConfig): Promise<TestServer> {
|
|
101
101
|
const testServer = new TestServer({
|
|
102
|
-
port:
|
|
102
|
+
port: config.port,
|
|
103
103
|
})
|
|
104
|
-
const appRouter = await createAppRouter(config)
|
|
104
|
+
const appRouter = await createAppRouter(config.directories)
|
|
105
105
|
await testServer.startAndRun(appRouter)
|
|
106
106
|
|
|
107
107
|
return testServer
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "fs"
|
|
2
2
|
import { buildTestDirectorySchema } from "./dirtree/index.js"
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type DirectoriesConfig = {
|
|
5
5
|
testEnvironmentPath: string
|
|
6
6
|
outputFilePath: string
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export type TestServerConfig = {
|
|
10
|
+
directories: DirectoriesConfig
|
|
11
|
+
port: number
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
export type UpdateTestdirectorySchemaFileResult = "updated" | "did-nothing"
|
|
10
15
|
|
|
11
16
|
export async function updateTestdirectorySchemaFile({
|
|
12
17
|
testEnvironmentPath,
|
|
13
18
|
outputFilePath,
|
|
14
|
-
}:
|
|
19
|
+
}: DirectoriesConfig): Promise<UpdateTestdirectorySchemaFileResult> {
|
|
15
20
|
const newSchema: string = await buildTestDirectorySchema(testEnvironmentPath)
|
|
16
21
|
let oldSchema = ""
|
|
17
22
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StartableApplication } from "./DisposableSingleApplication.js"
|
|
2
2
|
import { DisposableSingleApplication } from "./DisposableSingleApplication.js"
|
|
3
|
+
import type { ExitInfo } from "./TerminalApplication.js"
|
|
3
4
|
|
|
4
5
|
vi.spyOn(console, "log").mockImplementation(vi.fn())
|
|
5
6
|
|
|
@@ -9,11 +10,12 @@ class TestDisposableSingleApplication extends DisposableSingleApplication {
|
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const fakeApp
|
|
13
|
+
const fakeApp = {
|
|
13
14
|
processId: 123,
|
|
14
15
|
write: vi.fn(),
|
|
15
16
|
killAndWait: vi.fn(),
|
|
16
|
-
}
|
|
17
|
+
untilExit: Promise.resolve<ExitInfo>({ exitCode: 0, signal: undefined }),
|
|
18
|
+
} satisfies StartableApplication
|
|
17
19
|
|
|
18
20
|
describe("DisposableSingleApplication", () => {
|
|
19
21
|
it("has no application when created", () => {
|
|
@@ -45,6 +47,25 @@ describe("DisposableSingleApplication", () => {
|
|
|
45
47
|
)
|
|
46
48
|
})
|
|
47
49
|
|
|
50
|
+
describe("untilExit allows waiting for the application to exit", () => {
|
|
51
|
+
it("successful exit works", async () => {
|
|
52
|
+
const app = new TestDisposableSingleApplication()
|
|
53
|
+
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
54
|
+
fakeApp.untilExit = Promise.resolve({ exitCode: 1, signal: 9 })
|
|
55
|
+
await expect(app.untilExit()).resolves.toStrictEqual({
|
|
56
|
+
exitCode: 1,
|
|
57
|
+
signal: 9,
|
|
58
|
+
} satisfies ExitInfo)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("when the application throws an error, the error is propagated", async () => {
|
|
62
|
+
const app = new TestDisposableSingleApplication()
|
|
63
|
+
await app.startNextAndKillCurrent(async () => fakeApp)
|
|
64
|
+
fakeApp.untilExit = Promise.reject(new Error("fake error"))
|
|
65
|
+
await expect(app.untilExit()).rejects.toThrowError(new Error("fake error"))
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
48
69
|
describe("disposing", () => {
|
|
49
70
|
it("disposes the application when disposed", async () => {
|
|
50
71
|
// it's important to make sure there are no dangling applications when
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "assert"
|
|
2
|
-
import type { TerminalApplication } from "./TerminalApplication.js"
|
|
2
|
+
import type { ExitInfo, TerminalApplication } from "./TerminalApplication.js"
|
|
3
3
|
|
|
4
|
-
export type StartableApplication = Pick<TerminalApplication, "write" | "processId" | "killAndWait">
|
|
4
|
+
export type StartableApplication = Pick<TerminalApplication, "write" | "processId" | "killAndWait" | "untilExit">
|
|
5
5
|
|
|
6
6
|
/** A testable application that can be started, killed, and given input. For a
|
|
7
7
|
* single instance of this interface, only a single instance can be running at
|
|
@@ -15,6 +15,14 @@ export class DisposableSingleApplication implements AsyncDisposable {
|
|
|
15
15
|
this.application = await startNext()
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
public async untilExit(): Promise<ExitInfo> {
|
|
19
|
+
assert(
|
|
20
|
+
this.application,
|
|
21
|
+
"The application not started yet. It makes no sense to wait for it to exit, so this looks like a bug."
|
|
22
|
+
)
|
|
23
|
+
return this.application.untilExit
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
public async write(input: string): Promise<void> {
|
|
19
27
|
assert(
|
|
20
28
|
this.application,
|
|
@@ -6,6 +6,8 @@ import type { IPty } from "node-pty"
|
|
|
6
6
|
import pty from "node-pty"
|
|
7
7
|
import type { StartableApplication } from "./DisposableSingleApplication.js"
|
|
8
8
|
|
|
9
|
+
export type ExitInfo = { exitCode: number; signal: number | undefined }
|
|
10
|
+
|
|
9
11
|
// NOTE separating stdout and stderr is not supported by node-pty
|
|
10
12
|
// https://github.com/microsoft/node-pty/issues/71
|
|
11
13
|
export class TerminalApplication implements StartableApplication {
|
|
@@ -15,7 +17,8 @@ export class TerminalApplication implements StartableApplication {
|
|
|
15
17
|
|
|
16
18
|
private constructor(
|
|
17
19
|
private readonly subProcess: IPty,
|
|
18
|
-
public readonly onStdoutOrStderr: (data: string) => void
|
|
20
|
+
public readonly onStdoutOrStderr: (data: string) => void,
|
|
21
|
+
public readonly untilExit: Promise<ExitInfo>
|
|
19
22
|
) {
|
|
20
23
|
this.processId = subProcess.pid
|
|
21
24
|
|
|
@@ -60,14 +63,23 @@ export class TerminalApplication implements StartableApplication {
|
|
|
60
63
|
cols: dimensions.cols,
|
|
61
64
|
rows: dimensions.rows,
|
|
62
65
|
})
|
|
66
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
67
|
+
console.log(`Child process exited with code ${exitCode} and signal ${signal}`)
|
|
68
|
+
})
|
|
63
69
|
|
|
64
70
|
const processId = ptyProcess.pid
|
|
65
71
|
|
|
66
72
|
if (!processId) {
|
|
67
73
|
throw new Error("Failed to spawn child process")
|
|
68
74
|
}
|
|
75
|
+
const untilExit = new Promise<ExitInfo>(resolve => {
|
|
76
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
77
|
+
// console.log(`Child process ${processId} exited with code ${exitCode} and signal ${signal}`)
|
|
78
|
+
resolve({ exitCode, signal })
|
|
79
|
+
})
|
|
80
|
+
})
|
|
69
81
|
|
|
70
|
-
return new TerminalApplication(ptyProcess, onStdoutOrStderr)
|
|
82
|
+
return new TerminalApplication(ptyProcess, onStdoutOrStderr, untilExit)
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
/** Write to the terminal's stdin. */
|