@antelopejs/dms 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +36 -26
  3. package/bin/control/base.mjs +9 -4
  4. package/bin/control/browser-client.mjs +24 -5
  5. package/bin/control/browser-wire.mjs +41 -0
  6. package/bin/control/browser-wire.test.mjs +96 -0
  7. package/bin/control/browser.mjs +78 -27
  8. package/bin/control/commands-stack.mjs +58 -8
  9. package/bin/control/commands-verify.mjs +47 -1
  10. package/bin/control/doctor.mjs +49 -4
  11. package/bin/control/failure.mjs +9 -0
  12. package/bin/control/journey.mjs +158 -30
  13. package/bin/control/journey.test.mjs +61 -0
  14. package/bin/control/json.mjs +229 -0
  15. package/bin/control/json.test.mjs +130 -0
  16. package/bin/control/mail.mjs +57 -7
  17. package/bin/control/mail.test.mjs +55 -0
  18. package/bin/control/mongo.mjs +205 -27
  19. package/bin/control/mongo.test.mjs +298 -0
  20. package/bin/control/options.mjs +33 -0
  21. package/bin/control/processes.mjs +24 -2
  22. package/bin/control/renderer.mjs +39 -0
  23. package/bin/control/renderer.test.mjs +324 -0
  24. package/bin/control/run-state.mjs +119 -6
  25. package/bin/control/run-state.test.mjs +171 -0
  26. package/bin/control/session.mjs +30 -14
  27. package/bin/control/stack.mjs +95 -24
  28. package/bin/control/tab.mjs +117 -18
  29. package/bin/control/verify/behaviour.mjs +44 -6
  30. package/bin/control/verify/behaviour.test.mjs +130 -22
  31. package/bin/control/verify/decode.mjs +220 -0
  32. package/bin/control/verify/decode.test.mjs +245 -0
  33. package/bin/control/verify/doors.mjs +1 -1
  34. package/bin/control/verify/doors.test.mjs +40 -0
  35. package/bin/control/verify/emit.mjs +135 -9
  36. package/bin/control/verify/formal.mjs +61 -10
  37. package/bin/control/verify/formal.test.mjs +19 -3
  38. package/bin/control/verify/judgement.mjs +22 -3
  39. package/bin/control/verify/map.mjs +25 -5
  40. package/bin/control/verify/publish.mjs +20 -14
  41. package/bin/control/verify/seeded.mjs +65 -16
  42. package/bin/control/verify/skill.mjs +27 -6
  43. package/bin/control/verify/types.mjs +29 -0
  44. package/bin/control/verify/verdict.mjs +72 -7
  45. package/bin/dms-control.mjs +8 -28
  46. package/checks/api-error-presentation.d.mts +61 -0
  47. package/dist/attachments/access.js +1 -1
  48. package/dist/attachments/access.js.map +1 -1
  49. package/dist/attachments/fields.d.ts +2 -2
  50. package/dist/attachments/fields.js +26 -13
  51. package/dist/attachments/fields.js.map +1 -1
  52. package/dist/attachments/index.d.ts +1 -0
  53. package/dist/attachments/index.js +1 -0
  54. package/dist/attachments/index.js.map +1 -1
  55. package/dist/attachments/read.js.map +1 -1
  56. package/dist/attachments/save.d.ts +1 -1
  57. package/dist/attachments/save.js +49 -17
  58. package/dist/attachments/save.js.map +1 -1
  59. package/dist/attachments/table-view.d.ts +1 -2
  60. package/dist/attachments/table-view.js +23 -13
  61. package/dist/attachments/table-view.js.map +1 -1
  62. package/dist/automation/events.d.ts +10 -10
  63. package/dist/automation/index.js +3 -4
  64. package/dist/automation/index.js.map +1 -1
  65. package/dist/automation/send-notification.d.ts +2 -2
  66. package/dist/automation/send-notification.js +4 -6
  67. package/dist/automation/send-notification.js.map +1 -1
  68. package/dist/crons/cleanup-user-invites.js.map +1 -1
  69. package/dist/crons/sweep-expired-sessions.js.map +1 -1
  70. package/dist/crons/sweep-stale-exports.js.map +1 -1
  71. package/dist/db/guard-row.d.ts +2 -3
  72. package/dist/db/guard-row.js +1 -2
  73. package/dist/db/guard-row.js.map +1 -1
  74. package/dist/db/models/attachments.model.d.ts +7 -5
  75. package/dist/db/models/setupToken.model.d.ts +7 -5
  76. package/dist/db/models/signInAttempts.model.d.ts +7 -5
  77. package/dist/db/models/systemState.model.d.ts +7 -5
  78. package/dist/db/models/userEmailChanges.model.d.ts +7 -5
  79. package/dist/db/models/userKnownDevices.model.d.ts +7 -5
  80. package/dist/db/models/userNotificationPreferences.model.d.ts +7 -5
  81. package/dist/db/models/userNotifications.model.d.ts +9 -6
  82. package/dist/db/models/userNotifications.model.js.map +1 -1
  83. package/dist/dev/module-update-notifications.js.map +1 -1
  84. package/dist/hooks/internal/sign-in-monitor.js +6 -6
  85. package/dist/hooks/internal/sign-in-monitor.js.map +1 -1
  86. package/dist/implementations/dms/page/controller.d.ts +2 -2
  87. package/dist/implementations/dms/page/controller.js +3 -1
  88. package/dist/implementations/dms/page/controller.js.map +1 -1
  89. package/dist/implementations/dms/page/dynamic-menu.js +3 -2
  90. package/dist/implementations/dms/page/dynamic-menu.js.map +1 -1
  91. package/dist/implementations/dms/page/notifications.d.ts +6 -2
  92. package/dist/implementations/dms/page/notifications.js +1 -0
  93. package/dist/implementations/dms/page/notifications.js.map +1 -1
  94. package/dist/implementations/dms/page/preview-pages.js +6 -3
  95. package/dist/implementations/dms/page/preview-pages.js.map +1 -1
  96. package/dist/implementations/dms/page/registry.js +10 -4
  97. package/dist/implementations/dms/page/registry.js.map +1 -1
  98. package/dist/implementations/dms/permissions.js +12 -12
  99. package/dist/implementations/dms/permissions.js.map +1 -1
  100. package/dist/implementations/dms-base/bulk-selection.d.ts +3 -4
  101. package/dist/implementations/dms-base/bulk-selection.js +18 -6
  102. package/dist/implementations/dms-base/bulk-selection.js.map +1 -1
  103. package/dist/implementations/dms-base/filter-tokens.js.map +1 -1
  104. package/dist/implementations/dms-base/footer-summary.js +44 -27
  105. package/dist/implementations/dms-base/footer-summary.js.map +1 -1
  106. package/dist/implementations/dms-base/row-rule-operand.d.ts +6 -0
  107. package/dist/implementations/dms-base/row-rule-operand.js +41 -0
  108. package/dist/implementations/dms-base/row-rule-operand.js.map +1 -0
  109. package/dist/implementations/dms-base/search-route.d.ts +9 -13
  110. package/dist/implementations/dms-base/search-route.js +7 -6
  111. package/dist/implementations/dms-base/search-route.js.map +1 -1
  112. package/dist/implementations/dms-base/services.d.ts +1 -0
  113. package/dist/implementations/dms-base/services.js +1 -0
  114. package/dist/implementations/dms-base/services.js.map +1 -1
  115. package/dist/implementations/dms-base/table-view.d.ts +5 -6
  116. package/dist/implementations/dms-base/table-view.js +17 -6
  117. package/dist/implementations/dms-base/table-view.js.map +1 -1
  118. package/dist/implementations/dms-html-render/index.js +16 -6
  119. package/dist/implementations/dms-html-render/index.js.map +1 -1
  120. package/dist/implementations/dms-notifications/delivery-guard.d.ts +3 -2
  121. package/dist/implementations/dms-notifications/delivery-guard.js.map +1 -1
  122. package/dist/implementations/dms-notifications/index.js.map +1 -1
  123. package/dist/index.js.map +1 -1
  124. package/dist/pages/public/preview/block-types.d.ts +1 -2
  125. package/dist/pages/public/preview/block-types.js.map +1 -1
  126. package/dist/pages/settings/overview/account-summary.js +4 -1
  127. package/dist/pages/settings/overview/account-summary.js.map +1 -1
  128. package/dist/pages/settings/users/account-activity-feed.js +3 -2
  129. package/dist/pages/settings/users/account-activity-feed.js.map +1 -1
  130. package/dist/pages/settings/users/account-credentials.d.ts +1 -1
  131. package/dist/pages/settings/users/account-credentials.js +1 -2
  132. package/dist/pages/settings/users/account-credentials.js.map +1 -1
  133. package/dist/pages/settings/users/account-data-store.d.ts +1 -1
  134. package/dist/pages/settings/users/account-data-store.js +1 -3
  135. package/dist/pages/settings/users/account-data-store.js.map +1 -1
  136. package/dist/pages/settings/users/email-change.d.ts +2 -2
  137. package/dist/pages/settings/users/email-change.js +2 -3
  138. package/dist/pages/settings/users/email-change.js.map +1 -1
  139. package/dist/pages/settings/users/invite-extension-routes.d.ts +2 -2
  140. package/dist/pages/settings/users/invite-extension-routes.js +11 -4
  141. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -1
  142. package/dist/pages/settings/users/invites.d.ts +2 -2
  143. package/dist/pages/settings/users/member-invite-batch.d.ts +2 -2
  144. package/dist/pages/settings/users/member-invite-batch.js +7 -3
  145. package/dist/pages/settings/users/member-invite-batch.js.map +1 -1
  146. package/dist/pages/settings/users/member-roles-field.js +1 -4
  147. package/dist/pages/settings/users/member-roles-field.js.map +1 -1
  148. package/dist/pages/settings/users/members.d.ts +2 -3
  149. package/dist/pages/settings/users/members.js.map +1 -1
  150. package/dist/pages/settings/users/notifications.d.ts +2 -3
  151. package/dist/pages/settings/users/notifications.js +2 -1
  152. package/dist/pages/settings/users/notifications.js.map +1 -1
  153. package/dist/pages/settings/users/profile-helpers.d.ts +0 -1
  154. package/dist/pages/settings/users/profile-helpers.js +0 -17
  155. package/dist/pages/settings/users/profile-helpers.js.map +1 -1
  156. package/dist/pages/settings/users/profile.d.ts +3 -4
  157. package/dist/pages/settings/users/profile.js +12 -12
  158. package/dist/pages/settings/users/profile.js.map +1 -1
  159. package/dist/pages/settings/users/region.d.ts +3 -4
  160. package/dist/pages/settings/users/region.js.map +1 -1
  161. package/dist/pages/settings/users/regional-preferences.d.ts +8 -1
  162. package/dist/pages/settings/users/regional-preferences.js +10 -6
  163. package/dist/pages/settings/users/regional-preferences.js.map +1 -1
  164. package/dist/pages/settings/users/role-preview.js +3 -2
  165. package/dist/pages/settings/users/role-preview.js.map +1 -1
  166. package/dist/pages/settings/users/roles.d.ts +4 -5
  167. package/dist/pages/settings/users/roles.js.map +1 -1
  168. package/dist/pages/settings/users/security.d.ts +9 -12
  169. package/dist/pages/settings/users/security.js +11 -9
  170. package/dist/pages/settings/users/security.js.map +1 -1
  171. package/dist/pages/settings/users/two-factor-operations.d.ts +7 -7
  172. package/dist/pages/settings/users/two-factor-operations.js +5 -7
  173. package/dist/pages/settings/users/two-factor-operations.js.map +1 -1
  174. package/dist/realtime/broker.js.map +1 -1
  175. package/dist/realtime/heartbeat.js.map +1 -1
  176. package/dist/realtime/redis-broker.js.map +1 -1
  177. package/dist/realtime/sessions.js.map +1 -1
  178. package/dist/realtime/sse.js +1 -2
  179. package/dist/realtime/sse.js.map +1 -1
  180. package/dist/routes/auth/forgot-password.d.ts +1 -1
  181. package/dist/routes/auth/forgot-password.js +1 -3
  182. package/dist/routes/auth/forgot-password.js.map +1 -1
  183. package/dist/routes/auth/login.d.ts +1 -1
  184. package/dist/routes/auth/login.js +1 -2
  185. package/dist/routes/auth/login.js.map +1 -1
  186. package/dist/routes/auth/logout.d.ts +1 -1
  187. package/dist/routes/auth/logout.js +1 -2
  188. package/dist/routes/auth/logout.js.map +1 -1
  189. package/dist/routes/auth/oauth/providers.js +30 -6
  190. package/dist/routes/auth/oauth/providers.js.map +1 -1
  191. package/dist/routes/auth/oauth/state.js +8 -5
  192. package/dist/routes/auth/oauth/state.js.map +1 -1
  193. package/dist/routes/auth/oauth/token-exchange.js +8 -1
  194. package/dist/routes/auth/oauth/token-exchange.js.map +1 -1
  195. package/dist/routes/auth/refresh.d.ts +1 -1
  196. package/dist/routes/auth/refresh.js +1 -2
  197. package/dist/routes/auth/refresh.js.map +1 -1
  198. package/dist/routes/auth/request-2fa-email.d.ts +1 -1
  199. package/dist/routes/auth/request-2fa-email.js +1 -2
  200. package/dist/routes/auth/request-2fa-email.js.map +1 -1
  201. package/dist/routes/auth/reset-password.d.ts +1 -1
  202. package/dist/routes/auth/reset-password.js +1 -3
  203. package/dist/routes/auth/reset-password.js.map +1 -1
  204. package/dist/routes/auth/session-handoff.d.ts +1 -1
  205. package/dist/routes/auth/session-handoff.js +1 -2
  206. package/dist/routes/auth/session-handoff.js.map +1 -1
  207. package/dist/routes/auth/signup.d.ts +1 -1
  208. package/dist/routes/auth/signup.js +1 -2
  209. package/dist/routes/auth/signup.js.map +1 -1
  210. package/dist/routes/auth/switch-tenant.d.ts +2 -3
  211. package/dist/routes/auth/switch-tenant.js +3 -2
  212. package/dist/routes/auth/switch-tenant.js.map +1 -1
  213. package/dist/routes/auth/validate-forgot-password-token.d.ts +1 -1
  214. package/dist/routes/auth/validate-forgot-password-token.js +1 -3
  215. package/dist/routes/auth/validate-forgot-password-token.js.map +1 -1
  216. package/dist/routes/auth/validate-invite-token.d.ts +1 -1
  217. package/dist/routes/auth/validate-invite-token.js +1 -3
  218. package/dist/routes/auth/validate-invite-token.js.map +1 -1
  219. package/dist/routes/auth/verify-2fa.d.ts +1 -1
  220. package/dist/routes/auth/verify-2fa.js +1 -2
  221. package/dist/routes/auth/verify-2fa.js.map +1 -1
  222. package/dist/routes/auth/verify-email.d.ts +1 -1
  223. package/dist/routes/auth/verify-email.js +1 -2
  224. package/dist/routes/auth/verify-email.js.map +1 -1
  225. package/dist/routes/auth.d.ts +14 -15
  226. package/dist/routes/auth.js +15 -13
  227. package/dist/routes/auth.js.map +1 -1
  228. package/dist/routes/files.d.ts +1 -2
  229. package/dist/routes/files.js.map +1 -1
  230. package/dist/routes/onboarding.d.ts +1 -2
  231. package/dist/routes/onboarding.js.map +1 -1
  232. package/dist/routes/realtime.js +5 -6
  233. package/dist/routes/realtime.js.map +1 -1
  234. package/dist/routes/store.d.ts +6 -7
  235. package/dist/routes/store.js +19 -13
  236. package/dist/routes/store.js.map +1 -1
  237. package/dist/store/catalog.d.ts +13 -5
  238. package/dist/store/catalog.js +19 -9
  239. package/dist/store/catalog.js.map +1 -1
  240. package/dist/store/compatibility.d.ts +1 -0
  241. package/dist/store/compatibility.js +24 -8
  242. package/dist/store/compatibility.js.map +1 -1
  243. package/dist/store/frontend-compatibility.js +15 -3
  244. package/dist/store/frontend-compatibility.js.map +1 -1
  245. package/dist/store/interfaces.js +35 -13
  246. package/dist/store/interfaces.js.map +1 -1
  247. package/dist/store/internal/frontend/frontend-modules.js +7 -3
  248. package/dist/store/internal/frontend/frontend-modules.js.map +1 -1
  249. package/dist/store/internal/frontend/frontend-ready.d.ts +2 -1
  250. package/dist/store/internal/frontend/frontend-ready.js +6 -4
  251. package/dist/store/internal/frontend/frontend-ready.js.map +1 -1
  252. package/dist/store/internal/frontend/manifest.js +6 -7
  253. package/dist/store/internal/frontend/manifest.js.map +1 -1
  254. package/dist/store/operations.js.map +1 -1
  255. package/dist/store/registry.js.map +1 -1
  256. package/dist/store/runtime.d.ts +13 -0
  257. package/dist/store/runtime.js +9 -1
  258. package/dist/store/runtime.js.map +1 -1
  259. package/dist/utils/admin-invite-email.js +3 -1
  260. package/dist/utils/admin-invite-email.js.map +1 -1
  261. package/dist/utils/email-subjects.js +3 -1
  262. package/dist/utils/email-subjects.js.map +1 -1
  263. package/dist/utils/export-jobs-run.js.map +1 -1
  264. package/dist/utils/export-jobs.js +3 -9
  265. package/dist/utils/export-jobs.js.map +1 -1
  266. package/dist/utils/index.d.ts +1 -0
  267. package/dist/utils/index.js +1 -0
  268. package/dist/utils/index.js.map +1 -1
  269. package/dist/utils/own-value.d.ts +1 -0
  270. package/dist/utils/own-value.js +7 -0
  271. package/dist/utils/own-value.js.map +1 -0
  272. package/dist/utils/row-action-rule-evaluator.js +31 -35
  273. package/dist/utils/row-action-rule-evaluator.js.map +1 -1
  274. package/dist/utils/sign-in-country.js +4 -2
  275. package/dist/utils/sign-in-country.js.map +1 -1
  276. package/dist/utils/tenant-export-archive.js +9 -14
  277. package/dist/utils/tenant-export-archive.js.map +1 -1
  278. package/dist/utils/user-agent.js +2 -1
  279. package/dist/utils/user-agent.js.map +1 -1
  280. package/dist/validation/auth.schema.d.ts +10 -10
  281. package/dist/validation/regional-preferences.schema.d.ts +2 -2
  282. package/dist/validation/role-editor.schema.d.ts +1 -1
  283. package/frontend-vue/build/backend-boundary.ts +49 -18
  284. package/frontend-vue/dms.frontend.ts +8 -12
  285. package/frontend-vue/layers/dms-auth/server/accounts.ts +44 -3
  286. package/frontend-vue/layers/dms-auth/server/actions.ts +17 -4
  287. package/frontend-vue/layers/dms-auth/server/oauth.ts +42 -4
  288. package/frontend-vue/layers/dms-core/app/types/watch.ts +1 -2
  289. package/frontend-vue/layers/dms-ui/app/build/composables/form/fieldValueRule.ts +57 -6
  290. package/frontend-vue/layers/dms-ui/app/build/composables/form/registerFormFunctions.ts +7 -9
  291. package/frontend-vue/layers/dms-ui/app/entries/composables/form/registerFormFunctions.ts +1 -0
  292. package/frontend-vue/lazy-plugin.ts +18 -0
  293. package/frontend-vue/package.json +2 -2
  294. package/package.json +19 -13
  295. package/skills/dms-project/SKILL.md +4 -3
