@lowdefy/server-dev 5.6.0 → 6.0.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 (214) hide show
  1. package/{pages/_app.js → client/App.jsx} +21 -33
  2. package/client/DevStreamContext.js +30 -0
  3. package/client/Inspector.jsx +226 -0
  4. package/{lib/client/Page.js → client/Page.jsx} +4 -8
  5. package/client/Reload.jsx +85 -0
  6. package/client/Routing.jsx +117 -0
  7. package/client/feedback/FeedbackMount.jsx +83 -0
  8. package/client/feedback/FeedbackOverlay.jsx +901 -0
  9. package/client/feedback/captureTabScreenshot.js +84 -0
  10. package/client/feedback/copyToClipboard.js +45 -0
  11. package/client/feedback/drawAnnotationsSvg.js +119 -0
  12. package/client/feedback/elementInspect.js +99 -0
  13. package/client/feedback/feedbackStyles.js +301 -0
  14. package/client/feedback/pinResponsiveImages.js +59 -0
  15. package/client/feedback/sendFeedback.js +39 -0
  16. package/client/feedback/useFeedbackToggle.js +70 -0
  17. package/client/main.jsx +42 -0
  18. package/client/openInEditor/OpenInEditorListener.jsx +268 -0
  19. package/client/openInEditor/findBlockLocation.js +51 -0
  20. package/lib/build/app.js +8 -1
  21. package/lib/build/appMeta.js +8 -1
  22. package/lib/build/auth.js +8 -1
  23. package/lib/build/config.js +8 -1
  24. package/lib/build/i18n.js +8 -1
  25. package/lib/build/logger.js +8 -1
  26. package/lib/build/theme.js +8 -1
  27. package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
  28. package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
  29. package/lib/client/setPageId.js +6 -5
  30. package/lib/client/utils/useMutateCache.js +14 -1
  31. package/lib/client/utils/usePageConfig.js +45 -43
  32. package/lib/client/utils/usePageConfig.test.mjs +62 -0
  33. package/lib/docs/captureAnnotatedScreenshot.js +88 -0
  34. package/lib/docs/checkpointPaths.js +91 -0
  35. package/lib/docs/checkpointStore.js +240 -0
  36. package/lib/docs/checkpointToMocks.js +65 -0
  37. package/lib/docs/clientErrorStore.js +36 -0
  38. package/lib/docs/configCheckpoints.test.mjs +156 -0
  39. package/lib/docs/createConfigCheckpoint.js +92 -0
  40. package/lib/docs/createDocsMcpServer.js +531 -0
  41. package/lib/docs/createDocsMcpServer.test.mjs +141 -0
  42. package/lib/docs/devMockRegistry.js +68 -0
  43. package/lib/docs/docs.test.mjs +448 -0
  44. package/lib/docs/enrichFeedback.js +71 -0
  45. package/lib/docs/evalOperator.js +47 -0
  46. package/lib/docs/evalOperator.test.mjs +157 -0
  47. package/lib/docs/evalOperatorHeadless.js +87 -0
  48. package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
  49. package/lib/docs/evalOperatorInTab.js +48 -0
  50. package/lib/docs/findConfig.js +260 -0
  51. package/lib/docs/formatFeedback.js +121 -0
  52. package/lib/docs/formatFeedback.test.mjs +204 -0
  53. package/lib/docs/getAppMap.js +180 -0
  54. package/lib/docs/getBrowser.js +127 -0
  55. package/lib/docs/getBrowser.test.mjs +122 -0
  56. package/lib/docs/getBuildStatus.js +37 -0
  57. package/lib/docs/getCoreDoc.js +63 -0
  58. package/lib/docs/getDocsManifest.js +40 -0
  59. package/lib/docs/getExamples.js +58 -0
  60. package/lib/docs/getOverview.js +115 -0
  61. package/lib/docs/getPageConfig.js +53 -0
  62. package/lib/docs/getPluginDoc.js +55 -0
  63. package/lib/docs/getSchema.js +65 -0
  64. package/lib/docs/inspectState.js +47 -0
  65. package/lib/docs/inspectState.test.mjs +140 -0
  66. package/lib/docs/inspectStateFromTab.js +40 -0
  67. package/lib/docs/inspectStateHeadless.js +84 -0
  68. package/lib/docs/inspectStateHeadless.test.mjs +60 -0
  69. package/lib/docs/isWriteRequestsAllowed.js +50 -0
  70. package/lib/docs/listConfigCheckpoints.js +55 -0
  71. package/lib/docs/listPlugins.js +80 -0
  72. package/lib/docs/listTypes.js +105 -0
  73. package/lib/docs/loadState.js +152 -0
  74. package/lib/docs/mapPageBuildErrors.js +28 -0
  75. package/lib/docs/normalizeTypeKind.js +40 -0
  76. package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
  77. package/lib/docs/readPageArtifact.js +26 -0
  78. package/lib/docs/resolvePluginDir.js +49 -0
  79. package/lib/docs/resolveSource.js +48 -0
  80. package/lib/docs/resolveSource.test.mjs +100 -0
  81. package/lib/docs/revertConfigCheckpoint.js +73 -0
  82. package/lib/docs/runRequest.js +141 -0
  83. package/lib/docs/saveAnnotatedScreenshot.js +43 -0
  84. package/lib/docs/scaffoldPage.js +75 -0
  85. package/lib/docs/scaffoldPage.test.mjs +97 -0
  86. package/lib/docs/screenshotPage.js +110 -0
  87. package/lib/docs/screenshotPage.test.mjs +59 -0
  88. package/lib/docs/screenshotPageClip.test.mjs +102 -0
  89. package/lib/docs/searchDocs.js +67 -0
  90. package/lib/docs/setupTestFixtures.mjs +335 -0
  91. package/lib/docs/snapshotState.js +59 -0
  92. package/lib/docs/stateCheckpoints.test.mjs +366 -0
  93. package/lib/docs/tabAvailable.js +28 -0
  94. package/lib/docs/tabChannel.js +119 -0
  95. package/lib/docs/tabChannel.test.mjs +130 -0
  96. package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
  97. package/lib/server/auth/getDevSession.js +70 -0
  98. package/lib/server/auth/getDevSession.test.mjs +138 -0
  99. package/lib/server/auth/getHeadlessUser.js +40 -0
  100. package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
  101. package/lib/server/auth/headlessUser.js +29 -0
  102. package/lib/server/auth/resolveHeadlessUser.js +38 -0
  103. package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
  104. package/lib/server/auth/session.js +39 -0
  105. package/lib/server/auth/strategies.js +47 -0
  106. package/lib/server/createLowdefyContext.js +122 -0
  107. package/lib/server/getPageJitEnrichment.test.mjs +105 -0
  108. package/lib/server/jitPageBuilder.js +125 -12
  109. package/lib/server/log/createHandleError.test.mjs +126 -0
  110. package/manager/getContext.mjs +3 -7
  111. package/manager/processes/initialBuild.mjs +0 -2
  112. package/manager/processes/installPlugins.mjs +19 -11
  113. package/manager/processes/lowdefyBuild.mjs +29 -7
  114. package/manager/processes/lowdefyBuild.test.mjs +102 -0
  115. package/manager/processes/readDotEnv.mjs +13 -1
  116. package/manager/processes/shutdownServer.mjs +6 -6
  117. package/manager/processes/startProxy.mjs +160 -0
  118. package/manager/processes/startProxy.test.mjs +102 -0
  119. package/manager/processes/startServer.mjs +33 -16
  120. package/manager/processes/startWatchers.mjs +2 -2
  121. package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
  122. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
  123. package/manager/run.mjs +50 -0
  124. package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
  125. package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
  126. package/manager/utils/formatNoticeBox.mjs +30 -0
  127. package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
  128. package/manager/utils/updatePageTailwindCss.mjs +7 -1
  129. package/manager/utils/writeBuildStatus.mjs +39 -0
  130. package/manager/utils/writeBuildStatus.test.mjs +76 -0
  131. package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
  132. package/manager/watchers/moduleBuildWatcher.mjs +0 -1
  133. package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
  134. package/package.json +61 -47
  135. package/package.original.json +64 -50
  136. package/postcss.config.cjs +1 -0
  137. package/src/app.js +175 -0
  138. package/src/html/renderDevPage.js +86 -0
  139. package/src/lib/getPathSegments.js +25 -0
  140. package/src/lib/safeScriptJson.js +38 -0
  141. package/src/middleware/apiContext.js +32 -0
  142. package/src/middleware/errorHandler.js +48 -0
  143. package/src/middleware/errorHandler.test.mjs +235 -0
  144. package/src/routes/agent.js +64 -0
  145. package/src/routes/apiPage.js +37 -0
  146. package/src/routes/auth.js +49 -0
  147. package/src/routes/auth.test.mjs +84 -0
  148. package/src/routes/clientError.js +57 -0
  149. package/src/routes/cron.js +47 -0
  150. package/src/routes/cronForward.js +47 -0
  151. package/src/routes/detached.js +47 -0
  152. package/src/routes/devInspect.js +111 -0
  153. package/src/routes/devInspect.test.mjs +119 -0
  154. package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
  155. package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
  156. package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
  157. package/src/routes/docs/checkpointsCreate.js +28 -0
  158. package/src/routes/docs/checkpointsList.js +23 -0
  159. package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
  160. package/src/routes/docs/content.js +31 -0
  161. package/src/routes/docs/evalOperator.js +56 -0
  162. package/src/routes/docs/examples.js +33 -0
  163. package/src/routes/docs/find.js +30 -0
  164. package/src/routes/docs/index.js +23 -0
  165. package/src/routes/docs/inspectState.js +44 -0
  166. package/src/routes/docs/loadState.js +49 -0
  167. package/src/routes/docs/mcp.js +30 -0
  168. package/src/routes/docs/pageConfig.js +36 -0
  169. package/src/routes/docs/parseUserParam.js +52 -0
  170. package/src/routes/docs/parseUserParam.test.mjs +45 -0
  171. package/src/routes/docs/pluginDoc.js +33 -0
  172. package/src/routes/docs/plugins.js +23 -0
  173. package/src/routes/docs/runRequest.js +36 -0
  174. package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
  175. package/src/routes/docs/screenshot.js +72 -0
  176. package/src/routes/docs/search.js +27 -0
  177. package/src/routes/docs/snapshotState.js +36 -0
  178. package/src/routes/docs/stateCheckpointsList.js +26 -0
  179. package/src/routes/docs/types.js +33 -0
  180. package/src/routes/endpoints.js +71 -0
  181. package/src/routes/feedback.js +110 -0
  182. package/src/routes/feedback.test.mjs +204 -0
  183. package/src/routes/jitPage.js +86 -0
  184. package/src/routes/mcp.js +34 -0
  185. package/{pages/api → src/routes}/ping.js +4 -2
  186. package/src/routes/reload.js +79 -0
  187. package/src/routes/request.js +74 -0
  188. package/src/routes/request.test.mjs +124 -0
  189. package/{pages/api → src/routes}/root.js +4 -4
  190. package/src/routes/usage.js +46 -0
  191. package/src/routes/websocket.js +33 -0
  192. package/src/websocket/devWebSocket.js +78 -0
  193. package/vite.config.js +111 -0
  194. package/.eslintrc.yaml +0 -1
  195. package/lib/client/App.js +0 -80
  196. package/lib/client/Reload.js +0 -60
  197. package/lib/server/apiWrapper.js +0 -109
  198. package/lib/server/compileCss.js +0 -39
  199. package/manager/processes/compileCss.mjs +0 -54
  200. package/manager/processes/nextBuild.mjs +0 -50
  201. package/next.config.js +0 -25
  202. package/pages/_document.js +0 -109
  203. package/pages/api/agent/[...path].js +0 -86
  204. package/pages/api/auth/[...nextauth].js +0 -48
  205. package/pages/api/client-error.js +0 -45
  206. package/pages/api/js/[env].js +0 -42
  207. package/pages/api/page/[...pageId].js +0 -74
  208. package/pages/api/reload.js +0 -53
  209. package/pages/api/request/[...path].js +0 -41
  210. /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
  211. /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
  212. /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
  213. /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
  214. /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
