@highstate/backend 0.7.2 → 0.7.3
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/{index.mjs → index.js} +1254 -915
- package/dist/library/source-resolution-worker.js +55 -0
- package/dist/library/worker/main.js +207 -0
- package/dist/{terminal-CqIsctlZ.mjs → library-BW5oPM7V.js} +210 -87
- package/dist/shared/index.js +6 -0
- package/dist/utils-ByadNcv4.js +102 -0
- package/package.json +14 -18
- package/src/common/index.ts +3 -0
- package/src/common/local.ts +22 -0
- package/src/common/pulumi.ts +230 -0
- package/src/common/utils.ts +137 -0
- package/src/config.ts +40 -0
- package/src/index.ts +6 -0
- package/src/library/abstractions.ts +83 -0
- package/src/library/factory.ts +20 -0
- package/src/library/index.ts +2 -0
- package/src/library/local.ts +404 -0
- package/src/library/source-resolution-worker.ts +96 -0
- package/src/library/worker/evaluator.ts +119 -0
- package/src/library/worker/loader.ts +93 -0
- package/src/library/worker/main.ts +82 -0
- package/src/library/worker/protocol.ts +38 -0
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/manager.ts +165 -0
- package/src/orchestrator/operation-workset.ts +483 -0
- package/src/orchestrator/operation.ts +647 -0
- package/src/preferences/shared.ts +1 -0
- package/src/project/abstractions.ts +89 -0
- package/src/project/factory.ts +11 -0
- package/src/project/index.ts +4 -0
- package/src/project/local.ts +412 -0
- package/src/project/lock.ts +39 -0
- package/src/project/manager.ts +374 -0
- package/src/runner/abstractions.ts +146 -0
- package/src/runner/factory.ts +22 -0
- package/src/runner/index.ts +2 -0
- package/src/runner/local.ts +698 -0
- package/src/secret/abstractions.ts +59 -0
- package/src/secret/factory.ts +22 -0
- package/src/secret/index.ts +2 -0
- package/src/secret/local.ts +152 -0
- package/src/services.ts +133 -0
- package/src/shared/index.ts +10 -0
- package/src/shared/library.ts +77 -0
- package/src/shared/operation.ts +85 -0
- package/src/shared/project.ts +62 -0
- package/src/shared/resolvers/graph-resolver.ts +111 -0
- package/src/shared/resolvers/input-hash.ts +77 -0
- package/src/shared/resolvers/input.ts +314 -0
- package/src/shared/resolvers/registry.ts +10 -0
- package/src/shared/resolvers/validation.ts +94 -0
- package/src/shared/state.ts +262 -0
- package/src/shared/terminal.ts +13 -0
- package/src/state/abstractions.ts +222 -0
- package/src/state/factory.ts +22 -0
- package/src/state/index.ts +3 -0
- package/src/state/local.ts +605 -0
- package/src/state/manager.ts +33 -0
- package/src/terminal/docker.ts +90 -0
- package/src/terminal/factory.ts +20 -0
- package/src/terminal/index.ts +3 -0
- package/src/terminal/manager.ts +330 -0
- package/src/terminal/run.sh.ts +37 -0
- package/src/terminal/shared.ts +50 -0
- package/src/workspace/abstractions.ts +41 -0
- package/src/workspace/factory.ts +14 -0
- package/src/workspace/index.ts +2 -0
- package/src/workspace/local.ts +54 -0
- package/dist/index.d.ts +0 -760
- package/dist/library/worker/main.mjs +0 -164
- package/dist/runner/source-resolution-worker.mjs +0 -22
- package/dist/shared/index.d.ts +0 -85
- package/dist/shared/index.mjs +0 -54
- package/dist/terminal-Cm2WqcyB.d.ts +0 -1589
@@ -0,0 +1,90 @@
|
|
1
|
+
import type { TerminalRunOptions, TerminalBackend } from "./shared"
|
2
|
+
import type { Logger } from "pino"
|
3
|
+
import { Readable } from "node:stream"
|
4
|
+
import { tmpdir } from "node:os"
|
5
|
+
import { resolve } from "node:path"
|
6
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
7
|
+
import { z } from "zod"
|
8
|
+
import spawn from "nano-spawn"
|
9
|
+
import { runScript } from "./run.sh"
|
10
|
+
|
11
|
+
export const dockerTerminalBackendConfig = z.object({
|
12
|
+
HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY: z.string().default("docker"),
|
13
|
+
HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO: z.coerce.boolean().default(false),
|
14
|
+
HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST: z.string().optional(),
|
15
|
+
})
|
16
|
+
|
17
|
+
export class DockerTerminalBackend implements TerminalBackend {
|
18
|
+
constructor(
|
19
|
+
private readonly binary: string,
|
20
|
+
private readonly useSudo: boolean,
|
21
|
+
private readonly host: string | undefined,
|
22
|
+
private readonly logger: Logger,
|
23
|
+
) {}
|
24
|
+
|
25
|
+
async run({ factory, stdin, stdout, screenSize, signal }: TerminalRunOptions): Promise<void> {
|
26
|
+
const hsTempDir = resolve(tmpdir(), "highstate")
|
27
|
+
await mkdir(hsTempDir, { recursive: true })
|
28
|
+
|
29
|
+
const runScriptPath = resolve(hsTempDir, "run.sh")
|
30
|
+
await writeFile(runScriptPath, runScript, { mode: 0o755 })
|
31
|
+
|
32
|
+
const args = [
|
33
|
+
"run",
|
34
|
+
"-i",
|
35
|
+
"-v",
|
36
|
+
`${runScriptPath}:/run.sh:ro`,
|
37
|
+
factory.image,
|
38
|
+
"/bin/bash",
|
39
|
+
"/run.sh",
|
40
|
+
]
|
41
|
+
|
42
|
+
const initData = {
|
43
|
+
command: factory.command,
|
44
|
+
cwd: factory.cwd,
|
45
|
+
env: {
|
46
|
+
...factory.env,
|
47
|
+
TERM: "xterm-256color",
|
48
|
+
},
|
49
|
+
files: factory.files ?? {},
|
50
|
+
screenSize,
|
51
|
+
}
|
52
|
+
|
53
|
+
const initDataStream = Readable.from(JSON.stringify(initData) + "\n")
|
54
|
+
|
55
|
+
if (this.useSudo) {
|
56
|
+
args.unshift(this.binary)
|
57
|
+
}
|
58
|
+
|
59
|
+
const process = spawn(this.useSudo ? "sudo" : this.binary, args, {
|
60
|
+
env: {
|
61
|
+
DOCKER_HOST: this.host,
|
62
|
+
},
|
63
|
+
signal,
|
64
|
+
})
|
65
|
+
|
66
|
+
const childProcess = await process.nodeChildProcess
|
67
|
+
|
68
|
+
initDataStream.pipe(childProcess.stdin!, { end: false })
|
69
|
+
initDataStream.on("end", () => stdin.pipe(childProcess.stdin!))
|
70
|
+
|
71
|
+
childProcess.stdout!.pipe(stdout)
|
72
|
+
childProcess.stderr!.pipe(stdout)
|
73
|
+
|
74
|
+
this.logger.info({ pid: childProcess.pid }, "process started")
|
75
|
+
|
76
|
+
await process
|
77
|
+
}
|
78
|
+
|
79
|
+
static create(
|
80
|
+
config: z.infer<typeof dockerTerminalBackendConfig>,
|
81
|
+
logger: Logger,
|
82
|
+
): DockerTerminalBackend {
|
83
|
+
return new DockerTerminalBackend(
|
84
|
+
config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_BINARY,
|
85
|
+
config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_USE_SUDO,
|
86
|
+
config.HIGHSTATE_BACKEND_TERMINAL_DOCKER_HOST,
|
87
|
+
logger.child({ backend: "TerminalBackend", service: "DockerTerminalBackend" }),
|
88
|
+
)
|
89
|
+
}
|
90
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import type { Logger } from "pino"
|
2
|
+
import type { TerminalBackend } from "./shared"
|
3
|
+
import { z } from "zod"
|
4
|
+
import { DockerTerminalBackend, dockerTerminalBackendConfig } from "./docker"
|
5
|
+
|
6
|
+
export const terminalBackendConfig = z.object({
|
7
|
+
HIGHSTATE_BACKEND_TERMINAL_TYPE: z.enum(["docker"]).default("docker"),
|
8
|
+
...dockerTerminalBackendConfig.shape,
|
9
|
+
})
|
10
|
+
|
11
|
+
export function createTerminalBackend(
|
12
|
+
config: z.infer<typeof terminalBackendConfig>,
|
13
|
+
logger: Logger,
|
14
|
+
): TerminalBackend {
|
15
|
+
switch (config.HIGHSTATE_BACKEND_TERMINAL_TYPE) {
|
16
|
+
case "docker": {
|
17
|
+
return DockerTerminalBackend.create(config, logger)
|
18
|
+
}
|
19
|
+
}
|
20
|
+
}
|
@@ -0,0 +1,330 @@
|
|
1
|
+
import type { ScreenSize, TerminalBackend } from "./shared"
|
2
|
+
import type { InstanceTerminal, TerminalSession } from "../shared"
|
3
|
+
import type { Logger } from "pino"
|
4
|
+
import type { StateBackend, TerminalHistoryEntry } from "../state"
|
5
|
+
import type { RunnerBackend } from "../runner"
|
6
|
+
import { PassThrough, type Stream, type Writable } from "node:stream"
|
7
|
+
import { EventEmitter, on } from "node:events"
|
8
|
+
import { parseInstanceId } from "@highstate/contract"
|
9
|
+
import { nanoid } from "nanoid"
|
10
|
+
import { createAsyncBatcher, isAbortErrorLike } from "../common"
|
11
|
+
|
12
|
+
type ManagedTerminal = {
|
13
|
+
readonly session: TerminalSession
|
14
|
+
readonly abortController: AbortController
|
15
|
+
readonly stdin: PassThrough
|
16
|
+
readonly stdout: PassThrough
|
17
|
+
|
18
|
+
refCount: number
|
19
|
+
started: boolean
|
20
|
+
start(screenSize: ScreenSize): void
|
21
|
+
}
|
22
|
+
|
23
|
+
type TerminalEvents = {
|
24
|
+
[sessionId: string]: [terminal: TerminalSession]
|
25
|
+
}
|
26
|
+
|
27
|
+
const notAttachedTerminalLifetime = 5 * 60 * 1000 // 5 minutes
|
28
|
+
|
29
|
+
export class TerminalManager {
|
30
|
+
private readonly managedTerminals = new Map<string, ManagedTerminal>()
|
31
|
+
private readonly existingSessions = new Map<string, TerminalSession>()
|
32
|
+
private readonly terminalEE = new EventEmitter<TerminalEvents>()
|
33
|
+
|
34
|
+
private constructor(
|
35
|
+
private readonly terminalBackend: TerminalBackend,
|
36
|
+
private readonly stateBackend: StateBackend,
|
37
|
+
private readonly runnerBackend: RunnerBackend,
|
38
|
+
private readonly logger: Logger,
|
39
|
+
) {
|
40
|
+
void this.markFinishedSessions()
|
41
|
+
}
|
42
|
+
|
43
|
+
public isSessionActive(sessionId: string): boolean {
|
44
|
+
return this.managedTerminals.has(sessionId)
|
45
|
+
}
|
46
|
+
|
47
|
+
public async *watchSession(
|
48
|
+
projectId: string,
|
49
|
+
instanceId: string,
|
50
|
+
sessionId: string,
|
51
|
+
signal?: AbortSignal,
|
52
|
+
): AsyncIterable<TerminalSession> {
|
53
|
+
const managedTerminal = this.managedTerminals.get(sessionId)
|
54
|
+
|
55
|
+
// if terminal session is not active, just fetch it, return and stop watching
|
56
|
+
if (!managedTerminal) {
|
57
|
+
const session = await this.stateBackend.getTerminalSession(projectId, instanceId, sessionId)
|
58
|
+
|
59
|
+
if (!session) {
|
60
|
+
throw new Error(`Terminal session "${sessionId}" not found`)
|
61
|
+
}
|
62
|
+
|
63
|
+
yield session
|
64
|
+
return
|
65
|
+
}
|
66
|
+
|
67
|
+
yield managedTerminal.session
|
68
|
+
|
69
|
+
for await (const [terminalSession] of on(this.terminalEE, sessionId, {
|
70
|
+
signal,
|
71
|
+
}) as AsyncIterable<[TerminalSession]>) {
|
72
|
+
yield terminalSession
|
73
|
+
|
74
|
+
if (terminalSession.finishedAt) {
|
75
|
+
// terminal session is finished, stop watching
|
76
|
+
return
|
77
|
+
}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
public async createSession(
|
82
|
+
projectId: string,
|
83
|
+
instanceId: string,
|
84
|
+
terminalName: string,
|
85
|
+
): Promise<TerminalSession> {
|
86
|
+
const [instanceType, instanceName] = parseInstanceId(instanceId)
|
87
|
+
|
88
|
+
const factory = await this.runnerBackend.getTerminalFactory(
|
89
|
+
{ projectId, instanceType, instanceName },
|
90
|
+
terminalName,
|
91
|
+
)
|
92
|
+
|
93
|
+
if (!factory) {
|
94
|
+
throw new Error(
|
95
|
+
`Terminal factory for instance "${instanceId}" with name "${terminalName}" not found`,
|
96
|
+
)
|
97
|
+
}
|
98
|
+
|
99
|
+
const terminalSession: TerminalSession = {
|
100
|
+
id: nanoid(),
|
101
|
+
projectId,
|
102
|
+
instanceId,
|
103
|
+
terminalName,
|
104
|
+
terminalTitle: factory.title,
|
105
|
+
createdAt: new Date(),
|
106
|
+
}
|
107
|
+
|
108
|
+
await this.stateBackend.putTerminalSession(projectId, instanceId, terminalSession)
|
109
|
+
this.logger.info({ msg: "terminal session created", id: terminalSession.id })
|
110
|
+
|
111
|
+
this.createManagedTerminal(factory, terminalSession)
|
112
|
+
|
113
|
+
return terminalSession
|
114
|
+
}
|
115
|
+
|
116
|
+
public async getOrCreateSession(
|
117
|
+
projectId: string,
|
118
|
+
instanceId: string,
|
119
|
+
terminalName: string,
|
120
|
+
): Promise<TerminalSession> {
|
121
|
+
const existingSession = this.existingSessions.get(`${projectId}:${instanceId}.${terminalName}`)
|
122
|
+
|
123
|
+
if (existingSession) {
|
124
|
+
return existingSession
|
125
|
+
}
|
126
|
+
|
127
|
+
return await this.createSession(projectId, instanceId, terminalName)
|
128
|
+
}
|
129
|
+
|
130
|
+
public close(sessionId: string): void {
|
131
|
+
this.logger.info({ msg: "closing terminal session", id: sessionId })
|
132
|
+
|
133
|
+
const managedTerminal = this.managedTerminals.get(sessionId)
|
134
|
+
|
135
|
+
if (managedTerminal) {
|
136
|
+
managedTerminal.abortController.abort()
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
public attach(
|
141
|
+
sessionId: string,
|
142
|
+
stdin: Stream,
|
143
|
+
stdout: Writable,
|
144
|
+
screenSize: ScreenSize,
|
145
|
+
signal: AbortSignal,
|
146
|
+
): void {
|
147
|
+
const terminal = this.managedTerminals.get(sessionId)
|
148
|
+
if (!terminal) {
|
149
|
+
throw new Error(`Terminal session "${sessionId}" not found, check if it's still active`)
|
150
|
+
}
|
151
|
+
|
152
|
+
const handleStdin = (data: unknown) => {
|
153
|
+
terminal.stdin.write(data)
|
154
|
+
}
|
155
|
+
|
156
|
+
const handleStdout = (data: unknown) => {
|
157
|
+
stdout.write(data)
|
158
|
+
}
|
159
|
+
|
160
|
+
terminal.stdout.on("data", handleStdout)
|
161
|
+
stdin.on("data", handleStdin)
|
162
|
+
|
163
|
+
terminal.refCount += 1
|
164
|
+
|
165
|
+
this.logger.info(
|
166
|
+
"terminal attached (sessionId: %s, refCount: %d)",
|
167
|
+
sessionId,
|
168
|
+
terminal.refCount,
|
169
|
+
)
|
170
|
+
|
171
|
+
signal.addEventListener("abort", () => {
|
172
|
+
terminal.refCount -= 1
|
173
|
+
terminal.stdout.off("data", handleStdout)
|
174
|
+
stdin.off("data", handleStdin)
|
175
|
+
|
176
|
+
this.logger.info(
|
177
|
+
"terminal detached (sessionId: %s, refCount: %d)",
|
178
|
+
sessionId,
|
179
|
+
terminal.refCount,
|
180
|
+
)
|
181
|
+
})
|
182
|
+
|
183
|
+
if (!terminal.started) {
|
184
|
+
terminal.start(screenSize)
|
185
|
+
terminal.started = true
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
private async updateActiveTerminalSessions(): Promise<void> {
|
190
|
+
await this.stateBackend.putActiveTerminalSessions(
|
191
|
+
Array.from(this.managedTerminals.values()).map(t => t.session),
|
192
|
+
)
|
193
|
+
}
|
194
|
+
|
195
|
+
private createManagedTerminal(
|
196
|
+
factory: InstanceTerminal,
|
197
|
+
terminalSession: TerminalSession,
|
198
|
+
): ManagedTerminal {
|
199
|
+
const managedTerminal: ManagedTerminal = {
|
200
|
+
session: terminalSession,
|
201
|
+
abortController: new AbortController(),
|
202
|
+
stdin: new PassThrough(),
|
203
|
+
stdout: new PassThrough(),
|
204
|
+
|
205
|
+
refCount: 0,
|
206
|
+
started: false,
|
207
|
+
|
208
|
+
start: (screenSize: ScreenSize) => {
|
209
|
+
void this.terminalBackend
|
210
|
+
.run({
|
211
|
+
factory,
|
212
|
+
stdin: managedTerminal.stdin,
|
213
|
+
stdout: managedTerminal.stdout,
|
214
|
+
screenSize,
|
215
|
+
signal: managedTerminal.abortController.signal,
|
216
|
+
})
|
217
|
+
.catch(error => {
|
218
|
+
if (isAbortErrorLike(error)) {
|
219
|
+
return
|
220
|
+
}
|
221
|
+
|
222
|
+
this.logger.error({
|
223
|
+
msg: "managed terminal failed",
|
224
|
+
id: managedTerminal.session.id,
|
225
|
+
error: error as unknown,
|
226
|
+
})
|
227
|
+
})
|
228
|
+
.finally(() => {
|
229
|
+
this.logger.info({ msg: "managed terminal closed", id: managedTerminal.session.id })
|
230
|
+
this.managedTerminals.delete(managedTerminal.session.id)
|
231
|
+
this.existingSessions.delete(
|
232
|
+
`${managedTerminal.session.projectId}:${managedTerminal.session.instanceId}.${managedTerminal.session.terminalName}`,
|
233
|
+
)
|
234
|
+
|
235
|
+
managedTerminal.session.finishedAt = new Date()
|
236
|
+
this.terminalEE.emit(managedTerminal.session.id, managedTerminal.session)
|
237
|
+
|
238
|
+
void this.stateBackend.putTerminalSession(
|
239
|
+
managedTerminal.session.projectId,
|
240
|
+
managedTerminal.session.instanceId,
|
241
|
+
managedTerminal.session,
|
242
|
+
)
|
243
|
+
|
244
|
+
void this.updateActiveTerminalSessions()
|
245
|
+
})
|
246
|
+
|
247
|
+
setTimeout(
|
248
|
+
() => this.closeTerminalIfNotAttached(managedTerminal),
|
249
|
+
notAttachedTerminalLifetime,
|
250
|
+
)
|
251
|
+
|
252
|
+
this.logger.info({ msg: "managed terminal created", id: managedTerminal.session.id })
|
253
|
+
},
|
254
|
+
}
|
255
|
+
|
256
|
+
managedTerminal.stdout.on("data", data => {
|
257
|
+
const entry = String(data)
|
258
|
+
|
259
|
+
void this.persistHistory.call([terminalSession.id, entry])
|
260
|
+
})
|
261
|
+
|
262
|
+
this.managedTerminals.set(managedTerminal.session.id, managedTerminal)
|
263
|
+
this.existingSessions.set(
|
264
|
+
`${managedTerminal.session.projectId}:${managedTerminal.session.instanceId}.${managedTerminal.session.terminalName}`,
|
265
|
+
managedTerminal.session,
|
266
|
+
)
|
267
|
+
|
268
|
+
void this.updateActiveTerminalSessions()
|
269
|
+
|
270
|
+
return managedTerminal
|
271
|
+
}
|
272
|
+
|
273
|
+
private closeTerminalIfNotAttached(terminal: ManagedTerminal) {
|
274
|
+
if (!this.managedTerminals.has(terminal.session.id)) {
|
275
|
+
// Already closed
|
276
|
+
return
|
277
|
+
}
|
278
|
+
|
279
|
+
if (terminal.refCount <= 0) {
|
280
|
+
this.logger.info({
|
281
|
+
msg: "terminal not attached for too long, closing",
|
282
|
+
id: terminal.session.id,
|
283
|
+
})
|
284
|
+
|
285
|
+
terminal.abortController.abort()
|
286
|
+
this.managedTerminals.delete(terminal.session.id)
|
287
|
+
this.existingSessions.delete(
|
288
|
+
`${terminal.session.projectId}:${terminal.session.instanceId}.${terminal.session.terminalName}`,
|
289
|
+
)
|
290
|
+
return
|
291
|
+
}
|
292
|
+
|
293
|
+
setTimeout(() => this.closeTerminalIfNotAttached(terminal), notAttachedTerminalLifetime)
|
294
|
+
}
|
295
|
+
|
296
|
+
static create(
|
297
|
+
terminalBackend: TerminalBackend,
|
298
|
+
stateBackend: StateBackend,
|
299
|
+
runnerBackend: RunnerBackend,
|
300
|
+
logger: Logger,
|
301
|
+
): TerminalManager {
|
302
|
+
return new TerminalManager(
|
303
|
+
terminalBackend,
|
304
|
+
stateBackend,
|
305
|
+
runnerBackend,
|
306
|
+
logger.child({ service: "TerminalManager" }),
|
307
|
+
)
|
308
|
+
}
|
309
|
+
|
310
|
+
private persistHistory = createAsyncBatcher(async (entries: TerminalHistoryEntry[]) => {
|
311
|
+
this.logger.trace({ msg: "persisting history entries", count: entries.length })
|
312
|
+
|
313
|
+
// TODO: buffer entries and save them as lines
|
314
|
+
|
315
|
+
await this.stateBackend.appendTerminalSessionHistory(entries)
|
316
|
+
})
|
317
|
+
|
318
|
+
private async markFinishedSessions(): Promise<void> {
|
319
|
+
const sessions = await this.stateBackend.getActiveTerminalSessions()
|
320
|
+
|
321
|
+
for (const session of sessions) {
|
322
|
+
await this.stateBackend.putTerminalSession(session.projectId, session.instanceId, {
|
323
|
+
...session,
|
324
|
+
finishedAt: new Date(),
|
325
|
+
})
|
326
|
+
}
|
327
|
+
|
328
|
+
await this.updateActiveTerminalSessions()
|
329
|
+
}
|
330
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
export const runScript = `set -e -o pipefail
|
2
|
+
read -r data
|
3
|
+
|
4
|
+
# Extract env and files as key-value pairs, and command as an array
|
5
|
+
envKeys=($(jq -r '.env | keys[]' <<<"$data"))
|
6
|
+
filesKeys=($(jq -r '.files | keys[]' <<<"$data"))
|
7
|
+
commandArr=($(jq -r '.command[]' <<<"$data"))
|
8
|
+
cols=$(jq -r '.screenSize.cols' <<<"$data")
|
9
|
+
rows=$(jq -r '.screenSize.rows' <<<"$data")
|
10
|
+
|
11
|
+
# Set environment variables
|
12
|
+
for key in "\${envKeys[@]}"; do
|
13
|
+
value=$(jq -r ".env[\\"$key\\"]" <<<"$data")
|
14
|
+
export "$key=$value"
|
15
|
+
done
|
16
|
+
|
17
|
+
# Create files
|
18
|
+
for key in "\${filesKeys[@]}"; do
|
19
|
+
isBinary=$(jq -r ".files[\\"$key\\"].isBinary // false" <<<"$data")
|
20
|
+
content=$(jq -r ".files[\\"$key\\"].content" <<<"$data")
|
21
|
+
mode=$(jq -r ".files[\\"$key\\"].mode // 0" <<<"$data")
|
22
|
+
|
23
|
+
if [ "$isBinary" = "true" ]; then
|
24
|
+
echo "$content" | base64 -d > "$key"
|
25
|
+
else
|
26
|
+
echo "$content" > "$key"
|
27
|
+
fi
|
28
|
+
|
29
|
+
if [ "$mode" -ne 0 ]; then
|
30
|
+
chmod $(printf "%o" "$mode") "$key"
|
31
|
+
fi
|
32
|
+
done
|
33
|
+
|
34
|
+
# Execute the command, keeping stdin/stdout open and spawnin a new TTY
|
35
|
+
cmd=$(printf "%q " "\${commandArr[@]}")
|
36
|
+
exec script -q -c "stty cols $cols rows $rows; $cmd" /dev/null
|
37
|
+
`
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import type { Stream } from "node:stream"
|
2
|
+
import type { InstanceTerminal } from "../shared"
|
3
|
+
|
4
|
+
export type ScreenSize = {
|
5
|
+
/**
|
6
|
+
* The number of columns.
|
7
|
+
*/
|
8
|
+
cols: number
|
9
|
+
|
10
|
+
/**
|
11
|
+
* The number of rows.
|
12
|
+
*/
|
13
|
+
rows: number
|
14
|
+
}
|
15
|
+
|
16
|
+
export type TerminalRunOptions = {
|
17
|
+
/**
|
18
|
+
* The factory to use.
|
19
|
+
*/
|
20
|
+
factory: InstanceTerminal
|
21
|
+
|
22
|
+
/**
|
23
|
+
* The input stream.
|
24
|
+
*/
|
25
|
+
stdin: Stream
|
26
|
+
|
27
|
+
/**
|
28
|
+
* The output stream.
|
29
|
+
*/
|
30
|
+
stdout: NodeJS.WritableStream
|
31
|
+
|
32
|
+
/**
|
33
|
+
* The size of the screen to set before running the terminal.
|
34
|
+
*/
|
35
|
+
screenSize: ScreenSize
|
36
|
+
|
37
|
+
/**
|
38
|
+
* The signal to abort the terminal.
|
39
|
+
*/
|
40
|
+
signal?: AbortSignal
|
41
|
+
}
|
42
|
+
|
43
|
+
export interface TerminalBackend {
|
44
|
+
/**
|
45
|
+
* Creates a new terminal and runs it.
|
46
|
+
*
|
47
|
+
* @param options The options.
|
48
|
+
*/
|
49
|
+
run(options: TerminalRunOptions): Promise<void>
|
50
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
export interface WorkspaceBackend {
|
2
|
+
/**
|
3
|
+
* Gets the layout of the workspace.
|
4
|
+
*/
|
5
|
+
getWorkspaceLayout(): Promise<unknown>
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Sets the layout of the workspace.
|
9
|
+
*/
|
10
|
+
setWorkspaceLayout(layout: unknown): Promise<void>
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Gets the viewport of the project.
|
14
|
+
*
|
15
|
+
* @param projectId The ID of the project.
|
16
|
+
*/
|
17
|
+
getProjectViewport(projectId: string): Promise<unknown>
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Sets the viewport of the project.
|
21
|
+
*
|
22
|
+
* @param projectId The ID of the project.
|
23
|
+
*/
|
24
|
+
setProjectViewport(projectId: string, viewport: unknown): Promise<void>
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Gets the viewport of the composite instance.
|
28
|
+
*
|
29
|
+
* @param projectId The ID of the project.
|
30
|
+
* @param instanceId The ID of the instance.
|
31
|
+
*/
|
32
|
+
getInstanceViewport(projectId: string, instanceId: string): Promise<unknown>
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Sets the viewport of the composite instance.
|
36
|
+
*
|
37
|
+
* @param projectId The ID of the project.
|
38
|
+
* @param instanceId The ID of the instance.
|
39
|
+
*/
|
40
|
+
setInstanceViewport(projectId: string, instanceId: string, viewport: unknown): Promise<void>
|
41
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import type { WorkspaceBackend } from "./abstractions"
|
2
|
+
import { z } from "zod"
|
3
|
+
import { LocalWorkspaceBackend, localWorkspaceBackendConfig } from "./local"
|
4
|
+
|
5
|
+
export const workspaceBackendConfig = z.object({
|
6
|
+
HIGHSTATE_BACKEND_WORKSPACE_TYPE: z.enum(["local"]).default("local"),
|
7
|
+
...localWorkspaceBackendConfig.shape,
|
8
|
+
})
|
9
|
+
|
10
|
+
export function createWorkspaceBackend(
|
11
|
+
config: z.infer<typeof workspaceBackendConfig>,
|
12
|
+
): Promise<WorkspaceBackend> {
|
13
|
+
return LocalWorkspaceBackend.create(config)
|
14
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import type { ClassicLevel } from "classic-level"
|
2
|
+
import type { WorkspaceBackend } from "./abstractions"
|
3
|
+
import { homedir } from "node:os"
|
4
|
+
import { resolve } from "node:path"
|
5
|
+
import { z } from "zod"
|
6
|
+
|
7
|
+
export const localWorkspaceBackendConfig = z.object({
|
8
|
+
HIGHSTATE_BACKEND_WORKSPACE_LOCAL_DIR: z.string().optional(),
|
9
|
+
})
|
10
|
+
|
11
|
+
export class LocalWorkspaceBackend implements WorkspaceBackend {
|
12
|
+
constructor(private readonly db: ClassicLevel<string, unknown>) {}
|
13
|
+
|
14
|
+
async getWorkspaceLayout(): Promise<unknown> {
|
15
|
+
return await this.db.get("layout")
|
16
|
+
}
|
17
|
+
|
18
|
+
async setWorkspaceLayout(layout: unknown): Promise<void> {
|
19
|
+
await this.db.put("layout", layout)
|
20
|
+
}
|
21
|
+
|
22
|
+
async getProjectViewport(projectId: string): Promise<unknown> {
|
23
|
+
return await this.db.get(`viewports/projects/${projectId}`)
|
24
|
+
}
|
25
|
+
|
26
|
+
async setProjectViewport(projectId: string, viewport: unknown): Promise<void> {
|
27
|
+
await this.db.put(`viewports/projects/${projectId}`, viewport)
|
28
|
+
}
|
29
|
+
|
30
|
+
async getInstanceViewport(projectId: string, instanceId: string): Promise<unknown> {
|
31
|
+
return await this.db.get(`viewports/instances/${projectId}/${instanceId}`)
|
32
|
+
}
|
33
|
+
|
34
|
+
async setInstanceViewport(
|
35
|
+
projectId: string,
|
36
|
+
instanceId: string,
|
37
|
+
viewport: unknown,
|
38
|
+
): Promise<void> {
|
39
|
+
await this.db.put(`viewports/instances/${projectId}/${instanceId}`, viewport)
|
40
|
+
}
|
41
|
+
|
42
|
+
static async create(config: z.infer<typeof localWorkspaceBackendConfig>) {
|
43
|
+
let location = config.HIGHSTATE_BACKEND_WORKSPACE_LOCAL_DIR
|
44
|
+
if (!location) {
|
45
|
+
const home = homedir()
|
46
|
+
location = resolve(home, ".highstate/workspace")
|
47
|
+
}
|
48
|
+
|
49
|
+
const { ClassicLevel } = await import("classic-level")
|
50
|
+
const db = new ClassicLevel<string, unknown>(location, { valueEncoding: "json" })
|
51
|
+
|
52
|
+
return new LocalWorkspaceBackend(db)
|
53
|
+
}
|
54
|
+
}
|