@highstate/backend 0.7.2 → 0.7.4

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.
Files changed (74) hide show
  1. package/dist/{index.mjs → index.js} +1254 -915
  2. package/dist/library/source-resolution-worker.js +55 -0
  3. package/dist/library/worker/main.js +216 -0
  4. package/dist/{terminal-CqIsctlZ.mjs → library-BW5oPM7V.js} +210 -87
  5. package/dist/shared/index.js +6 -0
  6. package/dist/utils-ByadNcv4.js +102 -0
  7. package/package.json +15 -19
  8. package/src/common/index.ts +3 -0
  9. package/src/common/local.ts +22 -0
  10. package/src/common/pulumi.ts +230 -0
  11. package/src/common/utils.ts +137 -0
  12. package/src/config.ts +40 -0
  13. package/src/index.ts +6 -0
  14. package/src/library/abstractions.ts +83 -0
  15. package/src/library/factory.ts +20 -0
  16. package/src/library/index.ts +2 -0
  17. package/src/library/local.ts +404 -0
  18. package/src/library/source-resolution-worker.ts +96 -0
  19. package/src/library/worker/evaluator.ts +119 -0
  20. package/src/library/worker/loader.ts +110 -0
  21. package/src/library/worker/main.ts +82 -0
  22. package/src/library/worker/protocol.ts +38 -0
  23. package/src/orchestrator/index.ts +1 -0
  24. package/src/orchestrator/manager.ts +165 -0
  25. package/src/orchestrator/operation-workset.ts +483 -0
  26. package/src/orchestrator/operation.ts +647 -0
  27. package/src/preferences/shared.ts +1 -0
  28. package/src/project/abstractions.ts +89 -0
  29. package/src/project/factory.ts +11 -0
  30. package/src/project/index.ts +4 -0
  31. package/src/project/local.ts +412 -0
  32. package/src/project/lock.ts +39 -0
  33. package/src/project/manager.ts +374 -0
  34. package/src/runner/abstractions.ts +146 -0
  35. package/src/runner/factory.ts +22 -0
  36. package/src/runner/index.ts +2 -0
  37. package/src/runner/local.ts +698 -0
  38. package/src/secret/abstractions.ts +59 -0
  39. package/src/secret/factory.ts +22 -0
  40. package/src/secret/index.ts +2 -0
  41. package/src/secret/local.ts +152 -0
  42. package/src/services.ts +133 -0
  43. package/src/shared/index.ts +10 -0
  44. package/src/shared/library.ts +77 -0
  45. package/src/shared/operation.ts +85 -0
  46. package/src/shared/project.ts +62 -0
  47. package/src/shared/resolvers/graph-resolver.ts +111 -0
  48. package/src/shared/resolvers/input-hash.ts +77 -0
  49. package/src/shared/resolvers/input.ts +314 -0
  50. package/src/shared/resolvers/registry.ts +10 -0
  51. package/src/shared/resolvers/validation.ts +94 -0
  52. package/src/shared/state.ts +262 -0
  53. package/src/shared/terminal.ts +13 -0
  54. package/src/state/abstractions.ts +222 -0
  55. package/src/state/factory.ts +22 -0
  56. package/src/state/index.ts +3 -0
  57. package/src/state/local.ts +605 -0
  58. package/src/state/manager.ts +33 -0
  59. package/src/terminal/docker.ts +90 -0
  60. package/src/terminal/factory.ts +20 -0
  61. package/src/terminal/index.ts +3 -0
  62. package/src/terminal/manager.ts +330 -0
  63. package/src/terminal/run.sh.ts +37 -0
  64. package/src/terminal/shared.ts +50 -0
  65. package/src/workspace/abstractions.ts +41 -0
  66. package/src/workspace/factory.ts +14 -0
  67. package/src/workspace/index.ts +2 -0
  68. package/src/workspace/local.ts +54 -0
  69. package/dist/index.d.ts +0 -760
  70. package/dist/library/worker/main.mjs +0 -164
  71. package/dist/runner/source-resolution-worker.mjs +0 -22
  72. package/dist/shared/index.d.ts +0 -85
  73. package/dist/shared/index.mjs +0 -54
  74. package/dist/terminal-Cm2WqcyB.d.ts +0 -1589
