@fugood/bricks-ctor 2.25.0-beta.6 → 2.25.0-beta.61

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 (133) hide show
  1. package/package.json +4 -24
  2. package/tools/deploy.ts +19 -165
  3. package/tools/mcp-server.ts +17 -26
  4. package/tools/postinstall.ts +21 -235
  5. package/tools/pull.ts +18 -121
  6. package/tools/push-config.ts +19 -0
  7. package/tools/simulator.ts +21 -0
  8. package/compile/action-name-map.ts +0 -1015
  9. package/compile/index.ts +0 -1278
  10. package/compile/util.ts +0 -358
  11. package/index.ts +0 -6
  12. package/skills/bricks-ctor/SKILL.md +0 -32
  13. package/skills/bricks-ctor/rules/animation.md +0 -159
  14. package/skills/bricks-ctor/rules/architecture-patterns.md +0 -69
  15. package/skills/bricks-ctor/rules/automations.md +0 -221
  16. package/skills/bricks-ctor/rules/buttress.md +0 -156
  17. package/skills/bricks-ctor/rules/data-calculation.md +0 -209
  18. package/skills/bricks-ctor/rules/local-sync.md +0 -129
  19. package/skills/bricks-ctor/rules/media-flow.md +0 -158
  20. package/skills/bricks-ctor/rules/remote-data-bank.md +0 -196
  21. package/skills/bricks-ctor/rules/standby-transition.md +0 -124
  22. package/skills/bricks-design/LICENSE.txt +0 -180
  23. package/skills/bricks-design/SKILL.md +0 -66
  24. package/skills/rive-marketplace/SKILL.md +0 -99
  25. package/tools/_git-author.ts +0 -29
  26. package/tools/_shell.ts +0 -173
  27. package/tools/icons/.gitattributes +0 -1
  28. package/tools/icons/fa6pro-glyphmap.json +0 -4686
  29. package/tools/icons/fa6pro-meta.json +0 -1
  30. package/tools/mcp-tools/compile.ts +0 -92
  31. package/tools/mcp-tools/huggingface.ts +0 -762
  32. package/tools/mcp-tools/icons.ts +0 -81
  33. package/tools/mcp-tools/lottie.ts +0 -102
  34. package/tools/mcp-tools/media.ts +0 -110
  35. package/tools/preview-main.mjs +0 -293
  36. package/tools/preview.ts +0 -150
  37. package/types/animation.ts +0 -100
  38. package/types/automation.ts +0 -235
  39. package/types/brick-base.ts +0 -80
  40. package/types/bricks/Camera.ts +0 -246
  41. package/types/bricks/Chart.ts +0 -372
  42. package/types/bricks/GenerativeMedia.ts +0 -290
  43. package/types/bricks/Icon.ts +0 -98
  44. package/types/bricks/Image.ts +0 -114
  45. package/types/bricks/Items.ts +0 -476
  46. package/types/bricks/Lottie.ts +0 -168
  47. package/types/bricks/Maps.ts +0 -262
  48. package/types/bricks/QrCode.ts +0 -117
  49. package/types/bricks/Rect.ts +0 -150
  50. package/types/bricks/RichText.ts +0 -128
  51. package/types/bricks/Rive.ts +0 -220
  52. package/types/bricks/Sketch.ts +0 -254
  53. package/types/bricks/Slideshow.ts +0 -201
  54. package/types/bricks/Svg.ts +0 -99
  55. package/types/bricks/Text.ts +0 -148
  56. package/types/bricks/TextInput.ts +0 -242
  57. package/types/bricks/Video.ts +0 -175
  58. package/types/bricks/VideoStreaming.ts +0 -112
  59. package/types/bricks/WebRtcStream.ts +0 -65
  60. package/types/bricks/WebView.ts +0 -168
  61. package/types/bricks/index.ts +0 -22
  62. package/types/canvas.ts +0 -82
  63. package/types/common.ts +0 -144
  64. package/types/data-calc-command.ts +0 -7005
  65. package/types/data-calc-script.ts +0 -21
  66. package/types/data-calc.ts +0 -11
  67. package/types/data.ts +0 -95
  68. package/types/generators/AlarmClock.ts +0 -110
  69. package/types/generators/Assistant.ts +0 -621
  70. package/types/generators/BleCentral.ts +0 -247
  71. package/types/generators/BlePeripheral.ts +0 -208
  72. package/types/generators/CanvasMap.ts +0 -74
  73. package/types/generators/CastlesPay.ts +0 -87
  74. package/types/generators/DataBank.ts +0 -160
  75. package/types/generators/File.ts +0 -432
  76. package/types/generators/GraphQl.ts +0 -132
  77. package/types/generators/Http.ts +0 -222
  78. package/types/generators/HttpServer.ts +0 -176
  79. package/types/generators/Information.ts +0 -103
  80. package/types/generators/Intent.ts +0 -168
  81. package/types/generators/Iterator.ts +0 -108
  82. package/types/generators/Keyboard.ts +0 -105
  83. package/types/generators/LlmAnthropicCompat.ts +0 -212
  84. package/types/generators/LlmAppleBuiltin.ts +0 -159
  85. package/types/generators/LlmGgml.ts +0 -861
  86. package/types/generators/LlmMediaTekNeuroPilot.ts +0 -235
  87. package/types/generators/LlmMlx.ts +0 -227
  88. package/types/generators/LlmOnnx.ts +0 -213
  89. package/types/generators/LlmOpenAiCompat.ts +0 -312
  90. package/types/generators/LlmQualcommAiEngine.ts +0 -247
  91. package/types/generators/Mcp.ts +0 -637
  92. package/types/generators/McpServer.ts +0 -289
  93. package/types/generators/MediaFlow.ts +0 -170
  94. package/types/generators/MqttBroker.ts +0 -141
  95. package/types/generators/MqttClient.ts +0 -141
  96. package/types/generators/Question.ts +0 -408
  97. package/types/generators/RealtimeTranscription.ts +0 -287
  98. package/types/generators/RerankerGgml.ts +0 -191
  99. package/types/generators/SerialPort.ts +0 -151
  100. package/types/generators/SoundPlayer.ts +0 -94
  101. package/types/generators/SoundRecorder.ts +0 -130
  102. package/types/generators/SpeechToTextGgml.ts +0 -419
  103. package/types/generators/SpeechToTextOnnx.ts +0 -236
  104. package/types/generators/SpeechToTextPlatform.ts +0 -85
  105. package/types/generators/SqLite.ts +0 -159
  106. package/types/generators/Step.ts +0 -107
  107. package/types/generators/SttAppleBuiltin.ts +0 -130
  108. package/types/generators/Tcp.ts +0 -126
  109. package/types/generators/TcpServer.ts +0 -147
  110. package/types/generators/TextToSpeechAppleBuiltin.ts +0 -127
  111. package/types/generators/TextToSpeechGgml.ts +0 -221
  112. package/types/generators/TextToSpeechOnnx.ts +0 -178
  113. package/types/generators/TextToSpeechOpenAiLike.ts +0 -121
  114. package/types/generators/ThermalPrinter.ts +0 -191
  115. package/types/generators/Tick.ts +0 -83
  116. package/types/generators/Udp.ts +0 -120
  117. package/types/generators/VadGgml.ts +0 -250
  118. package/types/generators/VadOnnx.ts +0 -231
  119. package/types/generators/VadTraditional.ts +0 -138
  120. package/types/generators/VectorStore.ts +0 -257
  121. package/types/generators/Watchdog.ts +0 -107
  122. package/types/generators/WebCrawler.ts +0 -103
  123. package/types/generators/WebRtc.ts +0 -181
  124. package/types/generators/WebSocket.ts +0 -148
  125. package/types/generators/index.ts +0 -57
  126. package/types/index.ts +0 -13
  127. package/types/subspace.ts +0 -60
  128. package/types/switch.ts +0 -51
  129. package/types/system.ts +0 -707
  130. package/utils/calc.ts +0 -126
  131. package/utils/data.ts +0 -497
  132. package/utils/event-props.ts +0 -886
  133. 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,293 +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
