@fugood/bricks-project 2.24.0-beta.2 → 2.24.0-beta.21

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 (110) hide show
  1. package/compile/action-name-map.ts +14 -0
  2. package/compile/index.ts +377 -129
  3. package/package.json +8 -3
  4. package/skills/bricks-project/rules/architecture-patterns.md +7 -0
  5. package/skills/bricks-project/rules/automations.md +74 -28
  6. package/skills/bricks-project/rules/buttress.md +9 -6
  7. package/tools/deploy.ts +39 -10
  8. package/tools/mcp-server.ts +10 -877
  9. package/tools/mcp-tools/compile.ts +91 -0
  10. package/tools/mcp-tools/huggingface.ts +762 -0
  11. package/tools/mcp-tools/icons.ts +60 -0
  12. package/tools/mcp-tools/lottie.ts +102 -0
  13. package/tools/mcp-tools/media.ts +110 -0
  14. package/tools/postinstall.ts +121 -33
  15. package/tools/preview-main.mjs +12 -8
  16. package/tools/pull.ts +37 -19
  17. package/tsconfig.json +16 -0
  18. package/types/animation.ts +4 -0
  19. package/types/automation.ts +3 -0
  20. package/types/brick-base.ts +1 -1
  21. package/types/bricks/Camera.ts +34 -7
  22. package/types/bricks/Chart.ts +1 -1
  23. package/types/bricks/GenerativeMedia.ts +6 -6
  24. package/types/bricks/Icon.ts +3 -3
  25. package/types/bricks/Image.ts +4 -4
  26. package/types/bricks/Items.ts +7 -7
  27. package/types/bricks/Lottie.ts +4 -4
  28. package/types/bricks/Maps.ts +4 -4
  29. package/types/bricks/QrCode.ts +4 -4
  30. package/types/bricks/Rect.ts +4 -4
  31. package/types/bricks/RichText.ts +3 -3
  32. package/types/bricks/Rive.ts +1 -1
  33. package/types/bricks/Slideshow.ts +4 -4
  34. package/types/bricks/Svg.ts +3 -3
  35. package/types/bricks/Text.ts +4 -4
  36. package/types/bricks/TextInput.ts +11 -7
  37. package/types/bricks/Video.ts +4 -4
  38. package/types/bricks/VideoStreaming.ts +3 -3
  39. package/types/bricks/WebRtcStream.ts +1 -1
  40. package/types/bricks/WebView.ts +4 -4
  41. package/types/canvas.ts +4 -2
  42. package/types/common.ts +9 -4
  43. package/types/data-calc-command.ts +2 -0
  44. package/types/data-calc.ts +1 -0
  45. package/types/data.ts +2 -0
  46. package/types/generators/AlarmClock.ts +5 -5
  47. package/types/generators/Assistant.ts +57 -12
  48. package/types/generators/BleCentral.ts +12 -4
  49. package/types/generators/BlePeripheral.ts +5 -5
  50. package/types/generators/CanvasMap.ts +4 -4
  51. package/types/generators/CastlesPay.ts +3 -3
  52. package/types/generators/DataBank.ts +31 -4
  53. package/types/generators/File.ts +63 -14
  54. package/types/generators/GraphQl.ts +3 -3
  55. package/types/generators/Http.ts +27 -8
  56. package/types/generators/HttpServer.ts +9 -9
  57. package/types/generators/Information.ts +2 -2
  58. package/types/generators/Intent.ts +8 -2
  59. package/types/generators/Iterator.ts +6 -6
  60. package/types/generators/Keyboard.ts +18 -8
  61. package/types/generators/LlmAnthropicCompat.ts +12 -6
  62. package/types/generators/LlmAppleBuiltin.ts +6 -6
  63. package/types/generators/LlmGgml.ts +75 -25
  64. package/types/generators/LlmMlx.ts +210 -0
  65. package/types/generators/LlmOnnx.ts +18 -9
  66. package/types/generators/LlmOpenAiCompat.ts +22 -6
  67. package/types/generators/LlmQualcommAiEngine.ts +32 -8
  68. package/types/generators/Mcp.ts +332 -17
  69. package/types/generators/McpServer.ts +38 -11
  70. package/types/generators/MediaFlow.ts +26 -8
  71. package/types/generators/MqttBroker.ts +10 -4
  72. package/types/generators/MqttClient.ts +11 -5
  73. package/types/generators/Question.ts +6 -6
  74. package/types/generators/RealtimeTranscription.ts +70 -11
  75. package/types/generators/RerankerGgml.ts +23 -9
  76. package/types/generators/SerialPort.ts +6 -6
  77. package/types/generators/SoundPlayer.ts +2 -2
  78. package/types/generators/SoundRecorder.ts +5 -5
  79. package/types/generators/SpeechToTextGgml.ts +34 -14
  80. package/types/generators/SpeechToTextOnnx.ts +8 -8
  81. package/types/generators/SpeechToTextPlatform.ts +4 -4
  82. package/types/generators/SqLite.ts +10 -6
  83. package/types/generators/Step.ts +3 -3
  84. package/types/generators/SttAppleBuiltin.ts +6 -6
  85. package/types/generators/Tcp.ts +5 -5
  86. package/types/generators/TcpServer.ts +7 -7
  87. package/types/generators/TextToSpeechApple.ts +1 -1
  88. package/types/generators/TextToSpeechAppleBuiltin.ts +5 -5
  89. package/types/generators/TextToSpeechGgml.ts +8 -8
  90. package/types/generators/TextToSpeechOnnx.ts +9 -9
  91. package/types/generators/TextToSpeechOpenAiLike.ts +5 -5
  92. package/types/generators/ThermalPrinter.ts +6 -6
  93. package/types/generators/Tick.ts +3 -3
  94. package/types/generators/Udp.ts +9 -4
  95. package/types/generators/VadGgml.ts +39 -10
  96. package/types/generators/VadOnnx.ts +31 -8
  97. package/types/generators/VadTraditional.ts +15 -9
  98. package/types/generators/VectorStore.ts +26 -9
  99. package/types/generators/Watchdog.ts +11 -6
  100. package/types/generators/WebCrawler.ts +5 -5
  101. package/types/generators/WebRtc.ts +17 -11
  102. package/types/generators/WebSocket.ts +5 -5
  103. package/types/generators/index.ts +1 -0
  104. package/types/subspace.ts +1 -0
  105. package/types/system.ts +1 -1
  106. package/utils/calc.ts +12 -8
  107. package/utils/event-props.ts +104 -87
  108. package/utils/id.ts +4 -0
  109. package/api/index.ts +0 -1
  110. package/api/instance.ts +0 -213