@@ -0,0 +1,698 @@
1
+ import type { ConfigMap, Stack } from "@pulumi/pulumi/automation"
2
+ import type { LibraryBackend } from "../library"
3
+ import { EventEmitter, on } from "node:events"
4
+ import { resolve } from "node:path"
5
+ import { getInstanceId } from "@highstate/contract"
6
+ import { ensureDependencyInstalled } from "nypm"
7
+ import { mapValues, omit, pick, pickBy } from "remeda"
8
+ import { z } from "zod"
9
+ import { sha256 } from "crypto-hash"
10
+ import { errorToString, LocalPulumiHost, runWithRetryOnError, updateResourceCount } from "../common"
11
+ import {
12
+ createInstanceState,
13
+ instanceFileSchema,
14
+ instancePageSchema,
15
+ instanceStatusFieldSchema,
16
+ instanceTerminalSchema,
17
+ instanceTriggerSchema,
18
+ type InstancePageBlock,
19
+ type InstanceState,
20
+ type InstanceStateUpdate,
21
+ type InstanceStatus,
22
+ type InstanceTerminal,
23
+ } from "../shared"
24
+ import {
25
+ type RunnerBackend,
26
+ type RunnerBaseOptions,
27
+ type InstanceUpdateOptions,
28
+ type InstanceWatchOptions,
29
+ InvalidInstanceStatusError,
30
+ type InstanceDestroyOptions,
31
+ } from "./abstractions"
32
+
33
+ type Events = {
34
+ [K in `state:${string}`]: [Partial<InstanceState>]
35
+ }
36
+
37
+ export const localRunnerBackendConfig = z.object({
38
+ HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_STATE_CHECK: z.boolean({ coerce: true }).default(false),
39
+ HIGHSTATE_BACKEND_RUNNER_LOCAL_PRINT_OUTPUT: z.boolean({ coerce: true }).default(true),
40
+ HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR: z.string().optional(),
41
+ })
42
+
43
+ export class LocalRunnerBackend implements RunnerBackend {
44
+ private readonly events = new EventEmitter<Events>()
45
+
46
+ constructor(
47
+ private readonly skipStateCheck: boolean,
48
+ private readonly printOutput: boolean,
49
+ private readonly cacheDir: string,
50
+ private readonly pulumiProjectHost: LocalPulumiHost,
51
+ private readonly libraryBackend: LibraryBackend,
52
+ ) {}
53
+
54
+ async *watch(options: InstanceWatchOptions): AsyncIterable<InstanceStateUpdate> {
55
+ const stream = on(
56
+ //
57
+ this.events,
58
+ `state:${LocalRunnerBackend.getInstanceId(options)}`,
59
+ { signal: options.signal },
60
+ ) as AsyncIterable<[InstanceStateUpdate]>
61
+
62
+ for await (const [statePatch] of stream) {
63
+ yield statePatch
64
+
65
+ if (statePatch.status && options.finalStatuses?.includes(statePatch.status)) {
66
+ return
67
+ }
68
+ }
69
+ }
70
+
71
+ getState(options: RunnerBaseOptions): Promise<InstanceState> {
72
+ return this.pulumiProjectHost.runEmpty(
73
+ {
74
+ projectId: options.projectId,
75
+ pulumiProjectName: options.instanceType,
76
+ pulumiStackName: LocalRunnerBackend.getStackName(options),
77
+ },
78
+ async stack => {
79
+ const info = await stack.info()
80
+ const instanceId = getInstanceId(options.instanceType, options.instanceName)
81
+
82
+ if (!info) {
83
+ return createInstanceState(instanceId)
84
+ }
85
+
86
+ if (info.result === "failed") {
87
+ return createInstanceState(instanceId, "error")
88
+ }
89
+
90
+ if (info.result !== "succeeded") {
91
+ return createInstanceState(instanceId, "unknown")
92
+ }
93
+
94
+ const summary = await stack.workspace.stack()
95
+ const resourceCount = summary?.resourceCount
96
+
97
+ if (!resourceCount) {
98
+ return createInstanceState(instanceId, "not_created")
99
+ }
100
+
101
+ return createInstanceState(instanceId, "created", {
102
+ currentResourceCount: resourceCount,
103
+ totalResourceCount: resourceCount,
104
+ })
105
+ },
106
+ )
107
+ }
108
+
109
+ getTerminalFactory(
110
+ options: RunnerBaseOptions,
111
+ terminalName: string,
112
+ ): Promise<InstanceTerminal | null> {
113
+ return this.pulumiProjectHost.runEmpty(
114
+ {
115
+ projectId: options.projectId,
116
+ pulumiProjectName: options.instanceType,
117
+ pulumiStackName: LocalRunnerBackend.getStackName(options),
118
+ },
119
+ async stack => {
120
+ const outputs = await stack.outputs()
121
+
122
+ if (!outputs["$terminals"]) {
123
+ return null
124
+ }
125
+
126
+ const terminals = z.array(instanceTerminalSchema).parse(outputs["$terminals"].value)
127
+ const terminal = terminals.find(t => t.name === terminalName)
128
+
129
+ if (!terminal) {
130
+ return null
131
+ }
132
+
133
+ return terminal
134
+ },
135
+ )
136
+ }
137
+
138
+ getPageContent(
139
+ options: RunnerBaseOptions,
140
+ pageName: string,
141
+ ): Promise<InstancePageBlock[] | null> {
142
+ return this.pulumiProjectHost.runEmpty(
143
+ {
144
+ projectId: options.projectId,
145
+ pulumiProjectName: options.instanceType,
146
+ pulumiStackName: LocalRunnerBackend.getStackName(options),
147
+ },
148
+ async stack => {
149
+ const outputs = await stack.outputs()
150
+
151
+ if (!outputs["$pages"]) {
152
+ return null
153
+ }
154
+
155
+ const pages = z.array(instancePageSchema).parse(outputs["$pages"].value)
156
+ const page = pages.find(p => p.name === pageName)
157
+
158
+ if (!page) {
159
+ return null
160
+ }
161
+
162
+ return page.content
163
+ },
164
+ )
165
+ }
166
+
167
+ getFileContent(options: RunnerBaseOptions, fileName: string): Promise<string | null> {
168
+ return this.pulumiProjectHost.runEmpty(
169
+ {
170
+ projectId: options.projectId,
171
+ pulumiProjectName: options.instanceType,
172
+ pulumiStackName: LocalRunnerBackend.getStackName(options),
173
+ },
174
+ async stack => {
175
+ const outputs = await stack.outputs()
176
+
177
+ if (!outputs["$files"]) {
178
+ return null
179
+ }
180
+
181
+ const files = z.array(instanceFileSchema).parse(outputs["$files"].value)
182
+ const file = files.find(f => f.meta.name === fileName)
183
+
184
+ if (!file) {
185
+ return null
186
+ }
187
+
188
+ return file.content
189
+ },
190
+ )
191
+ }
192
+
193
+ async update(options: InstanceUpdateOptions): Promise<void> {
194
+ const currentStatus = await this.validateStatus(options, [
195
+ "not_created",
196
+ "updating",
197
+ "created",
198
+ "error",
199
+ ])
200
+
201
+ if (currentStatus === "updating") {
202
+ return
203
+ }
204
+
205
+ const configMap: ConfigMap = {
206
+ ...mapValues(options.config, value => ({ value })),
207
+ ...mapValues(options.secrets, value => ({ value, secret: true })),
208
+ }
209
+
210
+ void this.updateWorker(options, configMap, false)
211
+ }
212
+
213
+ async preview(options: InstanceUpdateOptions): Promise<void> {
214
+ const currentStatus = await this.validateStatus(options, [
215
+ "not_created",
216
+ "previewing",
217
+ "created",
218
+ "error",
219
+ ])
220
+
221
+ if (currentStatus === "previewing") {
222
+ return
223
+ }
224
+
225
+ const configMap: ConfigMap = {
226
+ ...mapValues(options.config, value => ({ value })),
227
+ ...mapValues(options.secrets, value => ({ value, secret: true })),
228
+ }
229
+
230
+ void this.updateWorker(options, configMap, true)
231
+ }
232
+
233
+ private async updateWorker(
234
+ options: InstanceUpdateOptions,
235
+ configMap: ConfigMap,
236
+ preview: boolean,
237
+ ): Promise<void> {
238
+ const instanceId = LocalRunnerBackend.getInstanceId(options)
239
+
240
+ try {
241
+ const resolvedSource = await this.libraryBackend.getResolvedUnitSource(options.instanceType)
242
+ if (!resolvedSource) {
243
+ throw new Error(`Resolved unit source not found for ${options.instanceType}`)
244
+ }
245
+
246
+ await this.pulumiProjectHost.runLocal(
247
+ {
248
+ projectId: options.projectId,
249
+ pulumiProjectName: options.instanceType,
250
+ pulumiStackName: LocalRunnerBackend.getStackName(options),
251
+ projectPath: resolvedSource.projectPath,
252
+ stackConfig: configMap,
253
+ envVars: {
254
+ HIGHSTATE_CACHE_DIR: this.cacheDir,
255
+ },
256
+ },
257
+ async stack => {
258
+ await this.setStackConfig(stack, configMap)
259
+
260
+ this.updateState({
261
+ id: instanceId,
262
+ status: preview ? "previewing" : "updating",
263
+ currentResourceCount: 0,
264
+ totalResourceCount: 0,
265
+ })
266
+
267
+ let currentResourceCount = 0
268
+ let totalResourceCount = 0
269
+
270
+ await runWithRetryOnError(
271
+ async () => {
272
+ await stack[preview ? "preview" : "up"]({
273
+ color: "always",
274
+ refresh: options.refresh,
275
+ signal: options.signal,
276
+ diff: preview,
277
+
278
+ onEvent: event => {
279
+ if (event.resourcePreEvent) {
280
+ totalResourceCount = updateResourceCount(
281
+ event.resourcePreEvent.metadata.op,
282
+ totalResourceCount,
283
+ )
284
+
285
+ this.updateState({ id: instanceId, totalResourceCount })
286
+ return
287
+ }
288
+
289
+ if (event.resOutputsEvent) {
290
+ currentResourceCount = updateResourceCount(
291
+ event.resOutputsEvent.metadata.op,
292
+ currentResourceCount,
293
+ )
294
+
295
+ this.updateState({ id: instanceId, currentResourceCount })
296
+ return
297
+ }
298
+ },
299
+
300
+ onOutput: message => {
301
+ this.updateState({ id: instanceId, logLine: message })
302
+
303
+ if (this.printOutput) {
304
+ console.log(message)
305
+ }
306
+ },
307
+ })
308
+
309
+ const extraOutputs = await this.getExtraOutputsStatePatch(stack)
310
+
311
+ this.updateState({
312
+ id: instanceId,
313
+ status: "created",
314
+ totalResourceCount: currentResourceCount,
315
+ ...extraOutputs,
316
+ })
317
+ },
318
+ async error => {
319
+ const isUnlocked = await this.pulumiProjectHost.tryUnlockStack(stack, error)
320
+ if (isUnlocked) return true
321
+
322
+ const isResolved = await this.tryInstallMissingDependencies(
323
+ error,
324
+ resolvedSource.allowedDependencies,
325
+ )
326
+ if (isResolved) return true
327
+
328
+ return false
329
+ },
330
+ )
331
+ },
332
+ )
333
+ } catch (error) {
334
+ this.updateState({
335
+ id: instanceId,
336
+ status: "error",
337
+ error: errorToString(error),
338
+ })
339
+ }
340
+ }
341
+
342
+ private async setStackConfig(stack: Stack, configMap: ConfigMap): Promise<void> {
343
+ const currentConfig = await stack.getAllConfig()
344
+ const currentConfigKeys = Object.keys(currentConfig)
345
+
346
+ await stack.removeAllConfig(currentConfigKeys)
347
+ await stack.setAllConfig(configMap)
348
+ }
349
+
350
+ async destroy(options: InstanceDestroyOptions): Promise<void> {
351
+ const currentStatus = await this.validateStatus(options, [
352
+ "not_created",
353
+ "destroying",
354
+ "created",
355
+ "error",
356
+ ])
357
+
358
+ if (currentStatus === "destroying") {
359
+ return
360
+ }
361
+
362
+ void this.destroyWorker(options)
363
+ }
364
+
365
+ private async destroyWorker(options: InstanceDestroyOptions): Promise<void> {
366
+ const instanceId = LocalRunnerBackend.getInstanceId(options)
367
+
368
+ try {
369
+ const resolvedSource = await this.libraryBackend.getResolvedUnitSource(options.instanceType)
370
+ if (!resolvedSource) {
371
+ throw new Error(`Resolved unit source not found for ${options.instanceType}`)
372
+ }
373
+
374
+ await this.pulumiProjectHost.runLocal(
375
+ {
376
+ projectId: options.projectId,
377
+ pulumiProjectName: options.instanceType,
378
+ pulumiStackName: LocalRunnerBackend.getStackName(options),
379
+ projectPath: resolvedSource.projectPath,
380
+ envVars: {
381
+ HIGHSTATE_CACHE_DIR: this.cacheDir,
382
+ PULUMI_K8S_DELETE_UNREACHABLE: options.deleteUnreachable ? "true" : "",
383
+ },
384
+ },
385
+ async stack => {
386
+ const summary = await stack.workspace.stack()
387
+ let currentResourceCount = summary?.resourceCount ?? 0
388
+
389
+ this.updateState({
390
+ id: instanceId,
391
+ status: "destroying",
392
+ currentResourceCount,
393
+ totalResourceCount: currentResourceCount,
394
+ })
395
+
396
+ await runWithRetryOnError(
397
+ async () => {
398
+ await stack.destroy({
399
+ color: "always",
400
+ refresh: options.refresh,
401
+ remove: true,
402
+ signal: options.signal,
403
+
404
+ onEvent: event => {
405
+ if (event.resOutputsEvent) {
406
+ currentResourceCount = updateResourceCount(
407
+ event.resOutputsEvent.metadata.op,
408
+ currentResourceCount,
409
+ )
410
+
411
+ this.updateState({ id: instanceId, currentResourceCount })
412
+ return
413
+ }
414
+ },
415
+
416
+ onOutput: message => {
417
+ this.updateState({ id: instanceId, logLine: message })
418
+
419
+ if (this.printOutput) {
420
+ console.log(message)
421
+ }
422
+ },
423
+ })
424
+
425
+ const extraOutputs = await this.getExtraOutputsStatePatch(stack)
426
+
427
+ this.updateState({
428
+ id: instanceId,
429
+ status: "not_created",
430
+ totalResourceCount: currentResourceCount,
431
+ ...extraOutputs,
432
+ })
433
+ },
434
+ error => this.pulumiProjectHost.tryUnlockStack(stack, error),
435
+ )
436
+ },
437
+ )
438
+ } catch (error) {
439
+ const { StackNotFoundError } = await import("@pulumi/pulumi/automation/index.js")
440
+
441
+ if (error instanceof StackNotFoundError) {
442
+ this.updateState({
443
+ id: instanceId,
444
+ status: "not_created",
445
+ totalResourceCount: 0,
446
+ currentResourceCount: 0,
447
+ statusFields: [],
448
+ pages: [],
449
+ files: [],
450
+ terminals: [],
451
+ triggers: [],
452
+ })
453
+ return
454
+ }
455
+
456
+ this.updateState({
457
+ id: instanceId,
458
+ status: "error",
459
+ error: errorToString(error),
460
+ })
461
+ }
462
+ }
463
+
464
+ async refresh(options: RunnerBaseOptions): Promise<void> {
465
+ const currentStatus = await this.validateStatus(options, [
466
+ "not_created",
467
+ "created",
468
+ "refreshing",
469
+ "error",
470
+ ])
471
+
472
+ if (currentStatus === "refreshing") {
473
+ return
474
+ }
475
+
476
+ void this.refreshWorker(options)
477
+ }
478
+
479
+ private async refreshWorker(options: RunnerBaseOptions): Promise<void> {
480
+ const instanceId = LocalRunnerBackend.getInstanceId(options)
481
+
482
+ try {
483
+ await this.pulumiProjectHost.runEmpty(
484
+ {
485
+ projectId: options.projectId,
486
+ pulumiProjectName: options.instanceType,
487
+ pulumiStackName: LocalRunnerBackend.getStackName(options),
488
+ },
489
+ async stack => {
490
+ const summary = await stack.workspace.stack()
491
+
492
+ let currentResourceCount = 0
493
+ let totalResourceCount = summary?.resourceCount ?? 0
494
+
495
+ this.updateState({
496
+ id: instanceId,
497
+ status: "refreshing",
498
+ currentResourceCount,
499
+ totalResourceCount,
500
+ })
501
+
502
+ await runWithRetryOnError(
503
+ async () => {
504
+ await stack.refresh({
505
+ color: "always",
506
+ onEvent: event => {
507
+ if (event.resourcePreEvent) {
508
+ totalResourceCount = updateResourceCount(
509
+ event.resourcePreEvent.metadata.op,
510
+ totalResourceCount,
511
+ )
512
+
513
+ this.updateState({ id: instanceId, totalResourceCount })
514
+ return
515
+ }
516
+
517
+ if (event.resOutputsEvent) {
518
+ currentResourceCount = updateResourceCount(
519
+ event.resOutputsEvent.metadata.op,
520
+ currentResourceCount,
521
+ )
522
+
523
+ this.updateState({ id: instanceId, currentResourceCount })
524
+ return
525
+ }
526
+ },
527
+ onOutput: message => {
528
+ this.updateState({ id: instanceId, logLine: message })
529
+
530
+ if (this.printOutput) {
531
+ console.log(message)
532
+ }
533
+ },
534
+ signal: options.signal,
535
+ })
536
+
537
+ const extraOutputs = await this.getExtraOutputsStatePatch(stack)
538
+
539
+ this.updateState({
540
+ id: instanceId,
541
+ status: "created",
542
+ totalResourceCount: currentResourceCount,
543
+ ...extraOutputs,
544
+ })
545
+ },
546
+ error => this.pulumiProjectHost.tryUnlockStack(stack, error),
547
+ )
548
+ },
549
+ )
550
+ } catch (error) {
551
+ this.updateState({
552
+ id: instanceId,
553
+ status: "error",
554
+ error: errorToString(error),
555
+ })
556
+ }
557
+ }
558
+
559
+ private async getExtraOutputsStatePatch(stack: Stack) {
560
+ const outputs = await stack.outputs()
561
+ const patch: Omit<InstanceStateUpdate, "id"> = {}
562
+
563
+ if (outputs["$status"]) {
564
+ patch.statusFields = z.array(instanceStatusFieldSchema).parse(outputs["$status"].value)
565
+ } else {
566
+ patch.statusFields = []
567
+ }
568
+
569
+ if (outputs["$pages"]) {
570
+ const pages = z.array(instancePageSchema).parse(outputs["$pages"].value)
571
+
572
+ patch.pages = pages.map(page => omit(page, ["content"]))
573
+ } else {
574
+ patch.pages = []
575
+ }
576
+
577
+ if (outputs["$files"]) {
578
+ const files = z.array(instanceFileSchema).parse(outputs["$files"].value)
579
+
580
+ patch.files = files.map(file => file.meta)
581
+ } else {
582
+ patch.files = []
583
+ }
584
+
585
+ if (outputs["$terminals"]) {
586
+ const terminals = z.array(instanceTerminalSchema).parse(outputs["$terminals"].value)
587
+
588
+ patch.terminals = terminals.map(terminal => pick(terminal, ["name", "title", "description"]))
589
+ } else {
590
+ patch.terminals = []
591
+ }
592
+
593
+ if (outputs["$triggers"]) {
594
+ patch.triggers = z.array(instanceTriggerSchema).parse(outputs["$triggers"].value)
595
+ } else {
596
+ patch.triggers = []
597
+ }
598
+
599
+ if (outputs["$secrets"]) {
600
+ patch.secrets = pickBy(
601
+ z.record(z.string().nullish()).parse(outputs["$secrets"].value),
602
+ v => !!v,
603
+ ) as Record<string, string>
604
+ } else {
605
+ patch.secrets = null
606
+ }
607
+
608
+ patch.outputHash = await sha256(JSON.stringify(outputs))
609
+
610
+ return patch
611
+ }
612
+
613
+ private updateState(patch: InstanceStateUpdate) {
614
+ this.events.emit(`state:${patch.id}`, patch)
615
+ }
616
+
617
+ private async validateStatus(
618
+ options: RunnerBaseOptions,
619
+ expectedStatuses: InstanceStatus[],
620
+ ): Promise<InstanceStatus | undefined> {
621
+ if (this.skipStateCheck) {
622
+ return
623
+ }
624
+
625
+ const existingState = await this.getState(options)
626
+
627
+ if (!existingState) {
628
+ return
629
+ }
630
+
631
+ if (!expectedStatuses.includes(existingState.status)) {
632
+ throw new InvalidInstanceStatusError(existingState.status, expectedStatuses)
633
+ }
634
+
635
+ return existingState.status
636
+ }
637
+
638
+ private static getInstanceId(options: RunnerBaseOptions) {
639
+ return getInstanceId(options.instanceType, options.instanceName)
640
+ }
641
+
642
+ private async tryInstallMissingDependencies(
643
+ error: unknown,
644
+ allowedDependencies: string[],
645
+ ): Promise<boolean> {
646
+ if (!(error instanceof Error)) {
647
+ return false
648
+ }
649
+
650
+ const pattern = /Cannot find module '(.*)'/
651
+ const match = error.message.match(pattern)
652
+
653
+ if (!match) {
654
+ return false
655
+ }
656
+
657
+ const packageName = match[1]
658
+
659
+ if (!allowedDependencies.includes(packageName)) {
660
+ throw new Error(
661
+ `Dependency '${packageName}' was requested to be auto-installed, but it is not allowed. Please add it to the 'peerDependencies' in the package.json of the unit.`,
662
+ )
663
+ }
664
+
665
+ await ensureDependencyInstalled(packageName)
666
+ return true
667
+ }
668
+
669
+ private static getStackName(options: RunnerBaseOptions) {
670
+ return `${options.projectId}_${options.instanceName}`
671
+ }
672
+
673
+ public static create(
674
+ config: z.infer<typeof localRunnerBackendConfig>,
675
+ pulumiProjectHost: LocalPulumiHost,
676
+ libraryBackend: LibraryBackend,
677
+ ): RunnerBackend {
678
+ let cacheDir = config.HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR
679
+ if (!cacheDir) {
680
+ const homeDir = process.env.HOME ?? process.env.USERPROFILE
681
+ if (!homeDir) {
682
+ throw new Error(
683
+ "Failed to determine the home directory, please set HIGHSTATE_BACKEND_RUNNER_LOCAL_CACHE_DIR",
684
+ )
685
+ }
686
+
687
+ cacheDir = resolve(homeDir, ".cache", "highstate")
688
+ }
689
+
690
+ return new LocalRunnerBackend(
691
+ config.HIGHSTATE_BACKEND_RUNNER_LOCAL_SKIP_STATE_CHECK,
692
+ config.HIGHSTATE_BACKEND_RUNNER_LOCAL_PRINT_OUTPUT,
693
+ cacheDir,
694
+ pulumiProjectHost,
695
+ libraryBackend,
696
+ )
697
+ }
698
+ }