@fugood/bricks-project 2.25.0-beta.4 → 2.25.0-beta.40

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 (159) hide show
  1. package/compile/action-name-map.ts +63 -0
  2. package/compile/index.ts +208 -19
  3. package/package.json +4 -3
  4. package/package.json.bak +4 -3
  5. package/skills/bricks-ctor/SKILL.md +21 -17
  6. package/skills/bricks-ctor/{rules → references}/animation.md +3 -2
  7. package/skills/bricks-ctor/{rules → references}/architecture-patterns.md +12 -0
  8. package/skills/bricks-ctor/{rules → references}/automations.md +11 -0
  9. package/skills/bricks-ctor/references/buttress.md +245 -0
  10. package/skills/bricks-ctor/{rules → references}/data-calculation.md +5 -5
  11. package/skills/bricks-ctor/references/simulator.md +115 -0
  12. package/skills/bricks-ctor/references/verification-toolchain.md +179 -0
  13. package/skills/bricks-design/SKILL.md +160 -45
  14. package/skills/bricks-design/references/architecture-truths.md +132 -0
  15. package/skills/bricks-design/references/avoiding-complexity.md +91 -0
  16. package/skills/bricks-design/references/design-critique.md +195 -0
  17. package/skills/bricks-design/references/design-languages.md +265 -0
  18. package/skills/bricks-design/references/performance.md +116 -0
  19. package/skills/bricks-design/references/presentation-and-slideshow.md +137 -0
  20. package/skills/bricks-design/references/translating-inputs.md +152 -0
  21. package/skills/bricks-design/references/variations-and-tweaks.md +124 -0
  22. package/skills/bricks-design/references/when-the-brief-is-branded.md +284 -0
  23. package/skills/bricks-design/references/when-the-brief-is-vague.md +85 -0
  24. package/skills/bricks-design/references/workflow.md +134 -0
  25. package/skills/bricks-ux/SKILL.md +120 -0
  26. package/skills/bricks-ux/references/accessibility.md +162 -0
  27. package/skills/bricks-ux/references/flow-states.md +175 -0
  28. package/skills/bricks-ux/references/interaction-archetypes.md +189 -0
  29. package/skills/bricks-ux/references/monitoring-screens.md +153 -0
  30. package/skills/bricks-ux/references/pressable-composition.md +126 -0
  31. package/skills/bricks-ux/references/user-journey.md +168 -0
  32. package/skills/bricks-ux/references/ux-critique.md +256 -0
  33. package/tools/_git-author.ts +10 -2
  34. package/tools/_last-pushed-commit.ts +28 -0
  35. package/tools/_shell.ts +8 -1
  36. package/tools/deploy.ts +15 -0
  37. package/tools/mcp-tools/compile.ts +19 -9
  38. package/tools/mcp-tools/media.ts +4 -1
  39. package/tools/pull.ts +91 -16
  40. package/tools/push-config.ts +118 -0
  41. package/tools/{preview-main.mjs → simulator-main.mjs} +207 -12
  42. package/tools/simulator-preload.cjs +16 -0
  43. package/tools/{preview.ts → simulator.ts} +4 -4
  44. package/types/{animation.ts → animation.d.ts} +24 -8
  45. package/types/{automation.ts → automation.d.ts} +16 -20
  46. package/types/{brick-base.ts → brick-base.d.ts} +1 -1
  47. package/types/bricks/{Camera.ts → Camera.d.ts} +8 -8
  48. package/types/bricks/{Chart.ts → Chart.d.ts} +4 -4
  49. package/types/bricks/{GenerativeMedia.ts → GenerativeMedia.d.ts} +15 -15
  50. package/types/bricks/{Icon.ts → Icon.d.ts} +7 -7
  51. package/types/bricks/{Image.ts → Image.d.ts} +21 -9
  52. package/types/bricks/{Items.ts → Items.d.ts} +7 -7
  53. package/types/bricks/{Lottie.ts → Lottie.d.ts} +10 -10
  54. package/types/bricks/{Maps.ts → Maps.d.ts} +11 -11
  55. package/types/bricks/{QrCode.ts → QrCode.d.ts} +7 -7
  56. package/types/bricks/{Rect.ts → Rect.d.ts} +7 -7
  57. package/types/bricks/{RichText.ts → RichText.d.ts} +12 -9
  58. package/types/bricks/{Rive.ts → Rive.d.ts} +9 -9
  59. package/types/bricks/Scene3D.d.ts +596 -0
  60. package/types/bricks/{Sketch.ts → Sketch.d.ts} +6 -6
  61. package/types/bricks/{Slideshow.ts → Slideshow.d.ts} +7 -7
  62. package/types/bricks/{Svg.ts → Svg.d.ts} +7 -7
  63. package/types/bricks/{Text.ts → Text.d.ts} +9 -9
  64. package/types/bricks/{TextInput.ts → TextInput.d.ts} +10 -10
  65. package/types/bricks/{Video.ts → Video.d.ts} +12 -12
  66. package/types/bricks/{VideoStreaming.ts → VideoStreaming.d.ts} +10 -10
  67. package/types/bricks/{WebRtcStream.ts → WebRtcStream.d.ts} +1 -1
  68. package/types/bricks/{WebView.ts → WebView.d.ts} +4 -4
  69. package/types/bricks/{index.ts → index.d.ts} +1 -0
  70. package/types/{common.ts → common.d.ts} +3 -6
  71. package/types/data-calc-command/base.d.ts +57 -0
  72. package/types/data-calc-command/collection.d.ts +418 -0
  73. package/types/data-calc-command/color.d.ts +432 -0
  74. package/types/data-calc-command/constant.d.ts +50 -0
  75. package/types/data-calc-command/datetime.d.ts +147 -0
  76. package/types/data-calc-command/file.d.ts +129 -0
  77. package/types/data-calc-command/index.d.ts +13 -0
  78. package/types/data-calc-command/iteratee.d.ts +23 -0
  79. package/types/data-calc-command/logictype.d.ts +190 -0
  80. package/types/data-calc-command/math.d.ts +275 -0
  81. package/types/data-calc-command/object.d.ts +119 -0
  82. package/types/data-calc-command/sandbox.d.ts +58 -0
  83. package/types/data-calc-command/string.d.ts +407 -0
  84. package/types/{data-calc.ts → data-calc.d.ts} +1 -0
  85. package/types/{data.ts → data.d.ts} +4 -2
  86. package/types/generators/{Assistant.ts → Assistant.d.ts} +19 -0
  87. package/types/generators/{LlmGgml.ts → LlmGgml.d.ts} +43 -1
  88. package/types/generators/{LlmMlx.ts → LlmMlx.d.ts} +1 -0
  89. package/types/generators/{RerankerGgml.ts → RerankerGgml.d.ts} +5 -1
  90. package/types/generators/{SoundRecorder.ts → SoundRecorder.d.ts} +10 -1
  91. package/types/generators/{SpeechToTextGgml.ts → SpeechToTextGgml.d.ts} +6 -1
  92. package/types/generators/{SttAppleBuiltin.ts → SttAppleBuiltin.d.ts} +27 -4
  93. package/types/generators/{ThermalPrinter.ts → ThermalPrinter.d.ts} +9 -7
  94. package/types/generators/{VadGgml.ts → VadGgml.d.ts} +12 -2
  95. package/types/{subspace.ts → subspace.d.ts} +1 -1
  96. package/utils/data.ts +2 -2
  97. package/utils/event-props.ts +17 -0
  98. package/utils/id.ts +78 -27
  99. package/skills/bricks-ctor/rules/buttress.md +0 -156
  100. package/skills/bricks-design/LICENSE.txt +0 -180
  101. package/types/data-calc-command.ts +0 -7005
  102. /package/skills/bricks-ctor/{rules → references}/local-sync.md +0 -0
  103. /package/skills/bricks-ctor/{rules → references}/media-flow.md +0 -0
  104. /package/skills/bricks-ctor/{rules → references}/remote-data-bank.md +0 -0
  105. /package/skills/bricks-ctor/{rules → references}/standby-transition.md +0 -0
  106. /package/types/{canvas.ts → canvas.d.ts} +0 -0
  107. /package/types/{data-calc-script.ts → data-calc-script.d.ts} +0 -0
  108. /package/types/generators/{AlarmClock.ts → AlarmClock.d.ts} +0 -0
  109. /package/types/generators/{BleCentral.ts → BleCentral.d.ts} +0 -0
  110. /package/types/generators/{BlePeripheral.ts → BlePeripheral.d.ts} +0 -0
  111. /package/types/generators/{CanvasMap.ts → CanvasMap.d.ts} +0 -0
  112. /package/types/generators/{CastlesPay.ts → CastlesPay.d.ts} +0 -0
  113. /package/types/generators/{DataBank.ts → DataBank.d.ts} +0 -0
  114. /package/types/generators/{File.ts → File.d.ts} +0 -0
  115. /package/types/generators/{GraphQl.ts → GraphQl.d.ts} +0 -0
  116. /package/types/generators/{Http.ts → Http.d.ts} +0 -0
  117. /package/types/generators/{HttpServer.ts → HttpServer.d.ts} +0 -0
  118. /package/types/generators/{Information.ts → Information.d.ts} +0 -0
  119. /package/types/generators/{Intent.ts → Intent.d.ts} +0 -0
  120. /package/types/generators/{Iterator.ts → Iterator.d.ts} +0 -0
  121. /package/types/generators/{Keyboard.ts → Keyboard.d.ts} +0 -0
  122. /package/types/generators/{LlmAnthropicCompat.ts → LlmAnthropicCompat.d.ts} +0 -0
  123. /package/types/generators/{LlmAppleBuiltin.ts → LlmAppleBuiltin.d.ts} +0 -0
  124. /package/types/generators/{LlmMediaTekNeuroPilot.ts → LlmMediaTekNeuroPilot.d.ts} +0 -0
  125. /package/types/generators/{LlmOnnx.ts → LlmOnnx.d.ts} +0 -0
  126. /package/types/generators/{LlmOpenAiCompat.ts → LlmOpenAiCompat.d.ts} +0 -0
  127. /package/types/generators/{LlmQualcommAiEngine.ts → LlmQualcommAiEngine.d.ts} +0 -0
  128. /package/types/generators/{Mcp.ts → Mcp.d.ts} +0 -0
  129. /package/types/generators/{McpServer.ts → McpServer.d.ts} +0 -0
  130. /package/types/generators/{MediaFlow.ts → MediaFlow.d.ts} +0 -0
  131. /package/types/generators/{MqttBroker.ts → MqttBroker.d.ts} +0 -0
  132. /package/types/generators/{MqttClient.ts → MqttClient.d.ts} +0 -0
  133. /package/types/generators/{Question.ts → Question.d.ts} +0 -0
  134. /package/types/generators/{RealtimeTranscription.ts → RealtimeTranscription.d.ts} +0 -0
  135. /package/types/generators/{SerialPort.ts → SerialPort.d.ts} +0 -0
  136. /package/types/generators/{SoundPlayer.ts → SoundPlayer.d.ts} +0 -0
  137. /package/types/generators/{SpeechToTextOnnx.ts → SpeechToTextOnnx.d.ts} +0 -0
  138. /package/types/generators/{SpeechToTextPlatform.ts → SpeechToTextPlatform.d.ts} +0 -0
  139. /package/types/generators/{SqLite.ts → SqLite.d.ts} +0 -0
  140. /package/types/generators/{Step.ts → Step.d.ts} +0 -0
  141. /package/types/generators/{Tcp.ts → Tcp.d.ts} +0 -0
  142. /package/types/generators/{TcpServer.ts → TcpServer.d.ts} +0 -0
  143. /package/types/generators/{TextToSpeechAppleBuiltin.ts → TextToSpeechAppleBuiltin.d.ts} +0 -0
  144. /package/types/generators/{TextToSpeechGgml.ts → TextToSpeechGgml.d.ts} +0 -0
  145. /package/types/generators/{TextToSpeechOnnx.ts → TextToSpeechOnnx.d.ts} +0 -0
  146. /package/types/generators/{TextToSpeechOpenAiLike.ts → TextToSpeechOpenAiLike.d.ts} +0 -0
  147. /package/types/generators/{Tick.ts → Tick.d.ts} +0 -0
  148. /package/types/generators/{Udp.ts → Udp.d.ts} +0 -0
  149. /package/types/generators/{VadOnnx.ts → VadOnnx.d.ts} +0 -0
  150. /package/types/generators/{VadTraditional.ts → VadTraditional.d.ts} +0 -0
  151. /package/types/generators/{VectorStore.ts → VectorStore.d.ts} +0 -0
  152. /package/types/generators/{Watchdog.ts → Watchdog.d.ts} +0 -0
  153. /package/types/generators/{WebCrawler.ts → WebCrawler.d.ts} +0 -0
  154. /package/types/generators/{WebRtc.ts → WebRtc.d.ts} +0 -0
  155. /package/types/generators/{WebSocket.ts → WebSocket.d.ts} +0 -0
  156. /package/types/generators/{index.ts → index.d.ts} +0 -0
  157. /package/types/{index.ts → index.d.ts} +0 -0
  158. /package/types/{switch.ts → switch.d.ts} +0 -0
  159. /package/types/{system.ts → system.d.ts} +0 -0
