@highstate/backend 0.2.0 → 0.2.1

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 ADDED
@@ -0,0 +1,832 @@
1
+ import { BetterLock } from 'better-lock';
2
+ import { z } from 'zod';
3
+ import { basename, relative, dirname } from 'node:path';
4
+ import { findWorkspaceDir, readPackageJSON } from 'pkg-types';
5
+ import { pickBy, mapValues, debounce } from 'remeda';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { EventEmitter, on } from 'node:events';
8
+ import { Worker } from 'node:worker_threads';
9
+ import Watcher from 'watcher';
10
+ import { resolve } from 'import-meta-resolve';
11
+ import { consola } from 'consola';
12
+ import { readFile, writeFile, mkdir } from 'fs/promises';
13
+ import { isUnitModel } from '@highstate/contract';
14
+ import { addDependency } from 'nypm';
15
+
16
+ class LocalPulumiProjectHost {
17
+ lock = new BetterLock();
18
+ password = process.env.PULUMI_CONFIG_PASSPHRASE ?? "";
19
+ hasPassword() {
20
+ return !!this.password;
21
+ }
22
+ setPassword(password) {
23
+ this.password = password;
24
+ }
25
+ async runInline(projectName, stackName, program, fn) {
26
+ return this.lock.acquire(`${projectName}.${stackName}`, async () => {
27
+ const { LocalWorkspace } = await import('@pulumi/pulumi/automation');
28
+ const stack = await LocalWorkspace.createOrSelectStack(
29
+ {
30
+ projectName,
31
+ stackName,
32
+ program
33
+ },
34
+ {
35
+ projectSettings: {
36
+ name: projectName,
37
+ runtime: "nodejs"
38
+ },
39
+ envVars: { PULUMI_CONFIG_PASSPHRASE: this.password }
40
+ }
41
+ );
42
+ return await fn(stack);
43
+ });
44
+ }
45
+ async runLocal(projectName, stackName, programPathResolver, fn) {
46
+ return this.lock.acquire(`${projectName}.${stackName}`, async () => {
47
+ const { LocalWorkspace } = await import('@pulumi/pulumi/automation');
48
+ const stack = await LocalWorkspace.createOrSelectStack(
49
+ {
50
+ stackName,
51
+ workDir: await programPathResolver()
52
+ },
53
+ {
54
+ envVars: { PULUMI_CONFIG_PASSPHRASE: this.password }
55
+ }
56
+ );
57
+ return await fn(stack);
58
+ });
59
+ }
60
+ }
61
+
62
+ const instanceStatusSchema = z.enum([
63
+ "not_created",
64
+ "updating",
65
+ "destroying",
66
+ "refreshing",
67
+ "created",
68
+ "error"
69
+ ]);
70
+ const instanceStateSchema = z.object({
71
+ key: z.string(),
72
+ parentKey: z.string().optional(),
73
+ status: instanceStatusSchema,
74
+ resources: z.number(),
75
+ totalResources: z.number(),
76
+ error: z.string().optional(),
77
+ message: z.string().optional()
78
+ });
79
+ const operationType = z.enum(["update", "destroy", "recreate", "refresh"]);
80
+ const projectOperation = z.object({
81
+ type: operationType,
82
+ instanceIds: z.array(z.string())
83
+ });
84
+ const projectStateSchema = z.object({
85
+ currentOperation: projectOperation.optional(),
86
+ instances: z.record(instanceStateSchema.optional())
87
+ });
88
+ const positionSchema = z.object({
89
+ x: z.number(),
90
+ y: z.number()
91
+ });
92
+ const outputRefSchema = z.object({
93
+ instanceId: z.string(),
94
+ output: z.string()
95
+ });
96
+ const instanceModelSchema = z.object({
97
+ id: z.string().nanoid(),
98
+ type: z.string(),
99
+ name: z.string(),
100
+ position: positionSchema,
101
+ args: z.record(z.any()),
102
+ inputs: z.record(z.union([outputRefSchema, z.array(outputRefSchema)]))
103
+ });
104
+
105
+ const stringArrayType = z.string().transform((args) => args.split(",").map((arg) => arg.trim()));
106
+
107
+ async function resolveMainLocalProject(projectPath, projectName) {
108
+ if (!projectPath) {
109
+ projectPath = await findWorkspaceDir();
110
+ }
111
+ if (!projectName) {
112
+ const packageJson = await readPackageJSON(projectPath);
113
+ projectName = packageJson.name;
114
+ }
115
+ if (!projectName) {
116
+ projectName = basename(projectPath);
117
+ }
118
+ return [projectPath, projectName];
119
+ }
120
+
121
+ class SecretAccessDeniedError extends Error {
122
+ constructor(projectId, key) {
123
+ super(`Access to the secrets of component "${projectId}.${key}" is denied.`);
124
+ }
125
+ }
126
+
127
+ const localSecretBackendConfig = z.object({
128
+ HIGHSTATE_BACKEND_SECRET_PROJECT_PATH: z.string().optional(),
129
+ HIGHSTATE_BACKEND_SECRET_PROJECT_NAME: z.string().optional()
130
+ });
131
+ class LocalSecretBackend {
132
+ constructor(projectPath, projectName, pulumiProjectHost) {
133
+ this.projectPath = projectPath;
134
+ this.projectName = projectName;
135
+ this.pulumiProjectHost = pulumiProjectHost;
136
+ }
137
+ get(projectId, componentKey) {
138
+ return this.pulumiProjectHost.runLocal(
139
+ this.projectName,
140
+ projectId,
141
+ () => this.projectPath,
142
+ async (stack) => {
143
+ const config = await stack.getAllConfig();
144
+ const componentSecrets = pickBy(config, (_, key) => key.startsWith(`${componentKey}.`));
145
+ return mapValues(componentSecrets, (value) => value.value);
146
+ }
147
+ );
148
+ }
149
+ set(projectId, componentKey, values) {
150
+ return this.pulumiProjectHost.runLocal(
151
+ this.projectName,
152
+ projectId,
153
+ () => this.projectPath,
154
+ async (stack) => {
155
+ const componentSecrets = mapValues(values, (value, key) => ({
156
+ key: `${componentKey}.${key}`,
157
+ value
158
+ }));
159
+ const config = await stack.getAllConfig();
160
+ Object.assign(config, componentSecrets);
161
+ await stack.setAllConfig(config);
162
+ }
163
+ );
164
+ }
165
+ static async create(config, pulumiProjectHost) {
166
+ const [projectPath, projectName] = await resolveMainLocalProject(
167
+ config.HIGHSTATE_BACKEND_SECRET_PROJECT_PATH,
168
+ config.HIGHSTATE_BACKEND_SECRET_PROJECT_NAME
169
+ );
170
+ return new LocalSecretBackend(projectPath, projectName, pulumiProjectHost);
171
+ }
172
+ }
173
+
174
+ const secretBackendConfig = z.object({
175
+ HIGHSTATE_BACKEND_SECRET_TYPE: z.enum(["local"]).default("local"),
176
+ ...localSecretBackendConfig.shape
177
+ });
178
+ function createSecretBackend(config, pulumiProjectHost) {
179
+ return LocalSecretBackend.create(config, pulumiProjectHost);
180
+ }
181
+
182
+ const localLibraryBackendConfig = z.object({
183
+ HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES: stringArrayType.default("@highstate/library")
184
+ });
185
+ class LocalLibraryBackend {
186
+ constructor(modulePaths) {
187
+ this.modulePaths = modulePaths;
188
+ this.watcher = new Watcher(modulePaths, { recursive: true, ignoreInitial: true });
189
+ this.watcher.on("all", (event, path) => {
190
+ const prefixPath = modulePaths.find((modulePath) => path.startsWith(modulePath));
191
+ consola.info("Library event:", event, relative(prefixPath, path));
192
+ void this.updateLibrary();
193
+ });
194
+ consola.info("Library watchers set to:", modulePaths);
195
+ }
196
+ watcher;
197
+ lock = new BetterLock();
198
+ eventEmitter = new EventEmitter();
199
+ library = null;
200
+ async loadLibrary() {
201
+ if (this.library) {
202
+ return this.library;
203
+ }
204
+ return await this.updateLibrary();
205
+ }
206
+ async *watchLibrary(signal) {
207
+ for await (const [library] of on(this.eventEmitter, "library", { signal })) {
208
+ yield library;
209
+ }
210
+ }
211
+ async evaluateInstances(instances, instanceIds) {
212
+ const worker = this.createWorker("library-evaluator", {
213
+ modulePaths: this.modulePaths,
214
+ instances,
215
+ instanceIds
216
+ });
217
+ for await (const [registrations] of on(worker, "message")) {
218
+ return registrations;
219
+ }
220
+ throw new Error("Worker ended without sending registrations.");
221
+ }
222
+ async updateLibrary() {
223
+ return await this.lock.acquire(async () => {
224
+ const worker = this.createWorker("library-loader", { modulePaths: this.modulePaths });
225
+ for await (const [library] of on(worker, "message")) {
226
+ this.eventEmitter.emit("library", library);
227
+ this.library = library;
228
+ consola.success("Library reloaded");
229
+ return this.library;
230
+ }
231
+ throw new Error("Worker ended without sending library model.");
232
+ });
233
+ }
234
+ createWorker(module, workerData) {
235
+ const workerPathUrl = resolve(`@highstate/backend/${module}`, import.meta.url);
236
+ const workerPath = fileURLToPath(workerPathUrl);
237
+ return new Worker(workerPath, { workerData });
238
+ }
239
+ // eslint-disable-next-line @typescript-eslint/require-await
240
+ static async create(config) {
241
+ const modulePaths = [];
242
+ for (const module of config.HIGHSTATE_BACKEND_LIBRARY_LOCAL_MODULES) {
243
+ const url = resolve(module, import.meta.url);
244
+ let path = fileURLToPath(url);
245
+ if (basename(path).includes(".")) {
246
+ path = dirname(path);
247
+ }
248
+ modulePaths.push(path);
249
+ }
250
+ return new LocalLibraryBackend(modulePaths);
251
+ }
252
+ }
253
+
254
+ const libraryBackendConfig = z.object({
255
+ HIGHSTATE_BACKEND_LIBRARY_TYPE: z.enum(["local"]).default("local"),
256
+ ...localLibraryBackendConfig.shape
257
+ });
258
+ function createLibraryBackend(config) {
259
+ switch (config.HIGHSTATE_BACKEND_LIBRARY_TYPE) {
260
+ case "local": {
261
+ return LocalLibraryBackend.create(config);
262
+ }
263
+ }
264
+ }
265
+
266
+ const localProjectBackendConfig = z.object({
267
+ HIGHSTATE_BACKEND_PROJECT_BLUEPRINTS_DIR: z.string().default("blueprints")
268
+ });
269
+ const blueprintModelSchema = z.object({
270
+ instances: z.record(instanceModelSchema)
271
+ });
272
+ class LocalProjectBackend {
273
+ constructor(blueprintsDir) {
274
+ this.blueprintsDir = blueprintsDir;
275
+ }
276
+ async getInstances(projectId) {
277
+ try {
278
+ const blueprint = await this.loadBlueprint(projectId);
279
+ return blueprint.instances;
280
+ } catch (error) {
281
+ console.error("Failed to read blueprint", error);
282
+ return {};
283
+ }
284
+ }
285
+ async updateInstance(projectId, instance) {
286
+ try {
287
+ const blueprint = await this.loadBlueprint(projectId);
288
+ blueprint.instances[instance.id] = instance;
289
+ await this.writeBlueprint(projectId, blueprint);
290
+ } catch (error) {
291
+ console.error("Failed to update blueprint instance", error);
292
+ }
293
+ }
294
+ async deleteInstance(projectId, instanceId) {
295
+ try {
296
+ const blueprint = await this.loadBlueprint(projectId);
297
+ delete blueprint.instances[instanceId];
298
+ await this.writeBlueprint(projectId, blueprint);
299
+ } catch (error) {
300
+ console.error("Failed to delete blueprint instance", error);
301
+ }
302
+ }
303
+ getBlueprintPath(projectId) {
304
+ return `${this.blueprintsDir}/${projectId}.json`;
305
+ }
306
+ async loadBlueprint(projectId) {
307
+ const blueprintPath = this.getBlueprintPath(projectId);
308
+ try {
309
+ const content = await readFile(blueprintPath, "utf-8");
310
+ return blueprintModelSchema.parse(JSON.parse(content));
311
+ } catch (error) {
312
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
313
+ return { instances: {} };
314
+ }
315
+ throw error;
316
+ }
317
+ }
318
+ async writeBlueprint(projectId, blueprint) {
319
+ const blueprintPath = this.getBlueprintPath(projectId);
320
+ const content = JSON.stringify(blueprint, void 0, 2);
321
+ await writeFile(blueprintPath, content);
322
+ }
323
+ static async create(config) {
324
+ await mkdir(config.HIGHSTATE_BACKEND_PROJECT_BLUEPRINTS_DIR, { recursive: true });
325
+ return new LocalProjectBackend(config.HIGHSTATE_BACKEND_PROJECT_BLUEPRINTS_DIR);
326
+ }
327
+ }
328
+
329
+ const projectBackendConfig = z.object({
330
+ HIGHSTATE_BACKEND_PROJECT_TYPE: z.enum(["local"]).default("local"),
331
+ ...localProjectBackendConfig.shape
332
+ });
333
+ function createProjectBackend(config) {
334
+ return LocalProjectBackend.create(config);
335
+ }
336
+
337
+ const localStateBackendConfig = z.object({
338
+ HIGHSTATE_BACKEND_STATE_PROJECT_PATH: z.string().optional(),
339
+ HIGHSTATE_BACKEND_STATE_PROJECT_NAME: z.string().optional()
340
+ });
341
+ class LocalStateBackend {
342
+ constructor(projectName, pulumiProjectHost) {
343
+ this.projectName = projectName;
344
+ this.pulumiProjectHost = pulumiProjectHost;
345
+ }
346
+ get(projectId) {
347
+ return this.pulumiProjectHost.runInline(
348
+ this.projectName,
349
+ projectId,
350
+ () => Promise.resolve(),
351
+ (stack) => this.getAllState(stack)
352
+ );
353
+ }
354
+ set(projectId, state) {
355
+ return this.pulumiProjectHost.runInline(
356
+ this.projectName,
357
+ projectId,
358
+ () => Promise.resolve(),
359
+ async (stack) => {
360
+ await stack.setConfig("state", { value: JSON.stringify(state) });
361
+ await stack.up();
362
+ }
363
+ );
364
+ }
365
+ async getAllState(stack) {
366
+ const history = await stack.history(1);
367
+ if (!history.length) {
368
+ return { instances: {} };
369
+ }
370
+ if (!history[0].config["state"]) {
371
+ return { instances: {} };
372
+ }
373
+ const jsonValue = JSON.parse(history[0].config["state"].value);
374
+ return projectStateSchema.parse(jsonValue);
375
+ }
376
+ static async create(config, pulumiProjectHost) {
377
+ const [, projectName] = await resolveMainLocalProject(
378
+ config.HIGHSTATE_BACKEND_STATE_PROJECT_PATH,
379
+ config.HIGHSTATE_BACKEND_STATE_PROJECT_NAME
380
+ );
381
+ return new LocalStateBackend(projectName, pulumiProjectHost);
382
+ }
383
+ }
384
+
385
+ const stateBackendConfig = z.object({
386
+ HIGHSTATE_BACKEND_STATE_TYPE: z.enum(["local"]).default("local"),
387
+ ...localStateBackendConfig.shape
388
+ });
389
+ function createStateBackend(config, pulumiProjectHost) {
390
+ return LocalStateBackend.create(config, pulumiProjectHost);
391
+ }
392
+
393
+ const configSchema = z.object({
394
+ ...libraryBackendConfig.shape,
395
+ ...projectBackendConfig.shape,
396
+ ...secretBackendConfig.shape,
397
+ ...stateBackendConfig.shape
398
+ });
399
+ async function loadConfig(env = process.env, useDotenv = true) {
400
+ if (useDotenv) {
401
+ await import('dotenv/config');
402
+ }
403
+ return configSchema.parse(env);
404
+ }
405
+
406
+ class ProjectOperator {
407
+ constructor(projectId, state, runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend) {
408
+ this.projectId = projectId;
409
+ this.state = state;
410
+ this.runnerBackend = runnerBackend;
411
+ this.stateBackend = stateBackend;
412
+ this.libraryBackend = libraryBackend;
413
+ this.projectBackend = projectBackend;
414
+ this.secretBackend = secretBackend;
415
+ }
416
+ unitPromises = /* @__PURE__ */ new Map();
417
+ eventEmitter = new EventEmitter();
418
+ debouncedUpdateState = debounce(() => this.stateBackend.set(this.projectId, this.state), {
419
+ waitMs: 1e3,
420
+ maxWaitMs: 1e4
421
+ });
422
+ abortController = new AbortController();
423
+ async *watchInstanceStates(signal) {
424
+ for await (const [state] of on(this.eventEmitter, "state", { signal })) {
425
+ yield state;
426
+ }
427
+ }
428
+ async *watchCurrentOperation(signal) {
429
+ for await (const [operation] of on(this.eventEmitter, "operation", { signal })) {
430
+ yield operation;
431
+ }
432
+ }
433
+ /**
434
+ * Launches the project operation.
435
+ * Throws an error if the operation is already running.
436
+ *
437
+ * @param operation The operation to launch.
438
+ */
439
+ launch(operation) {
440
+ if (this.state.currentOperation) {
441
+ throw new Error("An operation is already running.");
442
+ }
443
+ this.abortController = new AbortController();
444
+ this.updateCurrentOperation(operation);
445
+ void this.operate().finally(() => {
446
+ this.updateCurrentOperation(void 0);
447
+ });
448
+ }
449
+ /**
450
+ * Cancels the current operation.
451
+ * Does nothing if no operation is running.
452
+ */
453
+ cancel() {
454
+ if (!this.state.currentOperation) {
455
+ return;
456
+ }
457
+ this.abortController.abort();
458
+ }
459
+ async operate() {
460
+ await this.debouncedUpdateState.flush();
461
+ const operation = this.state.currentOperation;
462
+ const instances = await this.projectBackend.getInstances(this.projectId);
463
+ const library = await this.libraryBackend.loadLibrary();
464
+ const registrations = await this.libraryBackend.evaluateInstances(
465
+ instances,
466
+ operation.instanceIds
467
+ );
468
+ const promises = [];
469
+ for (const registration of Object.values(registrations)) {
470
+ const promise = this.upUnit(library, registrations, registration);
471
+ promises.push(promise);
472
+ }
473
+ await Promise.all(promises);
474
+ }
475
+ async upUnit(library, registrations, registration) {
476
+ const model = library.components[registration.type];
477
+ if (!isUnitModel(model)) {
478
+ throw new Error(`The component "${registration.type}" is not a unit.`);
479
+ }
480
+ const key = `${registration.type}.${registration.name}`;
481
+ const dependencyPromises = [];
482
+ for (const dependency of registration.dependencies) {
483
+ const state = this.state.instances[dependency];
484
+ if (state?.status === "created") {
485
+ continue;
486
+ }
487
+ const dependencyPromise = this.getOrCreateUnitPromise(
488
+ key,
489
+ () => this.upUnit(library, registrations, registrations[dependency])
490
+ );
491
+ dependencyPromises.push(dependencyPromise);
492
+ }
493
+ await Promise.all(dependencyPromises);
494
+ await this.runnerBackend.update({
495
+ projectName: registration.type,
496
+ stackName: registration.name,
497
+ config: await this.getUnitConfig(model, key, registration),
498
+ source: model.source
499
+ });
500
+ const stream = this.runnerBackend.watch({
501
+ projectName: registration.type,
502
+ stackName: registration.name,
503
+ finalStatuses: ["created", "error"]
504
+ });
505
+ await this.watchStateStream(stream);
506
+ }
507
+ async getUnitConfig(model, key, registration) {
508
+ const secrets = await this.secretBackend.get(this.projectId, `${model.type}.${key}`);
509
+ return {
510
+ ...registration.config,
511
+ ...secrets
512
+ };
513
+ }
514
+ async watchStateStream(stream) {
515
+ let state;
516
+ for await (state of stream) {
517
+ this.updateInstanceState(state);
518
+ }
519
+ if (!state) {
520
+ throw new Error("The stream ended without emitting any state.");
521
+ }
522
+ if (state.status === "error") {
523
+ throw new Error(`The operation on unit "${state.key}" failed.`);
524
+ }
525
+ }
526
+ updateCurrentOperation(operation) {
527
+ this.state.currentOperation = operation;
528
+ this.eventEmitter.emit("operation", operation);
529
+ void this.debouncedUpdateState.call();
530
+ }
531
+ updateInstanceState(state) {
532
+ this.state.instances[state.key] = state;
533
+ this.eventEmitter.emit("state", state);
534
+ void this.debouncedUpdateState.call();
535
+ }
536
+ getOrCreateUnitPromise(unitKey, fn) {
537
+ let promise = this.unitPromises.get(unitKey);
538
+ if (!promise) {
539
+ promise = fn().then(() => {
540
+ this.unitPromises.delete(unitKey);
541
+ });
542
+ this.unitPromises.set(unitKey, promise);
543
+ }
544
+ return promise;
545
+ }
546
+ static async create(projectId, runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend) {
547
+ const state = await stateBackend.get(projectId);
548
+ return new ProjectOperator(
549
+ projectId,
550
+ state,
551
+ runnerBackend,
552
+ stateBackend,
553
+ libraryBackend,
554
+ projectBackend,
555
+ secretBackend
556
+ );
557
+ }
558
+ }
559
+
560
+ class OperatorManager {
561
+ constructor(runnerBackend, stateBackend, libraryBackend, projectBackend, secretBackend) {
562
+ this.runnerBackend = runnerBackend;
563
+ this.stateBackend = stateBackend;
564
+ this.libraryBackend = libraryBackend;
565
+ this.projectBackend = projectBackend;
566
+ this.secretBackend = secretBackend;
567
+ }
568
+ operators = /* @__PURE__ */ new Map();
569
+ lock = new BetterLock();
570
+ async get(projectId) {
571
+ return await this.lock.acquire(projectId, async () => {
572
+ let orchestrator = this.operators.get(projectId);
573
+ if (!orchestrator) {
574
+ orchestrator = await ProjectOperator.create(
575
+ projectId,
576
+ this.runnerBackend,
577
+ this.stateBackend,
578
+ this.libraryBackend,
579
+ this.projectBackend,
580
+ this.secretBackend
581
+ );
582
+ this.operators.set(projectId, orchestrator);
583
+ }
584
+ return orchestrator;
585
+ });
586
+ }
587
+ }
588
+
589
+ class InvalidInstanceStatusError extends Error {
590
+ constructor(currentStatus, expectedStatuses) {
591
+ const expectedString = expectedStatuses.join(", ");
592
+ super(`The current state is "${currentStatus}", but it should be one of "${expectedString}".`);
593
+ this.currentStatus = currentStatus;
594
+ this.expectedStatuses = expectedStatuses;
595
+ }
596
+ }
597
+
598
+ class LocalRunnerBackend {
599
+ host = new LocalPulumiProjectHost();
600
+ events = new EventEmitter();
601
+ states = /* @__PURE__ */ new Map();
602
+ async *watch(options) {
603
+ const stream = on(
604
+ //
605
+ this.events,
606
+ `state:${LocalRunnerBackend.getInstanceKey(options)}`,
607
+ { signal: options.signal }
608
+ );
609
+ for await (const [state] of stream) {
610
+ yield state;
611
+ if (options.finalStatuses.includes(state.status)) {
612
+ return;
613
+ }
614
+ }
615
+ }
616
+ // eslint-disable-next-line @typescript-eslint/require-await
617
+ async update(options) {
618
+ const currentStatus = this.validateStatus(options, [
619
+ "not_created",
620
+ "updating",
621
+ "created",
622
+ "error"
623
+ ]);
624
+ if (currentStatus === "updating") {
625
+ return;
626
+ }
627
+ void this.host.runLocal(
628
+ options.projectName,
629
+ options.stackName,
630
+ () => LocalRunnerBackend.resolveProjectPath(options.source),
631
+ async (stack) => {
632
+ const configMap = mapValues(options.config, (value) => ({ value }));
633
+ await stack.setAllConfig(configMap);
634
+ const state = {
635
+ key: LocalRunnerBackend.getInstanceKey(options),
636
+ status: "updating",
637
+ resources: 0,
638
+ totalResources: 0
639
+ };
640
+ this.updateState(state);
641
+ try {
642
+ await stack.up({
643
+ onEvent: (event) => {
644
+ if (event.resourcePreEvent) {
645
+ state.totalResources++;
646
+ this.updateState(state);
647
+ return;
648
+ }
649
+ if (event.resOutputsEvent) {
650
+ state.resources++;
651
+ this.updateState(state);
652
+ return;
653
+ }
654
+ },
655
+ onOutput: (message) => {
656
+ state.message = message;
657
+ this.updateState(state);
658
+ },
659
+ signal: options.signal
660
+ });
661
+ state.status = "created";
662
+ this.updateState(state);
663
+ } catch (e) {
664
+ state.status = "error";
665
+ state.error = e instanceof Error ? e.message : String(e);
666
+ this.updateState(state);
667
+ }
668
+ }
669
+ );
670
+ }
671
+ // eslint-disable-next-line @typescript-eslint/require-await
672
+ async destroy(options) {
673
+ const currentStatus = this.validateStatus(options, [
674
+ "not_created",
675
+ "destroying",
676
+ "created",
677
+ "error"
678
+ ]);
679
+ if (currentStatus === "destroying" || currentStatus === "not_created") {
680
+ return;
681
+ }
682
+ void this.host.runLocal(
683
+ options.projectName,
684
+ options.stackName,
685
+ () => LocalRunnerBackend.resolveProjectPath(options.source),
686
+ async (stack) => {
687
+ const stackSumary = await stack.workspace.stack();
688
+ const resources = stackSumary?.resourceCount ?? 0;
689
+ const state = {
690
+ key: LocalRunnerBackend.getInstanceKey(options),
691
+ status: "destroying",
692
+ resources,
693
+ totalResources: resources
694
+ };
695
+ this.updateState(state);
696
+ try {
697
+ await stack.destroy({
698
+ onEvent: (event) => {
699
+ if (event.resOutputsEvent) {
700
+ state.resources--;
701
+ this.updateState(state);
702
+ return;
703
+ }
704
+ },
705
+ signal: options.signal
706
+ });
707
+ state.status = "not_created";
708
+ this.updateState(state);
709
+ } catch (e) {
710
+ state.status = "error";
711
+ state.error = e instanceof Error ? e.message : String(e);
712
+ this.updateState(state);
713
+ }
714
+ }
715
+ );
716
+ }
717
+ // eslint-disable-next-line @typescript-eslint/require-await
718
+ async refresh(options) {
719
+ const currentStatus = this.validateStatus(options, [
720
+ "not_created",
721
+ "created",
722
+ "refreshing",
723
+ "error"
724
+ ]);
725
+ if (currentStatus === "refreshing") {
726
+ return;
727
+ }
728
+ void this.host.runLocal(
729
+ options.projectName,
730
+ options.stackName,
731
+ () => LocalRunnerBackend.resolveProjectPath(options.source),
732
+ async (stack) => {
733
+ const stackSumary = await stack.workspace.stack();
734
+ const resources = stackSumary?.resourceCount ?? 0;
735
+ const state = {
736
+ key: LocalRunnerBackend.getInstanceKey(options),
737
+ status: "refreshing",
738
+ resources,
739
+ totalResources: resources
740
+ };
741
+ this.updateState(state);
742
+ try {
743
+ await stack.refresh({
744
+ onEvent: (event) => {
745
+ if (event.resOutputsEvent) {
746
+ if (event.resOutputsEvent.metadata.op === "create") {
747
+ state.resources++;
748
+ this.updateState(state);
749
+ return;
750
+ }
751
+ if (event.resOutputsEvent.metadata.op === "delete") {
752
+ state.totalResources--;
753
+ this.updateState(state);
754
+ return;
755
+ }
756
+ return;
757
+ }
758
+ },
759
+ signal: options.signal
760
+ });
761
+ state.status = "created";
762
+ this.updateState(state);
763
+ } catch (e) {
764
+ state.status = "error";
765
+ state.error = e instanceof Error ? e.message : String(e);
766
+ this.updateState(state);
767
+ }
768
+ }
769
+ );
770
+ }
771
+ updateState(state) {
772
+ this.states.set(state.key, state);
773
+ this.events.emit(`state:${state.key}`, state);
774
+ }
775
+ validateStatus(options, expectedStatuses) {
776
+ const key = LocalRunnerBackend.getInstanceKey(options);
777
+ const existingState = this.states.get(key);
778
+ if (!existingState) {
779
+ return;
780
+ }
781
+ if (!expectedStatuses.includes(existingState.status)) {
782
+ throw new InvalidInstanceStatusError(existingState.status, expectedStatuses);
783
+ }
784
+ return existingState.status;
785
+ }
786
+ static getInstanceKey(options) {
787
+ return `${options.projectName}.${options.stackName}`;
788
+ }
789
+ static async resolveProjectPath(source) {
790
+ await addDependency(source.package);
791
+ const url = resolve(source.package, import.meta.url);
792
+ const path = fileURLToPath(url);
793
+ const projectPath = dirname(path);
794
+ return projectPath;
795
+ }
796
+ // eslint-disable-next-line @typescript-eslint/require-await
797
+ static async create() {
798
+ return new LocalRunnerBackend();
799
+ }
800
+ }
801
+
802
+ function createRunnerBackend() {
803
+ return LocalRunnerBackend.create();
804
+ }
805
+
806
+ async function createServices(config) {
807
+ config ??= await loadConfig();
808
+ const pulumiProjectHost = new LocalPulumiProjectHost();
809
+ const libraryBackend = await createLibraryBackend(config);
810
+ const secretBackend = await createSecretBackend(config, pulumiProjectHost);
811
+ const runnerBackend = await createRunnerBackend();
812
+ const projectBackend = await createProjectBackend(config);
813
+ const stateBackend = await createStateBackend(config, pulumiProjectHost);
814
+ const operatorManager = new OperatorManager(
815
+ runnerBackend,
816
+ stateBackend,
817
+ libraryBackend,
818
+ projectBackend,
819
+ secretBackend
820
+ );
821
+ consola.success("Backend services initialized");
822
+ return {
823
+ libraryBackend,
824
+ secretBackend,
825
+ runnerBackend,
826
+ projectBackend,
827
+ stateBackend,
828
+ operatorManager
829
+ };
830
+ }
831
+
832
+ export { LocalPulumiProjectHost, OperatorManager, ProjectOperator, SecretAccessDeniedError, createLibraryBackend, createSecretBackend, createServices, createStateBackend, instanceModelSchema, instanceStateSchema, instanceStatusSchema, libraryBackendConfig, loadConfig, operationType, outputRefSchema, positionSchema, projectOperation, projectStateSchema, resolveMainLocalProject, secretBackendConfig, stateBackendConfig, stringArrayType };