@fugood/bricks-project 2.23.3 → 2.23.5-1

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 (97) hide show
  1. package/compile/action-name-map.ts +14 -0
  2. package/compile/index.ts +29 -0
  3. package/package.json +3 -3
  4. package/skills/bricks-project/rules/architecture-patterns.md +7 -0
  5. package/skills/bricks-project/rules/buttress.md +9 -6
  6. package/tools/mcp-server.ts +10 -880
  7. package/tools/mcp-tools/compile.ts +91 -0
  8. package/tools/mcp-tools/huggingface.ts +763 -0
  9. package/tools/mcp-tools/icons.ts +60 -0
  10. package/tools/mcp-tools/lottie.ts +102 -0
  11. package/tools/mcp-tools/media.ts +110 -0
  12. package/tools/postinstall.ts +113 -28
  13. package/tools/pull.ts +25 -9
  14. package/types/animation.ts +4 -0
  15. package/types/automation.ts +3 -0
  16. package/types/bricks/Camera.ts +33 -6
  17. package/types/bricks/GenerativeMedia.ts +5 -5
  18. package/types/bricks/Icon.ts +2 -2
  19. package/types/bricks/Image.ts +3 -3
  20. package/types/bricks/Items.ts +6 -6
  21. package/types/bricks/Lottie.ts +3 -3
  22. package/types/bricks/Maps.ts +3 -3
  23. package/types/bricks/QrCode.ts +3 -3
  24. package/types/bricks/Rect.ts +3 -3
  25. package/types/bricks/RichText.ts +2 -2
  26. package/types/bricks/Slideshow.ts +1 -1
  27. package/types/bricks/Svg.ts +2 -2
  28. package/types/bricks/Text.ts +3 -3
  29. package/types/bricks/TextInput.ts +10 -6
  30. package/types/bricks/Video.ts +3 -3
  31. package/types/bricks/VideoStreaming.ts +2 -2
  32. package/types/bricks/WebView.ts +3 -3
  33. package/types/canvas.ts +2 -0
  34. package/types/common.ts +5 -0
  35. package/types/data-calc-command.ts +2 -0
  36. package/types/data-calc.ts +1 -0
  37. package/types/data.ts +2 -0
  38. package/types/generators/AlarmClock.ts +4 -4
  39. package/types/generators/Assistant.ts +53 -8
  40. package/types/generators/BleCentral.ts +11 -3
  41. package/types/generators/BlePeripheral.ts +3 -3
  42. package/types/generators/CanvasMap.ts +3 -3
  43. package/types/generators/CastlesPay.ts +2 -2
  44. package/types/generators/DataBank.ts +29 -2
  45. package/types/generators/File.ts +62 -13
  46. package/types/generators/GraphQl.ts +2 -2
  47. package/types/generators/Http.ts +25 -6
  48. package/types/generators/HttpServer.ts +4 -4
  49. package/types/generators/Information.ts +1 -1
  50. package/types/generators/Intent.ts +7 -1
  51. package/types/generators/Iterator.ts +5 -5
  52. package/types/generators/Keyboard.ts +15 -5
  53. package/types/generators/LlmAnthropicCompat.ts +9 -3
  54. package/types/generators/LlmAppleBuiltin.ts +4 -4
  55. package/types/generators/LlmGgml.ts +63 -13
  56. package/types/generators/LlmMlx.ts +210 -0
  57. package/types/generators/LlmOnnx.ts +13 -4
  58. package/types/generators/LlmOpenAiCompat.ts +19 -3
  59. package/types/generators/LlmQualcommAiEngine.ts +29 -5
  60. package/types/generators/Mcp.ts +331 -16
  61. package/types/generators/McpServer.ts +34 -7
  62. package/types/generators/MediaFlow.ts +24 -6
  63. package/types/generators/MqttBroker.ts +9 -3
  64. package/types/generators/MqttClient.ts +10 -4
  65. package/types/generators/Question.ts +4 -4
  66. package/types/generators/RealtimeTranscription.ts +81 -10
  67. package/types/generators/RerankerGgml.ts +19 -5
  68. package/types/generators/SerialPort.ts +5 -5
  69. package/types/generators/SoundPlayer.ts +1 -1
  70. package/types/generators/SoundRecorder.ts +16 -5
  71. package/types/generators/SpeechToTextGgml.ts +27 -7
  72. package/types/generators/SpeechToTextOnnx.ts +3 -3
  73. package/types/generators/SpeechToTextPlatform.ts +3 -3
  74. package/types/generators/SqLite.ts +9 -5
  75. package/types/generators/Step.ts +2 -2
  76. package/types/generators/SttAppleBuiltin.ts +4 -4
  77. package/types/generators/Tcp.ts +3 -3
  78. package/types/generators/TcpServer.ts +5 -5
  79. package/types/generators/TextToSpeechAppleBuiltin.ts +3 -3
  80. package/types/generators/TextToSpeechGgml.ts +3 -3
  81. package/types/generators/TextToSpeechOnnx.ts +3 -3
  82. package/types/generators/TextToSpeechOpenAiLike.ts +3 -3
  83. package/types/generators/ThermalPrinter.ts +4 -4
  84. package/types/generators/Tick.ts +2 -2
  85. package/types/generators/Udp.ts +8 -3
  86. package/types/generators/VadGgml.ts +34 -5
  87. package/types/generators/VadOnnx.ts +27 -4
  88. package/types/generators/VadTraditional.ts +13 -7
  89. package/types/generators/VectorStore.ts +22 -5
  90. package/types/generators/Watchdog.ts +10 -5
  91. package/types/generators/WebCrawler.ts +3 -3
  92. package/types/generators/WebRtc.ts +14 -8
  93. package/types/generators/WebSocket.ts +4 -4
  94. package/types/generators/index.ts +1 -0
  95. package/types/subspace.ts +1 -0
  96. package/types/system.ts +1 -1
  97. package/utils/event-props.ts +104 -87