@@ -0,0 +1,60 @@
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
+ const iconMeta = glyphmapMeta as Record<IconStyle, string[]>
10
+
11
+ const iconList = Object.entries(glyphmap as Record<string, number>).map(([name, code]) => {
12
+ const styles = (Object.keys(iconMeta) as IconStyle[]).filter((style) =>
13
+ iconMeta[style].includes(name),
14
+ )
15
+ return { name, code, styles }
16
+ })
17
+
18
+ const iconFuse = new Fuse(iconList, {
19
+ keys: ['name'],
20
+ threshold: 0.3,
21
+ includeScore: true,
22
+ })
23
+
24
+ export function register(server: McpServer) {
25
+ server.tool(
26
+ 'icon_search',
27
+ {
28
+ query: z.string().describe('Search keywords for FontAwesome 6 Pro icons'),
29
+ limit: z.number().min(1).max(100).optional().default(10),
30
+ style: z
31
+ .enum(['brands', 'duotone', 'light', 'regular', 'solid', 'thin'])
32
+ .optional()
33
+ .describe('Filter by icon style'),
34
+ },
35
+ async ({ query, limit, style }) => {
36
+ let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
37
+
38
+ if (style) {
39
+ results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
40
+ }
41
+
42
+ const icons = results.map((r) => ({
43
+ name: r.item.name,
44
+ code: r.item.code,
45
+ unicode: `U+${r.item.code.toString(16).toUpperCase()}`,
46
+ styles: r.item.styles,
47
+ score: r.score,
48
+ }))
49
+
50
+ return {
51
+ content: [
52
+ {
53
+ type: 'text',
54
+ text: TOON.encode({ count: icons.length, icons }),
55
+ },
56
+ ],
57
+ }
58
+ },
59
+ )
60
+ }
@@ -0,0 +1,102 @@
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
+ }
@@ -0,0 +1,110 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { z } from 'zod'
3
+ import { $ } from 'bun'
4
+
5
+ const runBricks = async (projectDir: string, ...args: string[]) => {
6
+ try {
7
+ return await $`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,8 +1,25 @@
1
1
  import { $ } from 'bun'
2
- import { stat, readFile, writeFile, readdir } from 'fs/promises'
2
+ import {
3
+ cp,
4
+ lstat,
5
+ mkdir,
6
+ readFile,
7
+ readdir,
8
+ readlink,
9
+ rm,
10
+ stat,
11
+ symlink,
12
+ writeFile,
13
+ } from 'fs/promises'
14
+ import * as path from 'path'
3
15
  import TOML from '@iarna/toml'
4
16
 
5
17
  const cwd = process.cwd()
18
+ const projectSkillsDir = path.join(cwd, '.bricks', 'skills')
19
+ const compatibilitySkillLinks = [
20
+ path.join(cwd, '.claude', 'skills'),
21
+ path.join(cwd, '.codex', 'skills'),
22
+ ]
6
23
 
7
24
  async function exists(f: string) {
8
25
  try {
@@ -13,6 +30,15 @@ async function exists(f: string) {
13
30
  }
14
31
  }
15
32
 
33
+ async function pathExists(f: string) {
34
+ try {
35
+ await lstat(f)
36
+ return true
37
+ } catch {
38
+ return false
39
+ }
40
+ }
41
+
16
42
  // handle flag --skip-copy
17
43
  const skipCopyProject = process.argv.includes('--skip-copy-project')
18
44
  if (skipCopyProject) {
@@ -30,6 +56,11 @@ const projectMcpServer = {
30
56
  args: [`${cwd}/node_modules/@fugood/bricks-project/tools/mcp-server.ts`],
31
57
  }
32
58
 
59
+ type CodexMcpConfig = {
60
+ mcp_servers: Record<string, typeof projectMcpServer>
61
+ }
62
+
63
+ // Claude Code and AGENTS.md projects both use the shared project .mcp.json file.
33
64
  const defaultMcpConfig = {
34
65
  mcpServers: {
35
66
  'bricks-project': projectMcpServer,
@@ -44,7 +75,7 @@ const handleMcpConfigOverride = async (mcpConfigPath: string) => {
44
75
  mcpConfig = JSON.parse(configStr)
45
76
  if (!mcpConfig?.mcpServers) throw new Error('mcpServers is not defined')
46
77
  mcpConfig.mcpServers['bricks-project'] = projectMcpServer
47
- } catch (e) {
78
+ } catch {
48
79
  mcpConfig = defaultMcpConfig
49
80
  }
50
81
  } else {
@@ -60,43 +91,99 @@ const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
60
91
  const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)
61
92
 
62
93
  if (hasClaudeCode || hasAgentsMd) {
94
+ // Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
63
95
  const mcpConfigPath = `${cwd}/.mcp.json`
64
96
  await handleMcpConfigOverride(mcpConfigPath)
65
97
  }
66
98
 
67
- const setupSkills = async (skillsDir) => {
99
+ const copyMissingSkills = async (sourceDir: string, targetDir: string) => {
100
+ if (!(await exists(sourceDir))) return
101
+
102
+ const packageSkills = await readdir(sourceDir, { withFileTypes: true })
103
+ const skillsToInstall = packageSkills.filter(
104
+ (entry) => entry.isDirectory() && !entry.name.startsWith('.'),
105
+ )
106
+
107
+ await mkdir(targetDir, { recursive: true })
108
+
109
+ await Promise.all(
110
+ skillsToInstall.map(async (entry) => {
111
+ const targetSkillDir = path.join(targetDir, entry.name)
112
+ if (await exists(targetSkillDir)) {
113
+ console.log(`Skill '${entry.name}' already exists, skipping`)
114
+ } else {
115
+ await cp(path.join(sourceDir, entry.name), targetSkillDir, { recursive: true })
116
+ console.log(`Installed skill '${entry.name}' to ${targetDir}/`)
117
+ }
118
+ }),
119
+ )
120
+ }
121
+
122
+ const migrateSkillsDir = async (legacySkillsDir: string, canonicalSkillsDir: string) => {
123
+ if (!(await pathExists(legacySkillsDir))) return
124
+
125
+ const legacyStats = await lstat(legacySkillsDir)
126
+
127
+ if (legacyStats.isSymbolicLink()) {
128
+ const linkTarget = await readlink(legacySkillsDir)
129
+ const resolvedTarget = path.resolve(path.dirname(legacySkillsDir), linkTarget)
130
+ if (resolvedTarget === canonicalSkillsDir) return
131
+
132
+ await copyMissingSkills(resolvedTarget, canonicalSkillsDir)
133
+ await rm(legacySkillsDir, { force: true, recursive: true })
134
+ return
135
+ }
136
+
137
+ if (legacyStats.isDirectory()) {
138
+ await copyMissingSkills(legacySkillsDir, canonicalSkillsDir)
139
+ await rm(legacySkillsDir, { force: true, recursive: true })
140
+ return
141
+ }
142
+
143
+ console.warn(`Skipping skills migration for ${legacySkillsDir}; expected a directory or symlink`)
144
+ }
145
+
146
+ const ensureCompatibilitySkillLink = async (linkPath: string, targetDir: string) => {
147
+ await mkdir(path.dirname(linkPath), { recursive: true })
148
+
149
+ if (await pathExists(linkPath)) {
150
+ const linkStats = await lstat(linkPath)
151
+ if (linkStats.isSymbolicLink()) {
152
+ const linkTarget = await readlink(linkPath)
153
+ const resolvedTarget = path.resolve(path.dirname(linkPath), linkTarget)
154
+ if (resolvedTarget === targetDir) return
155
+ } else {
156
+ console.warn(
157
+ `Skipping skills symlink at ${linkPath}; path already exists and is not a symlink`,
158
+ )
159
+ return
160
+ }
161
+ }
162
+
163
+ const relativeTarget = path.relative(path.dirname(linkPath), targetDir)
164
+ const symlinkType = process.platform === 'win32' ? 'junction' : 'dir'
165
+ await symlink(relativeTarget, linkPath, symlinkType)
166
+ console.log(`Linked ${linkPath} -> ${relativeTarget}`)
167
+ }
168
+
169
+ const setupSkills = async () => {
68
170
  const packageSkillsDir = `${__dirname}/../skills`
171
+ await mkdir(projectSkillsDir, { recursive: true })
172
+ await copyMissingSkills(packageSkillsDir, projectSkillsDir)
69
173
 
70
- if (await exists(packageSkillsDir)) {
71
- const packageSkills = await readdir(packageSkillsDir)
72
- const skillsToInstall = packageSkills.filter((skill) => !skill.startsWith('.'))
73
-
74
- await $`mkdir -p ${skillsDir}`
75
-
76
- await Promise.all(
77
- skillsToInstall.map(async (skill) => {
78
- const targetSkillDir = `${skillsDir}/${skill}`
79
- if (await exists(targetSkillDir)) {
80
- console.log(`Skill '${skill}' already exists, skipping`)
81
- } else {
82
- await $`cp -r ${packageSkillsDir}/${skill} ${targetSkillDir}`
83
- console.log(`Installed skill '${skill}' to ${skillsDir}/`)
84
- }
85
- }),
86
- )
174
+ for (const linkPath of compatibilitySkillLinks) {
175
+ await migrateSkillsDir(linkPath, projectSkillsDir)
176
+ await ensureCompatibilitySkillLink(linkPath, projectSkillsDir)
87
177
  }
88
178
  }
89
179
 
90
- if (hasClaudeCode) {
91
- // Install skills that don't already exist in the project
92
- await setupSkills(`${cwd}/.claude/skills`)
180
+ if (hasClaudeCode || hasAgentsMd) {
181
+ // Install project skills once and expose them through compatibility symlinks.
182
+ await setupSkills()
93
183
  }
94
184
 
95
185
  if (hasAgentsMd) {
96
- // Handle codex skills
97
- // Currently no signal file for codex skills, so we just check if AGENTS.md exists
98
- await setupSkills(`${cwd}/.codex/skills`)
99
-
186
+ // Codex stores its project-local MCP config in .codex/config.toml.
100
187
  const defaultCodexMcpConfig = {
101
188
  mcp_servers: {
102
189
  'bricks-project': projectMcpServer,
@@ -104,26 +191,27 @@ if (hasAgentsMd) {
104
191
  }
105
192
 
106
193
  const handleCodexMcpConfigOverride = async (mcpConfigPath: string) => {
107
- let mcpConfig: { mcp_servers: Record<string, typeof projectMcpServer> } | null = null
194
+ let mcpConfig: CodexMcpConfig | null = null
108
195
  if (await exists(mcpConfigPath)) {
109
196
  const configStr = await readFile(mcpConfigPath, 'utf-8')
110
197
  try {
111
- mcpConfig = TOML.parse(configStr)
112
- if (!mcpConfig?.mcp_servers) throw new Error('mcp_servers is not defined')
198
+ const parsed = TOML.parse(configStr) as Partial<CodexMcpConfig>
199
+ if (!parsed?.mcp_servers) throw new Error('mcp_servers is not defined')
200
+ mcpConfig = { mcp_servers: parsed.mcp_servers }
113
201
  mcpConfig.mcp_servers['bricks-project'] = projectMcpServer
114
- } catch (e) {
202
+ } catch {
115
203
  mcpConfig = defaultCodexMcpConfig
116
204
  }
117
205
  } else {
118
206
  mcpConfig = defaultCodexMcpConfig
119
207
  }
120
208
 
121
- await writeFile(mcpConfigPath, `${TOML.stringify(mcpConfig, null, 2)}\n`)
209
+ await writeFile(mcpConfigPath, `${TOML.stringify(mcpConfig)}\n`)
122
210
 
123
211
  console.log(`Updated ${mcpConfigPath}`)
124
212
  }
125
213
 
126
- // Setup MCP config (.codex/config.toml)
214
+ // Keep the Codex TOML MCP config aligned with the same bricks-project server entry.
127
215
  const codexConfigPath = `${cwd}/.codex/config.toml`
128
216
  await handleCodexMcpConfigOverride(codexConfigPath)
129
217
  }
@@ -42,13 +42,17 @@ let config = JSON.parse(await readFile(`${cwd}/.bricks/build/application-config.
42
42
  let testId = values['test-id'] || null
43
43
  if (!testId && values['test-title-like']) {
44
44
  const titleLike = values['test-title-like'].toLowerCase()
45
- const testMap = config.test_map || {}
46
- const found = Object.entries(testMap).find(([, test]) =>
47
- test.title?.toLowerCase().includes(titleLike),
48
- )
49
- if (found) {
50
- ;[testId] = found
51
- } else {
45
+ const automationMap = config.automation_map || {}
46
+ for (const group of Object.values(automationMap)) {
47
+ const found = Object.entries(group.map || {}).find(([, test]) =>
48
+ test.title?.toLowerCase().includes(titleLike),
49
+ )
50
+ if (found) {
51
+ ;[testId] = found
52
+ break
53
+ }
54
+ }
55
+ if (!testId) {
52
56
  throw new Error(`No automation found matching title: ${values['test-title-like']}`)
53
57
  }
54
58
  }
@@ -124,7 +128,7 @@ app.on('ready', () => {
124
128
 
125
129
  // Capture console messages from the preview
126
130
  if (testId) {
127
- mainWindow.webContents.on('console-message', (_, __, message) => {
131
+ mainWindow.webContents.on('console-message', (_, { message }) => {
128
132
  if (message.startsWith('[TEST_RESULT]')) {
129
133
  const data = JSON.parse(message.replace('[TEST_RESULT]', ''))
130
134
  console.log(`[TEST_RESULT_TOON]${TOON.encode(data.result)}`)
package/tools/pull.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { $ } from 'bun'
2
- import { format } from 'prettier'
2
+ import { format } from 'oxfmt'
3
3
 
4
4
  const cwd = process.cwd()
5
+ const args = process.argv.slice(2)
6
+ const force = args.includes('--force') || args.includes('-f')
5
7
 
6
8
  // Check git status
7
9
  const { exitCode } = await $`cd ${cwd} && git status`.nothrow()
@@ -9,8 +11,15 @@ const isGitRepo = exitCode === 0
9
11
 
10
12
  if (isGitRepo) {
11
13
  const unstagedChanges = await $`cd ${cwd} && git diff --name-only --diff-filter=ACMR`.text()
12
- if (unstagedChanges)
13
- throw new Error('Unstaged changes found, please commit or stash your changes before pulling')
14
+ if (unstagedChanges) {
15
+ if (force) {
16
+ console.log('Force mode: committing unstaged changes before pull...')
17
+ await $`cd ${cwd} && git add .`
18
+ await $`cd ${cwd} && git commit -m ${'chore(force-pull): saved unstaged changes before pull'}`
19
+ } else {
20
+ throw new Error('Unstaged changes found, please commit or stash your changes before pulling')
21
+ }
22
+ }
14
23
  } else {
15
24
  const confirmContinue = prompt(
16
25
  'No git repository found, so it will not be safe to pull, continue? (y/n)',
@@ -25,6 +34,7 @@ const isModule = app.type === 'module'
25
34
  const command = isModule ? 'module' : 'app'
26
35
 
27
36
  // Fetch project files using CLI
37
+ console.log(`Pulling ${command} project (${app.id})...`)
28
38
  const result = await $`bricks ${command} project-pull ${app.id} --json`.quiet().nothrow()
29
39
 
30
40
  if (result.exitCode !== 0) {
@@ -40,7 +50,8 @@ if (result.exitCode !== 0) {
40
50
  const { files, lastCommitId } = JSON.parse(result.stdout.toString())
41
51
 
42
52
  let useMain = false
43
- if (isGitRepo) {
53
+ if (isGitRepo && !force) {
54
+ console.log(`Checking commit ${lastCommitId}...`)
44
55
  const found = (await $`cd ${cwd} && git rev-list -1 ${lastCommitId}`.nothrow().text())
45
56
  .trim()
46
57
  .match(/^[\da-f]{40}$/)
@@ -63,7 +74,7 @@ if (isGitRepo) {
63
74
  }
64
75
  }
65
76
 
66
- const prettierConfig = await Bun.file(`${cwd}/.prettierrc`)
77
+ const oxfmtConfig = await Bun.file(`${cwd}/.oxfmtrc.json`)
67
78
  .json()
68
79
  .catch(() => ({
69
80
  trailingComma: 'all',
@@ -74,25 +85,32 @@ const prettierConfig = await Bun.file(`${cwd}/.prettierrc`)
74
85
  }))
75
86
 
76
87
  await Promise.all(
77
- files.map(async (file: { name: string; input: string; formatable?: boolean }) =>
78
- Bun.write(
79
- `${cwd}/${file.name}`,
80
- file.formatable
81
- ? await format(file.input, { parser: 'typescript', ...prettierConfig })
82
- : file.input,
83
- ),
84
- ),
88
+ files.map(async (file: { name: string; input: string; formatable?: boolean }) => {
89
+ let content = file.input
90
+ if (file.formatable) {
91
+ const result = await format(file.name, file.input, oxfmtConfig)
92
+ content = result.code
93
+ }
94
+ return Bun.write(`${cwd}/${file.name}`, content)
95
+ }),
85
96
  )
86
97
 
87
98
  if (isGitRepo) {
88
99
  await $`cd ${cwd} && git add .`
89
- const commitMsg = isModule
90
- ? 'chore(project): apply file changes from BRICKS module'
91
- : 'chore(project): apply file changes from BRICKS application'
92
- await $`cd ${cwd} && git commit -m ${commitMsg}`
93
- if (!useMain) {
100
+ const hasChanges = !!(await $`cd ${cwd} && git diff --cached --name-only`.text()).trim()
101
+ if (hasChanges) {
102
+ const commitMsg = force
103
+ ? `chore(force-pull): apply force pull-${command}`
104
+ : isModule
105
+ ? 'chore(project): apply file changes from BRICKS module'
106
+ : 'chore(project): apply file changes from BRICKS application'
107
+ await $`cd ${cwd} && git commit -m ${commitMsg}`
108
+ }
109
+ if (!force && !useMain) {
94
110
  await $`cd ${cwd} && git merge main`
95
111
  }
96
112
  }
97
113
 
98
- console.log(`${isModule ? 'Module' : 'App'} project pulled: ${files.length} files`)
114
+ console.log(
115
+ `${isModule ? 'Module' : 'App'} project pulled: ${files.length} files${force ? ' (force)' : ''}`,
116
+ )
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["ESNext"],
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleDetection": "force",
7
+ "allowJs": true,
8
+ "resolveJsonModule": true,
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "verbatimModuleSyntax": true,
12
+ "noEmit": true,
13
+ "skipLibCheck": true,
14
+ },
15
+ "exclude": ["node_modules"],
16
+ }
@@ -61,8 +61,10 @@ export interface AnimationDecayConfig {
61
61
  export interface AnimationDef {
62
62
  __typename: 'Animation'
63
63
  id: string
64
+ alias?: string
64
65
  title: string
65
66
  description?: string
67
+ hideShortRef?: boolean
66
68
  runType?: 'once' | 'loop'
67
69
  property:
68
70
  | 'transform.translateX'
@@ -80,8 +82,10 @@ export interface AnimationDef {
80
82
  export interface AnimationComposeDef {
81
83
  __typename: 'AnimationCompose'
82
84
  id: string
85
+ alias?: string
83
86
  title: string
84
87
  description?: string
88
+ hideShortRef?: boolean
85
89
  runType?: 'once' | 'loop'
86
90
  composeType: 'parallel' | 'sequence'
87
91
  items: Array<() => Animation>