@@ -0,0 +1,324 @@
1
+ import assert from "node:assert/strict";
2
+ import {
3
+ lstatSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ realpathSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join, resolve } from "node:path";
12
+ import test from "node:test";
13
+ import { withBaseBackend } from "./base.mjs";
14
+ import { targetOf } from "./commands-stack.mjs";
15
+ import { git } from "./git.mjs";
16
+ import { resolveRenderer } from "./renderer.mjs";
17
+ import { decodeSkillConfig } from "./verify/decode.mjs";
18
+ import { upArguments } from "./verify/seeded.mjs";
19
+ import { cliPaths, layOutCopy } from "./stack.mjs";
20
+
21
+ /**
22
+ * @param {import("node:test").TestContext} t The test
23
+ * @returns {string} A real scratch folder removed when the test ends
24
+ */
25
+ function scratch(t) {
26
+ const dir = realpathSync(
27
+ mkdtempSync(join(tmpdir(), "dms-control-renderer-test-")),
28
+ );
29
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
30
+ return dir;
31
+ }
32
+
33
+ /**
34
+ * @param {string} folder The package folder
35
+ * @param {string} name Its name
36
+ * @returns {string} The folder
37
+ */
38
+ function writePackage(folder, name) {
39
+ mkdirSync(folder, { recursive: true });
40
+ writeFileSync(join(folder, "package.json"), JSON.stringify({ name }));
41
+ return folder;
42
+ }
43
+
44
+ /**
45
+ * @param {string} folder The renderer checkout
46
+ * @returns {string} The folder
47
+ */
48
+ function builtRenderer(folder) {
49
+ writePackage(folder, "@antelopejs/dms-frontend");
50
+ mkdirSync(join(folder, "dist"), { recursive: true });
51
+ writeFileSync(join(folder, "dist", "index.js"), "");
52
+ return folder;
53
+ }
54
+
55
+ /**
56
+ * A checkout whose playground has installed modules: core, a published
57
+ * dms-frontend and one other package.
58
+ *
59
+ * @param {string} root The scratch folder
60
+ * @returns {{ checkout: string, playground: string, modules: string }} Its paths
61
+ */
62
+ function checkoutWithPlayground(root) {
63
+ const checkout = join(root, "dms");
64
+ const playground = join(checkout, "playground");
65
+ const modules = join(playground, "node_modules");
66
+ mkdirSync(playground, { recursive: true });
67
+ writeFileSync(join(playground, "antelope.config.ts"), "");
68
+ writePackage(join(modules, "@antelopejs", "core"), "@antelopejs/core");
69
+ writePackage(
70
+ join(modules, "@antelopejs", "dms-frontend"),
71
+ "@antelopejs/dms-frontend",
72
+ );
73
+ writePackage(join(modules, "zod"), "zod");
74
+ return { checkout, playground, modules };
75
+ }
76
+
77
+ await test("--renderer takes a built dms-frontend checkout as an absolute folder", (t) => {
78
+ const folder = builtRenderer(join(scratch(t), "dms-frontend"));
79
+ assert.equal(resolveRenderer(folder), folder);
80
+ });
81
+
82
+ await test("--renderer refuses a folder without a package.json", (t) => {
83
+ assert.throws(() => resolveRenderer(scratch(t)), /has no package\.json/);
84
+ });
85
+
86
+ await test("--renderer refuses a package that is not dms-frontend", (t) => {
87
+ const folder = writePackage(join(scratch(t), "other"), "@antelopejs/dms");
88
+ assert.throws(
89
+ () => resolveRenderer(folder),
90
+ /is @antelopejs\/dms, not @antelopejs\/dms-frontend/,
91
+ );
92
+ });
93
+
94
+ await test("--renderer refuses a checkout that is not built", (t) => {
95
+ const folder = writePackage(
96
+ join(scratch(t), "dms-frontend"),
97
+ "@antelopejs/dms-frontend",
98
+ );
99
+ assert.throws(() => resolveRenderer(folder), /has no dist.index\.js/);
100
+ });
101
+
102
+ await test("the run's playground resolves dms-frontend to the renderer and everything else to the playground's modules", (t) => {
103
+ const root = scratch(t);
104
+ const { checkout, playground, modules } = checkoutWithPlayground(root);
105
+ const renderer = builtRenderer(join(root, "dms-frontend"));
106
+ const copy = layOutCopy(playground, checkout, join(root, "run"), renderer);
107
+ const linked = join(copy, "node_modules");
108
+ assert.equal(
109
+ realpathSync(join(linked, "@antelopejs", "dms-frontend")),
110
+ renderer,
111
+ );
112
+ assert.equal(
113
+ realpathSync(join(linked, "@antelopejs", "core")),
114
+ realpathSync(join(modules, "@antelopejs", "core")),
115
+ );
116
+ assert.equal(
117
+ realpathSync(join(linked, "zod")),
118
+ realpathSync(join(modules, "zod")),
119
+ );
120
+ });
121
+
122
+ await test("linking a renderer leaves the playground's own modules alone", (t) => {
123
+ const root = scratch(t);
124
+ const { checkout, playground, modules } = checkoutWithPlayground(root);
125
+ const published = realpathSync(join(modules, "@antelopejs", "dms-frontend"));
126
+ layOutCopy(
127
+ playground,
128
+ checkout,
129
+ join(root, "run"),
130
+ builtRenderer(join(root, "dms-frontend")),
131
+ );
132
+ assert.equal(
133
+ realpathSync(join(modules, "@antelopejs", "dms-frontend")),
134
+ published,
135
+ );
136
+ });
137
+
138
+ await test("a playground that installs no @antelopejs scope still gets the renderer", (t) => {
139
+ const root = scratch(t);
140
+ const { checkout, playground, modules } = checkoutWithPlayground(root);
141
+ rmSync(join(modules, "@antelopejs"), { recursive: true });
142
+ const renderer = builtRenderer(join(root, "dms-frontend"));
143
+ const copy = layOutCopy(playground, checkout, join(root, "run"), renderer);
144
+ assert.equal(
145
+ realpathSync(join(copy, "node_modules", "@antelopejs", "dms-frontend")),
146
+ renderer,
147
+ );
148
+ });
149
+
150
+ await test("without a renderer the run's node_modules is the playground's", (t) => {
151
+ const root = scratch(t);
152
+ const { checkout, playground, modules } = checkoutWithPlayground(root);
153
+ const copy = layOutCopy(playground, checkout, join(root, "run"));
154
+ const linked = join(copy, "node_modules");
155
+ assert.equal(lstatSync(linked).isSymbolicLink(), true);
156
+ assert.equal(realpathSync(linked), realpathSync(modules));
157
+ });
158
+
159
+ await test("the dashboard CLI is the renderer's build, the backend CLI stays the playground's core", (t) => {
160
+ const root = scratch(t);
161
+ const { playground } = checkoutWithPlayground(root);
162
+ const renderer = builtRenderer(join(root, "dms-frontend"));
163
+ const published = cliPaths(playground);
164
+ const served = cliPaths(playground, renderer);
165
+ assert.equal(served.dms, join(renderer, "dist", "index.js"));
166
+ assert.notEqual(published.dms, served.dms);
167
+ assert.equal(served.ajs, published.ajs);
168
+ });
169
+
170
+ /**
171
+ * @param {string} root The scratch folder
172
+ * @param {string} config The text after `export default` in `verify.config.ts`
173
+ * @returns {string} The checkout holding a verification skill with that config
174
+ */
175
+ function checkoutWithSkill(root, config) {
176
+ const checkout = join(root, "dms-frontend");
177
+ const skill = join(checkout, ".agents", "skills", "verify-dms-frontend");
178
+ mkdirSync(skill, { recursive: true });
179
+ writeFileSync(join(skill, "verify.config.ts"), `export default ${config};\n`);
180
+ git(["init", "--quiet"], checkout);
181
+ git(["add", "."], checkout);
182
+ git(
183
+ [
184
+ "-c",
185
+ "user.name=t",
186
+ "-c",
187
+ "user.email=t@example.com",
188
+ "commit",
189
+ "--quiet",
190
+ "-m",
191
+ "skill",
192
+ ],
193
+ checkout,
194
+ );
195
+ return checkout;
196
+ }
197
+
198
+ const EMPTY_CONFIG = `prepare: [], userFacing: [], smoke: "", smokeJourney: "", doors: {}, formal: []`;
199
+
200
+ await test("up takes the playground and the renderer from the skill's config, relative ones from its repository", async (t) => {
201
+ const root = scratch(t);
202
+ const checkout = checkoutWithSkill(
203
+ root,
204
+ `{ playground: "/dms/playground", renderer: ".", ${EMPTY_CONFIG} }`,
205
+ );
206
+ assert.deepEqual(await targetOf({}, checkout), {
207
+ playground: "/dms/playground",
208
+ renderer: checkout,
209
+ });
210
+ });
211
+
212
+ await test("--renderer and --playground win over the skill's config", async (t) => {
213
+ const checkout = checkoutWithSkill(
214
+ scratch(t),
215
+ `{ playground: "/dms/playground", renderer: ".", ${EMPTY_CONFIG} }`,
216
+ );
217
+ assert.deepEqual(
218
+ await targetOf(
219
+ { playground: "/other/playground", renderer: "/other/ui" },
220
+ checkout,
221
+ ),
222
+ { playground: "/other/playground", renderer: "/other/ui" },
223
+ );
224
+ });
225
+
226
+ await test("a skill without a renderer serves the published one", async (t) => {
227
+ const checkout = checkoutWithSkill(
228
+ scratch(t),
229
+ `{ playground: "packages/dms/playground", ${EMPTY_CONFIG} }`,
230
+ );
231
+ assert.deepEqual(await targetOf({}, checkout), {
232
+ playground: resolve(checkout, "packages/dms/playground"),
233
+ renderer: undefined,
234
+ });
235
+ });
236
+
237
+ const CONFIG = {
238
+ playground: "p",
239
+ prepare: [],
240
+ userFacing: [],
241
+ smoke: "",
242
+ smokeJourney: "",
243
+ doors: {},
244
+ formal: [],
245
+ };
246
+
247
+ await test("a skill config keeps its renderer, and refuses one that is not a path", () => {
248
+ assert.equal(
249
+ decodeSkillConfig({ ...CONFIG, renderer: "." }, "verify.config.ts")
250
+ .renderer,
251
+ ".",
252
+ );
253
+ assert.equal(
254
+ decodeSkillConfig(CONFIG, "verify.config.ts").renderer,
255
+ undefined,
256
+ );
257
+ assert.throws(
258
+ () => decodeSkillConfig({ ...CONFIG, renderer: 3 }, "verify.config.ts"),
259
+ /verify\.config\.ts/,
260
+ );
261
+ });
262
+
263
+ await test("a run with a renderer has no base backend", async () => {
264
+ await assert.rejects(
265
+ withBaseBackend({ renderer: "/ui" }, {}, async () => {}),
266
+ /no base backend/,
267
+ );
268
+ });
269
+
270
+ /**
271
+ * @param {Partial<import("./verify/skill.mjs").SkillConfig>} config What differs from a skill with a playground in its repository
272
+ * @returns {import("./verify/skill.mjs").Skill} The skill
273
+ */
274
+ function skillWith(config) {
275
+ return {
276
+ dir: "/repo/.agents/skills/verify-x",
277
+ name: "verify-x",
278
+ repo: "/repo",
279
+ config: { ...decodeSkillConfig(CONFIG, "verify.config.ts"), ...config },
280
+ featuresDir: "",
281
+ journeysDir: "",
282
+ seededBreaksDir: "",
283
+ seededDoorsDir: "",
284
+ };
285
+ }
286
+
287
+ await test("seeded starts each run on the worktree's renderer and an external playground as it is", () => {
288
+ const prepared = {
289
+ worktree: "/work/seeded-auth-1",
290
+ skill: "/work/seeded-auth-1/skill",
291
+ };
292
+ assert.deepEqual(
293
+ upArguments(
294
+ skillWith({ playground: "/dms/playground", renderer: "." }),
295
+ prepared,
296
+ ),
297
+ [
298
+ "up",
299
+ "--playground",
300
+ "/dms/playground",
301
+ "--renderer",
302
+ "/work/seeded-auth-1",
303
+ "--skill",
304
+ "/work/seeded-auth-1/skill",
305
+ ],
306
+ );
307
+ });
308
+
309
+ await test("seeded without a renderer starts the worktree's own playground", () => {
310
+ const prepared = {
311
+ worktree: "/work/seeded-auth-1",
312
+ skill: "/work/seeded-auth-1/skill",
313
+ };
314
+ assert.deepEqual(
315
+ upArguments(skillWith({ playground: "packages/dms/playground" }), prepared),
316
+ [
317
+ "up",
318
+ "--playground",
319
+ "/work/seeded-auth-1/packages/dms/playground",
320
+ "--skill",
321
+ "/work/seeded-auth-1/skill",
322
+ ],
323
+ );
324
+ });
@@ -2,11 +2,29 @@ import { randomBytes } from "node:crypto";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
+ import {
6
+ arrayOf,
7
+ decode,
8
+ either,
9
+ isBoolean,
10
+ isNumber,
11
+ isString,
12
+ nullable,
13
+ objectOf,
14
+ recordOf,
15
+ } from "./json.mjs";
5
16
 