- const { values } = parseArgs({
10
- args: process.argv,
11
- options: {
12
- 'clear-cache': { type: 'boolean' },
13
- 'take-screenshot': { type: 'string' },
14
- 'show-menu': { type: 'boolean' },
15
- 'test-id': { type: 'string' },
16
- 'test-title-like': { type: 'string' },
17
- 'no-keep-open': { type: 'boolean' },
18
- 'cdp-port': { type: 'string' },
19
- },
20
- strict: true,
21
- allowPositionals: true,
22
- })
23
-
24
- const cwd = process.cwd()
25
-
26
- let takeScreenshotConfig = null
27
- try {
28
- if (values['take-screenshot']) {
29
- takeScreenshotConfig = JSON.parse(values['take-screenshot'])
30
- if (!takeScreenshotConfig.path) takeScreenshotConfig.path = `${cwd}/screenshot.jpg`
31
- if (!takeScreenshotConfig.width) takeScreenshotConfig.width = 1280
32
- if (!takeScreenshotConfig.height) takeScreenshotConfig.height = 768
33
- if (!takeScreenshotConfig.delay) takeScreenshotConfig.delay = 1000
34
- }
35
- } catch (e) {
36
- console.error('Invalid take-screenshot config', e)
37
- // eslint-disable-next-line unicorn/no-process-exit
38
- process.exit(1)
39
- }
40
-
41
- let config = JSON.parse(await readFile(`${cwd}/.bricks/build/application-config.json`))
42
-
43
- // Resolve testId from testTitleLike
44
- let testId = values['test-id'] || null
45
- if (!testId && values['test-title-like']) {
46
- const titleLike = values['test-title-like'].toLowerCase()
47
- const automationMap = config.automation_map || {}
48
- for (const group of Object.values(automationMap)) {
49
- const found = Object.entries(group.map || {}).find(([, test]) =>
50
- test.title?.toLowerCase().includes(titleLike),
51
- )
52
- if (found) {
53
- ;[testId] = found
54
- break
55
- }
56
- }
57
- if (!testId) {
58
- throw new Error(`No automation found matching title: ${values['test-title-like']}`)
59
- }
60
- }
61
-
62
- const noKeepOpen = values['no-keep-open'] ?? false
63
-
64
- const stage = process.env.BRICKS_STAGE || 'production'
65
-
66
- const previewUrlMap = {
67
- production: 'https://control.bricks.tools/applicationPreview.html',
68
- beta: 'https://control-beta.bricks.tools/applicationPreview.html',
69
- development: 'http://localhost:3006/dev-applicationPreview.html',
70
- }
71
-
72
- const previewUrl = previewUrlMap[stage]
73
- if (!previewUrl) throw new Error(`Invalid BRICKS_STAGE: ${stage}`)
74
-
75
- // --- CDP WebSocket Server ---
76
- // Bridges external CDP clients to the preview's postMessage-based CDP bridge.
77
- // Usage: --cdp-port 9222
78
-
79
- const startCdpServer = async (mainWindow, port) => {
80
- const { WebSocketServer } = await import('ws')
81
- const clients = new Set()
82
-
83
- // Inject a listener in the preview that forwards CDP responses/events via console
84
- const injectCdpRelay = () =>
85
- mainWindow.webContents.executeJavaScript(`
86
- if (!window.__cdpRelayInstalled) {
87
- window.__cdpRelayInstalled = true
88
- window.addEventListener('message', (evt) => {
89
- try {
90
- const d = typeof evt.data === 'string' ? JSON.parse(evt.data) : evt.data
91
- if (d && (d.type === 'cdp-response' || d.type === 'cdp-event'))
92
- console.log('[CDP]' + JSON.stringify(d))
93
- } catch {}
94
- })
95
- }
96
- `)
97
-
98
- // Capture CDP messages from the preview's console output
99
- mainWindow.webContents.on('console-message', (_event, ...rest) => {
100
- const message = typeof rest[0] === 'object' ? rest[0].message : rest[1]
101
- if (!message?.startsWith('[CDP]')) return
102
- try {
103
- const data = JSON.parse(message.slice(5))
104
- // Translate to standard CDP wire format (strip internal type field)
105
- const wireMsg =
106
- data.type === 'cdp-response'
107
- ? { id: data.id, result: data.result, error: data.error }
108
- : { method: data.method, params: data.params }
109
- if (data.sessionId) wireMsg.sessionId = data.sessionId
110
- const payload = JSON.stringify(wireMsg)
111
- for (const c of clients) if (c.readyState === 1) c.send(payload)
112
- } catch {}
113
- })
114
-
115
- // Find an available port before binding
116
- const findPort = (p) =>
117
- new Promise((resolve) => {
118
- const probe = createServer()
119
- probe.once('error', () => {
120
- if (p < port + 100) resolve(findPort(p + 1))
121
- else resolve(null)
122
- })
123
- probe.listen(p, () => probe.close(() => resolve(p)))
124
- })
125
-
126
- const actualPort = await findPort(port)
127
- if (!actualPort) {
128
- console.warn(`CDP server: no available port in range ${port}-${port + 99}`)
129
- return
130
- }
131
-
132
- // HTTP discovery endpoints (chrome://inspect, Playwright, etc.)
133
- const httpServer = createServer((req, res) => {
134
- if (req.url === '/json/list' || req.url === '/json') {
135
- res.writeHead(200, { 'Content-Type': 'application/json' })
136
- res.end(
137
- JSON.stringify([
138
- {
139
- description: 'BRICKS Preview (CTOR Preview)',
140
- id: 'bricks-preview',
141
- title: 'BRICKS Preview',
142
- type: 'page',
143
- url: previewUrl,
144
- webSocketDebuggerUrl: `ws://localhost:${actualPort}/ws`,
145
- },
146
- ]),
147
- )
148
- return
149
- }
150
- if (req.url === '/json/version') {
151
- res.writeHead(200, { 'Content-Type': 'application/json' })
152
- res.end(JSON.stringify({ Browser: 'BRICKS Preview', 'Protocol-Version': '1.3' }))
153
- return
154
- }
155
- // bricks-cli discovery endpoint
156
- if (req.url === '/devtools/info') {
157
- res.writeHead(200, { 'Content-Type': 'application/json' })
158
- res.end(
159
- JSON.stringify({
160
- name: 'BRICKS Preview',
161
- port: actualPort,
162
- protocols: ['cdp'],
163
- hasPasscode: false,
164
- }),
165
- )
166
- return
167
- }
168
- res.writeHead(404)
169
- res.end()
170
- })
171
-
172
- const wss = new WebSocketServer({ server: httpServer, path: '/ws' })
173
- wss.on('connection', (ws) => {
174
- clients.add(ws)
175
- injectCdpRelay()
176
-
177
- ws.on('message', (raw) => {
178
- try {
179
- const req = JSON.parse(raw.toString())
180
- const cdpReq = JSON.stringify({
181
- type: 'cdp-request',
182
- id: req.id,
183
- method: req.method,
184
- params: req.params || {},
185
- sessionId: req.sessionId,
186
- })
187
- mainWindow.webContents.executeJavaScript(`window.postMessage(${JSON.stringify(cdpReq)})`)
188
- } catch {}
189
- })
190
-
191
- ws.on('close', () => clients.delete(ws))
192
- })
193
-
194
- httpServer.listen(actualPort, () => {
195
- console.log(`CDP server: ws://localhost:${actualPort}/ws`)
196
- console.log(
197
- `Inspect: devtools://devtools/bundled/inspector.html?ws=localhost:${actualPort}/ws`,
198
- )
199
- })
200
- }
201
-
202
- app.on('ready', () => {
203
- let show = true
204
- if (takeScreenshotConfig && !takeScreenshotConfig.noHeadless) show = false
205
- const mainWindow = new BrowserWindow({
206
- width: takeScreenshotConfig?.width || 1280,
207
- height: takeScreenshotConfig?.height || 768,
208
- frame: !takeScreenshotConfig,
209
- show,
210
- })
211
- mainWindow.setBackgroundColor('#333')
212
- mainWindow.loadURL(previewUrl)
213
-
214
- const sendConfig = () => {
215
- const payload = {
216
- type: 'config',
217
- configFile: { _originTitle: 'Test', ...config, title: Math.random().toString() },
218
- workspace: { billing: { lock: {}, plan: 'free' } },
219
- showMenu: values['show-menu'] || false,
220
- testId,
221
- }
222
- mainWindow.webContents.executeJavaScript(
223
- `window.postMessage(JSON.stringify(${JSON.stringify(payload)}))`,
224
- )
225
- if (takeScreenshotConfig) {
226
- const { delay, width, height, path } = takeScreenshotConfig
227
- setTimeout(() => {
228
- console.log('Taking screenshot')
229
- mainWindow.webContents.capturePage().then((image) => {
230
- console.log('Writing screenshot to', path)
231
- writeFile(path, image.resize({ width, height }).toJPEG(75))
232
- if (noKeepOpen) {
233
- console.log('Closing app')
234
- app.quit()
235
- }
236
- })
237
- }, delay)
238
- }
239
- }
240
-
241
- mainWindow.webContents.once('dom-ready', () => {
242
- sendConfig()
243
- // Listen for test result messages from the preview
244
- if (testId) {
245
- mainWindow.webContents.executeJavaScript(`
246
- window.addEventListener('message', (evt) => {
247
- try {
248
- const data = JSON.parse(evt.data)
249
- if (data.type === 'bricks-preview-test-result') {
250
- console.log('[TEST_RESULT]' + JSON.stringify(data))
251
- }
252
- } catch (e) {}
253
- })
254
- `)
255
- }
256
- // Start CDP WebSocket server if requested
257
- const cdpPort = values['cdp-port'] ? parseInt(values['cdp-port'], 10) : null
258
- if (cdpPort) startCdpServer(mainWindow, cdpPort)
259
- })
260
-
261
- // Capture console messages from the preview
262
- if (testId) {
263
- mainWindow.webContents.on('console-message', (_event, ...rest) => {
264
- const message = typeof rest[0] === 'object' ? rest[0].message : rest[1]
265
- if (message?.startsWith('[TEST_RESULT]')) {
266
- const data = JSON.parse(message.replace('[TEST_RESULT]', ''))
267
- console.log(`[TEST_RESULT_TOON]${TOON.encode(data.result)}`)
268
- if (!takeScreenshotConfig && noKeepOpen) app.quit()
269
- }
270
- })
271
- }
272
-
273
- if (values['clear-cache']) {
274
- console.log('Clearing cache')
275
- mainWindow.webContents.session.clearCache()
276
- }
277
-
278
- mainWindow.on('close', () => app.quit())
279
-
280
- watchFile(
281
- `${cwd}/.bricks/build/application-config.json`,
282
- {
283
- bigint: false,
284
- persistent: true,
285
- interval: 1000,
286
- },
287
- async () => {
288
- console.log('Detected config changed')
289
- config = JSON.parse(await readFile(`${cwd}/.bricks/build/application-config.json`))
290
- sendConfig()
291
- },
292
- )
293
- })