@@ -0,0 +1,240 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import { isReserved, serializer, setKey, type } from '@lowdefy/helpers';
20
+
21
+ import { getConfigDirectory } from './checkpointPaths.js';
22
+
23
+ // State checkpoints (page state + recorded request/api responses) live under
24
+ // <configDir>/.lowdefy/state-checkpoints/<name>/ — inside .lowdefy because a
25
+ // checkpoint captures the signed-in user's data and live backend responses,
26
+ // which must not sit in a committable spot by default (.lowdefy is gitignored
27
+ // by convention and ignored by the dev file watcher). A sibling of
28
+ // .lowdefy/checkpoints (the config-directory-snapshot checkpoints in
29
+ // checkpointPaths.js/createConfigCheckpoint.js) rather than a subdirectory,
30
+ // because that store prunes its oldest directories beyond a cap. These two
31
+ // "checkpoint" concepts are unrelated: this one is for putting the *running
32
+ // app* into a specific state (agent verification, human testing, e2e test
33
+ // generation), not for reverting config files.
34
+ const NAME_PATTERN = /^[a-z0-9-_]+$/i;
35
+
36
+ function getStateCheckpointsRoot() {
37
+ return path.join(getConfigDirectory(), '.lowdefy', 'state-checkpoints');
38
+ }
39
+
40
+ function validateName(name) {
41
+ if (type.isNone(name) || !type.isString(name)) {
42
+ throw new Error(`Checkpoint "name" must be a string. Received ${JSON.stringify(name)}.`);
43
+ }
44
+ if (!NAME_PATTERN.test(name)) {
45
+ throw new Error(
46
+ `Checkpoint name "${name}" is invalid — use only letters, numbers, "-" and "_".`
47
+ );
48
+ }
49
+ }
50
+
51
+ function writeJsonFile(filePath, value) {
52
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
+ fs.writeFileSync(filePath, JSON.stringify(serializer.serialize(value), null, 2));
54
+ }
55
+
56
+ function readJsonFile(filePath, defaultValue) {
57
+ if (!fs.existsSync(filePath)) {
58
+ return defaultValue;
59
+ }
60
+ return serializer.deserialize(JSON.parse(fs.readFileSync(filePath, 'utf8')));
61
+ }
62
+
63
+ // Requests/api responses arrive as either an array of entries (newest first —
64
+ // the shape of context.requests in both Requests.js and Inspector.jsx's
65
+ // buildSnapshot) or a single entry object. Either way only the newest
66
+ // recording is kept per id.
67
+ function newestEntry(value) {
68
+ return Array.isArray(value) ? value[0] : value;
69
+ }
70
+
71
+ function writeEntries({ dir, subdir, entries, parts }) {
72
+ Object.entries(entries ?? {}).forEach(([id, value]) => {
73
+ const entry = newestEntry(value);
74
+ if (type.isNone(entry)) {
75
+ return;
76
+ }
77
+ const { payload, response, error, responseTime } = entry;
78
+ writeJsonFile(path.join(dir, subdir, `${id}.json`), { payload, response, error, responseTime });
79
+ parts.push(`${subdir}/${id}.json`);
80
+ });
81
+ }
82
+
83
+ function readEntries({ dir, subdir }) {
84
+ const entriesDir = path.join(dir, subdir);
85
+ const entries = {};
86
+ if (!fs.existsSync(entriesDir)) {
87
+ return entries;
88
+ }
89
+ fs.readdirSync(entriesDir)
90
+ .filter((fileName) => fileName.endsWith('.json'))
91
+ .forEach((fileName) => {
92
+ const id = fileName.slice(0, -'.json'.length);
93
+ // A checkpoint directory is on-disk state a developer can edit by hand.
94
+ // A reserved file name can't round-trip through the entries map, so skip
95
+ // it rather than fail the whole checkpoint load.
96
+ if (isReserved(id)) {
97
+ return;
98
+ }
99
+ setKey(entries, id, readJsonFile(path.join(entriesDir, fileName), null));
100
+ });
101
+ return entries;
102
+ }
103
+
104
+ // Snapshots a running page's state + recorded request/api responses into a
105
+ // checkpoint folder. Refuses to clobber an existing checkpoint unless
106
+ // `overwrite: true` — when overwriting, the old folder is removed first so a
107
+ // checkpoint written with fewer requests than a previous one doesn't leave
108
+ // stale per-request files behind.
109
+ function writeCheckpoint({ name, snapshot, notes, overwrite }) {
110
+ validateName(name);
111
+ if (type.isNone(snapshot) || !type.isObject(snapshot)) {
112
+ throw new Error(
113
+ `writeCheckpoint requires a "snapshot" object. Received ${JSON.stringify(snapshot)}.`
114
+ );
115
+ }
116
+
117
+ const dir = path.join(getStateCheckpointsRoot(), name);
118
+ const exists = fs.existsSync(dir);
119
+ if (exists && overwrite !== true) {
120
+ throw new Error(`Checkpoint "${name}" already exists. Pass overwrite: true to replace it.`);
121
+ }
122
+ if (exists) {
123
+ fs.rmSync(dir, { recursive: true, force: true });
124
+ }
125
+ fs.mkdirSync(dir, { recursive: true });
126
+
127
+ const parts = [];
128
+ const manifest = {
129
+ name,
130
+ pageId: snapshot.pageId ?? null,
131
+ url: snapshot.url ?? null,
132
+ capturedAt: new Date().toISOString(),
133
+ notes: notes ?? null,
134
+ };
135
+ writeJsonFile(path.join(dir, 'checkpoint.json'), manifest);
136
+ parts.push('checkpoint.json');
137
+
138
+ writeJsonFile(path.join(dir, 'state.json'), snapshot.state ?? {});
139
+ parts.push('state.json');
140
+ writeJsonFile(path.join(dir, 'urlQuery.json'), snapshot.urlQuery ?? '');
141
+ parts.push('urlQuery.json');
142
+ writeJsonFile(path.join(dir, 'inputs.json'), snapshot.input ?? {});
143
+ parts.push('inputs.json');
144
+ writeJsonFile(path.join(dir, 'user.json'), snapshot.user ?? null);
145
+ parts.push('user.json');
146
+ writeJsonFile(path.join(dir, 'global.json'), snapshot.global ?? {});
147
+ parts.push('global.json');
148
+
149
+ writeEntries({ dir, subdir: 'requests', entries: snapshot.requests, parts });
150
+ if (!type.isNone(snapshot.apiResponses)) {
151
+ writeEntries({ dir, subdir: 'api', entries: snapshot.apiResponses, parts });
152
+ }
153
+
154
+ return { dir, parts };
155
+ }
156
+
157
+ // Reads a checkpoint back, tolerant of individually missing parts (a
158
+ // developer may hand-edit or delete files in a committed checkpoint folder) —
159
+ // only a missing checkpoint directory itself is an error.
160
+ function readCheckpoint({ name }) {
161
+ validateName(name);
162
+ const dir = path.join(getStateCheckpointsRoot(), name);
163
+ if (!fs.existsSync(dir)) {
164
+ throw new Error(`Checkpoint "${name}" not found.`);
165
+ }
166
+
167
+ const checkpoint = readJsonFile(path.join(dir, 'checkpoint.json'), {
168
+ name,
169
+ pageId: null,
170
+ url: null,
171
+ capturedAt: null,
172
+ notes: null,
173
+ });
174
+
175
+ return {
176
+ name,
177
+ checkpoint,
178
+ state: readJsonFile(path.join(dir, 'state.json'), {}),
179
+ urlQuery: readJsonFile(path.join(dir, 'urlQuery.json'), ''),
180
+ input: readJsonFile(path.join(dir, 'inputs.json'), {}),
181
+ user: readJsonFile(path.join(dir, 'user.json'), null),
182
+ global: readJsonFile(path.join(dir, 'global.json'), {}),
183
+ requests: readEntries({ dir, subdir: 'requests' }),
184
+ api: readEntries({ dir, subdir: 'api' }),
185
+ };
186
+ }
187
+
188
+ function checkpointExists({ name }) {
189
+ validateName(name);
190
+ return fs.existsSync(path.join(getStateCheckpointsRoot(), name));
191
+ }
192
+
193
+ // Lists stored state checkpoints, most recent first. Corrupted/hand-edited
194
+ // checkpoints without a manifest still show up (with null fields) rather
195
+ // than being silently hidden, since these folders are meant to be
196
+ // human-editable.
197
+ function listStateCheckpoints() {
198
+ const root = getStateCheckpointsRoot();
199
+ if (!fs.existsSync(root)) {
200
+ return [];
201
+ }
202
+
203
+ const checkpoints = fs
204
+ .readdirSync(root, { withFileTypes: true })
205
+ .filter((entry) => entry.isDirectory())
206
+ .map((entry) => {
207
+ const dir = path.join(root, entry.name);
208
+ const manifest = readJsonFile(path.join(dir, 'checkpoint.json'), {
209
+ name: entry.name,
210
+ pageId: null,
211
+ capturedAt: null,
212
+ notes: null,
213
+ });
214
+ const requestsDir = path.join(dir, 'requests');
215
+ const requestCount = fs.existsSync(requestsDir)
216
+ ? fs.readdirSync(requestsDir).filter((fileName) => fileName.endsWith('.json')).length
217
+ : 0;
218
+ return {
219
+ name: entry.name,
220
+ pageId: manifest.pageId ?? null,
221
+ capturedAt: manifest.capturedAt ?? null,
222
+ notes: manifest.notes ?? null,
223
+ requestCount,
224
+ };
225
+ });
226
+
227
+ return checkpoints.sort((a, b) => {
228
+ if (type.isNone(a.capturedAt)) return 1;
229
+ if (type.isNone(b.capturedAt)) return -1;
230
+ return a.capturedAt < b.capturedAt ? 1 : -1;
231
+ });
232
+ }
233
+
234
+ export {
235
+ checkpointExists,
236
+ getStateCheckpointsRoot,
237
+ listStateCheckpoints,
238
+ readCheckpoint,
239
+ writeCheckpoint,
240
+ };
@@ -0,0 +1,65 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import YAML from 'yaml';
18
+
19
+ import { readCheckpoint } from './checkpointStore.js';
20
+
21
+ // Errors can't round-trip through YAML as Error instances (and don't need
22
+ // to) — @lowdefy/e2e-utils' createMockManager accepts a plain string and
23
+ // wraps it itself (`error instanceof Error ? error : new Error(error)`), so
24
+ // only the message is kept here.
25
+ function toMockError(error) {
26
+ if (!error) {
27
+ return undefined;
28
+ }
29
+ return error.message ?? String(error);
30
+ }
31
+
32
+ function toMockEntries(entries, idKey, extraFields = {}) {
33
+ return Object.entries(entries ?? {}).map(([id, entry]) => {
34
+ const error = toMockError(entry?.error);
35
+ const base = { [idKey]: id, ...extraFields };
36
+ if (error) {
37
+ return { ...base, error };
38
+ }
39
+ return { ...base, response: entry?.response ?? null };
40
+ });
41
+ }
42
+
43
+ // Converts a state checkpoint's recorded requests/api responses into the
44
+ // exact shape @lowdefy/e2e-utils' loadStaticMocks/createMockManager expect
45
+ // (`{ requests: [...], api: [...] }`), so a checkpoint recorded from a live
46
+ // dev session can seed a future AI-generated (or hand-written) e2e test's
47
+ // mocks file directly.
48
+ function checkpointToMocks({ name }) {
49
+ let checkpoint;
50
+ try {
51
+ checkpoint = readCheckpoint({ name });
52
+ } catch (error) {
53
+ return { error: error.message };
54
+ }
55
+
56
+ const pageId = checkpoint.checkpoint.pageId;
57
+ const requests = toMockEntries(checkpoint.requests, 'requestId', { pageId });
58
+ const api = toMockEntries(checkpoint.api, 'endpointId');
59
+
60
+ const yamlContent = YAML.stringify({ requests, api });
61
+
62
+ return { requests, api, yaml: yamlContent };
63
+ }
64
+
65
+ export default checkpointToMocks;
@@ -0,0 +1,36 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // Module-level ring buffer of recent client-reported errors — feeds the
18
+ // getBuildStatus feedback endpoint so agents can see browser errors without
19
+ // tailing server logs. Deliberately in-memory only: entries are lost on
20
+ // server restart, which is fine since this is a live-session debugging aid.
21
+ const MAX_ENTRIES = 50;
22
+
23
+ const entries = [];
24
+
25
+ function push(entry) {
26
+ entries.push(entry);
27
+ if (entries.length > MAX_ENTRIES) {
28
+ entries.shift();
29
+ }
30
+ }
31
+
32
+ function list() {
33
+ return [...entries];
34
+ }
35
+
36
+ export default { push, list };
@@ -0,0 +1,156 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import os from 'os';
19
+ import path from 'path';
20
+
21
+ import { MAX_CHECKPOINTS } from './checkpointPaths.js';
22
+ import createConfigCheckpoint from './createConfigCheckpoint.js';
23
+ import listConfigCheckpoints from './listConfigCheckpoints.js';
24
+ import revertConfigCheckpoint from './revertConfigCheckpoint.js';
25
+
26
+ let configDirectory;
27
+ let previousConfigDirectory;
28
+ let previousCwd;
29
+
30
+ function writeFile(relativePath, content) {
31
+ const filePath = path.join(configDirectory, relativePath);
32
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
33
+ fs.writeFileSync(filePath, content);
34
+ }
35
+
36
+ function readFile(relativePath) {
37
+ return fs.readFileSync(path.join(configDirectory, relativePath), 'utf8');
38
+ }
39
+
40
+ function exists(relativePath) {
41
+ return fs.existsSync(path.join(configDirectory, relativePath));
42
+ }
43
+
44
+ // Checkpoint ids are timestamp-prefixed (see createConfigCheckpoint.js), so
45
+ // tests asserting ordering/eviction need genuinely distinct timestamps
46
+ // between calls. A real (short) delay is used instead of mocking the clock —
47
+ // this Node/Jest combination's fake timers cannot redefine `performance`.
48
+ function sleep(ms) {
49
+ return new Promise((resolve) => setTimeout(resolve, ms));
50
+ }
51
+
52
+ beforeEach(() => {
53
+ configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-config-checkpoints-test-'));
54
+ previousConfigDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG;
55
+ process.env.LOWDEFY_DIRECTORY_CONFIG = configDirectory;
56
+ previousCwd = process.cwd();
57
+ process.chdir(configDirectory);
58
+
59
+ writeFile('lowdefy.yaml', 'name: my-app\n');
60
+ writeFile(path.join('pages', 'home.yaml'), 'id: home\n');
61
+ writeFile('.env', 'SECRET=abc\n');
62
+ writeFile('README.md', '# My App\n');
63
+ // Should never be snapshotted.
64
+ writeFile(path.join('node_modules', 'some-dep', 'index.js'), 'module.exports = {};\n');
65
+ writeFile(path.join('.git', 'HEAD'), 'ref: refs/heads/main\n');
66
+ });
67
+
68
+ afterEach(() => {
69
+ process.chdir(previousCwd);
70
+ fs.rmSync(configDirectory, { recursive: true, force: true });
71
+ if (previousConfigDirectory === undefined) {
72
+ delete process.env.LOWDEFY_DIRECTORY_CONFIG;
73
+ } else {
74
+ process.env.LOWDEFY_DIRECTORY_CONFIG = previousConfigDirectory;
75
+ }
76
+ });
77
+
78
+ test('createConfigCheckpoint snapshots config-relevant files and excludes build/vcs/dependency directories', () => {
79
+ const result = createConfigCheckpoint({ label: 'before change' });
80
+
81
+ expect(result.fileCount).toBe(4);
82
+ expect(result.id).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-before-change$/);
83
+
84
+ const snapshotDirectory = path.join(configDirectory, '.lowdefy', 'checkpoints', result.id);
85
+ const manifest = JSON.parse(
86
+ fs.readFileSync(path.join(snapshotDirectory, 'manifest.json'), 'utf8')
87
+ );
88
+ expect(manifest.files.sort()).toEqual(
89
+ ['lowdefy.yaml', path.join('pages', 'home.yaml'), '.env', 'README.md'].sort()
90
+ );
91
+
92
+ expect(fs.readFileSync(path.join(snapshotDirectory, 'lowdefy.yaml'), 'utf8')).toBe(
93
+ 'name: my-app\n'
94
+ );
95
+ expect(fs.existsSync(path.join(snapshotDirectory, 'node_modules'))).toBe(false);
96
+ expect(fs.existsSync(path.join(snapshotDirectory, '.git'))).toBe(false);
97
+ });
98
+
99
+ test('listConfigCheckpoints returns an empty array when no checkpoints exist', () => {
100
+ expect(listConfigCheckpoints()).toEqual([]);
101
+ });
102
+
103
+ test('listConfigCheckpoints lists checkpoints most recent first', async () => {
104
+ const first = createConfigCheckpoint({ label: 'first' });
105
+ await sleep(5);
106
+ const second = createConfigCheckpoint({ label: 'second' });
107
+
108
+ const checkpoints = listConfigCheckpoints();
109
+ expect(checkpoints).toEqual([
110
+ { id: second.id, label: 'second', createdAt: expect.any(String), fileCount: 4 },
111
+ { id: first.id, label: 'first', createdAt: expect.any(String), fileCount: 4 },
112
+ ]);
113
+ });
114
+
115
+ test('revertConfigCheckpoint restores modified and deleted files, and deletes files added since the checkpoint', () => {
116
+ const { id } = createConfigCheckpoint({ label: 'snapshot' });
117
+
118
+ // Modify an existing file.
119
+ writeFile('lowdefy.yaml', 'name: modified-app\n');
120
+ // Delete a file that was in the checkpoint.
121
+ fs.rmSync(path.join(configDirectory, 'pages', 'home.yaml'));
122
+ // Add a new config-relevant file that was not in the checkpoint.
123
+ writeFile(path.join('pages', 'extra.yaml'), 'id: extra\n');
124
+
125
+ const result = revertConfigCheckpoint({ id });
126
+
127
+ expect(readFile('lowdefy.yaml')).toBe('name: my-app\n');
128
+ expect(exists(path.join('pages', 'home.yaml'))).toBe(true);
129
+ expect(readFile(path.join('pages', 'home.yaml'))).toBe('id: home\n');
130
+ expect(exists(path.join('pages', 'extra.yaml'))).toBe(false);
131
+
132
+ expect(result.restored.sort()).toEqual(
133
+ ['lowdefy.yaml', path.join('pages', 'home.yaml'), '.env', 'README.md'].sort()
134
+ );
135
+ expect(result.deleted).toEqual([path.join('pages', 'extra.yaml')]);
136
+ });
137
+
138
+ test('revertConfigCheckpoint throws an actionable error when the checkpoint id does not exist', () => {
139
+ expect(() => revertConfigCheckpoint({ id: 'does-not-exist' })).toThrow(
140
+ 'Config checkpoint "does-not-exist" not found.'
141
+ );
142
+ });
143
+
144
+ test('createConfigCheckpoint evicts the oldest checkpoint once more than the cap are stored', async () => {
145
+ for (let i = 0; i < MAX_CHECKPOINTS + 1; i += 1) {
146
+ createConfigCheckpoint({ label: `checkpoint-${i}` });
147
+ await sleep(2);
148
+ }
149
+
150
+ const checkpoints = listConfigCheckpoints();
151
+ expect(checkpoints).toHaveLength(MAX_CHECKPOINTS);
152
+ expect(checkpoints.some((checkpoint) => checkpoint.label === 'checkpoint-0')).toBe(false);
153
+ expect(
154
+ checkpoints.some((checkpoint) => checkpoint.label === `checkpoint-${MAX_CHECKPOINTS}`)
155
+ ).toBe(true);
156
+ });
@@ -0,0 +1,92 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+
20
+ import {
21
+ getCheckpointsRoot,
22
+ getConfigDirectory,
23
+ listConfigFiles,
24
+ MANIFEST_FILE_NAME,
25
+ MAX_CHECKPOINTS,
26
+ slug,
27
+ } from './checkpointPaths.js';
28
+
29
+ // Checkpoint ids sort chronologically as plain strings (ISO timestamp
30
+ // prefix), which is what listConfigCheckpoints and the checkpoint cap rely
31
+ // on — ":" and "." are replaced since they're awkward/unsafe in directory
32
+ // names on some filesystems.
33
+ function buildCheckpointId(label) {
34
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
35
+ return `${timestamp}-${slug(label)}`;
36
+ }
37
+
38
+ function copyIntoSnapshot({ configDirectory, snapshotDirectory, relativeFilePath }) {
39
+ const source = path.join(configDirectory, relativeFilePath);
40
+ const destination = path.join(snapshotDirectory, relativeFilePath);
41
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
42
+ fs.copyFileSync(source, destination);
43
+ }
44
+
45
+ // Deletes the oldest checkpoints once the stored count exceeds MAX_CHECKPOINTS.
46
+ // Checkpoint directory names sort chronologically, so a plain string sort
47
+ // puts the oldest first.
48
+ function enforceCheckpointCap({ checkpointsRoot }) {
49
+ const ids = fs
50
+ .readdirSync(checkpointsRoot, { withFileTypes: true })
51
+ .filter((entry) => entry.isDirectory())
52
+ .map((entry) => entry.name)
53
+ .sort();
54
+
55
+ const excessCount = ids.length - MAX_CHECKPOINTS;
56
+ if (excessCount <= 0) {
57
+ return;
58
+ }
59
+ ids.slice(0, excessCount).forEach((id) => {
60
+ fs.rmSync(path.join(checkpointsRoot, id), { recursive: true, force: true });
61
+ });
62
+ }
63
+
64
+ // Snapshots every config-relevant file in the config directory into
65
+ // <configDir>/.lowdefy/checkpoints/<id>/, alongside a manifest.json listing
66
+ // the files it holds — revertConfigCheckpoint uses that manifest to restore
67
+ // exactly, including deleting files that didn't exist at checkpoint time.
68
+ function createConfigCheckpoint({ label }) {
69
+ const configDirectory = getConfigDirectory();
70
+ const checkpointsRoot = getCheckpointsRoot();
71
+ const id = buildCheckpointId(label);
72
+ const snapshotDirectory = path.join(checkpointsRoot, id);
73
+
74
+ const files = listConfigFiles({ configDirectory });
75
+
76
+ fs.mkdirSync(snapshotDirectory, { recursive: true });
77
+ files.forEach((relativeFilePath) => {
78
+ copyIntoSnapshot({ configDirectory, snapshotDirectory, relativeFilePath });
79
+ });
80
+
81
+ const createdAt = new Date().toISOString();
82
+ fs.writeFileSync(
83
+ path.join(snapshotDirectory, MANIFEST_FILE_NAME),
84
+ JSON.stringify({ id, label: label ?? null, createdAt, files }, null, 2)
85
+ );
86
+
87
+ enforceCheckpointCap({ checkpointsRoot });
88
+
89
+ return { id, fileCount: files.length };
90
+ }
91
+
92
+ export default createConfigCheckpoint;