@lowdefy/server-dev 5.5.1 → 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.
- package/{pages/_app.js → client/App.jsx} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/{apiWrapper.js → createLowdefyContext.js} +63 -57
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.js +9 -0
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +61 -47
- package/package.original.json +64 -55
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +235 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -107
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
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 { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
// inspectState.js only chooses between the tab and headless sources - the
|
|
20
|
+
// sources' own behavior is covered by inspectStateHeadless.test.mjs and (once
|
|
21
|
+
// written) inspectStateFromTab's own tests. Mock both, plus tabAvailable, so
|
|
22
|
+
// only the fallback logic is under test here.
|
|
23
|
+
const mockInspectStateFromTab = jest.fn();
|
|
24
|
+
const mockInspectStateHeadless = jest.fn();
|
|
25
|
+
const mockTabAvailable = jest.fn();
|
|
26
|
+
|
|
27
|
+
jest.unstable_mockModule('./inspectStateFromTab.js', () => ({
|
|
28
|
+
default: mockInspectStateFromTab,
|
|
29
|
+
}));
|
|
30
|
+
jest.unstable_mockModule('./inspectStateHeadless.js', () => ({
|
|
31
|
+
default: mockInspectStateHeadless,
|
|
32
|
+
}));
|
|
33
|
+
jest.unstable_mockModule('./tabAvailable.js', () => ({
|
|
34
|
+
default: mockTabAvailable,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const { default: inspectState } = await import('./inspectState.js');
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
jest.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('inspectState uses the tab when source is "tab" and it succeeds', async () => {
|
|
44
|
+
mockInspectStateFromTab.mockResolvedValue({ state: { a: 1 } });
|
|
45
|
+
|
|
46
|
+
const result = await inspectState({
|
|
47
|
+
origin: 'http://localhost:3001',
|
|
48
|
+
pageId: 'home',
|
|
49
|
+
source: 'tab',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual({ state: { a: 1 }, source: 'tab' });
|
|
53
|
+
expect(mockInspectStateFromTab).toHaveBeenCalledWith({ pageId: 'home' });
|
|
54
|
+
expect(mockInspectStateHeadless).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('inspectState falls back to headless when source is "tab" but the tab errors', async () => {
|
|
58
|
+
mockInspectStateFromTab.mockResolvedValue({ error: 'No browser tab connected.' });
|
|
59
|
+
mockInspectStateHeadless.mockResolvedValue({ state: { a: 1 } });
|
|
60
|
+
|
|
61
|
+
const result = await inspectState({
|
|
62
|
+
origin: 'http://localhost:3001',
|
|
63
|
+
pageId: 'home',
|
|
64
|
+
source: 'tab',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual({ state: { a: 1 }, source: 'headless' });
|
|
68
|
+
expect(mockInspectStateHeadless).toHaveBeenCalledWith({
|
|
69
|
+
origin: 'http://localhost:3001',
|
|
70
|
+
pageId: 'home',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('inspectState goes straight to headless when source is "headless"', async () => {
|
|
75
|
+
mockInspectStateHeadless.mockResolvedValue({ state: { a: 1 } });
|
|
76
|
+
|
|
77
|
+
const result = await inspectState({
|
|
78
|
+
origin: 'http://localhost:3001',
|
|
79
|
+
pageId: 'home',
|
|
80
|
+
source: 'headless',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual({ state: { a: 1 }, source: 'headless' });
|
|
84
|
+
expect(mockTabAvailable).not.toHaveBeenCalled();
|
|
85
|
+
expect(mockInspectStateFromTab).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('inspectState prefers the tab when source is unset and a tab is available', async () => {
|
|
89
|
+
mockTabAvailable.mockReturnValue(true);
|
|
90
|
+
mockInspectStateFromTab.mockResolvedValue({ state: { a: 1 } });
|
|
91
|
+
|
|
92
|
+
const result = await inspectState({ origin: 'http://localhost:3001', pageId: 'home' });
|
|
93
|
+
|
|
94
|
+
expect(result).toEqual({ state: { a: 1 }, source: 'tab' });
|
|
95
|
+
expect(mockInspectStateHeadless).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('inspectState goes straight to headless when source is unset and no tab is available', async () => {
|
|
99
|
+
mockTabAvailable.mockReturnValue(false);
|
|
100
|
+
mockInspectStateHeadless.mockResolvedValue({ state: { a: 1 } });
|
|
101
|
+
|
|
102
|
+
const result = await inspectState({ origin: 'http://localhost:3001', pageId: 'home' });
|
|
103
|
+
|
|
104
|
+
expect(result).toEqual({ state: { a: 1 }, source: 'headless' });
|
|
105
|
+
expect(mockInspectStateFromTab).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('inspectState ignores an available tab and passes the user to headless when a user is given', async () => {
|
|
109
|
+
mockTabAvailable.mockReturnValue(true);
|
|
110
|
+
mockInspectStateHeadless.mockResolvedValue({ state: { a: 1 } });
|
|
111
|
+
|
|
112
|
+
const result = await inspectState({
|
|
113
|
+
origin: 'http://localhost:3001',
|
|
114
|
+
pageId: 'home',
|
|
115
|
+
user: { roles: ['admin'] },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result).toEqual({ state: { a: 1 }, source: 'headless' });
|
|
119
|
+
expect(mockInspectStateFromTab).not.toHaveBeenCalled();
|
|
120
|
+
expect(mockInspectStateHeadless).toHaveBeenCalledWith({
|
|
121
|
+
origin: 'http://localhost:3001',
|
|
122
|
+
pageId: 'home',
|
|
123
|
+
user: { roles: ['admin'] },
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('inspectState errors when a user is combined with source "tab"', async () => {
|
|
128
|
+
const result = await inspectState({
|
|
129
|
+
origin: 'http://localhost:3001',
|
|
130
|
+
pageId: 'home',
|
|
131
|
+
source: 'tab',
|
|
132
|
+
user: { roles: ['admin'] },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.error).toMatch(/cannot apply "user" to the developer's live tab/);
|
|
136
|
+
// Flagged as the caller's mistake so the HTTP route answers 400, not 502.
|
|
137
|
+
expect(result.invalidInput).toBe(true);
|
|
138
|
+
expect(mockInspectStateFromTab).not.toHaveBeenCalled();
|
|
139
|
+
expect(mockInspectStateHeadless).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { type, serializer } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import { requestFromTab } from './tabChannel.js';
|
|
20
|
+
|
|
21
|
+
// Reads the live state of a page open in a developer's real browser tab, via
|
|
22
|
+
// the SSE + POST /api/dev-inspect round trip set up in Inspector.jsx. Lets an
|
|
23
|
+
// agent inspect actual client state (not just what the build would produce).
|
|
24
|
+
//
|
|
25
|
+
// requestFromTab resolves directly with whatever the tab posted as `result`
|
|
26
|
+
// (see tabChannel.js) — a serialized snapshot string on success, or an
|
|
27
|
+
// `{ error }` object if no tab is connected, the request timed out, or
|
|
28
|
+
// Inspector.jsx itself failed to build the snapshot.
|
|
29
|
+
async function inspectStateFromTab({ pageId } = {}) {
|
|
30
|
+
const response = await requestFromTab({ pageId, event: 'inspect-request' });
|
|
31
|
+
if (response?.error) {
|
|
32
|
+
return { error: response.error };
|
|
33
|
+
}
|
|
34
|
+
if (!type.isString(response)) {
|
|
35
|
+
return { error: 'Browser tab responded with an unexpected payload.' };
|
|
36
|
+
}
|
|
37
|
+
return serializer.deserializeFromString(response);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default inspectStateFromTab;
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import { getBrowser, openPage, buildPageUrl } from './getBrowser.js';
|
|
20
|
+
|
|
21
|
+
// Collects a state snapshot from a headless Chromium tab navigated to the
|
|
22
|
+
// page's own route. Mirrors Inspector.jsx's buildSnapshot (the live-tab
|
|
23
|
+
// equivalent), but window.lowdefy does not expose the app's serializer, so
|
|
24
|
+
// the snapshot is round-tripped through JSON.parse(JSON.stringify(...))
|
|
25
|
+
// inside the page instead. That strips functions/undefined and turns Dates
|
|
26
|
+
// into plain ISO strings — good enough for agent inspection, just not a
|
|
27
|
+
// byte-for-byte match of the app's own `~d`-tagged serialization.
|
|
28
|
+
async function inspectStateHeadless({ origin, pageId, user, timeout = 15000 }) {
|
|
29
|
+
if (type.isNone(origin) || !type.isString(origin)) {
|
|
30
|
+
return {
|
|
31
|
+
error: `inspectStateHeadless requires an "origin" string. Received ${JSON.stringify(origin)}.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (type.isNone(pageId) || !type.isString(pageId)) {
|
|
35
|
+
return {
|
|
36
|
+
error: `inspectStateHeadless requires a "pageId" string. Received ${JSON.stringify(pageId)}.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let browser;
|
|
41
|
+
try {
|
|
42
|
+
browser = await getBrowser();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
error: `No Chromium available. Run: npx playwright install chromium (${error.message})`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const url = buildPageUrl({ origin, pageId });
|
|
50
|
+
|
|
51
|
+
let context;
|
|
52
|
+
try {
|
|
53
|
+
const opened = await openPage({ browser, origin, pageId, user, timeout });
|
|
54
|
+
context = opened.context;
|
|
55
|
+
const snapshot = await opened.page.evaluate((id) => {
|
|
56
|
+
const lowdefy = window.lowdefy;
|
|
57
|
+
const pageContext = lowdefy?.contexts?.[`page:${id}`];
|
|
58
|
+
if (!pageContext) {
|
|
59
|
+
return { error: `No live context for page "${id}".` };
|
|
60
|
+
}
|
|
61
|
+
return JSON.parse(
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
pageId: id,
|
|
64
|
+
state: pageContext.state,
|
|
65
|
+
requests: pageContext.requests,
|
|
66
|
+
eventLog: (pageContext.eventLog ?? []).slice(-50),
|
|
67
|
+
global: lowdefy.lowdefyGlobal,
|
|
68
|
+
user: lowdefy.user,
|
|
69
|
+
input: lowdefy.inputs?.[`page:${id}`],
|
|
70
|
+
urlQuery: window.location.search,
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}, pageId);
|
|
74
|
+
return snapshot;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { error: `Failed to inspect state at "${url}": ${error.message}` };
|
|
77
|
+
} finally {
|
|
78
|
+
if (context) {
|
|
79
|
+
await context.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default inspectStateHeadless;
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
import { jest } from '@jest/globals';
|
|
21
|
+
|
|
22
|
+
// A real headless inspection needs an installed browser, which jest can't
|
|
23
|
+
// rely on, so this only covers the error paths: bad input, and
|
|
24
|
+
// chromium.launch failing (the "no browser installed" case). Mirrors
|
|
25
|
+
// screenshotPage.test.mjs, which exercises the same getBrowser.js singleton.
|
|
26
|
+
jest.unstable_mockModule('playwright-core', () => ({
|
|
27
|
+
chromium: { launch: jest.fn().mockRejectedValue(new Error("Executable doesn't exist")) },
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// lib/build/config.js reads build/config.json from process.cwd() at import
|
|
31
|
+
// time — chdir into a fixture that has one before getBrowser.js (which
|
|
32
|
+
// imports it transitively) is loaded. Mirrors the pattern in
|
|
33
|
+
// screenshotPage.test.mjs.
|
|
34
|
+
const originalCwd = process.cwd();
|
|
35
|
+
const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-inspect-state-test-'));
|
|
36
|
+
fs.mkdirSync(path.join(fixtureDir, 'build'), { recursive: true });
|
|
37
|
+
fs.writeFileSync(path.join(fixtureDir, 'build', 'config.json'), JSON.stringify({ basePath: '' }));
|
|
38
|
+
process.chdir(fixtureDir);
|
|
39
|
+
|
|
40
|
+
const { default: inspectStateHeadless } = await import('./inspectStateHeadless.js');
|
|
41
|
+
|
|
42
|
+
afterAll(() => {
|
|
43
|
+
process.chdir(originalCwd);
|
|
44
|
+
fs.rmSync(fixtureDir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('inspectStateHeadless returns an error when origin is missing', async () => {
|
|
48
|
+
const result = await inspectStateHeadless({ pageId: 'home' });
|
|
49
|
+
expect(result.error).toMatch(/requires an "origin" string/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('inspectStateHeadless returns an error when pageId is missing', async () => {
|
|
53
|
+
const result = await inspectStateHeadless({ origin: 'http://localhost:3001' });
|
|
54
|
+
expect(result.error).toMatch(/requires a "pageId" string/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('inspectStateHeadless returns an actionable error when no browser is available', async () => {
|
|
58
|
+
const result = await inspectStateHeadless({ origin: 'http://localhost:3001', pageId: 'home' });
|
|
59
|
+
expect(result.error).toMatch(/No Chromium available. Run: npx playwright install chromium/);
|
|
60
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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 path from 'node:path';
|
|
18
|
+
import { get, type } from '@lowdefy/helpers';
|
|
19
|
+
import { readFile } from '@lowdefy/node-utils';
|
|
20
|
+
import YAML from 'yaml';
|
|
21
|
+
|
|
22
|
+
// The top-level "cli" key in lowdefy.yaml never reaches a build artifact:
|
|
23
|
+
// the build's root schema (packages/build/src/lowdefySchema.js) accepts an
|
|
24
|
+
// arbitrary "cli" object, but no builder copies it onto `components`, so it
|
|
25
|
+
// is never written to build/config.json (which only ever holds
|
|
26
|
+
// components.config — see packages/build/src/build/writeConfig.js). The
|
|
27
|
+
// Lowdefy CLI itself reads "cli" straight from lowdefy.yaml rather than from
|
|
28
|
+
// a build artifact (packages/cli/src/utils/getLowdefyYaml.js) — this mirrors
|
|
29
|
+
// that precedent instead of inventing a new build artifact for one dev-only
|
|
30
|
+
// opt-in flag. Read fresh (no caching) so toggling the flag in lowdefy.yaml
|
|
31
|
+
// takes effect without a server restart.
|
|
32
|
+
async function isWriteRequestsAllowed() {
|
|
33
|
+
const configDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd();
|
|
34
|
+
let raw = await readFile(path.join(configDirectory, 'lowdefy.yaml'));
|
|
35
|
+
if (type.isNone(raw)) {
|
|
36
|
+
raw = await readFile(path.join(configDirectory, 'lowdefy.yml'));
|
|
37
|
+
}
|
|
38
|
+
if (type.isNone(raw)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
let lowdefy;
|
|
42
|
+
try {
|
|
43
|
+
lowdefy = YAML.parse(raw);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return get(lowdefy, 'cli.agentTools.allowWriteRequests', { default: false }) === true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default isWriteRequestsAllowed;
|
|
@@ -0,0 +1,55 @@
|
|
|
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 { getCheckpointsRoot, MANIFEST_FILE_NAME } from './checkpointPaths.js';
|
|
21
|
+
|
|
22
|
+
// Lists stored config checkpoints, most recent first. Reads manifest.json
|
|
23
|
+
// from each snapshot directory rather than re-scanning the config
|
|
24
|
+
// directory — the manifest is the source of truth for what a checkpoint
|
|
25
|
+
// contains, and stays accurate even after later checkpoints or reverts.
|
|
26
|
+
function listConfigCheckpoints() {
|
|
27
|
+
const checkpointsRoot = getCheckpointsRoot();
|
|
28
|
+
if (!fs.existsSync(checkpointsRoot)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const checkpoints = fs
|
|
33
|
+
.readdirSync(checkpointsRoot, { withFileTypes: true })
|
|
34
|
+
.filter((entry) => entry.isDirectory())
|
|
35
|
+
.map((entry) => {
|
|
36
|
+
const manifestPath = path.join(checkpointsRoot, entry.name, MANIFEST_FILE_NAME);
|
|
37
|
+
if (!fs.existsSync(manifestPath)) {
|
|
38
|
+
// A checkpoint directory without a manifest is not one this module
|
|
39
|
+
// wrote (or was interrupted mid-write) — skip rather than guess.
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
43
|
+
return {
|
|
44
|
+
id: manifest.id,
|
|
45
|
+
label: manifest.label,
|
|
46
|
+
createdAt: manifest.createdAt,
|
|
47
|
+
fileCount: manifest.files.length,
|
|
48
|
+
};
|
|
49
|
+
})
|
|
50
|
+
.filter((checkpoint) => checkpoint !== null);
|
|
51
|
+
|
|
52
|
+
return checkpoints.sort((a, b) => (a.id < b.id ? 1 : -1));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default listConfigCheckpoints;
|
|
@@ -0,0 +1,80 @@
|
|
|
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 readBuildArtifact from './readBuildArtifact.js';
|
|
18
|
+
|
|
19
|
+
function addTypes({ plugins, store, kind }) {
|
|
20
|
+
for (const [typeName, definition] of Object.entries(store ?? {})) {
|
|
21
|
+
if (!plugins[definition.package]) {
|
|
22
|
+
plugins[definition.package] = { package: definition.package, version: definition.version, types: {} };
|
|
23
|
+
}
|
|
24
|
+
const plugin = plugins[definition.package];
|
|
25
|
+
if (!plugin.types[kind]) {
|
|
26
|
+
plugin.types[kind] = [];
|
|
27
|
+
}
|
|
28
|
+
plugin.types[kind].push(typeName);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function listPlugins() {
|
|
33
|
+
const availableTypes = readBuildArtifact({ name: 'plugins/availableTypes.json' }) ?? {};
|
|
34
|
+
const customTypesMap = readBuildArtifact({ name: 'customTypesMap.json' }) ?? {};
|
|
35
|
+
const installedPackages = readBuildArtifact({ name: 'installedPluginPackages.json' }) ?? [];
|
|
36
|
+
|
|
37
|
+
const plugins = {};
|
|
38
|
+
addTypes({ plugins, store: availableTypes.actions, kind: 'actions' });
|
|
39
|
+
addTypes({ plugins, store: availableTypes.agents, kind: 'agents' });
|
|
40
|
+
addTypes({ plugins, store: availableTypes.blocks, kind: 'blocks' });
|
|
41
|
+
addTypes({ plugins, store: availableTypes.connections, kind: 'connections' });
|
|
42
|
+
addTypes({ plugins, store: availableTypes.notifications, kind: 'notifications' });
|
|
43
|
+
addTypes({ plugins, store: availableTypes.operators?.client, kind: 'operators' });
|
|
44
|
+
addTypes({ plugins, store: availableTypes.operators?.server, kind: 'operators' });
|
|
45
|
+
addTypes({ plugins, store: availableTypes.requests, kind: 'requests' });
|
|
46
|
+
addTypes({ plugins, store: availableTypes.websockets, kind: 'websockets' });
|
|
47
|
+
|
|
48
|
+
const customPackages = new Set();
|
|
49
|
+
const customStores = [
|
|
50
|
+
customTypesMap.actions,
|
|
51
|
+
customTypesMap.agents,
|
|
52
|
+
customTypesMap.blocks,
|
|
53
|
+
customTypesMap.connections,
|
|
54
|
+
customTypesMap.operators?.client,
|
|
55
|
+
customTypesMap.operators?.server,
|
|
56
|
+
customTypesMap.requests,
|
|
57
|
+
customTypesMap.websockets,
|
|
58
|
+
];
|
|
59
|
+
for (const store of customStores) {
|
|
60
|
+
for (const definition of Object.values(store ?? {})) {
|
|
61
|
+
customPackages.add(definition.package);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Object.values(plugins)
|
|
66
|
+
.map((plugin) => ({
|
|
67
|
+
...plugin,
|
|
68
|
+
custom: customPackages.has(plugin.package),
|
|
69
|
+
installed: installedPackages.includes(plugin.package),
|
|
70
|
+
types: Object.fromEntries(
|
|
71
|
+
Object.entries(plugin.types).map(([kind, typeNames]) => [
|
|
72
|
+
kind,
|
|
73
|
+
[...new Set(typeNames)].sort(),
|
|
74
|
+
])
|
|
75
|
+
),
|
|
76
|
+
}))
|
|
77
|
+
.sort((a, b) => a.package.localeCompare(b.package));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default listPlugins;
|
|
@@ -0,0 +1,105 @@
|
|
|
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 { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import getDocsManifest from './getDocsManifest.js';
|
|
20
|
+
import normalizeTypeKind from './normalizeTypeKind.js';
|
|
21
|
+
import readBuildArtifact from './readBuildArtifact.js';
|
|
22
|
+
|
|
23
|
+
function usedTypeNames({ store }) {
|
|
24
|
+
const used = new Set();
|
|
25
|
+
for (const [typeName, definition] of Object.entries(store ?? {})) {
|
|
26
|
+
if (type.isObject(definition) && (definition.count ?? 0) > 0) {
|
|
27
|
+
used.add(typeName);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return used;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function flattenKind({ kind, availableTypes }) {
|
|
34
|
+
if (kind === 'operators') {
|
|
35
|
+
const merged = {};
|
|
36
|
+
for (const [typeName, definition] of Object.entries(availableTypes.operators?.client ?? {})) {
|
|
37
|
+
merged[typeName] = { ...definition, environments: ['client'] };
|
|
38
|
+
}
|
|
39
|
+
for (const [typeName, definition] of Object.entries(availableTypes.operators?.server ?? {})) {
|
|
40
|
+
if (merged[typeName]) {
|
|
41
|
+
merged[typeName].environments.push('server');
|
|
42
|
+
} else {
|
|
43
|
+
merged[typeName] = { ...definition, environments: ['server'] };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
return availableTypes[kind] ?? {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function listTypes({ kind }) {
|
|
52
|
+
const normalizedKind = normalizeTypeKind({ kind });
|
|
53
|
+
if (type.isNone(normalizedKind)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Unknown type kind. Received ${JSON.stringify(kind)}. Use one of: blocks, operators, actions, connections, requests, agents, notifications, websockets.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const availableTypes = readBuildArtifact({ name: 'plugins/availableTypes.json' }) ?? {};
|
|
59
|
+
const types = readBuildArtifact({ name: 'types.json', deserialize: true }) ?? {};
|
|
60
|
+
const blockMetas = readBuildArtifact({ name: 'plugins/blockMetas.json' }) ?? {};
|
|
61
|
+
const manifest = getDocsManifest();
|
|
62
|
+
|
|
63
|
+
const docSlugsByType = new Map();
|
|
64
|
+
const kindSingular = normalizedKind.slice(0, -1);
|
|
65
|
+
for (const doc of manifest?.docs ?? []) {
|
|
66
|
+
if (doc.kind === kindSingular && doc.typeName) {
|
|
67
|
+
docSlugsByType.set(doc.typeName, doc.slug);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let used;
|
|
72
|
+
if (normalizedKind === 'operators') {
|
|
73
|
+
used = new Set([
|
|
74
|
+
...usedTypeNames({ store: types.operators?.client }),
|
|
75
|
+
...usedTypeNames({ store: types.operators?.server }),
|
|
76
|
+
]);
|
|
77
|
+
} else {
|
|
78
|
+
used = usedTypeNames({ store: types[normalizedKind] });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const flattened = flattenKind({ kind: normalizedKind, availableTypes });
|
|
82
|
+
return Object.entries(flattened)
|
|
83
|
+
.map(([typeName, definition]) => {
|
|
84
|
+
const entry = {
|
|
85
|
+
type: typeName,
|
|
86
|
+
kind: normalizedKind,
|
|
87
|
+
package: definition.package,
|
|
88
|
+
version: definition.version,
|
|
89
|
+
used: used.has(typeName),
|
|
90
|
+
};
|
|
91
|
+
if (definition.environments) {
|
|
92
|
+
entry.environments = definition.environments;
|
|
93
|
+
}
|
|
94
|
+
if (normalizedKind === 'blocks' && blockMetas[typeName]?.category) {
|
|
95
|
+
entry.category = blockMetas[typeName].category;
|
|
96
|
+
}
|
|
97
|
+
if (docSlugsByType.has(typeName)) {
|
|
98
|
+
entry.docSlug = docSlugsByType.get(typeName);
|
|
99
|
+
}
|
|
100
|
+
return entry;
|
|
101
|
+
})
|
|
102
|
+
.sort((a, b) => a.type.localeCompare(b.type));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default listTypes;
|