@fugood/bricks-ctor 2.25.0-beta.8 → 2.25.0-beta.81

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 (137) hide show
  1. package/package.json +4 -24
  2. package/tools/__tests__/legacy-forwarder.test.js +91 -0
  3. package/tools/_forward.ts +26 -0
  4. package/tools/deploy.ts +3 -164
  5. package/tools/mcp-server.ts +3 -27
  6. package/tools/postinstall.ts +3 -234
  7. package/tools/pull.ts +3 -121
  8. package/tools/push-config.ts +4 -0
  9. package/tools/simulator.ts +4 -0
  10. package/compile/__tests__/index.test.js +0 -115
  11. package/compile/action-name-map.ts +0 -1015
  12. package/compile/index.ts +0 -1458
  13. package/compile/util.ts +0 -358
  14. package/index.ts +0 -6
  15. package/skills/bricks-ctor/SKILL.md +0 -32
  16. package/skills/bricks-ctor/rules/animation.md +0 -160
  17. package/skills/bricks-ctor/rules/architecture-patterns.md +0 -69
  18. package/skills/bricks-ctor/rules/automations.md +0 -221
  19. package/skills/bricks-ctor/rules/buttress.md +0 -156
  20. package/skills/bricks-ctor/rules/data-calculation.md +0 -209
  21. package/skills/bricks-ctor/rules/local-sync.md +0 -129
  22. package/skills/bricks-ctor/rules/media-flow.md +0 -158
  23. package/skills/bricks-ctor/rules/remote-data-bank.md +0 -196
  24. package/skills/bricks-ctor/rules/standby-transition.md +0 -124
  25. package/skills/bricks-design/LICENSE.txt +0 -180
  26. package/skills/bricks-design/SKILL.md +0 -66
  27. package/skills/rive-marketplace/SKILL.md +0 -99
  28. package/tools/_git-author.ts +0 -29
  29. package/tools/_shell.ts +0 -173
  30. package/tools/icons/.gitattributes +0 -1
  31. package/tools/icons/fa6pro-glyphmap.json +0 -4686
  32. package/tools/icons/fa6pro-meta.json +0 -1
  33. package/tools/mcp-tools/compile.ts +0 -92
  34. package/tools/mcp-tools/huggingface.ts +0 -762
  35. package/tools/mcp-tools/icons.ts +0 -81
  36. package/tools/mcp-tools/lottie.ts +0 -102
  37. package/tools/mcp-tools/media.ts +0 -110
  38. package/tools/preview-main.mjs +0 -306
  39. package/tools/preview.ts +0 -150
  40. package/types/animation.ts +0 -111
  41. package/types/automation.ts +0 -235
  42. package/types/brick-base.ts +0 -80
  43. package/types/bricks/Camera.ts +0 -246
  44. package/types/bricks/Chart.ts +0 -372
  45. package/types/bricks/GenerativeMedia.ts +0 -290
  46. package/types/bricks/Icon.ts +0 -98
  47. package/types/bricks/Image.ts +0 -126
  48. package/types/bricks/Items.ts +0 -476
  49. package/types/bricks/Lottie.ts +0 -168
  50. package/types/bricks/Maps.ts +0 -262
  51. package/types/bricks/QrCode.ts +0 -117
  52. package/types/bricks/Rect.ts +0 -150
  53. package/types/bricks/RichText.ts +0 -128
  54. package/types/bricks/Rive.ts +0 -220
  55. package/types/bricks/Sketch.ts +0 -254
  56. package/types/bricks/Slideshow.ts +0 -201
  57. package/types/bricks/Svg.ts +0 -99
  58. package/types/bricks/Text.ts +0 -148
  59. package/types/bricks/TextInput.ts +0 -242
  60. package/types/bricks/Video.ts +0 -175
  61. package/types/bricks/VideoStreaming.ts +0 -112
  62. package/types/bricks/WebRtcStream.ts +0 -65
  63. package/types/bricks/WebView.ts +0 -168
  64. package/types/bricks/index.ts +0 -22
  65. package/types/canvas.ts +0 -82
  66. package/types/common.ts +0 -144
  67. package/types/data-calc-command.ts +0 -7005
  68. package/types/data-calc-script.ts +0 -21
  69. package/types/data-calc.ts +0 -11
  70. package/types/data.ts +0 -95
  71. package/types/generators/AlarmClock.ts +0 -110
  72. package/types/generators/Assistant.ts +0 -621
  73. package/types/generators/BleCentral.ts +0 -247
  74. package/types/generators/BlePeripheral.ts +0 -208
  75. package/types/generators/CanvasMap.ts +0 -74
  76. package/types/generators/CastlesPay.ts +0 -87
  77. package/types/generators/DataBank.ts +0 -160
  78. package/types/generators/File.ts +0 -432
  79. package/types/generators/GraphQl.ts +0 -132
  80. package/types/generators/Http.ts +0 -222
  81. package/types/generators/HttpServer.ts +0 -176
  82. package/types/generators/Information.ts +0 -103
  83. package/types/generators/Intent.ts +0 -168
  84. package/types/generators/Iterator.ts +0 -108
  85. package/types/generators/Keyboard.ts +0 -105
  86. package/types/generators/LlmAnthropicCompat.ts +0 -212
  87. package/types/generators/LlmAppleBuiltin.ts +0 -159
  88. package/types/generators/LlmGgml.ts +0 -861
  89. package/types/generators/LlmMediaTekNeuroPilot.ts +0 -235
  90. package/types/generators/LlmMlx.ts +0 -227
  91. package/types/generators/LlmOnnx.ts +0 -213
  92. package/types/generators/LlmOpenAiCompat.ts +0 -312
  93. package/types/generators/LlmQualcommAiEngine.ts +0 -247
  94. package/types/generators/Mcp.ts +0 -637
  95. package/types/generators/McpServer.ts +0 -289
  96. package/types/generators/MediaFlow.ts +0 -170
  97. package/types/generators/MqttBroker.ts +0 -141
  98. package/types/generators/MqttClient.ts +0 -141
  99. package/types/generators/Question.ts +0 -408
  100. package/types/generators/RealtimeTranscription.ts +0 -287
  101. package/types/generators/RerankerGgml.ts +0 -191
  102. package/types/generators/SerialPort.ts +0 -151
  103. package/types/generators/SoundPlayer.ts +0 -94
  104. package/types/generators/SoundRecorder.ts +0 -130
  105. package/types/generators/SpeechToTextGgml.ts +0 -419
  106. package/types/generators/SpeechToTextOnnx.ts +0 -236
  107. package/types/generators/SpeechToTextPlatform.ts +0 -85
  108. package/types/generators/SqLite.ts +0 -159
  109. package/types/generators/Step.ts +0 -107
  110. package/types/generators/SttAppleBuiltin.ts +0 -130
  111. package/types/generators/Tcp.ts +0 -126
  112. package/types/generators/TcpServer.ts +0 -147
  113. package/types/generators/TextToSpeechAppleBuiltin.ts +0 -127
  114. package/types/generators/TextToSpeechGgml.ts +0 -221
  115. package/types/generators/TextToSpeechOnnx.ts +0 -178
  116. package/types/generators/TextToSpeechOpenAiLike.ts +0 -121
  117. package/types/generators/ThermalPrinter.ts +0 -191
  118. package/types/generators/Tick.ts +0 -83
  119. package/types/generators/Udp.ts +0 -120
  120. package/types/generators/VadGgml.ts +0 -250
  121. package/types/generators/VadOnnx.ts +0 -231
  122. package/types/generators/VadTraditional.ts +0 -138
  123. package/types/generators/VectorStore.ts +0 -257
  124. package/types/generators/Watchdog.ts +0 -107
  125. package/types/generators/WebCrawler.ts +0 -103
  126. package/types/generators/WebRtc.ts +0 -181
  127. package/types/generators/WebSocket.ts +0 -148
  128. package/types/generators/index.ts +0 -57
  129. package/types/index.ts +0 -13
  130. package/types/subspace.ts +0 -60
  131. package/types/switch.ts +0 -51
  132. package/types/system.ts +0 -707
  133. package/utils/__tests__/id.test.js +0 -58
  134. package/utils/calc.ts +0 -126
  135. package/utils/data.ts +0 -497
  136. package/utils/event-props.ts +0 -886
  137. package/utils/id.ts +0 -80
