@highstate/backend 0.9.16 → 0.9.19

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 (302) hide show
  1. package/dist/chunk-5WVU2AK4.js +1535 -0
  2. package/dist/chunk-5WVU2AK4.js.map +1 -0
  3. package/dist/chunk-I7BWSAN6.js +49 -0
  4. package/dist/chunk-I7BWSAN6.js.map +1 -0
  5. package/dist/{chunk-RCB4AFGD.js → chunk-VB4YL327.js} +51 -71
  6. package/dist/chunk-VB4YL327.js.map +1 -0
  7. package/dist/database/local/prisma.config.js +26 -0
  8. package/dist/database/local/prisma.config.js.map +1 -0
  9. package/dist/highstate.manifest.json +5 -4
  10. package/dist/index.js +7676 -6634
  11. package/dist/index.js.map +1 -1
  12. package/dist/library/package-resolution-worker.js +8 -6
  13. package/dist/library/package-resolution-worker.js.map +1 -1
  14. package/dist/library/worker/main.js +63 -58
  15. package/dist/library/worker/main.js.map +1 -1
  16. package/dist/shared/index.js +3 -216
  17. package/dist/shared/index.js.map +1 -1
  18. package/package.json +23 -11
  19. package/prisma/backend/_schema/layout.prisma +7 -0
  20. package/prisma/backend/_schema/library.prisma +17 -0
  21. package/prisma/backend/_schema/project.prisma +101 -0
  22. package/prisma/backend/_schema/pulumi.prisma +17 -0
  23. package/prisma/backend/postgresql/main.prisma +17 -0
  24. package/prisma/backend/sqlite/main.prisma +17 -0
  25. package/prisma/backend/sqlite/migrations/20250817070609_initiial/migration.sql +34 -0
  26. package/prisma/backend/sqlite/migrations/20250817104948_add_fields/migration.sql +59 -0
  27. package/prisma/backend/sqlite/migrations/20250818082732_add_models/migration.sql +41 -0
  28. package/prisma/backend/sqlite/migrations/20250818083106_a/migration.sql +19 -0
  29. package/prisma/backend/sqlite/migrations/20250818101945_hi/migration.sql +1 -0
  30. package/prisma/backend/sqlite/migrations/20250819082315_a/migration.sql +5 -0
  31. package/prisma/backend/sqlite/migrations/migration_lock.toml +3 -0
  32. package/prisma/project/api-key.prisma +27 -0
  33. package/prisma/project/artifact.prisma +52 -0
  34. package/prisma/project/custom-status.prisma +46 -0
  35. package/prisma/project/evaluation.prisma +35 -0
  36. package/prisma/project/instance.prisma +160 -0
  37. package/prisma/project/layout.prisma +23 -0
  38. package/prisma/project/lock.prisma +18 -0
  39. package/prisma/project/main.prisma +17 -0
  40. package/prisma/project/migrations/20250816081310_initial/migration.sql +300 -0
  41. package/prisma/project/migrations/20250816082523_test/migration.sql +72 -0
  42. package/prisma/project/migrations/20250818065643_update/migration.sql +42 -0
  43. package/prisma/project/migrations/20250818070758_a/migration.sql +8 -0
  44. package/prisma/project/migrations/20250818070913_a/migration.sql +8 -0
  45. package/prisma/project/migrations/20250818082720_add_motels/migration.sql +11 -0
  46. package/prisma/project/migrations/20250818112523_hello/migration.sql +35 -0
  47. package/prisma/project/migrations/20250819082305_a/migration.sql +14 -0
  48. package/prisma/project/migrations/20250819165004_add_missing_fields/migration.sql +216 -0
  49. package/prisma/project/migrations/20250819171309_a/migration.sql +22 -0
  50. package/prisma/project/migrations/20250820113949_a/migration.sql +66 -0
  51. package/prisma/project/migrations/20250820144256_b/migration.sql +31 -0
  52. package/prisma/project/migrations/20250820145547_a/migration.sql +24 -0
  53. package/prisma/project/migrations/20250820182517_b/migration.sql +2 -0
  54. package/prisma/project/migrations/20250821172324_a/migration.sql +2 -0
  55. package/prisma/project/migrations/20250822081339_a/migration.sql +219 -0
  56. package/prisma/project/migrations/20250822083742_b/migration.sql +1 -0
  57. package/prisma/project/migrations/20250822105134_boom/migration.sql +1 -0
  58. package/prisma/project/migrations/20250822141028_b/migration.sql +1 -0
  59. package/prisma/project/migrations/20250822142342_b/migration.sql +16 -0
  60. package/prisma/project/migrations/20250824072720_a/migration.sql +1 -0
  61. package/prisma/project/migrations/20250824093656_b/migration.sql +21 -0
  62. package/prisma/project/migrations/20250825082518_a/migration.sql +1 -0
  63. package/prisma/project/migrations/20250825085343_b/migration.sql +1 -0
  64. package/prisma/project/migrations/20250825091312_a/migration.sql +1 -0
  65. package/prisma/project/migrations/20250903095431_hi/migration.sql +44 -0
  66. package/prisma/project/migrations/20250903174255_a/migration.sql +24 -0
  67. package/prisma/project/migrations/20250908095205_hi/migration.sql +18 -0
  68. package/prisma/project/migrations/20250909155857_hi/migration.sql +15 -0
  69. package/prisma/project/migrations/migration_lock.toml +3 -0
  70. package/prisma/project/model.prisma +37 -0
  71. package/prisma/project/operation.prisma +148 -0
  72. package/prisma/project/page.prisma +41 -0
  73. package/prisma/project/secret.prisma +42 -0
  74. package/prisma/project/service-account.prisma +36 -0
  75. package/prisma/project/terminal.prisma +90 -0
  76. package/prisma/project/trigger.prisma +31 -0
  77. package/prisma/project/unlock-method.prisma +32 -0
  78. package/prisma/project/worker.prisma +138 -0
  79. package/src/artifact/abstractions.ts +13 -13
  80. package/src/artifact/encryption.ts +31 -15
  81. package/src/artifact/factory.ts +7 -10
  82. package/src/artifact/local.ts +33 -50
  83. package/src/business/api-key.ts +24 -36
  84. package/src/business/artifact.test.ts +978 -0
  85. package/src/business/artifact.ts +136 -215
  86. package/src/business/evaluation.ts +328 -0
  87. package/src/business/index.ts +5 -1
  88. package/src/business/instance-lock.test.ts +1060 -0
  89. package/src/business/instance-lock.ts +387 -77
  90. package/src/business/instance-state.test.ts +735 -0
  91. package/src/business/instance-state.ts +604 -217
  92. package/src/business/operation.test.ts +439 -0
  93. package/src/business/operation.ts +174 -208
  94. package/src/business/project-model.ts +258 -0
  95. package/src/business/project-unlock.ts +172 -112
  96. package/src/business/project.ts +407 -0
  97. package/src/business/secret.test.ts +513 -0
  98. package/src/business/secret.ts +194 -131
  99. package/src/business/settings.test.ts +695 -0
  100. package/src/business/settings.ts +855 -0
  101. package/src/business/terminal-session.ts +90 -0
  102. package/src/business/unit-extra.test.ts +539 -0
  103. package/src/business/unit-extra.ts +160 -0
  104. package/src/business/worker.test.ts +391 -0
  105. package/src/business/worker.ts +250 -114
  106. package/src/common/codebase.ts +65 -0
  107. package/src/common/index.ts +3 -2
  108. package/src/common/logger.ts +5 -0
  109. package/src/common/utils.ts +4 -3
  110. package/src/config.ts +15 -12
  111. package/src/database/_generated/backend/postgresql/client.ts +72 -0
  112. package/src/database/_generated/backend/postgresql/commonInputTypes.ts +350 -0
  113. package/src/database/_generated/backend/postgresql/enums.ts +13 -0
  114. package/src/database/_generated/backend/postgresql/internal/class.ts +320 -0
  115. package/src/database/_generated/backend/postgresql/internal/prismaNamespace.ts +1238 -0
  116. package/src/database/_generated/backend/postgresql/models/Library.ts +1263 -0
  117. package/src/database/_generated/backend/postgresql/models/Project.ts +2175 -0
  118. package/src/database/_generated/backend/postgresql/models/ProjectModelStorage.ts +1263 -0
  119. package/src/database/_generated/backend/postgresql/models/ProjectSpace.ts +1602 -0
  120. package/src/database/_generated/backend/postgresql/models/PulumiBackend.ts +1263 -0
  121. package/src/database/_generated/backend/postgresql/models/UserWorkspaseLayout.ts +1065 -0
  122. package/src/database/_generated/backend/postgresql/models.ts +16 -0
  123. package/src/database/_generated/backend/postgresql/pjtg.ts +182 -0
  124. package/src/database/_generated/backend/sqlite/client.ts +72 -0
  125. package/src/database/_generated/backend/sqlite/commonInputTypes.ts +331 -0
  126. package/src/database/_generated/backend/sqlite/enums.ts +13 -0
  127. package/src/database/_generated/backend/sqlite/internal/class.ts +318 -0
  128. package/src/database/_generated/backend/sqlite/internal/prismaNamespace.ts +1207 -0
  129. package/src/database/_generated/backend/sqlite/models/Library.ts +1261 -0
  130. package/src/database/_generated/backend/sqlite/models/Project.ts +2169 -0
  131. package/src/database/_generated/backend/sqlite/models/ProjectModelStorage.ts +1261 -0
  132. package/src/database/_generated/backend/sqlite/models/ProjectSpace.ts +1599 -0
  133. package/src/database/_generated/backend/sqlite/models/PulumiBackend.ts +1261 -0
  134. package/src/database/_generated/backend/sqlite/models/UserWorkspaseLayout.ts +1063 -0
  135. package/src/database/_generated/backend/sqlite/models.ts +16 -0
  136. package/src/database/_generated/backend/sqlite/pjtg.ts +182 -0
  137. package/src/database/_generated/project/client.ts +204 -0
  138. package/src/database/_generated/project/commonInputTypes.ts +827 -0
  139. package/src/database/_generated/project/enums.ts +104 -0
  140. package/src/database/_generated/project/internal/class.ts +479 -0
  141. package/src/database/_generated/project/internal/prismaNamespace.ts +2974 -0
  142. package/src/database/_generated/project/models/ApiKey.ts +1506 -0
  143. package/src/database/_generated/project/models/Artifact.ts +2051 -0
  144. package/src/database/_generated/project/models/HubModel.ts +1125 -0
  145. package/src/database/_generated/project/models/InstanceCustomStatus.ts +1713 -0
  146. package/src/database/_generated/project/models/InstanceEvaluationState.ts +1312 -0
  147. package/src/database/_generated/project/models/InstanceLock.ts +1268 -0
  148. package/src/database/_generated/project/models/InstanceModel.ts +1125 -0
  149. package/src/database/_generated/project/models/InstanceOperationState.ts +1707 -0
  150. package/src/database/_generated/project/models/InstanceState.ts +4613 -0
  151. package/src/database/_generated/project/models/Operation.ts +1647 -0
  152. package/src/database/_generated/project/models/OperationLog.ts +1455 -0
  153. package/src/database/_generated/project/models/Page.ts +1838 -0
  154. package/src/database/_generated/project/models/Secret.ts +1692 -0
  155. package/src/database/_generated/project/models/ServiceAccount.ts +2165 -0
  156. package/src/database/_generated/project/models/Terminal.ts +2038 -0
  157. package/src/database/_generated/project/models/TerminalSession.ts +1454 -0
  158. package/src/database/_generated/project/models/TerminalSessionLog.ts +1280 -0
  159. package/src/database/_generated/project/models/Trigger.ts +1430 -0
  160. package/src/database/_generated/project/models/UnlockMethod.ts +1220 -0
  161. package/src/database/_generated/project/models/UserCompositeViewport.ts +1280 -0
  162. package/src/database/_generated/project/models/UserProjectViewport.ts +1059 -0
  163. package/src/database/_generated/project/models/Worker.ts +1459 -0
  164. package/src/database/_generated/project/models/WorkerUnitRegistration.ts +1524 -0
  165. package/src/database/_generated/project/models/WorkerVersion.ts +1974 -0
  166. package/src/database/_generated/project/models/WorkerVersionLog.ts +1318 -0
  167. package/src/database/_generated/project/models.ts +35 -0
  168. package/src/database/_generated/project/pjtg.ts +182 -0
  169. package/src/database/abstractions.ts +19 -0
  170. package/src/database/factory.ts +37 -0
  171. package/src/database/index.ts +6 -0
  172. package/src/database/local/backend.ts +134 -0
  173. package/src/database/local/index.ts +3 -0
  174. package/src/database/local/meta.ts +46 -0
  175. package/src/database/local/prisma.config.ts +25 -0
  176. package/src/database/local/project.ts +39 -0
  177. package/src/database/manager.ts +181 -0
  178. package/src/database/migrate.ts +35 -0
  179. package/src/database/prisma.ts +56 -0
  180. package/src/database/well-known.ts +38 -0
  181. package/src/index.ts +4 -4
  182. package/src/library/abstractions.ts +21 -14
  183. package/src/library/factory.ts +1 -1
  184. package/src/library/local.ts +86 -38
  185. package/src/library/package-resolution-worker.ts +1 -1
  186. package/src/library/worker/evaluator.ts +61 -48
  187. package/src/library/worker/loader.lite.ts +14 -1
  188. package/src/library/worker/main.ts +9 -16
  189. package/src/library/worker/protocol.ts +0 -12
  190. package/src/lock/manager.ts +12 -7
  191. package/src/orchestrator/manager.ts +198 -131
  192. package/src/orchestrator/operation-context.ts +357 -0
  193. package/src/orchestrator/operation-plan.destroy.test.md +357 -0
  194. package/src/orchestrator/operation-plan.destroy.test.ts +775 -0
  195. package/src/orchestrator/operation-plan.fixtures.ts +213 -0
  196. package/src/orchestrator/operation-plan.md +198 -0
  197. package/src/orchestrator/operation-plan.refresh.test.md +199 -0
  198. package/src/orchestrator/operation-plan.refresh.test.ts +367 -0
  199. package/src/orchestrator/operation-plan.ts +709 -0
  200. package/src/orchestrator/operation-plan.update.test.md +485 -0
  201. package/src/orchestrator/operation-plan.update.test.ts +1066 -0
  202. package/src/orchestrator/operation-workset.ts +235 -583
  203. package/src/orchestrator/operation.ts +446 -904
  204. package/src/orchestrator/plan-test-builder.ts +267 -0
  205. package/src/project-model/abstractions.ts +118 -0
  206. package/src/project-model/backends/codebase.ts +365 -0
  207. package/src/project-model/backends/database.ts +440 -0
  208. package/src/project-model/errors.ts +81 -0
  209. package/src/project-model/factory.ts +24 -0
  210. package/src/project-model/index.ts +4 -0
  211. package/src/project-model/utils.test.ts +544 -0
  212. package/src/project-model/utils.ts +242 -0
  213. package/src/pubsub/abstractions.ts +10 -1
  214. package/src/pubsub/factory.ts +4 -4
  215. package/src/pubsub/index.ts +1 -0
  216. package/src/pubsub/manager.ts +49 -25
  217. package/src/pubsub/memory.ts +31 -0
  218. package/src/runner/abstractions.ts +38 -26
  219. package/src/runner/artifact-env.ts +17 -6
  220. package/src/runner/factory.ts +6 -6
  221. package/src/runner/force-abort.ts +3 -6
  222. package/src/runner/local.ts +79 -72
  223. package/src/runner/pulumi.ts +26 -63
  224. package/src/services.ts +214 -103
  225. package/src/shared/models/backend/index.ts +3 -1
  226. package/src/shared/models/backend/library.ts +12 -4
  227. package/src/shared/models/backend/project.ts +43 -23
  228. package/src/shared/models/backend/pulumi.ts +14 -0
  229. package/src/shared/models/backend/unlock-method.ts +1 -1
  230. package/src/shared/models/backend/well-known.ts +58 -0
  231. package/src/shared/models/base.ts +40 -109
  232. package/src/shared/models/errors.ts +82 -1
  233. package/src/shared/models/index.ts +3 -2
  234. package/src/shared/models/prisma.ts +36 -0
  235. package/src/shared/models/project/api-key.ts +37 -56
  236. package/src/shared/models/project/artifact.ts +15 -105
  237. package/src/shared/models/project/custom-status.ts +12 -0
  238. package/src/shared/models/project/index.ts +9 -9
  239. package/src/shared/models/project/lock.ts +10 -78
  240. package/src/shared/models/project/model.ts +32 -0
  241. package/src/shared/models/project/operation.ts +222 -99
  242. package/src/shared/models/project/page.ts +37 -48
  243. package/src/shared/models/project/secret.ts +29 -103
  244. package/src/shared/models/project/service-account.ts +12 -17
  245. package/src/shared/models/project/state.ts +100 -390
  246. package/src/shared/models/project/terminal.ts +75 -89
  247. package/src/shared/models/project/trigger.ts +13 -49
  248. package/src/shared/models/project/unlock-method.ts +21 -20
  249. package/src/shared/models/project/worker.ts +89 -88
  250. package/src/shared/resolvers/graph-resolver.ts +62 -26
  251. package/src/shared/resolvers/index.ts +1 -1
  252. package/src/shared/resolvers/input-hash.ts +24 -14
  253. package/src/shared/resolvers/input.ts +48 -6
  254. package/src/shared/resolvers/registry.ts +5 -4
  255. package/src/shared/resolvers/state.ts +12 -1
  256. package/src/shared/resolvers/validation.ts +29 -9
  257. package/src/shared/utils/index.ts +1 -1
  258. package/src/shared/utils/promise-tracker.ts +30 -3
  259. package/src/terminal/abstractions.ts +1 -1
  260. package/src/terminal/docker.ts +3 -3
  261. package/src/terminal/manager.ts +102 -118
  262. package/src/test-utils/database.ts +119 -0
  263. package/src/test-utils/index.ts +2 -0
  264. package/src/test-utils/services.ts +134 -0
  265. package/src/unlock/abstractions.ts +31 -0
  266. package/src/unlock/index.ts +2 -0
  267. package/src/unlock/memory.ts +27 -0
  268. package/src/worker/abstractions.ts +7 -4
  269. package/src/worker/docker.ts +14 -19
  270. package/src/worker/manager.ts +376 -79
  271. package/dist/chunk-RCB4AFGD.js.map +0 -1
  272. package/dist/chunk-WHALQHEZ.js +0 -2017
  273. package/dist/chunk-WHALQHEZ.js.map +0 -1
  274. package/src/business/backend-unlock.ts +0 -10
  275. package/src/common/performance.ts +0 -44
  276. package/src/hotstate/abstractions.ts +0 -48
  277. package/src/hotstate/factory.ts +0 -17
  278. package/src/hotstate/index.ts +0 -3
  279. package/src/hotstate/manager.ts +0 -192
  280. package/src/hotstate/memory.ts +0 -100
  281. package/src/hotstate/validation.ts +0 -101
  282. package/src/project/abstractions.ts +0 -102
  283. package/src/project/factory.ts +0 -11
  284. package/src/project/index.ts +0 -3
  285. package/src/project/local.ts +0 -469
  286. package/src/project/manager.ts +0 -574
  287. package/src/pubsub/local.ts +0 -36
  288. package/src/pubsub/validation.ts +0 -33
  289. package/src/shared/models/project/component.ts +0 -45
  290. package/src/shared/models/project/instance.ts +0 -74
  291. package/src/state/abstractions.ts +0 -450
  292. package/src/state/encryption.ts +0 -59
  293. package/src/state/factory.ts +0 -20
  294. package/src/state/index.ts +0 -6
  295. package/src/state/local/backend.ts +0 -299
  296. package/src/state/local/collection.ts +0 -342
  297. package/src/state/local/index.ts +0 -2
  298. package/src/state/manager.ts +0 -819
  299. package/src/state/repository/index.ts +0 -2
  300. package/src/state/repository/repository.index.ts +0 -193
  301. package/src/state/repository/repository.ts +0 -458
  302. /package/src/{state → database/local}/keyring.ts +0 -0