@@ -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) {
@@ -34,6 +60,7 @@ type CodexMcpConfig = {
34
60
  mcp_servers: Record<string, typeof projectMcpServer>
35
61
  }
36
62
 
63
+ // Claude Code and AGENTS.md projects both use the shared project .mcp.json file.
37
64
  const defaultMcpConfig = {
38
65
  mcpServers: {
39
66
  'bricks-project': projectMcpServer,
@@ -64,43 +91,101 @@ const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
64
91
  const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)
65
92
 
66
93
  if (hasClaudeCode || hasAgentsMd) {
94
+ // Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
67
95
  const mcpConfigPath = `${cwd}/.mcp.json`
68
96
  await handleMcpConfigOverride(mcpConfigPath)
69
97
  }
70
98
 
71
- const setupSkills = async (skillsDir) => {
72
- const packageSkillsDir = `${__dirname}/../skills`
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
73
131
 
74
- if (await exists(packageSkillsDir)) {
75
- const packageSkills = await readdir(packageSkillsDir)
76
- const skillsToInstall = packageSkills.filter((skill) => !skill.startsWith('.'))
77
-
78
- await $`mkdir -p ${skillsDir}`
79
-
80
- await Promise.all(
81
- skillsToInstall.map(async (skill) => {
82
- const targetSkillDir = `${skillsDir}/${skill}`
83
- if (await exists(targetSkillDir)) {
84
- console.log(`Skill '${skill}' already exists, skipping`)
85
- } else {
86
- await $`cp -r ${packageSkillsDir}/${skill} ${targetSkillDir}`
87
- console.log(`Installed skill '${skill}' to ${skillsDir}/`)
88
- }
89
- }),
90
- )
132
+ await copyMissingSkills(resolvedTarget, canonicalSkillsDir)
133
+ await rm(legacySkillsDir, { force: true, recursive: true })
134
+ return
91
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`)
92
144
  }
93
145
 
94
- if (hasClaudeCode) {
95
- // Install skills that don't already exist in the project
96
- await setupSkills(`${cwd}/.claude/skills`)
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}`)
97
167
  }
98
168
 
99
- if (hasAgentsMd) {
100
- // Handle codex skills
101
- // Currently no signal file for codex skills, so we just check if AGENTS.md exists
102
- await setupSkills(`${cwd}/.codex/skills`)
169
+ const setupSkills = async () => {
170
+ const packageSkillsDir = `${__dirname}/../skills`
171
+ await mkdir(projectSkillsDir, { recursive: true })
172
+ await copyMissingSkills(packageSkillsDir, projectSkillsDir)
173
+
174
+ await Promise.all(
175
+ compatibilitySkillLinks.map(async (linkPath) => {
176
+ await migrateSkillsDir(linkPath, projectSkillsDir)
177
+ await ensureCompatibilitySkillLink(linkPath, projectSkillsDir)
178
+ }),
179
+ )
180
+ }
103
181
 
182
+ if (hasClaudeCode || hasAgentsMd) {
183
+ // Install project skills once and expose them through compatibility symlinks.
184
+ await setupSkills()
185
+ }
186
+
187
+ if (hasAgentsMd) {
188
+ // Codex stores its project-local MCP config in .codex/config.toml.
104
189
  const defaultCodexMcpConfig = {
105
190
  mcp_servers: {
106
191
  'bricks-project': projectMcpServer,
@@ -128,7 +213,7 @@ if (hasAgentsMd) {
128
213
  console.log(`Updated ${mcpConfigPath}`)
129
214
  }
130
215
 
131
- // Setup MCP config (.codex/config.toml)
216
+ // Keep the Codex TOML MCP config aligned with the same bricks-project server entry.
132
217
  const codexConfigPath = `${cwd}/.codex/config.toml`
133
218
  await handleCodexMcpConfigOverride(codexConfigPath)
134
219
  }
package/tools/pull.ts CHANGED
@@ -2,6 +2,8 @@ import { $ } from 'bun'
2
2
  import { format } from 'prettier'
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}$/)
@@ -86,13 +97,18 @@ await Promise.all(
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
+ let commitMsg = 'chore(project): apply file changes from BRICKS application'
103
+ if (force) commitMsg = `chore(force-pull): apply force pull-${command}`
104
+ else if (isModule) commitMsg = 'chore(project): apply file changes from BRICKS module'
105
+ await $`cd ${cwd} && git commit -m ${commitMsg}`
106
+ }
107
+ if (!force && !useMain) {
94
108
  await $`cd ${cwd} && git merge main`
95
109
  }
96
110
  }
97
111
 
98
- console.log(`${isModule ? 'Module' : 'App'} project pulled: ${files.length} files`)
112
+ console.log(
113
+ `${isModule ? 'Module' : 'App'} project pulled: ${files.length} files${force ? ' (force)' : ''}`,
114
+ )
@@ -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>
@@ -182,6 +182,7 @@ export interface TestCase {
182
182
  __typename: 'TestCase'
183
183
  id: string
184
184
  name: string
185
+ hideShortRef?: boolean
185
186
  run: TestMethodRun
186
187
  exit_on_failed: boolean
187
188
  commented: boolean
@@ -203,6 +204,7 @@ export interface AutomationTest {
203
204
  __typename: 'AutomationTest'
204
205
  id: string
205
206
  title: string
207
+ hideShortRef?: boolean
206
208
  timeout: number
207
209
  trigger_type?: TestTriggerType
208
210
  cron?: string // Cron expression when trigger_type is 'cron'
@@ -220,6 +222,7 @@ export interface AutomationTestMap {
220
222
  __typename: 'AutomationTestMap'
221
223
  id: string
222
224
  title: string
225
+ hideShortRef?: boolean
223
226
  createdAt: number
224
227
  tests: AutomationTest[]
225
228
  }
@@ -167,15 +167,42 @@ Default property:
167
167
  }
168
168
  outlets?: {
169
169
  /* Camera device and format information */
170
- info?: () => Data
170
+ info?: () => Data<{ [key: string]: any }>
171
171
  /* Picture taken result */
172
- pictureTaken?: () => Data
172
+ pictureTaken?: () => Data<{
173
+ width?: number
174
+ height?: number
175
+ uri?: string
176
+ base64?: string
177
+ [key: string]: any
178
+ }>
173
179
  /* Record video result */
174
- recordVideo?: () => Data
180
+ recordVideo?: () => Data<{
181
+ uri?: string
182
+ [key: string]: any
183
+ }>
175
184
  /* Barcode read result */
176
- barcodeRead?: () => Data
185
+ barcodeRead?: () => Data<{
186
+ type?: string
187
+ data?: string
188
+ rawData?: string
189
+ bounds?: {
190
+ origin?: {
191
+ x?: number
192
+ y?: number
193
+ [key: string]: any
194
+ }
195
+ size?: {
196
+ width?: number
197
+ height?: number
198
+ [key: string]: any
199
+ }
200
+ [key: string]: any
201
+ }
202
+ [key: string]: any
203
+ }>
177
204
  /* Faces detected result */
178
- faceDetected?: () => Data
205
+ faceDetected?: () => Data<Array<{ [key: string]: any }>>
179
206
  }
180
207
  animation?: AnimationBasicEvents & {
181
208
  stateChange?: Animation
@@ -188,7 +215,7 @@ Default property:
188
215
  }
189
216
  }
190
217
 
191
- /* Camera view brick ([Tutorial](https://intercom.help/bricks-dag-inc/articles/5378589-camera)) */
218
+ /* Camera view brick */
192
219
  export type BrickCamera = Brick &
193
220
  BrickCameraDef & {
194
221
  templateKey: 'BRICK_CAMERA'
@@ -209,15 +209,15 @@ Default property:
209
209
  }
210
210
  outlets?: {
211
211
  /* Brick is pressing */
212
- brickPressing?: () => Data
212
+ brickPressing?: () => Data<boolean>
213
213
  /* Brick is focusing (Use TV Device with controller) */
214
- brickFocusing?: () => Data
214
+ brickFocusing?: () => Data<boolean>
215
215
  /* Generated media URL */
216
- url?: () => Data
216
+ url?: () => Data<string>
217
217
  /* Generated media error */
218
- error?: () => Data
218
+ error?: () => Data<string>
219
219
  /* Loading state */
220
- loading?: () => Data
220
+ loading?: () => Data<boolean>
221
221
  }
222
222
  animation?: AnimationBasicEvents & {
223
223
  generativeMediaOnPress?: Animation
@@ -57,9 +57,9 @@ Default property:
57
57
  }
58
58
  outlets?: {
59
59
  /* Brick is pressing */
60
- brickPressing?: () => Data
60
+ brickPressing?: () => Data<boolean>
61
61
  /* Brick is focusing (Use TV Device with controller) */
62
- brickFocusing?: () => Data
62
+ brickFocusing?: () => Data<boolean>
63
63
  }
64
64
  animation?: AnimationBasicEvents & {
65
65
  onPress?: Animation
@@ -71,9 +71,9 @@ Default property:
71
71
  }
72
72
  outlets?: {
73
73
  /* Brick is pressing */
74
- brickPressing?: () => Data
74
+ brickPressing?: () => Data<boolean>
75
75
  /* Brick is focusing (Use TV Device with controller) */
76
- brickFocusing?: () => Data
76
+ brickFocusing?: () => Data<boolean>
77
77
  }
78
78
  animation?: AnimationBasicEvents & {
79
79
  onPress?: Animation
@@ -87,7 +87,7 @@ Default property:
87
87
  }
88
88
  }
89
89
 
90
- /* Image brick ([Tutorial](https://intercom.help/bricks-dag-inc/articles/5378576-image)) */
90
+ /* Image brick */
91
91
  export type BrickImage = Brick &
92
92
  BrickImageDef & {
93
93
  templateKey: 'BRICK_IMAGE'