@@ -0,0 +1,118 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import { parseArgs } from 'util'
3
+ import { sh } from './_shell'
4
+ import { buildCommitArgs } from './_git-author'
5
+ import { writeLastPushedCommit } from './_last-pushed-commit'
6
+
7
+ const cwd = process.cwd()
8
+
9
+ const readJson = async (p: string) => JSON.parse(await readFile(p, 'utf8'))
10
+
11
+ const {
12
+ values: { 'auto-commit': autoCommit, 'no-check': noCheck, 'no-validate': noValidate, yes, help },
13
+ } = parseArgs({
14
+ args: process.argv.slice(2),
15
+ options: {
16
+ 'auto-commit': { type: 'boolean' },
17
+ 'no-check': { type: 'boolean' },
18
+ 'no-validate': { type: 'boolean' },
19
+ yes: { type: 'boolean', short: 'y' },
20
+ help: { type: 'boolean', short: 'h' },
21
+ },
22
+ allowPositionals: true,
23
+ })
24
+
25
+ if (help) {
26
+ console.log(`Push compiled config to BRICKS without creating a release.
27
+
28
+ Options:
29
+ --auto-commit Auto-commit unstaged changes before pushing
30
+ --no-check Skip the conflict guard (don't pass --last-commit-id)
31
+ --no-validate Skip server-side config schema validation
32
+ -y, --yes Skip all prompts
33
+ -h, --help Show this help message`)
34
+ process.exit(0)
35
+ }
36
+
37
+ // Detect git repo (mirrors deploy.ts)
38
+ const { exitCode } = await sh`cd ${cwd} && git status`.quiet().nothrow()
39
+ const isGitRepo = exitCode === 0
40
+
41
+ if (!isGitRepo && !yes) {
42
+ const confirmContinue = prompt('No git repository found, continue? (y/n)')
43
+ if (confirmContinue !== 'y') throw new Error('Update cancelled')
44
+ }
45
+
46
+ // Read application.json + compiled config
47
+ const app = await readJson(`${cwd}/application.json`)
48
+ const config = await readJson(`${cwd}/.bricks/build/application-config.json`)
49
+
50
+ // Handle unstaged changes the same way deploy.ts does.
51
+ let commitId = ''
52
+ let parentCommitId = ''
53
+ if (isGitRepo) {
54
+ const unstagedChanges = await sh`cd ${cwd} && git diff --name-only --diff-filter=ACMR`.text()
55
+ if (unstagedChanges) {
56
+ if (autoCommit) {
57
+ // Capture the pre-commit HEAD so we can use it as the conflict-check
58
+ // baseline (the server should still hold it from the prior deploy).
59
+ parentCommitId = (await sh`cd ${cwd} && git rev-parse HEAD`.nothrow().text()).trim()
60
+ await sh`cd ${cwd} && git add -A`
61
+ const commitArgs = await buildCommitArgs(cwd, ['chore: update bricks config'])
62
+ await sh`cd ${cwd} && git ${commitArgs}`
63
+ } else {
64
+ throw new Error('Unstaged changes found, please commit or stash your changes before updating')
65
+ }
66
+ }
67
+ commitId = (await sh`cd ${cwd} && git rev-parse HEAD`.text()).trim()
68
+ }
69
+
70
+ // Auto-derive --last-commit-id for the server-side conflict guard.
71
+ // - parent of the auto-commit (server still holds it from the prior deploy)
72
+ // - current HEAD (clean tree — server should be at the same commit too)
73
+ let lastCommitId: string | undefined
74
+ if (!noCheck) {
75
+ if (parentCommitId) lastCommitId = parentCommitId
76
+ else if (commitId) lastCommitId = commitId
77
+ }
78
+
79
+ if (!yes) {
80
+ const confirm = prompt('Are you sure you want to push the new config? (y/n)')
81
+ if (confirm !== 'y') throw new Error('Update cancelled')
82
+ }
83
+
84
+ const isModule = app.type === 'module'
85
+ const command = isModule ? 'module' : 'app'
86
+
87
+ const updateConfig = {
88
+ ...config,
89
+ bricks_project_last_commit_id: commitId || undefined,
90
+ }
91
+ const configPath = `${cwd}/.bricks/build/push-config.json`
92
+ await writeFile(configPath, JSON.stringify(updateConfig))
93
+
94
+ const args = ['bricks', command, 'update', app.id, '-f', configPath, '--json']
95
+ if (noValidate) args.push('--no-validate')
96
+ if (lastCommitId) args.push('--last-commit-id', lastCommitId)
97
+
98
+ const result = await sh`${args}`.quiet().nothrow()
99
+
100
+ if (result.exitCode !== 0) {
101
+ const output = result.stderr.toString() || result.stdout.toString()
102
+ try {
103
+ const json = JSON.parse(output)
104
+ throw new Error(json.error?.message || json.error || 'Update failed')
105
+ } catch {
106
+ throw new Error(output || 'Update failed')
107
+ }
108
+ }
109
+
110
+ const output = JSON.parse(result.stdout.toString())
111
+
112
+ // Record the commit we just pushed from so a later pull can use it as the
113
+ // merge base regardless of what the server stores in
114
+ // bricks_project_last_commit_id (which may be an opaque nanoid for
115
+ // content-only edits).
116
+ if (commitId) await writeLastPushedCommit(cwd, commitId)
117
+
118
+ console.log(`${isModule ? 'Module' : 'App'} config updated: ${output.target?.name || app.name}`)
@@ -1,11 +1,19 @@
1
1
  // eslint-disable-next-line import/no-extraneous-dependencies