@@ -1,81 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
- import { z } from 'zod'
3
- import * as TOON from '@toon-format/toon'
4
- import Fuse from 'fuse.js'
5
- import glyphmap from '../icons/fa6pro-glyphmap.json'
6
- import glyphmapMeta from '../icons/fa6pro-meta.json'
7
-
8
- type IconStyle = 'brands' | 'duotone' | 'light' | 'regular' | 'solid' | 'thin'
9
-
10
- // Bitmask bits for icon family membership (compact metadata format)
11
- const STYLE_BITS: Record<IconStyle, number> = {
12
- brands: 1,
13
- thin: 2,
14
- light: 4,
15
- regular: 8,
16
- solid: 16,
17
- duotone: 256,
18
- }
19
- const ALL_STYLES = Object.keys(STYLE_BITS) as IconStyle[]
20
-
21
- // Metadata is shipped as { bitmaskValue: [iconName, ...] } with the dominant
22
- // 510 (all non-brand families) omitted. Expand to a flat lookup once.
23
- const DEFAULT_BITMASK = 510
24
- const compactMeta = glyphmapMeta as Record<string, string[]>
25
- const iconMeta: Record<string, number> = {}
26
- for (const name of Object.keys(glyphmap)) iconMeta[name] = DEFAULT_BITMASK
27
- for (const bitmaskKey of Object.keys(compactMeta)) {
28
- const value = Number(bitmaskKey)
29
- const names = compactMeta[bitmaskKey]
30
- for (const name of names) iconMeta[name] = value
31
- }
32
-
33
- const iconList = Object.entries(glyphmap as Record<string, number>).map(([name, code]) => {
34
- const bits = iconMeta[name] || 0
35
- const styles = ALL_STYLES.filter((s) => bits & STYLE_BITS[s])
36
- return { name, code, styles }
37
- })
38
-
39
- const iconFuse = new Fuse(iconList, {
40
- keys: ['name'],
41
- threshold: 0.3,
42
- includeScore: true,
43
- })
44
-
45
- export function register(server: McpServer) {
46
- server.tool(
47
- 'icon_search',
48
- {
49
- query: z.string().describe('Search keywords for FontAwesome 6 Pro icons'),
50
- limit: z.number().min(1).max(100).optional().default(10),
51
- style: z
52
- .enum(['brands', 'duotone', 'light', 'regular', 'solid', 'thin'])
53
- .optional()
54
- .describe('Filter by icon style'),
55
- },
56
- async ({ query, limit, style }) => {
57
- let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
58
-
59
- if (style) {
60
- results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
61
- }
62
-
63
- const icons = results.map((r) => ({
64
- name: r.item.name,
65
- code: r.item.code,
66
- unicode: `U+${r.item.code.toString(16).toUpperCase()}`,
67
- styles: r.item.styles,
68
- score: r.score,
69
- }))
70
-
71
- return {
72
- content: [
73
- {
74
- type: 'text',
75
- text: TOON.encode({ count: icons.length, icons }),
76
- },
77
- ],
78
- }
79
- },
80
- )
81
- }
@@ -1,102 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
- import { z } from 'zod'
3
- import * as TOON from '@toon-format/toon'
4
-
5
- const LOTTIEFILES_API_URL = 'https://lottiefiles.com/api'
6
-
7
- export function register(server: McpServer) {
8
- server.tool(
9
- 'lottie_search',
10
- {
11
- query: z.string().describe('Search keywords for animations'),
12
- page: z.number().min(1).optional().default(1),
13
- limit: z.number().min(1).max(100).optional().default(10),
14
- },
15
- async ({ query, page, limit }) => {
16
- try {
17
- const url = new URL(`${LOTTIEFILES_API_URL}/search/get-animations`)
18
- url.searchParams.set('query', query)
19
- url.searchParams.set('page', String(page))
20
- url.searchParams.set('limit', String(limit))
21
- url.searchParams.set('format', 'json')
22
-
23
- const response = await fetch(url.toString())
24
- const data = await response.json()
25
- const animations = data?.data?.data ?? []
26
-
27
- return {
28
- content: [
29
- {
30
- type: 'text',
31
- text: TOON.encode({ count: animations.length, animations }),
32
- },
33
- ],
34
- }
35
- } catch (err: any) {
36
- return {
37
- content: [{ type: 'text', text: `Failed to search animations: ${err.message}` }],
38
- }
39
- }
40
- },
41
- )
42
-
43
- server.tool(
44
- 'lottie_get_details',
45
- {
46
- id: z.string().describe('Animation file ID'),
47
- },
48
- async ({ id }) => {
49
- try {
50
- const url = new URL(`${LOTTIEFILES_API_URL}/animations/get-animation-data`)
51
- url.searchParams.set('fileId', id)
52
- url.searchParams.set('format', 'json')
53
-
54
- const response = await fetch(url.toString())
55
- const data = await response.json()
56
-
57
- return {
58
- content: [{ type: 'text', text: TOON.encode(data) }],
59
- }
60
- } catch (err: any) {
61
- return {
62
- content: [{ type: 'text', text: `Failed to get animation: ${err.message}` }],
63
- }
64
- }
65
- },
66
- )
67
-
68
- server.tool(
69
- 'lottie_popular',
70
- {
71
- page: z.number().min(1).optional().default(1),
72
- limit: z.number().min(1).max(100).optional().default(10),
73
- },
74
- async ({ page, limit }) => {
75
- try {
76
- const url = new URL(
77
- `${LOTTIEFILES_API_URL}/iconscout/popular-animations-weekly?api=%26sort%3Dpopular`,
78
- )
79
- url.searchParams.set('page', String(page))
80
- url.searchParams.set('limit', String(limit))
81
- url.searchParams.set('format', 'json')
82
-
83
- const response = await fetch(url.toString())
84
- const data = await response.json()
85
- const animations = data?.popularWeeklyData?.data ?? []
86
-
87
- return {
88
- content: [
89
- {
90
- type: 'text',
91
- text: TOON.encode({ count: animations.length, animations }),
92
- },
93
- ],
94
- }
95
- } catch (err: any) {
96
- return {
97
- content: [{ type: 'text', text: `Failed to get popular animations: ${err.message}` }],
98
- }
99
- }
100
- },
101
- )
102
- }
@@ -1,110 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
- import { z } from 'zod'
3
- import { sh } from '../_shell'
4
-
5
- const runBricks = async (projectDir: string, ...args: string[]) => {
6
- try {
7
- return await sh`bunx bricks ${args}`.cwd(projectDir).text()
8
- } catch (err: any) {
9
- throw new Error(err.stderr?.toString() || err.message)
10
- }
11
- }
12
-
13
- export function register(server: McpServer, projectDir: string) {
14
- server.tool('media_boxes', {}, async () => {
15
- try {
16
- const output = await runBricks(projectDir, 'media', 'boxes')
17
- return { content: [{ type: 'text', text: output }] }
18
- } catch (err: any) {
19
- return { content: [{ type: 'text', text: `Failed to list media boxes: ${err.message}` }] }
20
- }
21
- })
22
-
23
- server.tool('media_get_box', { id: z.string().describe('Media box ID') }, async ({ id }) => {
24
- try {
25
- const output = await runBricks(projectDir, 'media', 'box', id)
26
- return { content: [{ type: 'text', text: output }] }
27
- } catch (err: any) {
28
- return { content: [{ type: 'text', text: `Failed to get media box: ${err.message}` }] }
29
- }
30
- })
31
-
32
- server.tool(
33
- 'media_files',
34
- {
35
- boxId: z.string().describe('Media box ID'),
36
- types: z.string().describe('Comma-separated file types to include').optional(),
37
- userTag: z.array(z.string()).describe('Filter by user tags').optional(),
38
- limit: z.number().describe('Limit results').optional(),
39
- offset: z.number().describe('Offset results').optional(),
40
- },
41
- async ({ boxId, types, userTag, limit, offset }) => {
42
- try {
43
- const args = ['media', 'files', boxId]
44
- if (types) args.push('-t', types)
45
- if (userTag) userTag.forEach((tag) => args.push('-u', tag))
46
- if (limit != null) args.push('-l', String(limit))
47
- if (offset != null) args.push('-o', String(offset))
48
- const output = await runBricks(projectDir, ...args)
49
- return { content: [{ type: 'text', text: output }] }
50
- } catch (err: any) {
51
- return { content: [{ type: 'text', text: `Failed to list media files: ${err.message}` }] }
52
- }
53
- },
54
- )
55
-
56
- server.tool('media_file', { id: z.string().describe('Media file ID') }, async ({ id }) => {
57
- try {
58
- const output = await runBricks(projectDir, 'media', 'file', id)
59
- return { content: [{ type: 'text', text: output }] }
60
- } catch (err: any) {
61
- return { content: [{ type: 'text', text: `Failed to get media file: ${err.message}` }] }
62
- }
63
- })
64
-
65
- server.tool(
66
- 'media_upload_files',
67
- {
68
- boxId: z.string().describe('Target media box ID'),
69
- files: z.array(z.string()).min(1).describe('File paths to upload'),
70
- description: z.string().describe('File description').optional(),
71
- userTags: z.array(z.string()).max(15).describe('User tags (max 15)').optional(),
72
- imageVersion: z
73
- .array(z.string())
74
- .describe('Image resize specs as WxH or WxH:STRATEGY')
75
- .optional(),
76
- imageVersionType: z.enum(['jpg', 'png']).describe('Image output format').optional(),
77
- enableAiAnalysis: z.boolean().describe('Enable AI analysis').optional(),
78
- aiInstruction: z.string().describe('Custom AI analysis instruction').optional(),
79
- concurrency: z.number().min(1).describe('Max concurrent uploads').optional(),
80
- },
81
- async ({
82
- boxId,
83
- files,
84
- description,
85
- userTags,
86
- imageVersion,
87
- imageVersionType,
88
- enableAiAnalysis,
89
- aiInstruction,
90
- concurrency,
91
- }) => {
92
- try {
93
- const args = ['media', 'upload', boxId, ...files]
94
- if (description) args.push('-d', description)
95
- if (userTags) userTags.forEach((tag) => args.push('-t', tag))
96
- if (imageVersion) imageVersion.forEach((spec) => args.push('--image-version', spec))
97
- if (imageVersionType) args.push('--image-version-type', imageVersionType)
98
- if (enableAiAnalysis) args.push('--enable-ai-analysis')
99
- if (aiInstruction) args.push('--ai-instruction', aiInstruction)
100
- if (concurrency != null) args.push('--concurrency', String(concurrency))
101
- const output = await runBricks(projectDir, ...args)
102
- return { content: [{ type: 'text', text: output }] }
103
- } catch (err: any) {
104
- return {
105
- content: [{ type: 'text', text: `Failed to upload media files: ${err.message}` }],
106
- }
107
- }
108
- },
109
- )
110
- }
@@ -1,306 +0,0 @@
1
- // eslint-disable-next-line import/no-extraneous-dependencies
2
- import { app, BrowserWindow } from 'electron'
3
- import { readFile, writeFile } from 'fs/promises'
4
- import { watchFile } from 'fs'
5
- import { createServer } from 'http'
6
- import { parseArgs } from 'util'
7
- import * as TOON from '@toon-format/toon'
8
-
9
- for (const stream of [process.stdout, process.stderr]) {
10
- stream.on('error', (err) => {
11
- if (err.code !== 'EPIPE') throw err
12
- })
13
- }
14
-
15
- const { values } = parseArgs({
16
- args: process.argv,
17
- options: {
18
- 'clear-cache': { type: 'boolean' },
19
- 'take-screenshot': { type: 'string' },
20
- 'show-menu': { type: 'boolean' },
21
- 'test-id': { type: 'string' },
22
- 'test-title-like': { type: 'string' },
23
- 'no-keep-open': { type: 'boolean' },
24
- 'cdp-port': { type: 'string' },
25
- },
26
- strict: true,
27
- allowPositionals: true,
28
- })
29
-
30
- const cwd = process.cwd()
31
-
32
- let takeScreenshotConfig = null
33
- try {
34
- if (values['take-screenshot']) {
35
- takeScreenshotConfig = JSON.parse(values['take-screenshot'])
36
- if (!takeScreenshotConfig.path) takeScreenshotConfig.path = `${cwd}/screenshot.jpg`
37
- if (!takeScreenshotConfig.width) takeScreenshotConfig.width = 1280
38
- if (!takeScreenshotConfig.height) takeScreenshotConfig.height = 768
39
- if (!takeScreenshotConfig.delay) takeScreenshotConfig.delay = 1000
40
- }
41
- } catch (e) {
42
- console.error('Invalid take-screenshot config', e)
43
- // eslint-disable-next-line unicorn/no-process-exit
44
- process.exit(1)
45
- }
46
-
47
- let config = JSON.parse(await readFile(`${cwd}/.bricks/build/application-config.json`))
48
-
49
- // Resolve testId from testTitleLike
50
- let testId = values['test-id'] || null
51
- if (!testId && values['test-title-like']) {
52
- const titleLike = values['test-title-like'].toLowerCase()
53
- const automationMap = config.automation_map || {}
54
- for (const group of Object.values(automationMap)) {
55
- const found = Object.entries(group.map || {}).find(([, test]) =>
56
- test.title?.toLowerCase().includes(titleLike),
57
- )
58
- if (found) {
59
- ;[testId] = found
60
- break
61
- }
62
- }
63
- if (!testId) {
64
- throw new Error(`No automation found matching title: ${values['test-title-like']}`)
65
- }
66
- }
67
-
68
- const noKeepOpen = values['no-keep-open'] ?? false
69
-
70
- const stage = process.env.BRICKS_STAGE || 'production'
71
-
72
- const previewUrlMap = {
73
- production: 'https://control.bricks.tools/applicationPreview.html',
74
- beta: 'https://control-beta.bricks.tools/applicationPreview.html',
75
- development: 'http://localhost:3006/dev-applicationPreview.html',
76
- }
77
-
78
- const previewUrl = previewUrlMap[stage]
79
- if (!previewUrl) throw new Error(`Invalid BRICKS_STAGE: ${stage}`)
80
-
81
- // --- CDP WebSocket Server ---
82
- // Bridges external CDP clients to the preview's postMessage-based CDP bridge.
83
- // Usage: --cdp-port 9222
84
-
85
- const startCdpServer = async (mainWindow, port) => {
86
- const { WebSocketServer } = await import('ws')
87
- const clients = new Set()
88
-
89
- // Inject a listener in the preview that forwards CDP responses/events via console
90
- const injectCdpRelay = () =>
91
- mainWindow.webContents.executeJavaScript(`
92
- if (!window.__cdpRelayInstalled) {
93
- window.__cdpRelayInstalled = true
94
- window.addEventListener('message', (evt) => {
95
- try {
96
- const d = typeof evt.data === 'string' ? JSON.parse(evt.data) : evt.data
97
- if (d && (d.type === 'cdp-response' || d.type === 'cdp-event'))
98
- console.log('[CDP]' + JSON.stringify(d))
99
- } catch {}
100
- })
101
- }
102
- `)
103
-
104
- // Capture CDP messages from the preview's console output
105
- mainWindow.webContents.on('console-message', (_event, ...rest) => {
106
- const message = typeof rest[0] === 'object' ? rest[0].message : rest[1]
107
- if (!message?.startsWith('[CDP]')) return
108
- try {
109
- const data = JSON.parse(message.slice(5))
110
- // Translate to standard CDP wire format (strip internal type field)
111
- const wireMsg =
112
- data.type === 'cdp-response'
113
- ? { id: data.id, result: data.result, error: data.error }
114
- : { method: data.method, params: data.params }
115
- if (data.sessionId) wireMsg.sessionId = data.sessionId
116
- const payload = JSON.stringify(wireMsg)
117
- for (const c of clients) if (c.readyState === 1) c.send(payload)
118
- } catch {}
119
- })
120
-
121
- // Find an available port before binding
122
- const findPort = (p) =>
123
- new Promise((resolve) => {
124
- const probe = createServer()
125
- probe.once('error', () => {
126
- if (p < port + 100) resolve(findPort(p + 1))
127
- else resolve(null)
128
- })
129
- probe.listen(p, () => probe.close(() => resolve(p)))
130
- })
131
-
132
- const actualPort = await findPort(port)
133
- if (!actualPort) {
134
- console.warn(`CDP server: no available port in range ${port}-${port + 99}`)
135
- return
136
- }
137
-
138
- // HTTP discovery endpoints (chrome://inspect, Playwright, etc.)
139
- const httpServer = createServer((req, res) => {
140
- if (req.url === '/json/list' || req.url === '/json') {
141
- res.writeHead(200, { 'Content-Type': 'application/json' })
142
- res.end(
143
- JSON.stringify([
144
- {
145
- description: 'BRICKS Preview (CTOR Preview)',
146
- id: 'bricks-preview',
147
- title: 'BRICKS Preview',
148
- type: 'page',
149
- url: previewUrl,
150
- webSocketDebuggerUrl: `ws://localhost:${actualPort}/ws`,
151
- },
152
- ]),
153
- )
154
- return
155
- }
156
- if (req.url === '/json/version') {
157
- res.writeHead(200, { 'Content-Type': 'application/json' })
158
- res.end(JSON.stringify({ Browser: 'BRICKS Preview', 'Protocol-Version': '1.3' }))
159
- return
160
- }
161
- // bricks-cli discovery endpoint
162
- if (req.url === '/devtools/info') {
163
- res.writeHead(200, { 'Content-Type': 'application/json' })
164
- res.end(
165
- JSON.stringify({
166
- name: 'BRICKS Preview',
167
- port: actualPort,
168
- protocols: ['cdp'],
169
- hasPasscode: false,
170
- }),
171
- )
172
- return
173
- }
174
- res.writeHead(404)
175
- res.end()
176
- })
177
-
178
- const wss = new WebSocketServer({ server: httpServer, path: '/ws' })
179
- wss.on('connection', (ws) => {
180
- clients.add(ws)
181
- injectCdpRelay()
182
-
183
- ws.on('message', (raw) => {
184
- try {
185
- const req = JSON.parse(raw.toString())
186
- const cdpReq = JSON.stringify({
187
- type: 'cdp-request',
188
- id: req.id,
189
- method: req.method,
190
- params: req.params || {},
191
- sessionId: req.sessionId,
192
- })
193
- mainWindow.webContents.executeJavaScript(`window.postMessage(${JSON.stringify(cdpReq)})`)
194
- } catch {}
195
- })
196
-
197
- ws.on('close', () => clients.delete(ws))
198
- })
199
-
200
- httpServer.listen(actualPort, () => {
201
- console.log(`CDP server: ws://localhost:${actualPort}/ws`)
202
- console.log(
203
- `Inspect: devtools://devtools/bundled/inspector.html?ws=localhost:${actualPort}/ws`,
204
- )
205
- })
206
- }
207
-
208
- app.on('ready', () => {
209
- let show = true
210
- if (takeScreenshotConfig && !takeScreenshotConfig.noHeadless) show = false
211
- const mainWindow = new BrowserWindow({
212
- width: takeScreenshotConfig?.width || 1280,
213
- height: takeScreenshotConfig?.height || 768,
214
- frame: !takeScreenshotConfig,
215
- show,
216
- })
217
- mainWindow.setBackgroundColor('#333')
218
- mainWindow.loadURL(previewUrl)
219
-
220
- const sendConfig = () => {
221
- const payload = {
222
- type: 'config',
223
- configFile: { _originTitle: 'Test', ...config, title: Math.random().toString() },
224
- workspace: { billing: { lock: {}, plan: 'free' } },
225
- showMenu: values['show-menu'] || false,
226
- testId,
227
- }
228
- mainWindow.webContents.executeJavaScript(
229
- `window.postMessage(JSON.stringify(${JSON.stringify(payload)}))`,
230
- )
231
- if (takeScreenshotConfig) {
232
- const { delay, width, height, path } = takeScreenshotConfig
233
- setTimeout(async () => {
234
- let screenshotFailed = false
235
- console.log('Taking screenshot')
236
- try {
237
- const image = await mainWindow.webContents.capturePage()
238
- console.log('Writing screenshot to', path)
239
- await writeFile(path, image.resize({ width, height }).toJPEG(75))
240
- } catch (err) {
241
- screenshotFailed = true
242
- console.error('Screenshot capture/write failed', err)
243
- } finally {
244
- if (noKeepOpen) {
245
- console.log('Closing app')
246
- if (screenshotFailed) app.exit(1)
247
- else app.quit()
248
- }
249
- }
250
- }, delay)
251
- }
252
- }
253
-
254
- mainWindow.webContents.once('dom-ready', () => {
255
- sendConfig()
256
- // Listen for test result messages from the preview
257
- if (testId) {
258
- mainWindow.webContents.executeJavaScript(`
259
- window.addEventListener('message', (evt) => {
260
- try {
261
- const data = JSON.parse(evt.data)
262
- if (data.type === 'bricks-preview-test-result') {
263
- console.log('[TEST_RESULT]' + JSON.stringify(data))
264
- }
265
- } catch (e) {}
266
- })
267
- `)
268
- }
269
- // Start CDP WebSocket server if requested
270
- const cdpPort = values['cdp-port'] ? parseInt(values['cdp-port'], 10) : null
271
- if (cdpPort) startCdpServer(mainWindow, cdpPort)
272
- })
273
-
274
- // Capture console messages from the preview
275
- if (testId) {
276
- mainWindow.webContents.on('console-message', (_event, ...rest) => {
277
- const message = typeof rest[0] === 'object' ? rest[0].message : rest[1]
278
- if (message?.startsWith('[TEST_RESULT]')) {
279
- const data = JSON.parse(message.replace('[TEST_RESULT]', ''))
280
- console.log(`[TEST_RESULT_TOON]${TOON.encode(data.result)}`)
281
- if (!takeScreenshotConfig && noKeepOpen) app.quit()
282
- }
283
- })
284
- }
285
-
286
- if (values['clear-cache']) {
287
- console.log('Clearing cache')
288
- mainWindow.webContents.session.clearCache()
289
- }
290
-
291
- mainWindow.on('close', () => app.quit())
292
-
293
- watchFile(
294
- `${cwd}/.bricks/build/application-config.json`,
295
- {
296
- bigint: false,
297
- persistent: true,
298
- interval: 1000,
299
- },
300
- async () => {
301
- console.log('Detected config changed')
302
- config = JSON.parse(await readFile(`${cwd}/.bricks/build/application-config.json`))
303
- sendConfig()
304
- },
305
- )
306
- })