6
17
  const RUN_FILE = "run.json";
7
18
  const ID_BYTES = 3;
8
19
  const TIMESTAMP_LENGTH = 15;
9
20
 
21
+ /**
22
+ * @typedef {{ name: string, pid: number, command: string, cwd: string, log: string, startedAt: string, port?: number }} ProcessRecord
23
+ * @typedef {ProcessRecord & { port: number }} ServerRecord
24
+ * @typedef {{ email: string, password: string, userId?: string | null }} Account
25
+ * @typedef {{ dir: string, root: string, home: string, logs: string, mongo: string, browser?: ServerRecord, evidence: string, cache: string, id: string, createdAt: string, environment: string, playground: string, moduleRoot: string, repo: string | null, headSha: string | null, renderer?: string, baseRef: string, skill: string | null, apiPort: number, webPort: number, smtpPort: number, mail: string, apiUrl: string, webUrl: string, databasePrefix: string, database: string, mongoUrl: string, ownsMongo: boolean, secrets: Record<string, string>, adminPassword: string, extraEnv: Record<string, string>, processes: ProcessRecord[], accounts: Record<string, Account>, tokens: Record<string, string>, ready: boolean, error?: string, runPlayground?: string, baseRoot?: string, resetCount?: number, journeyCount?: number }} RunState
26
+ */
27
+
10
28
  /**
11
29
  * Where runs are created: `--runs`, else `DMS_CONTROL_RUNS`, else
12
30
  * `<tmpdir>/dms-control`. A run holds a playground copy and its frontend
@@ -39,7 +57,7 @@ export function newRunId() {
39
57
  * The folders of a run, all under its root.
40
58
  *
41
59
  * @param {string} dir The run root
42
- * @returns {{ dir: string, root: string, home: string, logs: string, mongo: string, browser: string, evidence: string, cache: string }} The run's folders
60
+ * @returns {{ dir: string, root: string, home: string, logs: string, mongo: string, evidence: string, cache: string }} The run's folders
43
61
  */