2
- import { app, BrowserWindow } from 'electron'
3
- import { readFile, writeFile } from 'fs/promises'
2
+ import { app, BrowserWindow, ipcMain } from 'electron'
3
+ import { mkdir, readdir, readFile, rm, writeFile } from 'fs/promises'
4
4
  import { watchFile } from 'fs'
5
5
  import { createServer } from 'http'
6
+ import path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
6
8
  import { parseArgs } from 'util'
7
9
  import * as TOON from '@toon-format/toon'
8
10
 
11
+ for (const stream of [process.stdout, process.stderr]) {
12
+ stream.on('error', (err) => {
13
+ if (err.code !== 'EPIPE') throw err
14
+ })
15
+ }
16
+
9
17
  const { values } = parseArgs({
10
18
  args: process.argv,
11
19
  options: {
@@ -22,6 +30,139 @@ const { values } = parseArgs({
22
30
  })
23
31
 
24
32
  const cwd = process.cwd()
33
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
34
+ const AUTOMATION_SCREENSHOT_PROJECT_DIR = path.join('.bricks', 'automation_screenshots')
35
+ const DEFAULT_SIMULATOR_SECURITY_SETTINGS = Object.freeze({
36
+ allowedInsecureHosts: [],
37
+ })
38
+
39
+ const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value)
40
+
41
+ const resolveAutomationScreenshotPath = (requestedPath = '') => {
42
+ const baseDir = path.resolve(cwd, AUTOMATION_SCREENSHOT_PROJECT_DIR)
43
+ const raw = String(requestedPath || AUTOMATION_SCREENSHOT_PROJECT_DIR)
44
+ const fromProject = path.resolve(cwd, raw)
45
+ const target =
46
+ fromProject === baseDir || fromProject.startsWith(`${baseDir}${path.sep}`)
47
+ ? fromProject
48
+ : path.resolve(baseDir, raw)
49
+
50
+ if (target !== baseDir && !target.startsWith(`${baseDir}${path.sep}`)) {
51
+ throw new Error('Automation screenshot path must stay inside the project screenshot directory.')
52
+ }
53
+ return target
54
+ }
55
+
56
+ const normalizeAutomationScreenshotEncoding = (encoding) =>
57
+ encoding === 'base64' ? 'base64' : 'utf8'
58
+
59
+ const registerAutomationScreenshotIpc = () => {
60
+ ipcMain.handle('bricks-automation-screenshots:exists', async (_event, filePath) => {
61
+ try {
62
+ await readFile(resolveAutomationScreenshotPath(filePath))
63
+ return true
64
+ } catch {
65
+ return false
66
+ }
67
+ })
68
+ ipcMain.handle('bricks-automation-screenshots:readdir', async (_event, dirPath) => {
69
+ try {
70
+ return await readdir(resolveAutomationScreenshotPath(dirPath))
71
+ } catch {
72
+ return []
73
+ }
74
+ })
75
+ ipcMain.handle('bricks-automation-screenshots:mkdir', async (_event, dirPath) => {
76
+ await mkdir(resolveAutomationScreenshotPath(dirPath), { recursive: true })
77
+ })
78
+ ipcMain.handle('bricks-automation-screenshots:unlink', async (_event, filePath) => {
79
+ await rm(resolveAutomationScreenshotPath(filePath), { recursive: true, force: true })
80
+ })
81
+ ipcMain.handle('bricks-automation-screenshots:read-file', async (_event, filePath, encoding) =>
82
+ readFile(
83
+ resolveAutomationScreenshotPath(filePath),
84
+ normalizeAutomationScreenshotEncoding(encoding),
85
+ ),
86
+ )
87
+ ipcMain.handle(
88
+ 'bricks-automation-screenshots:write-file',
89
+ async (_event, filePath, contents, encoding) => {
90
+ const target = resolveAutomationScreenshotPath(filePath)
91
+ await mkdir(path.dirname(target), { recursive: true })
92
+ await writeFile(target, contents, normalizeAutomationScreenshotEncoding(encoding))
93
+ },
94
+ )
95
+ }
96
+
97
+ const normalizeSimulatorSecuritySettings = (settings = {}) => {
98
+ const source = isObject(settings) ? settings : {}
99
+ const allowedHosts = Array.isArray(source.allowedInsecureHosts)
100
+ ? source.allowedInsecureHosts
101
+ : DEFAULT_SIMULATOR_SECURITY_SETTINGS.allowedInsecureHosts
102
+
103
+ return {
104
+ allowedInsecureHosts: allowedHosts
105
+ .filter((host) => typeof host === 'string')
106
+ .map((host) => host.trim().toLowerCase())
107
+ .filter(Boolean),
108
+ }
109
+ }
110
+
111
+ const normalizeAllowedConnection = (value) => {
112
+ const raw = String(value || '')
113
+ .trim()
114
+ .toLowerCase()
115
+ if (!raw) return null
116
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return { host: raw, protocol: null }
117
+ try {
118
+ const url = new URL(raw)
119
+ return {
120
+ host: url.host.toLowerCase(),
121
+ protocol: url.protocol,
122
+ }
123
+ } catch {
124
+ return null
125
+ }
126
+ }
127
+
128
+ const shouldBlockInsecureConnection = (rawUrl, settings = {}) => {
129
+ const normalized = normalizeSimulatorSecuritySettings(settings)
130
+
131
+ try {
132
+ const url = new URL(String(rawUrl || ''))
133
+ if (url.protocol !== 'http:' && url.protocol !== 'ws:') return false
134
+ const host = url.host.toLowerCase()
135
+ const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '')
136
+ return !normalized.allowedInsecureHosts.some((allowedHost) => {
137
+ const allowed = normalizeAllowedConnection(allowedHost)
138
+ if (!allowed) return false
139
+ if (allowed.protocol && allowed.protocol !== url.protocol) return false
140
+ return allowed.host === host || allowed.host === hostname
141
+ })
142
+ } catch {
143
+ return false
144
+ }
145
+ }
146
+
147
+ const getUnsecureConnectionHost = (rawUrl) => {
148
+ try {
149
+ const url = new URL(String(rawUrl || ''))
150
+ if (url.protocol !== 'http:' && url.protocol !== 'ws:') return null
151
+ return url.host.toLowerCase()
152
+ } catch {
153
+ return null
154
+ }
155
+ }
156
+
157
+ const readSimulatorProjectSettings = async () => {
158
+ try {
159
+ return normalizeSimulatorSecuritySettings(
160
+ JSON.parse(await readFile(`${cwd}/simulator.json`, 'utf8')),
161
+ )
162
+ } catch {
163
+ return normalizeSimulatorSecuritySettings()
164
+ }
165
+ }
25
166
 
26
167
  let takeScreenshotConfig = null
27
168
  try {
@@ -39,6 +180,7 @@ try {
39
180
  }
40
181
 
41
182
  let config = JSON.parse(await readFile(`${cwd}/.bricks/build/application-config.json`))
183
+ let simulatorSettings = await readSimulatorProjectSettings()
42
184
 
43
185
  // Resolve testId from testTitleLike
44
186
  let testId = values['test-id'] || null
@@ -71,6 +213,13 @@ const previewUrlMap = {
71
213
 
72
214
  const previewUrl = previewUrlMap[stage]
73
215
  if (!previewUrl) throw new Error(`Invalid BRICKS_STAGE: ${stage}`)
216
+ simulatorSettings = normalizeSimulatorSecuritySettings({
217
+ ...simulatorSettings,
218
+ allowedInsecureHosts: [
219
+ ...simulatorSettings.allowedInsecureHosts,
220
+ getUnsecureConnectionHost(previewUrl),
221
+ ].filter(Boolean),
222
+ })
74
223
 
75
224
  // --- CDP WebSocket Server ---
76
225
  // Bridges external CDP clients to the preview's postMessage-based CDP bridge.
@@ -136,9 +285,9 @@ const startCdpServer = async (mainWindow, port) => {
136
285
  res.end(
137
286
  JSON.stringify([
138
287
  {
139
- description: 'BRICKS Preview (CTOR Preview)',
140
- id: 'bricks-preview',
141
- title: 'BRICKS Preview',
288
+ description: 'BRICKS Simulator',
289
+ id: 'bricks-simulator',
290
+ title: 'BRICKS Simulator',
142
291
  type: 'page',
143
292
  url: previewUrl,
144
293
  webSocketDebuggerUrl: `ws://localhost:${actualPort}/ws`,
@@ -149,7 +298,7 @@ const startCdpServer = async (mainWindow, port) => {
149
298
  }
150
299
  if (req.url === '/json/version') {
151
300
  res.writeHead(200, { 'Content-Type': 'application/json' })
152
- res.end(JSON.stringify({ Browser: 'BRICKS Preview', 'Protocol-Version': '1.3' }))
301
+ res.end(JSON.stringify({ Browser: 'BRICKS Simulator', 'Protocol-Version': '1.3' }))
153
302
  return
154
303
  }
155
304
  // bricks-cli discovery endpoint
@@ -157,7 +306,7 @@ const startCdpServer = async (mainWindow, port) => {
157
306
  res.writeHead(200, { 'Content-Type': 'application/json' })
158
307
  res.end(
159
308
  JSON.stringify({
160
- name: 'BRICKS Preview',
309
+ name: 'BRICKS Simulator',
161
310
  port: actualPort,
162
311
  protocols: ['cdp'],
163
312
  hasPasscode: false,
@@ -200,6 +349,7 @@ const startCdpServer = async (mainWindow, port) => {
200
349
  }
201
350
 
202
351
  app.on('ready', () => {
352
+ registerAutomationScreenshotIpc()
203
353
  let show = true
204
354
  if (takeScreenshotConfig && !takeScreenshotConfig.noHeadless) show = false
205
355
  const mainWindow = new BrowserWindow({
@@ -207,8 +357,19 @@ app.on('ready', () => {
207
357
  height: takeScreenshotConfig?.height || 768,
208
358
  frame: !takeScreenshotConfig,
209
359
  show,
360
+ webPreferences: {
361
+ preload: path.join(__dirname, 'simulator-preload.cjs'),
362
+ contextIsolation: true,
363
+ nodeIntegration: false,
364
+ },
210
365
  })
211
366
  mainWindow.setBackgroundColor('#333')
367
+ mainWindow.webContents.session.webRequest.onBeforeRequest(
368
+ { urls: ['http://*/*', 'ws://*/*'] },
369
+ (details, callback) => {
370
+ callback({ cancel: shouldBlockInsecureConnection(details.url, simulatorSettings) })
371
+ },
372
+ )
212
373
  mainWindow.loadURL(previewUrl)
213
374
 
214
375
  const sendConfig = () => {
@@ -224,16 +385,23 @@ app.on('ready', () => {
224
385
  )
225
386
  if (takeScreenshotConfig) {
226
387
  const { delay, width, height, path } = takeScreenshotConfig
227
- setTimeout(() => {
388
+ setTimeout(async () => {
389
+ let screenshotFailed = false
228
390
  console.log('Taking screenshot')
229
- mainWindow.webContents.capturePage().then((image) => {
391
+ try {
392
+ const image = await mainWindow.webContents.capturePage()
230
393
  console.log('Writing screenshot to', path)
231
- writeFile(path, image.resize({ width, height }).toJPEG(75))
394
+ await writeFile(path, image.resize({ width, height }).toJPEG(75))
395
+ } catch (err) {
396
+ screenshotFailed = true
397
+ console.error('Screenshot capture/write failed', err)
398
+ } finally {
232
399
  if (noKeepOpen) {
233
400
  console.log('Closing app')
234
- app.quit()
401
+ if (screenshotFailed) app.exit(1)
402
+ else app.quit()
235
403
  }
236
- })
404
+ }
237
405
  }, delay)
238
406
  }
239
407
  }
@@ -287,7 +455,34 @@ app.on('ready', () => {
287
455
  async () => {
288
456
  console.log('Detected config changed')
289
457
  config = JSON.parse(await readFile(`${cwd}/.bricks/build/application-config.json`))
458
+ const nextSimulatorSettings = await readSimulatorProjectSettings()
459
+ simulatorSettings = normalizeSimulatorSecuritySettings({
460
+ ...nextSimulatorSettings,
461
+ allowedInsecureHosts: [
462
+ ...nextSimulatorSettings.allowedInsecureHosts,
463
+ getUnsecureConnectionHost(previewUrl),
464
+ ].filter(Boolean),
465
+ })
290
466
  sendConfig()
291
467
  },
292
468
  )
469
+ watchFile(
470
+ `${cwd}/simulator.json`,
471
+ {
472
+ bigint: false,
473
+ persistent: true,
474
+ interval: 1000,
475
+ },
476
+ async () => {
477
+ console.log('Detected simulator settings changed')
478
+ const nextSimulatorSettings = await readSimulatorProjectSettings()
479
+ simulatorSettings = normalizeSimulatorSecuritySettings({
480
+ ...nextSimulatorSettings,
481
+ allowedInsecureHosts: [
482
+ ...nextSimulatorSettings.allowedInsecureHosts,
483
+ getUnsecureConnectionHost(previewUrl),
484
+ ].filter(Boolean),
485
+ })
486
+ },
487
+ )
293
488
  })
@@ -0,0 +1,16 @@
1
+ const { contextBridge, ipcRenderer } = require('electron')
2
+
3
+ const AUTOMATION_SCREENSHOT_ROOT = '.bricks/automation_screenshots'
4
+
5
+ contextBridge.exposeInMainWorld('BRICKS_AUTOMATION_SCREENSHOTS', {
6
+ rootDir: AUTOMATION_SCREENSHOT_ROOT,
7
+ exists: (filePath) => ipcRenderer.invoke('bricks-automation-screenshots:exists', filePath),
8
+ readdir: (dirPath) => ipcRenderer.invoke('bricks-automation-screenshots:readdir', dirPath),
9
+ readDir: (dirPath) => ipcRenderer.invoke('bricks-automation-screenshots:readdir', dirPath),
10
+ mkdir: (dirPath) => ipcRenderer.invoke('bricks-automation-screenshots:mkdir', dirPath),
11
+ unlink: (filePath) => ipcRenderer.invoke('bricks-automation-screenshots:unlink', filePath),
12
+ readFile: (filePath, encoding) =>
13
+ ipcRenderer.invoke('bricks-automation-screenshots:read-file', filePath, encoding),
14
+ writeFile: (filePath, contents, encoding) =>
15
+ ipcRenderer.invoke('bricks-automation-screenshots:write-file', filePath, contents, encoding),
16
+ })
@@ -98,7 +98,7 @@ const cleanupDevtoolsInfo = () => {
98
98
  } catch {}
99
99
  }
100
100
 
101
- // Kill existing preview process if devtools.json contains a stale pid
101
+ // Kill existing simulator process if devtools.json contains a stale pid
102
102
  try {
103
103
  const devtoolsInfo = JSON.parse(await readFile(devtoolsInfoPath, 'utf8'))
104
104
  if (devtoolsInfo.pid) {
@@ -111,7 +111,7 @@ try {
111
111
 
112
112
  const proc = spawn(
113
113
  'bunx',
114
- ['--bun', 'electron', `${import.meta.dirname}/preview-main.mjs`, ...args],
114
+ ['--bun', 'electron', `${import.meta.dirname}/simulator-main.mjs`, ...args],
115
115
  {
116
116
  env: { ...process.env, BRICKS_STAGE: app.stage || 'production' },
117
117
  stdio: ['inherit', 'pipe', 'inherit'],
@@ -127,14 +127,14 @@ rl.on('line', (line) => {
127
127
  return
128
128
  }
129
129
  if (!line) return
130
- // Detect CDP server startup from preview-main output
130
+ // Detect CDP server startup from simulator-main output
131
131
  const cdpMatch = line.match(/^CDP server: ws:\/\/localhost:(\d+)/)
132
132
  if (cdpMatch) {
133
133
  const info = {
134
134
  port: parseInt(cdpMatch[1], 10),
135
135
  pid: proc.pid,
136
136
  address: 'localhost',
137
- name: `${app.name || 'Unnamed'} (CTOR Preview)`,
137
+ name: `${app.name || 'Unnamed'} (CTOR Simulator)`,
138
138
  startedAt: new Date().toISOString(),
139
139
  }
140
140
  writeFile(devtoolsInfoPath, JSON.stringify(info, null, 2))
@@ -44,10 +44,21 @@ export interface AnimationTimingConfig {
44
44
  export interface AnimationSpringConfig {
45
45
  __type: 'AnimationSpringConfig'
46
46
  toValue: number // BRICKS Grid unit
47
- friction: number
48
- tension: number
49
- speed: number
50
- bounciness: number
47
+ // Use one spring parameter family: tension/friction, speed/bounciness,
48
+ // or stiffness/damping/mass.
49
+ friction?: number
50
+ tension?: number
51
+ speed?: number
52
+ bounciness?: number
53
+ stiffness?: number
54
+ damping?: number
55
+ mass?: number
56
+ velocity?: number
57
+ delay?: number
58
+ isInteraction?: boolean
59
+ overshootClamping?: boolean
60
+ restDisplacementThreshold?: number
61
+ restSpeedThreshold?: number
51
62
  }
52
63
 
53
64
  export interface AnimationDecayConfig {
@@ -62,7 +73,7 @@ export interface AnimationDef {
62
73
  __typename: 'Animation'
63
74
  id: string
64
75
  alias?: string
65
- title: string
76
+ title?: string
66
77
  description?: string
67
78
  hideShortRef?: boolean
68
79
  runType?: 'once' | 'loop'
@@ -93,8 +104,13 @@ export interface AnimationComposeDef {
93
104
 
94
105
  export type Animation = AnimationDef | AnimationComposeDef
95
106
 
107
+ // Animation event handlers accept either a direct Animation or a getter that
108
+ // returns one. The getter form is useful for lazy/forward references between
109
+ // animations defined across files.
110
+ export type AnimationOrGetter = Animation | (() => Animation)
111
+
96
112
  export interface AnimationBasicEvents {
97
- showStart?: Animation
98
- standby?: Animation
99
- breatheStart?: Animation
113
+ showStart?: AnimationOrGetter
114
+ standby?: AnimationOrGetter
115
+ breatheStart?: AnimationOrGetter
100
116
  }
@@ -83,9 +83,22 @@ export type TestMethodName =
83
83
  | 'delay'
84
84
 
85
85
  /**
86
- * Run array types for each test method
87
- * Each method has a specific signature: [methodName, ...args]
86
+ * Union type for all test method run arrays
88
87
  */
88
+ export type TestMethodRun =
89
+ | TestMethodRunBrickPress
90
+ | TestMethodRunBrickExists
91
+ | TestMethodRunWaitUntilBrickExists
92
+ | TestMethodRunWaitUntilEventTrigger
93
+ | TestMethodRunWaitUntilCanvasChange
94
+ | TestMethodRunKeydown
95
+ | TestMethodRunKeyup
96
+ | TestMethodRunHttpRequest
97
+ | TestMethodRunAssertProperty
98
+ | TestMethodRunWaitUntilPropertyChange
99
+ | TestMethodRunExecuteAction
100
+ | TestMethodRunMatchScreenshot
101
+ | TestMethodRunDelay
89
102
 
90
103
  // [methodName, subspaceId, brickId, options?]
91
104
  export type TestMethodRunBrickPress = ['brick_press', SubspaceRef, BrickRef, Record<string, any>?]
@@ -157,24 +170,6 @@ export type TestMethodRunMatchScreenshot = ['match_screenshot', string, number?,
157
170
  // [methodName, subspaceId?, propertyId?, defaultValue?]
158
171
  export type TestMethodRunDelay = ['delay', SubspaceRef?, DataRef?, number?]
159
172
 
160
- /**
161
- * Union type for all test method run arrays
162
- */
163
- export type TestMethodRun =
164
- | TestMethodRunBrickPress
165
- | TestMethodRunBrickExists
166
- | TestMethodRunWaitUntilBrickExists
167
- | TestMethodRunWaitUntilEventTrigger
168
- | TestMethodRunWaitUntilCanvasChange
169
- | TestMethodRunKeydown
170
- | TestMethodRunKeyup
171
- | TestMethodRunHttpRequest
172
- | TestMethodRunAssertProperty
173
- | TestMethodRunWaitUntilPropertyChange
174
- | TestMethodRunExecuteAction
175
- | TestMethodRunMatchScreenshot
176
- | TestMethodRunDelay
177
-
178
173
  /**
179
174
  * Test case definition
180
175
  */
@@ -203,6 +198,7 @@ export type LocalSyncMode = 'main-only' | 'minor-only'
203
198
  export interface AutomationTest {
204
199
  __typename: 'AutomationTest'
205
200
  id: string
201
+ alias?: string
206
202
  title: string
207
203
  hideShortRef?: boolean
208
204
  timeout: number
@@ -1,7 +1,7 @@
1
1
  /* Auto generated by build script */
2
2
  import type { SwitchCondInnerStateCurrentCanvas, SwitchCondData, SwitchDef } from './switch'
3
3
  import type { Data, DataLink } from './data'
4
- import type { Animation, AnimationBasicEvents } from './animation'
4
+ import type { Animation, AnimationBasicEvents, AnimationOrGetter } from './animation'
5
5
  import type {
6
6
  Brick,
7
7
  EventAction,
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { SwitchCondInnerStateCurrentCanvas, SwitchCondData, SwitchDef } from '../switch'
6
6
  import type { Data, DataLink } from '../data'
7
- import type { Animation, AnimationBasicEvents } from '../animation'
7
+ import type { Animation, AnimationBasicEvents, AnimationOrGetter } from '../animation'
8
8
  import type {
9
9
  Brick,
10
10
  EventAction,
@@ -213,13 +213,13 @@ Default property:
213
213
  faceDetected?: () => Data<Array<{ [key: string]: any }>>
214
214
  }
215
215
  animation?: AnimationBasicEvents & {
216
- stateChange?: Animation
217
- recordStart?: Animation
218
- recordEnd?: Animation
219
- barcodeRead?: Animation
220
- pictureTaken?: Animation
221
- recordFinish?: Animation
222
- mountError?: Animation
216
+ stateChange?: AnimationOrGetter
217
+ recordStart?: AnimationOrGetter
218
+ recordEnd?: AnimationOrGetter
219
+ barcodeRead?: AnimationOrGetter
220
+ pictureTaken?: AnimationOrGetter
221
+ recordFinish?: AnimationOrGetter
222
+ mountError?: AnimationOrGetter
223
223
  }
224
224
  }
225
225
 
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { SwitchCondInnerStateCurrentCanvas, SwitchCondData, SwitchDef } from '../switch'
6
6
  import type { Data, DataLink } from '../data'
7
- import type { Animation, AnimationBasicEvents } from '../animation'
7
+ import type { Animation, AnimationBasicEvents, AnimationOrGetter } from '../animation'
8
8
  import type {
9
9
  Brick,
10
10
  EventAction,
@@ -343,9 +343,9 @@ Default property:
343
343
  >
344
344
  }
345
345
  animation?: AnimationBasicEvents & {
346
- onRender?: Animation
347
- onPress?: Animation
348
- onLegendSelectChanged?: Animation
346
+ onRender?: AnimationOrGetter
347
+ onPress?: AnimationOrGetter
348
+ onLegendSelectChanged?: AnimationOrGetter
349
349
  }
350
350
  }
351
351