@fugood/bricks-project 2.25.0-beta.9 → 2.25.6

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 (135) hide show
  1. package/package.json +4 -23
  2. package/package.json.bak +4 -23
  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/action-name-map.ts +0 -1015
  11. package/compile/index.ts +0 -1458
  12. package/compile/util.ts +0 -358
  13. package/index.ts +0 -6
  14. package/skills/bricks-ctor/SKILL.md +0 -32
  15. package/skills/bricks-ctor/rules/animation.md +0 -160
  16. package/skills/bricks-ctor/rules/architecture-patterns.md +0 -69
  17. package/skills/bricks-ctor/rules/automations.md +0 -221
  18. package/skills/bricks-ctor/rules/buttress.md +0 -156
  19. package/skills/bricks-ctor/rules/data-calculation.md +0 -209
  20. package/skills/bricks-ctor/rules/local-sync.md +0 -129
  21. package/skills/bricks-ctor/rules/media-flow.md +0 -158
  22. package/skills/bricks-ctor/rules/remote-data-bank.md +0 -196
  23. package/skills/bricks-ctor/rules/standby-transition.md +0 -124
  24. package/skills/bricks-design/LICENSE.txt +0 -180
  25. package/skills/bricks-design/SKILL.md +0 -66
  26. package/skills/rive-marketplace/SKILL.md +0 -99
  27. package/tools/_git-author.ts +0 -29
  28. package/tools/_shell.ts +0 -173
  29. package/tools/icons/.gitattributes +0 -1
  30. package/tools/icons/fa6pro-glyphmap.json +0 -4686
  31. package/tools/icons/fa6pro-meta.json +0 -1
  32. package/tools/mcp-tools/compile.ts +0 -92
  33. package/tools/mcp-tools/huggingface.ts +0 -762
  34. package/tools/mcp-tools/icons.ts +0 -81
  35. package/tools/mcp-tools/lottie.ts +0 -102
  36. package/tools/mcp-tools/media.ts +0 -110
  37. package/tools/preview-main.mjs +0 -306
  38. package/tools/preview.ts +0 -150
  39. package/types/animation.ts +0 -111
  40. package/types/automation.ts +0 -235
  41. package/types/brick-base.ts +0 -80
  42. package/types/bricks/Camera.ts +0 -246
  43. package/types/bricks/Chart.ts +0 -372
  44. package/types/bricks/GenerativeMedia.ts +0 -290
  45. package/types/bricks/Icon.ts +0 -98
  46. package/types/bricks/Image.ts +0 -126
  47. package/types/bricks/Items.ts +0 -476
  48. package/types/bricks/Lottie.ts +0 -168
  49. package/types/bricks/Maps.ts +0 -262
  50. package/types/bricks/QrCode.ts +0 -117
  51. package/types/bricks/Rect.ts +0 -150
  52. package/types/bricks/RichText.ts +0 -128
  53. package/types/bricks/Rive.ts +0 -220
  54. package/types/bricks/Sketch.ts +0 -254
  55. package/types/bricks/Slideshow.ts +0 -201
  56. package/types/bricks/Svg.ts +0 -99
  57. package/types/bricks/Text.ts +0 -148
  58. package/types/bricks/TextInput.ts +0 -242
  59. package/types/bricks/Video.ts +0 -175
  60. package/types/bricks/VideoStreaming.ts +0 -112
  61. package/types/bricks/WebRtcStream.ts +0 -65
  62. package/types/bricks/WebView.ts +0 -168
  63. package/types/bricks/index.ts +0 -22
  64. package/types/canvas.ts +0 -82
  65. package/types/common.ts +0 -144
  66. package/types/data-calc-command.ts +0 -7005
  67. package/types/data-calc-script.ts +0 -21
  68. package/types/data-calc.ts +0 -11
  69. package/types/data.ts +0 -95
  70. package/types/generators/AlarmClock.ts +0 -110
  71. package/types/generators/Assistant.ts +0 -621
  72. package/types/generators/BleCentral.ts +0 -247
  73. package/types/generators/BlePeripheral.ts +0 -208
  74. package/types/generators/CanvasMap.ts +0 -74
  75. package/types/generators/CastlesPay.ts +0 -87
  76. package/types/generators/DataBank.ts +0 -160
  77. package/types/generators/File.ts +0 -432
  78. package/types/generators/GraphQl.ts +0 -132
  79. package/types/generators/Http.ts +0 -222
  80. package/types/generators/HttpServer.ts +0 -176
  81. package/types/generators/Information.ts +0 -103
  82. package/types/generators/Intent.ts +0 -168
  83. package/types/generators/Iterator.ts +0 -108
  84. package/types/generators/Keyboard.ts +0 -105
  85. package/types/generators/LlmAnthropicCompat.ts +0 -212
  86. package/types/generators/LlmAppleBuiltin.ts +0 -159
  87. package/types/generators/LlmGgml.ts +0 -862
  88. package/types/generators/LlmMediaTekNeuroPilot.ts +0 -235
  89. package/types/generators/LlmMlx.ts +0 -228
  90. package/types/generators/LlmOnnx.ts +0 -213
  91. package/types/generators/LlmOpenAiCompat.ts +0 -312
  92. package/types/generators/LlmQualcommAiEngine.ts +0 -247
  93. package/types/generators/Mcp.ts +0 -637
  94. package/types/generators/McpServer.ts +0 -289
  95. package/types/generators/MediaFlow.ts +0 -170
  96. package/types/generators/MqttBroker.ts +0 -141
  97. package/types/generators/MqttClient.ts +0 -141
  98. package/types/generators/Question.ts +0 -408
  99. package/types/generators/RealtimeTranscription.ts +0 -287
  100. package/types/generators/RerankerGgml.ts +0 -191
  101. package/types/generators/SerialPort.ts +0 -151
  102. package/types/generators/SoundPlayer.ts +0 -94
  103. package/types/generators/SoundRecorder.ts +0 -130
  104. package/types/generators/SpeechToTextGgml.ts +0 -420
  105. package/types/generators/SpeechToTextOnnx.ts +0 -236
  106. package/types/generators/SpeechToTextPlatform.ts +0 -85
  107. package/types/generators/SqLite.ts +0 -159
  108. package/types/generators/Step.ts +0 -107
  109. package/types/generators/SttAppleBuiltin.ts +0 -130
  110. package/types/generators/Tcp.ts +0 -126
  111. package/types/generators/TcpServer.ts +0 -147
  112. package/types/generators/TextToSpeechAppleBuiltin.ts +0 -127
  113. package/types/generators/TextToSpeechGgml.ts +0 -221
  114. package/types/generators/TextToSpeechOnnx.ts +0 -178
  115. package/types/generators/TextToSpeechOpenAiLike.ts +0 -121
  116. package/types/generators/ThermalPrinter.ts +0 -191
  117. package/types/generators/Tick.ts +0 -83
  118. package/types/generators/Udp.ts +0 -120
  119. package/types/generators/VadGgml.ts +0 -250
  120. package/types/generators/VadOnnx.ts +0 -231
  121. package/types/generators/VadTraditional.ts +0 -138
  122. package/types/generators/VectorStore.ts +0 -257
  123. package/types/generators/Watchdog.ts +0 -107
  124. package/types/generators/WebCrawler.ts +0 -103
  125. package/types/generators/WebRtc.ts +0 -181
  126. package/types/generators/WebSocket.ts +0 -148
  127. package/types/generators/index.ts +0 -57
  128. package/types/index.ts +0 -13
  129. package/types/subspace.ts +0 -60
  130. package/types/switch.ts +0 -51
  131. package/types/system.ts +0 -707
  132. package/utils/calc.ts +0 -126
  133. package/utils/data.ts +0 -497
  134. package/utils/event-props.ts +0 -886
  135. 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
- })