44
62
  export function runPaths(dir) {
45
63
  return {
@@ -48,7 +66,6 @@ export function runPaths(dir) {
48
66
  home: join(dir, "home"),
49
67
  logs: join(dir, "logs"),
50
68
  mongo: join(dir, "mongo"),
51
- browser: join(dir, "browser"),
52
69
  evidence: join(dir, "evidence"),
53
70
  cache: join(dir, "cache"),
54
71
  };
@@ -80,10 +97,93 @@ export function createRunFolders(dir) {
80
97
  }
81
98
 
82
99
  /**
83
- * Reads the state `up` recorded for a run.
100
+ * @typedef {Omit<RunState, "browser" | "accounts"> & { browser?: string | ServerRecord, accounts: Record<string, Omit<Account, "password"> & { password?: string }> }} StoredRunState The state as a file may hold it. A run saved by an earlier release holds the browser's folder in `browser` until the browser starts, and `down` leaves an account without its `password`.
101
+ */
102
+
103
+ const isStrings = recordOf(isString);
104
+
105
+ const isProcessRecord = objectOf(
106
+ {
107
+ name: isString,
108
+ pid: isNumber,
109
+ command: isString,
110
+ cwd: isString,
111
+ log: isString,
112
+ startedAt: isString,
113
+ },
114
+ { port: isNumber },
115
+ );
116
+
117
+ const isServerRecord = objectOf({
118
+ name: isString,
119
+ pid: isNumber,
120
+ command: isString,
121
+ cwd: isString,
122
+ log: isString,
123
+ startedAt: isString,
124
+ port: isNumber,
125
+ });
126
+
127
+ const isAccount = objectOf(
128
+ { email: isString },
129
+ { password: isString, userId: nullable(isString) },
130
+ );
131
+
132
+ /** @type {import("./json.mjs").Guard<StoredRunState>} The state as `run.json` holds it. Keys it does not check are kept, so saving a loaded run writes them back. */
133
+ const isStoredRun = objectOf(
134
+ {
135
+ dir: isString,
136
+ root: isString,
137
+ home: isString,
138
+ logs: isString,
139
+ mongo: isString,
140
+ evidence: isString,
141
+ cache: isString,
142
+ id: isString,
143
+ createdAt: isString,
144
+ environment: isString,
145
+ playground: isString,
146
+ moduleRoot: isString,
147
+ repo: nullable(isString),
148
+ headSha: nullable(isString),
149
+ baseRef: isString,
150
+ skill: nullable(isString),
151
+ apiPort: isNumber,
152
+ webPort: isNumber,
153
+ smtpPort: isNumber,
154
+ mail: isString,
155
+ apiUrl: isString,
156
+ webUrl: isString,
157
+ databasePrefix: isString,
158
+ database: isString,
159
+ mongoUrl: isString,
160
+ ownsMongo: isBoolean,
161
+ secrets: isStrings,
162
+ adminPassword: isString,
163
+ extraEnv: isStrings,
164
+ processes: arrayOf(isProcessRecord),
165
+ accounts: recordOf(isAccount),
166
+ tokens: isStrings,
167
+ ready: isBoolean,
168
+ },
169
+ {
170
+ browser: either(isString, isServerRecord),
171
+ error: isString,
172
+ renderer: isString,
173
+ runPlayground: isString,
174
+ baseRoot: isString,
175
+ resetCount: isNumber,
176
+ journeyCount: isNumber,
177
+ },
178
+ );
179
+
180
+ /**
181
+ * Reads the state `up` recorded for a run. A folder an earlier release stored
182
+ * in `browser` is dropped, since nothing creates or reads that folder. An
183
+ * account that `down` left without its `password` reads with an empty one.
84
184
  *
85
185
  * @param {string} dir The run root, as `up` printed it
86
- * @returns {Record<string, any>} The run state
186
+ * @returns {RunState} The run state
87
187
  * @throws When the folder holds no run.json
88
188
  */
89
189
  export function loadRun(dir) {
@@ -93,14 +193,27 @@ export function loadRun(dir) {
93
193
  `No run at ${dir}: run.json is missing. Start one with dms-control up.`,
94
194
  );
95
195
  }
96
- return JSON.parse(readFileSync(file, "utf8"));
196
+ /** @type {unknown} */
197
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
198
+ const stored = decode(isStoredRun, parsed, file, "a run state");
199
+ const { browser, accounts, ...rest } = stored;
200
+ const run = {
201
+ ...rest,
202
+ accounts: Object.fromEntries(
203
+ Object.entries(accounts).map(([name, account]) => [
204
+ name,
205
+ { ...account, password: account.password ?? "" },
206
+ ]),
207
+ ),
208
+ };
209
+ return browser === undefined || isString(browser) ? run : { ...run, browser };
97
210
  }
98
211
 
99
212
  /**
100
213
  * Writes the run state, readable by its owner only since it holds the run's
101
214
  * secrets and the admin password.
102
215
  *
103
- * @param {Record<string, any>} state The run state
216
+ * @param {RunState} state The run state
104
217
  */
105
218
  export function saveRun(state) {
106
219
  writeFileSync(
@@ -0,0 +1,171 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { createRunFolders, loadRun, saveRun } from "./run-state.mjs";
7
+ import { down } from "./stack.mjs";
8
+
9
+ const SERVER = {
10
+ name: "browser",
11
+ pid: 4242,
12
+ command: "node serve",
13
+ cwd: "/run",
14
+ log: "/run/logs/browser.log",
15
+ startedAt: "2026-10-31T00:00:00.000Z",
16
+ port: 40123,
17
+ };
18
+
19
+ const RUN = {
20
+ root: "/run/root",
21
+ home: "/run/home",
22
+ logs: "/run/logs",
23
+ mongo: "/run/mongo",
24
+ evidence: "/run/evidence",
25
+ cache: "/run/cache",
26
+ id: "20261031T000000-abc123",
27
+ createdAt: "2026-10-31T00:00:00.000Z",
28
+ environment: "verify",
29
+ playground: "/repo/playground",
30
+ moduleRoot: "/repo",
31
+ repo: "/repo",
32
+ headSha: null,
33
+ baseRef: "origin/main",
34
+ skill: null,
35
+ apiPort: 5010,
36
+ webPort: 5011,
37
+ smtpPort: 5012,
38
+ mail: "/run/evidence/mail",
39
+ apiUrl: "http://127.0.0.1:5010",
40
+ webUrl: "http://127.0.0.1:5011",
41
+ databasePrefix: "verify_x",
42
+ database: "verify_x",
43
+ mongoUrl: "",
44
+ ownsMongo: true,
45
+ secrets: { JWT: "s" },
46
+ adminPassword: "pw",
47
+ extraEnv: {},
48
+ processes: [],
49
+ accounts: { admin: { email: "a@example.com", password: "pw" } },
50
+ tokens: {},
51
+ ready: false,
52
+ };
53
+
54
+ /**
55
+ * @param {import("node:test").TestContext} t The test
56
+ * @param {Record<string, unknown>} fields What the saved file holds besides `dir` and the fields every run has
57
+ * @returns {string} The run folder
58
+ */
59
+ function savedRun(t, fields) {
60
+ const dir = mkdtempSync(join(tmpdir(), "dms-control-run-state-test-"));
61
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
62
+ mkdirSync(dir, { recursive: true });
63
+ writeFileSync(
64
+ join(dir, "run.json"),
65
+ JSON.stringify({ ...RUN, dir, ...fields }),
66
+ );
67
+ return dir;
68
+ }
69
+
70
+ await test("a run saved by an earlier release drops the browser folder it held in `browser`", (t) => {
71
+ const dir = savedRun(t, { browser: "/runs/old/browser" });
72
+ const state = loadRun(dir);
73
+ assert.equal("browser" in state, false);
74
+ });
75
+
76
+ await test("a run saved by an earlier release keeps its server record once the browser started", (t) => {
77
+ const dir = savedRun(t, { browser: SERVER });
78
+ assert.deepEqual(loadRun(dir).browser, SERVER);
79
+ });
80
+
81
+ await test("a run without a browser reads back without one", (t) => {
82
+ const dir = savedRun(t, {});
83
+ assert.equal("browser" in loadRun(dir), false);
84
+ });
85
+
86
+ await test("saving a loaded run again keeps its server record", (t) => {
87
+ const dir = savedRun(t, { browser: SERVER });
88
+ const state = loadRun(dir);
89
+ saveRun(state);
90
+ assert.deepEqual(loadRun(dir), state);
91
+ });
92
+
93
+ await test("a run.json that holds no run is refused", (t) => {
94
+ for (const text of ["null", "[]", "5", '"s"', "{}"]) {
95
+ const dir = mkdtempSync(join(tmpdir(), "dms-control-run-state-test-"));
96
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
97
+ writeFileSync(join(dir, "run.json"), text);
98
+ assert.throws(
99
+ () => loadRun(dir),
100
+ /run\.json is malformed: expected a run state/,
101
+ );
102
+ }
103
+ });
104
+
105
+ await test("a run.json whose field has the wrong type is refused", (t) => {
106
+ const dir = savedRun(t, { ready: "yes" });
107
+ assert.throws(() => loadRun(dir), /malformed: expected a run state/);
108
+ const processes = savedRun(t, { processes: [{ name: "api", pid: "7" }] });
109
+ assert.throws(() => loadRun(processes), /malformed: expected a run state/);
110
+ });
111
+
112
+ await test("keys of a run.json that the schema does not name are written back", (t) => {
113
+ const dir = savedRun(t, { addedByANewerRelease: 1 });
114
+ saveRun(loadRun(dir));
115
+ assert.equal(
116
+ Object.getOwnPropertyDescriptor(loadRun(dir), "addedByANewerRelease")
117
+ ?.value,
118
+ 1,
119
+ );
120
+ });
121
+
122
+ await test("a new run's folders hold no browser folder, and loading a fresh run has no server", (t) => {
123
+ const parent = mkdtempSync(join(tmpdir(), "dms-control-run-state-test-"));
124
+ t.after(() => rmSync(parent, { recursive: true, force: true }));
125
+ const dir = join(parent, "run");
126
+ const paths = createRunFolders(dir);
127
+ assert.equal("browser" in paths, false);
128
+ saveRun({ ...RUN, ...paths, id: "x" });
129
+ assert.equal("browser" in loadRun(dir), false);
130
+ });
131
+
132
+ await test("a run that down stopped loads again, its accounts without a password", async (t) => {
133
+ const dir = savedRun(t, {
134
+ accounts: {
135
+ admin: { email: "a@example.com", password: "pw", userId: "u1" },
136
+ },
137
+ });
138
+ await down(loadRun(dir));
139
+ const stopped = loadRun(dir);
140
+ assert.deepEqual(stopped.accounts, {
141
+ admin: { email: "a@example.com", password: "" },
142
+ });
143
+ assert.equal(stopped.ready, false);
144
+ await down(stopped);
145
+ assert.deepEqual(loadRun(dir).accounts, {
146
+ admin: { email: "a@example.com", password: "" },
147
+ });
148
+ });
149
+
150
+ await test("a run.json without its accounts, or with an account without an email, is refused", (t) => {
151
+ const withoutAccounts = savedRun(t, {});
152
+ writeFileSync(
153
+ join(withoutAccounts, "run.json"),
154
+ JSON.stringify({ ...RUN, dir: withoutAccounts, accounts: undefined }),
155
+ );
156
+ assert.throws(() => loadRun(withoutAccounts), /malformed/);
157
+ const noEmail = savedRun(t, { accounts: { admin: { password: "pw" } } });
158
+ assert.throws(() => loadRun(noEmail), /malformed/);
159
+ });
160
+
161
+ await test("a run keeps the renderer checkout it serves across a save and a load", (t) => {
162
+ const dir = savedRun(t, { renderer: "/work/dms-frontend" });
163
+ const state = loadRun(dir);
164
+ assert.equal(state.renderer, "/work/dms-frontend");
165
+ saveRun(state);
166
+ assert.equal(loadRun(dir).renderer, "/work/dms-frontend");
167
+ });
168
+
169
+ await test("a run whose renderer is not a path is refused", (t) => {
170
+ assert.throws(() => loadRun(savedRun(t, { renderer: 3 })), /malformed/);
171
+ });