@@ -0,0 +1,90 @@
1
+ import type { DatabaseManager, TerminalSessionLog } from "../database"
2
+ import {
3
+ type TerminalSessionOutput,
4
+ toTerminalSessionOutput,
5
+ } from "../shared/models/project/terminal"
6
+
7
+ /**
8
+ * Business service for managing terminal sessions
9
+ */
10
+ export class TerminalSessionService {
11
+ constructor(private readonly database: DatabaseManager) {}
12
+
13
+ /**
14
+ * Get terminal sessions for a specific instance
15
+ *
16
+ * @param projectId The project ID
17
+ * @param instanceId The instance ID to get sessions for
18
+ * @returns Array of terminal session outputs with metadata
19
+ */
20
+ async getInstanceTerminalSessions(
21
+ projectId: string,
22
+ instanceId: string,
23
+ ): Promise<TerminalSessionOutput[]> {
24
+ const database = await this.database.forProject(projectId)
25
+
26
+ const sessions = await database.terminalSession.findMany({
27
+ include: {
28
+ terminal: {
29
+ select: { meta: true },
30
+ },
31
+ },
32
+ where: {
33
+ terminal: {
34
+ stateId: instanceId,
35
+ },
36
+ },
37
+ orderBy: { startedAt: "desc" },
38
+ take: 20,
39
+ })
40
+
41
+ return sessions.map(session => ({
42
+ ...session,
43
+ projectId,
44
+ meta: session.terminal.meta,
45
+ }))
46
+ }
47
+
48
+ /**
49
+ * Get a specific terminal session by ID
50
+ *
51
+ * @param projectId The project ID
52
+ * @param sessionId The session ID to get
53
+ * @returns Terminal session output with metadata or null if not found
54
+ */
55
+ async getTerminalSession(
56
+ projectId: string,
57
+ sessionId: string,
58
+ ): Promise<TerminalSessionOutput | null> {
59
+ const database = await this.database.forProject(projectId)
60
+
61
+ const session = await database.terminalSession.findUnique({
62
+ where: { id: sessionId },
63
+ include: {
64
+ terminal: true,
65
+ },
66
+ })
67
+
68
+ if (!session) {
69
+ return null
70
+ }
71
+
72
+ return toTerminalSessionOutput(session.terminal, session)
73
+ }
74
+
75
+ /**
76
+ * Get session history (logs) for a specific terminal session
77
+ *
78
+ * @param projectId The project ID
79
+ * @param sessionId The session ID to get history for
80
+ * @returns Array of session logs ordered by creation time
81
+ */
82
+ async getSessionHistory(projectId: string, sessionId: string): Promise<TerminalSessionLog[]> {
83
+ const database = await this.database.forProject(projectId)
84
+
85
+ return await database.terminalSessionLog.findMany({
86
+ where: { sessionId },
87
+ orderBy: { id: "asc" },
88
+ })
89
+ }
90
+ }
@@ -0,0 +1,539 @@
1
+ import type { UnitPage, UnitTerminal, UnitTrigger } from "@highstate/contract"
2
+ import { describe, expect } from "vitest"
3
+ import { test } from "../test-utils"
4
+ import { UnitExtraService } from "./unit-extra"
5
+
6
+ describe("UnitExtraService", () => {
7
+ describe("processUnitTerminals", () => {
8
+ test("should create new terminals", async ({
9
+ database,
10
+ project,
11
+ createInstanceState,
12
+ projectDatabase,
13
+ }) => {
14
+ const service = new UnitExtraService(database)
15
+ const instance = await createInstanceState(project.id)
16
+
17
+ const unitTerminals: UnitTerminal[] = [
18
+ {
19
+ name: "ssh",
20
+ meta: {
21
+ title: "SSH Terminal",
22
+ description: "Secure shell access to the server",
23
+ icon: "mdi:terminal",
24
+ iconColor: "#4CAF50",
25
+ },
26
+ spec: {
27
+ image: "alpine:3.18",
28
+ command: ["ssh", "-o", "StrictHostKeyChecking=no"],
29
+ cwd: "/root",
30
+ env: {
31
+ TERM: "xterm-256color",
32
+ SSH_AUTH_SOCK: "/tmp/ssh-agent.sock",
33
+ },
34
+ files: {
35
+ "/root/.ssh/config": {
36
+ meta: {
37
+ name: "ssh-config",
38
+ },
39
+
40
+ content: {
41
+ type: "embedded",
42
+ value: "Host *\n ServerAliveInterval 60\n ServerAliveCountMax 3\n",
43
+ },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ ]
49
+
50
+ await projectDatabase.$transaction(async tx => {
51
+ await service.processUnitTerminals(tx, instance.id, unitTerminals)
52
+ })
53
+ const terminals = await projectDatabase.terminal.findMany({ where: { stateId: instance.id } })
54
+
55
+ expect(terminals).toHaveLength(1)
56
+ expect(terminals[0].name).toBe("ssh")
57
+ expect(terminals[0].status).toBe("active")
58
+ expect(terminals[0].meta).toEqual({
59
+ title: "SSH Terminal",
60
+ description: "Secure shell access to the server",
61
+ icon: "mdi:terminal",
62
+ iconColor: "#4CAF50",
63
+ })
64
+ expect(terminals[0].spec).toMatchObject({
65
+ image: "alpine:3.18",
66
+ command: ["ssh", "-o", "StrictHostKeyChecking=no"],
67
+ })
68
+ })
69
+
70
+ test("should update existing terminals", async ({
71
+ database,
72
+ project,
73
+ createInstanceState,
74
+ projectDatabase,
75
+ }) => {
76
+ const service = new UnitExtraService(database)
77
+ const instance = await createInstanceState(project.id)
78
+
79
+ await projectDatabase.terminal.create({
80
+ data: {
81
+ stateId: instance.id,
82
+ name: "ssh",
83
+ meta: {
84
+ title: "Old Terminal",
85
+ icon: "mdi:console",
86
+ },
87
+ spec: {
88
+ image: "ubuntu:20.04",
89
+ command: ["bash"],
90
+ },
91
+ status: "active",
92
+ },
93
+ })
94
+
95
+ const unitTerminals: UnitTerminal[] = [
96
+ {
97
+ name: "ssh",
98
+ meta: {
99
+ title: "SSH Terminal",
100
+ description: "Updated terminal with SSH access",
101
+ icon: "mdi:terminal",
102
+ },
103
+ spec: {
104
+ image: "alpine:3.18",
105
+ command: ["ssh", "-i", "/root/.ssh/id_rsa"],
106
+ env: {
107
+ TERM: "xterm-256color",
108
+ },
109
+ },
110
+ },
111
+ ]
112
+
113
+ await projectDatabase.$transaction(async tx => {
114
+ await service.processUnitTerminals(tx, instance.id, unitTerminals)
115
+ })
116
+
117
+ const terminals = await projectDatabase.terminal.findMany({ where: { stateId: instance.id } })
118
+
119
+ expect(terminals).toHaveLength(1)
120
+ expect(terminals[0].meta).toEqual({
121
+ title: "SSH Terminal",
122
+ description: "Updated terminal with SSH access",
123
+ icon: "mdi:terminal",
124
+ })
125
+ expect(terminals[0].spec).toMatchObject({
126
+ image: "alpine:3.18",
127
+ command: ["ssh", "-i", "/root/.ssh/id_rsa"],
128
+ })
129
+ expect(terminals[0].status).toBe("active")
130
+ })
131
+
132
+ test("should mark dangling terminals as unavailable", async ({
133
+ database,
134
+ project,
135
+ createInstanceState,
136
+ }) => {
137
+ const service = new UnitExtraService(database)
138
+ const instance = await createInstanceState(project.id)
139
+
140
+ const db = await database.forProject(project.id)
141
+ await db.terminal.createMany({
142
+ data: [
143
+ {
144
+ stateId: instance.id,
145
+ name: "ssh",
146
+ meta: { title: "SSH Terminal" },
147
+ spec: { image: "alpine", command: ["ssh"] },
148
+ status: "active",
149
+ },
150
+ {
151
+ stateId: instance.id,
152
+ name: "backup",
153
+ meta: { title: "Backup Terminal" },
154
+ spec: { image: "restic", command: ["restic"] },
155
+ status: "active",
156
+ },
157
+ ],
158
+ })
159
+
160
+ const unitTerminals: UnitTerminal[] = [
161
+ {
162
+ name: "ssh",
163
+ meta: {
164
+ title: "SSH Terminal",
165
+ icon: "mdi:terminal",
166
+ },
167
+ spec: {
168
+ image: "alpine:3.18",
169
+ command: ["ssh"],
170
+ },
171
+ },
172
+ ]
173
+
174
+ await db.$transaction(async tx => {
175
+ await service.processUnitTerminals(tx, instance.id, unitTerminals)
176
+ })
177
+
178
+ const terminals = await db.terminal.findMany({
179
+ where: { stateId: instance.id },
180
+ orderBy: { name: "asc" },
181
+ })
182
+
183
+ expect(terminals).toHaveLength(2)
184
+ expect(terminals[0].name).toBe("backup")
185
+ expect(terminals[0].status).toBe("unavailable")
186
+ expect(terminals[1].name).toBe("ssh")
187
+ expect(terminals[1].status).toBe("active")
188
+ })
189
+
190
+ test("should not affect already unavailable terminals", async ({
191
+ database,
192
+ project,
193
+ createInstanceState,
194
+ projectDatabase,
195
+ }) => {
196
+ const service = new UnitExtraService(database)
197
+ const instance = await createInstanceState(project.id)
198
+
199
+ await projectDatabase.terminal.create({
200
+ data: {
201
+ stateId: instance.id,
202
+ name: "old-terminal",
203
+ meta: { title: "Old Terminal" },
204
+ spec: { image: "ubuntu", command: ["bash"] },
205
+ status: "unavailable",
206
+ },
207
+ })
208
+
209
+ await projectDatabase.$transaction(async tx => {
210
+ await service.processUnitTerminals(tx, instance.id, [])
211
+ })
212
+
213
+ const terminals = await projectDatabase.terminal.findMany({ where: { stateId: instance.id } })
214
+
215
+ expect(terminals).toHaveLength(1)
216
+ expect(terminals[0].status).toBe("unavailable")
217
+ })
218
+ })
219
+
220
+ describe("processUnitPages", () => {
221
+ test("should create new pages", async ({
222
+ database,
223
+ project,
224
+ createInstanceState,
225
+ projectDatabase,
226
+ }) => {
227
+ const service = new UnitExtraService(database)
228
+ const instance = await createInstanceState(project.id)
229
+
230
+ const unitPages: UnitPage[] = [
231
+ {
232
+ name: "dashboard",
233
+ meta: {
234
+ title: "Kubernetes Dashboard",
235
+ description: "Access to the Kubernetes dashboard with login token",
236
+ icon: "simple-icons:kubernetes",
237
+ iconColor: "#326CE5",
238
+ },
239
+ content: [
240
+ {
241
+ type: "markdown",
242
+ content: `# Kubernetes Dashboard
243
+
244
+ The dashboard is ready at [https://k8s.example.com](https://k8s.example.com)
245
+
246
+ To login, use the following token:`,
247
+ },
248
+ {
249
+ type: "qr",
250
+ content: "eyJhbGciOiJSUzI1NiIsImtpZCI6InRva2VuIn0...",
251
+ showContent: true,
252
+ language: "text",
253
+ },
254
+ ],
255
+ },
256
+ ]
257
+
258
+ await projectDatabase.$transaction(async tx => {
259
+ await service.processUnitPages(tx, instance.id, unitPages)
260
+ })
261
+
262
+ const pages = await projectDatabase.page.findMany({ where: { stateId: instance.id } })
263
+
264
+ expect(pages).toHaveLength(1)
265
+ expect(pages[0].name).toBe("dashboard")
266
+ expect(pages[0].meta).toEqual({
267
+ title: "Kubernetes Dashboard",
268
+ description: "Access to the Kubernetes dashboard with login token",
269
+ icon: "simple-icons:kubernetes",
270
+ iconColor: "#326CE5",
271
+ })
272
+ expect(pages[0].content).toHaveLength(2)
273
+ expect(pages[0].content[0]).toMatchObject({
274
+ type: "markdown",
275
+ content: expect.stringContaining("Kubernetes Dashboard"),
276
+ })
277
+ })
278
+
279
+ test("should update existing pages", async ({
280
+ database,
281
+ project,
282
+ createInstanceState,
283
+ projectDatabase,
284
+ }) => {
285
+ const service = new UnitExtraService(database)
286
+ const instance = await createInstanceState(project.id)
287
+
288
+ const db = await database.forProject(project.id)
289
+ await db.page.create({
290
+ data: {
291
+ stateId: instance.id,
292
+ name: "dashboard",
293
+ meta: {
294
+ title: "Old Dashboard",
295
+ icon: "mdi:view-dashboard",
296
+ },
297
+ content: [
298
+ {
299
+ type: "markdown",
300
+ content: "Old content",
301
+ },
302
+ ],
303
+ },
304
+ })
305
+
306
+ const unitPages: UnitPage[] = [
307
+ {
308
+ name: "dashboard",
309
+ meta: {
310
+ title: "Updated Dashboard",
311
+ description: "Updated dashboard with new features",
312
+ icon: "mdi:monitor-dashboard",
313
+ },
314
+ content: [
315
+ {
316
+ type: "markdown",
317
+ content: "# Updated Dashboard\n\nNew features available!",
318
+ },
319
+ ],
320
+ },
321
+ ]
322
+
323
+ await projectDatabase.$transaction(async tx => {
324
+ await service.processUnitPages(tx, instance.id, unitPages)
325
+ })
326
+
327
+ const pages = await projectDatabase.page.findMany({ where: { stateId: instance.id } })
328
+
329
+ expect(pages).toHaveLength(1)
330
+ expect(pages[0].meta).toEqual({
331
+ title: "Updated Dashboard",
332
+ description: "Updated dashboard with new features",
333
+ icon: "mdi:monitor-dashboard",
334
+ })
335
+ expect(pages[0].content).toHaveLength(1)
336
+ })
337
+
338
+ test("should delete dangling pages", async ({
339
+ database,
340
+ project,
341
+ createInstanceState,
342
+ projectDatabase,
343
+ }) => {
344
+ const service = new UnitExtraService(database)
345
+ const instance = await createInstanceState(project.id)
346
+
347
+ const db = await database.forProject(project.id)
348
+ await db.page.createMany({
349
+ data: [
350
+ {
351
+ stateId: instance.id,
352
+ name: "dashboard",
353
+ meta: { title: "Dashboard" },
354
+ content: [],
355
+ },
356
+ {
357
+ stateId: instance.id,
358
+ name: "settings",
359
+ meta: { title: "Settings" },
360
+ content: [],
361
+ },
362
+ ],
363
+ })
364
+
365
+ const unitPages: UnitPage[] = [
366
+ {
367
+ name: "dashboard",
368
+ meta: {
369
+ title: "Dashboard",
370
+ },
371
+ content: [
372
+ {
373
+ type: "markdown",
374
+ content: "Dashboard content",
375
+ },
376
+ ],
377
+ },
378
+ ]
379
+
380
+ await projectDatabase.$transaction(async tx => {
381
+ await service.processUnitPages(tx, instance.id, unitPages)
382
+ })
383
+
384
+ const pages = await projectDatabase.page.findMany({ where: { stateId: instance.id } })
385
+
386
+ expect(pages).toHaveLength(1)
387
+ expect(pages[0].name).toBe("dashboard")
388
+ })
389
+ })
390
+
391
+ describe("processUnitTriggers", () => {
392
+ test("should create new triggers", async ({
393
+ database,
394
+ project,
395
+ createInstanceState,
396
+ projectDatabase,
397
+ }) => {
398
+ const service = new UnitExtraService(database)
399
+ const instance = await createInstanceState(project.id)
400
+
401
+ const unitTriggers: UnitTrigger[] = [
402
+ {
403
+ name: "cleanup",
404
+ meta: {
405
+ title: "Cleanup Before Destroy",
406
+ description: "Runs backup and cleanup operations before instance destruction",
407
+ icon: "mdi:backup-restore",
408
+ },
409
+ spec: {
410
+ type: "before-destroy",
411
+ },
412
+ },
413
+ ]
414
+
415
+ await projectDatabase.$transaction(async tx => {
416
+ await service.processUnitTriggers(tx, instance.id, unitTriggers)
417
+ })
418
+
419
+ const triggers = await projectDatabase.trigger.findMany({ where: { stateId: instance.id } })
420
+
421
+ expect(triggers).toHaveLength(1)
422
+ expect(triggers[0].name).toBe("cleanup")
423
+ expect(triggers[0].meta).toEqual({
424
+ title: "Cleanup Before Destroy",
425
+ description: "Runs backup and cleanup operations before instance destruction",
426
+ icon: "mdi:backup-restore",
427
+ })
428
+ expect(triggers[0].spec).toEqual({
429
+ type: "before-destroy",
430
+ })
431
+ })
432
+
433
+ test("should update existing triggers", async ({
434
+ database,
435
+ project,
436
+ createInstanceState,
437
+ projectDatabase,
438
+ }) => {
439
+ const service = new UnitExtraService(database)
440
+ const instance = await createInstanceState(project.id)
441
+
442
+ const db = await database.forProject(project.id)
443
+ await db.trigger.create({
444
+ data: {
445
+ stateId: instance.id,
446
+ name: "cleanup",
447
+ meta: {
448
+ title: "Old Cleanup",
449
+ icon: "mdi:delete",
450
+ },
451
+ spec: {
452
+ type: "before-destroy",
453
+ },
454
+ },
455
+ })
456
+
457
+ const unitTriggers: UnitTrigger[] = [
458
+ {
459
+ name: "cleanup",
460
+ meta: {
461
+ title: "Advanced Cleanup",
462
+ description: "Enhanced cleanup with backup verification",
463
+ icon: "mdi:backup-restore",
464
+ iconColor: "#FF9800",
465
+ },
466
+ spec: {
467
+ type: "before-destroy",
468
+ },
469
+ },
470
+ ]
471
+
472
+ await projectDatabase.$transaction(async tx => {
473
+ await service.processUnitTriggers(tx, instance.id, unitTriggers)
474
+ })
475
+
476
+ const triggers = await projectDatabase.trigger.findMany({ where: { stateId: instance.id } })
477
+
478
+ expect(triggers).toHaveLength(1)
479
+ expect(triggers[0].meta).toEqual({
480
+ title: "Advanced Cleanup",
481
+ description: "Enhanced cleanup with backup verification",
482
+ icon: "mdi:backup-restore",
483
+ iconColor: "#FF9800",
484
+ })
485
+ expect(triggers[0].spec).toEqual({
486
+ type: "before-destroy",
487
+ })
488
+ })
489
+
490
+ test("should delete dangling triggers", async ({
491
+ database,
492
+ project,
493
+ createInstanceState,
494
+ projectDatabase,
495
+ }) => {
496
+ const service = new UnitExtraService(database)
497
+ const instance = await createInstanceState(project.id)
498
+
499
+ const db = await database.forProject(project.id)
500
+ await db.trigger.createMany({
501
+ data: [
502
+ {
503
+ stateId: instance.id,
504
+ name: "cleanup",
505
+ meta: { title: "Cleanup" },
506
+ spec: { type: "before-destroy" },
507
+ },
508
+ {
509
+ stateId: instance.id,
510
+ name: "backup",
511
+ meta: { title: "Backup" },
512
+ spec: { type: "before-destroy" },
513
+ },
514
+ ],
515
+ })
516
+
517
+ const unitTriggers: UnitTrigger[] = [
518
+ {
519
+ name: "cleanup",
520
+ meta: {
521
+ title: "Cleanup Before Destroy",
522
+ },
523
+ spec: {
524
+ type: "before-destroy",
525
+ },
526
+ },
527
+ ]
528
+
529
+ await projectDatabase.$transaction(async tx => {
530
+ await service.processUnitTriggers(tx, instance.id, unitTriggers)
531
+ })
532
+
533
+ const triggers = await projectDatabase.trigger.findMany({ where: { stateId: instance.id } })
534
+
535
+ expect(triggers).toHaveLength(1)
536
+ expect(triggers[0].name).toBe("cleanup")
537
+ })
538
+ })
539
+ })