@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.
- package/package.json +4 -24
- package/tools/deploy.ts +19 -165
- package/tools/mcp-server.ts +17 -26
- package/tools/postinstall.ts +21 -235
- package/tools/pull.ts +18 -121
- package/tools/push-config.ts +19 -0
- package/tools/simulator.ts +21 -0
- package/compile/action-name-map.ts +0 -1015
- package/compile/index.ts +0 -1278
- package/compile/util.ts +0 -358
- package/index.ts +0 -6
- package/skills/bricks-ctor/SKILL.md +0 -32
- package/skills/bricks-ctor/rules/animation.md +0 -159
- package/skills/bricks-ctor/rules/architecture-patterns.md +0 -69
- package/skills/bricks-ctor/rules/automations.md +0 -221
- package/skills/bricks-ctor/rules/buttress.md +0 -156
- package/skills/bricks-ctor/rules/data-calculation.md +0 -209
- package/skills/bricks-ctor/rules/local-sync.md +0 -129
- package/skills/bricks-ctor/rules/media-flow.md +0 -158
- package/skills/bricks-ctor/rules/remote-data-bank.md +0 -196
- package/skills/bricks-ctor/rules/standby-transition.md +0 -124
- package/skills/bricks-design/LICENSE.txt +0 -180
- package/skills/bricks-design/SKILL.md +0 -66
- package/skills/rive-marketplace/SKILL.md +0 -99
- package/tools/_git-author.ts +0 -29
- package/tools/_shell.ts +0 -173
- package/tools/icons/.gitattributes +0 -1
- package/tools/icons/fa6pro-glyphmap.json +0 -4686
- package/tools/icons/fa6pro-meta.json +0 -1
- package/tools/mcp-tools/compile.ts +0 -92
- package/tools/mcp-tools/huggingface.ts +0 -762
- package/tools/mcp-tools/icons.ts +0 -81
- package/tools/mcp-tools/lottie.ts +0 -102
- package/tools/mcp-tools/media.ts +0 -110
- package/tools/preview-main.mjs +0 -293
- package/tools/preview.ts +0 -150
- package/types/animation.ts +0 -100
- package/types/automation.ts +0 -235
- package/types/brick-base.ts +0 -80
- package/types/bricks/Camera.ts +0 -246
- package/types/bricks/Chart.ts +0 -372
- package/types/bricks/GenerativeMedia.ts +0 -290
- package/types/bricks/Icon.ts +0 -98
- package/types/bricks/Image.ts +0 -114
- package/types/bricks/Items.ts +0 -476
- package/types/bricks/Lottie.ts +0 -168
- package/types/bricks/Maps.ts +0 -262
- package/types/bricks/QrCode.ts +0 -117
- package/types/bricks/Rect.ts +0 -150
- package/types/bricks/RichText.ts +0 -128
- package/types/bricks/Rive.ts +0 -220
- package/types/bricks/Sketch.ts +0 -254
- package/types/bricks/Slideshow.ts +0 -201
- package/types/bricks/Svg.ts +0 -99
- package/types/bricks/Text.ts +0 -148
- package/types/bricks/TextInput.ts +0 -242
- package/types/bricks/Video.ts +0 -175
- package/types/bricks/VideoStreaming.ts +0 -112
- package/types/bricks/WebRtcStream.ts +0 -65
- package/types/bricks/WebView.ts +0 -168
- package/types/bricks/index.ts +0 -22
- package/types/canvas.ts +0 -82
- package/types/common.ts +0 -144
- package/types/data-calc-command.ts +0 -7005
- package/types/data-calc-script.ts +0 -21
- package/types/data-calc.ts +0 -11
- package/types/data.ts +0 -95
- package/types/generators/AlarmClock.ts +0 -110
- package/types/generators/Assistant.ts +0 -621
- package/types/generators/BleCentral.ts +0 -247
- package/types/generators/BlePeripheral.ts +0 -208
- package/types/generators/CanvasMap.ts +0 -74
- package/types/generators/CastlesPay.ts +0 -87
- package/types/generators/DataBank.ts +0 -160
- package/types/generators/File.ts +0 -432
- package/types/generators/GraphQl.ts +0 -132
- package/types/generators/Http.ts +0 -222
- package/types/generators/HttpServer.ts +0 -176
- package/types/generators/Information.ts +0 -103
- package/types/generators/Intent.ts +0 -168
- package/types/generators/Iterator.ts +0 -108
- package/types/generators/Keyboard.ts +0 -105
- package/types/generators/LlmAnthropicCompat.ts +0 -212
- package/types/generators/LlmAppleBuiltin.ts +0 -159
- package/types/generators/LlmGgml.ts +0 -861
- package/types/generators/LlmMediaTekNeuroPilot.ts +0 -235
- package/types/generators/LlmMlx.ts +0 -227
- package/types/generators/LlmOnnx.ts +0 -213
- package/types/generators/LlmOpenAiCompat.ts +0 -312
- package/types/generators/LlmQualcommAiEngine.ts +0 -247
- package/types/generators/Mcp.ts +0 -637
- package/types/generators/McpServer.ts +0 -289
- package/types/generators/MediaFlow.ts +0 -170
- package/types/generators/MqttBroker.ts +0 -141
- package/types/generators/MqttClient.ts +0 -141
- package/types/generators/Question.ts +0 -408
- package/types/generators/RealtimeTranscription.ts +0 -287
- package/types/generators/RerankerGgml.ts +0 -191
- package/types/generators/SerialPort.ts +0 -151
- package/types/generators/SoundPlayer.ts +0 -94
- package/types/generators/SoundRecorder.ts +0 -130
- package/types/generators/SpeechToTextGgml.ts +0 -419
- package/types/generators/SpeechToTextOnnx.ts +0 -236
- package/types/generators/SpeechToTextPlatform.ts +0 -85
- package/types/generators/SqLite.ts +0 -159
- package/types/generators/Step.ts +0 -107
- package/types/generators/SttAppleBuiltin.ts +0 -130
- package/types/generators/Tcp.ts +0 -126
- package/types/generators/TcpServer.ts +0 -147
- package/types/generators/TextToSpeechAppleBuiltin.ts +0 -127
- package/types/generators/TextToSpeechGgml.ts +0 -221
- package/types/generators/TextToSpeechOnnx.ts +0 -178
- package/types/generators/TextToSpeechOpenAiLike.ts +0 -121
- package/types/generators/ThermalPrinter.ts +0 -191
- package/types/generators/Tick.ts +0 -83
- package/types/generators/Udp.ts +0 -120
- package/types/generators/VadGgml.ts +0 -250
- package/types/generators/VadOnnx.ts +0 -231
- package/types/generators/VadTraditional.ts +0 -138
- package/types/generators/VectorStore.ts +0 -257
- package/types/generators/Watchdog.ts +0 -107
- package/types/generators/WebCrawler.ts +0 -103
- package/types/generators/WebRtc.ts +0 -181
- package/types/generators/WebSocket.ts +0 -148
- package/types/generators/index.ts +0 -57
- package/types/index.ts +0 -13
- package/types/subspace.ts +0 -60
- package/types/switch.ts +0 -51
- package/types/system.ts +0 -707
- package/utils/calc.ts +0 -126
- package/utils/data.ts +0 -497
- package/utils/event-props.ts +0 -886
- package/utils/id.ts +0 -80
package/tools/mcp-tools/icons.ts
DELETED
|
@@ -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
|
-
}
|
package/tools/mcp-tools/media.ts
DELETED
|
@@ -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
|
-
}
|
package/tools/preview-main.mjs
DELETED
|
@@ -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
|
-
})
|