@fugood/bricks-ctor 2.25.0-beta.5 → 2.25.0-beta.50

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 (185) hide show
  1. package/compile/__tests__/config-diff.test.js +100 -0
  2. package/compile/__tests__/index.test.js +365 -0
  3. package/compile/__tests__/util.test.js +317 -0
  4. package/compile/action-name-map.ts +64 -0
  5. package/compile/config-diff.ts +155 -0
  6. package/compile/index.ts +273 -32
  7. package/compile/util.ts +26 -7
  8. package/package.json +7 -3
  9. package/skills/bricks-ctor/SKILL.md +23 -17
  10. package/skills/bricks-ctor/{rules → references}/animation.md +3 -2
  11. package/skills/bricks-ctor/{rules → references}/architecture-patterns.md +18 -0
  12. package/skills/bricks-ctor/{rules → references}/automations.md +11 -0
  13. package/skills/bricks-ctor/references/buttress.md +245 -0
  14. package/skills/bricks-ctor/references/data-calculation.md +239 -0
  15. package/skills/bricks-ctor/references/simulator.md +132 -0
  16. package/skills/bricks-ctor/references/source-editing-tools.md +81 -0
  17. package/skills/bricks-ctor/references/verification-toolchain.md +200 -0
  18. package/skills/bricks-design/SKILL.md +150 -45
  19. package/skills/bricks-design/references/architecture-truths.md +132 -0
  20. package/skills/bricks-design/references/avoiding-complexity.md +91 -0
  21. package/skills/bricks-design/references/design-critique.md +195 -0
  22. package/skills/bricks-design/references/design-languages.md +265 -0
  23. package/skills/bricks-design/references/performance.md +116 -0
  24. package/skills/bricks-design/references/presentation-and-slideshow.md +137 -0
  25. package/skills/bricks-design/references/translating-inputs.md +152 -0
  26. package/skills/bricks-design/references/variations-and-tweaks.md +124 -0
  27. package/skills/bricks-design/references/when-the-brief-is-branded.md +284 -0
  28. package/skills/bricks-design/references/when-the-brief-is-vague.md +85 -0
  29. package/skills/bricks-design/references/workflow.md +134 -0
  30. package/skills/bricks-ux/SKILL.md +114 -0
  31. package/skills/bricks-ux/references/accessibility.md +162 -0
  32. package/skills/bricks-ux/references/flow-states.md +175 -0
  33. package/skills/bricks-ux/references/interaction-archetypes.md +189 -0
  34. package/skills/bricks-ux/references/monitoring-screens.md +153 -0
  35. package/skills/bricks-ux/references/pressable-composition.md +126 -0
  36. package/skills/bricks-ux/references/user-journey.md +168 -0
  37. package/skills/bricks-ux/references/ux-critique.md +256 -0
  38. package/tools/__tests__/_cli-error.test.ts +35 -0
  39. package/tools/_cli-error.ts +17 -0
  40. package/tools/_edits-log.ts +41 -0
  41. package/tools/_git-author.ts +10 -2
  42. package/tools/_last-pushed-commit.ts +28 -0
  43. package/tools/_shell.ts +8 -1
  44. package/tools/deploy.ts +17 -6
  45. package/tools/mcp-env.ts +13 -0
  46. package/tools/mcp-server.ts +8 -0
  47. package/tools/mcp-tools/__tests__/data-calc-editing.test.js +516 -0
  48. package/tools/mcp-tools/__tests__/entry-editing.test.js +866 -0
  49. package/tools/mcp-tools/__tests__/huggingface.test.ts +49 -0
  50. package/tools/mcp-tools/__tests__/icons.test.ts +21 -0
  51. package/tools/mcp-tools/__tests__/mcp-env.test.js +19 -0
  52. package/tools/mcp-tools/_editing-helpers.ts +58 -0
  53. package/tools/mcp-tools/_verify.ts +50 -0
  54. package/tools/mcp-tools/compile.ts +21 -9
  55. package/tools/mcp-tools/data-calc-editing.ts +1349 -0
  56. package/tools/mcp-tools/entry-editing.ts +2336 -0
  57. package/tools/mcp-tools/huggingface.ts +23 -13
  58. package/tools/mcp-tools/icons.ts +23 -7
  59. package/tools/mcp-tools/media.ts +4 -1
  60. package/tools/postinstall.ts +80 -3
  61. package/tools/pull.ts +93 -22
  62. package/tools/push-config.ts +114 -0
  63. package/tools/{preview-main.mjs → simulator-main.mjs} +207 -12
  64. package/tools/simulator-preload.cjs +16 -0
  65. package/tools/{preview.ts → simulator.ts} +4 -4
  66. package/types/{animation.ts → animation.d.ts} +24 -8
  67. package/types/{automation.ts → automation.d.ts} +16 -20
  68. package/types/{brick-base.ts → brick-base.d.ts} +1 -1
  69. package/types/bricks/{Camera.ts → Camera.d.ts} +8 -8
  70. package/types/bricks/{Chart.ts → Chart.d.ts} +4 -4
  71. package/types/bricks/{GenerativeMedia.ts → GenerativeMedia.d.ts} +15 -15
  72. package/types/bricks/{Icon.ts → Icon.d.ts} +7 -7
  73. package/types/bricks/{Image.ts → Image.d.ts} +21 -9
  74. package/types/bricks/{Items.ts → Items.d.ts} +7 -7
  75. package/types/bricks/{Lottie.ts → Lottie.d.ts} +10 -10
  76. package/types/bricks/{Maps.ts → Maps.d.ts} +11 -11
  77. package/types/bricks/{QrCode.ts → QrCode.d.ts} +7 -7
  78. package/types/bricks/{Rect.ts → Rect.d.ts} +7 -7
  79. package/types/bricks/{RichText.ts → RichText.d.ts} +12 -9
  80. package/types/bricks/{Rive.ts → Rive.d.ts} +9 -9
  81. package/types/bricks/Scene3D.d.ts +676 -0
  82. package/types/bricks/{Sketch.ts → Sketch.d.ts} +6 -6
  83. package/types/bricks/{Slideshow.ts → Slideshow.d.ts} +7 -7
  84. package/types/bricks/{Svg.ts → Svg.d.ts} +7 -7
  85. package/types/bricks/{Text.ts → Text.d.ts} +9 -9
  86. package/types/bricks/{TextInput.ts → TextInput.d.ts} +10 -10
  87. package/types/bricks/{Video.ts → Video.d.ts} +12 -12
  88. package/types/bricks/{VideoStreaming.ts → VideoStreaming.d.ts} +10 -10
  89. package/types/bricks/{WebRtcStream.ts → WebRtcStream.d.ts} +1 -1
  90. package/types/bricks/{WebView.ts → WebView.d.ts} +4 -4
  91. package/types/bricks/{index.ts → index.d.ts} +1 -0
  92. package/types/{common.ts → common.d.ts} +3 -6
  93. package/types/data-calc-command/base.d.ts +57 -0
  94. package/types/data-calc-command/collection.d.ts +418 -0
  95. package/types/data-calc-command/color.d.ts +432 -0
  96. package/types/data-calc-command/constant.d.ts +50 -0
  97. package/types/data-calc-command/datetime.d.ts +147 -0
  98. package/types/data-calc-command/file.d.ts +129 -0
  99. package/types/data-calc-command/index.d.ts +13 -0
  100. package/types/data-calc-command/iteratee.d.ts +23 -0
  101. package/types/data-calc-command/logictype.d.ts +190 -0
  102. package/types/data-calc-command/math.d.ts +275 -0
  103. package/types/data-calc-command/object.d.ts +119 -0
  104. package/types/data-calc-command/sandbox.d.ts +66 -0
  105. package/types/data-calc-command/string.d.ts +407 -0
  106. package/types/{data-calc.ts → data-calc.d.ts} +1 -0
  107. package/types/{data.ts → data.d.ts} +4 -2
  108. package/types/generators/{Assistant.ts → Assistant.d.ts} +19 -0
  109. package/types/generators/{LlmGgml.ts → LlmGgml.d.ts} +43 -1
  110. package/types/generators/{LlmMlx.ts → LlmMlx.d.ts} +1 -0
  111. package/types/generators/{RerankerGgml.ts → RerankerGgml.d.ts} +5 -1
  112. package/types/generators/{SoundRecorder.ts → SoundRecorder.d.ts} +10 -1
  113. package/types/generators/{SpeechToTextGgml.ts → SpeechToTextGgml.d.ts} +6 -1
  114. package/types/generators/{SttAppleBuiltin.ts → SttAppleBuiltin.d.ts} +27 -4
  115. package/types/generators/{ThermalPrinter.ts → ThermalPrinter.d.ts} +9 -7
  116. package/types/generators/{VadGgml.ts → VadGgml.d.ts} +12 -2
  117. package/types/{subspace.ts → subspace.d.ts} +1 -1
  118. package/utils/__tests__/calc.test.js +25 -0
  119. package/utils/__tests__/id.test.js +154 -0
  120. package/utils/calc.ts +5 -1
  121. package/utils/data.ts +5 -7
  122. package/utils/event-props.ts +17 -0
  123. package/utils/id.ts +109 -56
  124. package/skills/bricks-ctor/rules/buttress.md +0 -156
  125. package/skills/bricks-ctor/rules/data-calculation.md +0 -209
  126. package/skills/bricks-design/LICENSE.txt +0 -180
  127. package/types/data-calc-command.ts +0 -7005
  128. /package/skills/bricks-ctor/{rules → references}/local-sync.md +0 -0
  129. /package/skills/bricks-ctor/{rules → references}/media-flow.md +0 -0
  130. /package/skills/bricks-ctor/{rules → references}/remote-data-bank.md +0 -0
  131. /package/skills/bricks-ctor/{rules → references}/standby-transition.md +0 -0
  132. /package/types/{canvas.ts → canvas.d.ts} +0 -0
  133. /package/types/{data-calc-script.ts → data-calc-script.d.ts} +0 -0
  134. /package/types/generators/{AlarmClock.ts → AlarmClock.d.ts} +0 -0
  135. /package/types/generators/{BleCentral.ts → BleCentral.d.ts} +0 -0
  136. /package/types/generators/{BlePeripheral.ts → BlePeripheral.d.ts} +0 -0
  137. /package/types/generators/{CanvasMap.ts → CanvasMap.d.ts} +0 -0
  138. /package/types/generators/{CastlesPay.ts → CastlesPay.d.ts} +0 -0
  139. /package/types/generators/{DataBank.ts → DataBank.d.ts} +0 -0
  140. /package/types/generators/{File.ts → File.d.ts} +0 -0
  141. /package/types/generators/{GraphQl.ts → GraphQl.d.ts} +0 -0
  142. /package/types/generators/{Http.ts → Http.d.ts} +0 -0
  143. /package/types/generators/{HttpServer.ts → HttpServer.d.ts} +0 -0
  144. /package/types/generators/{Information.ts → Information.d.ts} +0 -0
  145. /package/types/generators/{Intent.ts → Intent.d.ts} +0 -0
  146. /package/types/generators/{Iterator.ts → Iterator.d.ts} +0 -0
  147. /package/types/generators/{Keyboard.ts → Keyboard.d.ts} +0 -0
  148. /package/types/generators/{LlmAnthropicCompat.ts → LlmAnthropicCompat.d.ts} +0 -0
  149. /package/types/generators/{LlmAppleBuiltin.ts → LlmAppleBuiltin.d.ts} +0 -0
  150. /package/types/generators/{LlmMediaTekNeuroPilot.ts → LlmMediaTekNeuroPilot.d.ts} +0 -0
  151. /package/types/generators/{LlmOnnx.ts → LlmOnnx.d.ts} +0 -0
  152. /package/types/generators/{LlmOpenAiCompat.ts → LlmOpenAiCompat.d.ts} +0 -0
  153. /package/types/generators/{LlmQualcommAiEngine.ts → LlmQualcommAiEngine.d.ts} +0 -0
  154. /package/types/generators/{Mcp.ts → Mcp.d.ts} +0 -0
  155. /package/types/generators/{McpServer.ts → McpServer.d.ts} +0 -0
  156. /package/types/generators/{MediaFlow.ts → MediaFlow.d.ts} +0 -0
  157. /package/types/generators/{MqttBroker.ts → MqttBroker.d.ts} +0 -0
  158. /package/types/generators/{MqttClient.ts → MqttClient.d.ts} +0 -0
  159. /package/types/generators/{Question.ts → Question.d.ts} +0 -0
  160. /package/types/generators/{RealtimeTranscription.ts → RealtimeTranscription.d.ts} +0 -0
  161. /package/types/generators/{SerialPort.ts → SerialPort.d.ts} +0 -0
  162. /package/types/generators/{SoundPlayer.ts → SoundPlayer.d.ts} +0 -0
  163. /package/types/generators/{SpeechToTextOnnx.ts → SpeechToTextOnnx.d.ts} +0 -0
  164. /package/types/generators/{SpeechToTextPlatform.ts → SpeechToTextPlatform.d.ts} +0 -0
  165. /package/types/generators/{SqLite.ts → SqLite.d.ts} +0 -0
  166. /package/types/generators/{Step.ts → Step.d.ts} +0 -0
  167. /package/types/generators/{Tcp.ts → Tcp.d.ts} +0 -0
  168. /package/types/generators/{TcpServer.ts → TcpServer.d.ts} +0 -0
  169. /package/types/generators/{TextToSpeechAppleBuiltin.ts → TextToSpeechAppleBuiltin.d.ts} +0 -0
  170. /package/types/generators/{TextToSpeechGgml.ts → TextToSpeechGgml.d.ts} +0 -0
  171. /package/types/generators/{TextToSpeechOnnx.ts → TextToSpeechOnnx.d.ts} +0 -0
  172. /package/types/generators/{TextToSpeechOpenAiLike.ts → TextToSpeechOpenAiLike.d.ts} +0 -0
  173. /package/types/generators/{Tick.ts → Tick.d.ts} +0 -0
  174. /package/types/generators/{Udp.ts → Udp.d.ts} +0 -0
  175. /package/types/generators/{VadOnnx.ts → VadOnnx.d.ts} +0 -0
  176. /package/types/generators/{VadTraditional.ts → VadTraditional.d.ts} +0 -0
  177. /package/types/generators/{VectorStore.ts → VectorStore.d.ts} +0 -0
  178. /package/types/generators/{Watchdog.ts → Watchdog.d.ts} +0 -0
  179. /package/types/generators/{WebCrawler.ts → WebCrawler.d.ts} +0 -0
  180. /package/types/generators/{WebRtc.ts → WebRtc.d.ts} +0 -0
  181. /package/types/generators/{WebSocket.ts → WebSocket.d.ts} +0 -0
  182. /package/types/generators/{index.ts → index.d.ts} +0 -0
  183. /package/types/{index.ts → index.d.ts} +0 -0
  184. /package/types/{switch.ts → switch.d.ts} +0 -0
  185. /package/types/{system.ts → system.d.ts} +0 -0
@@ -269,6 +269,28 @@ const fetchHFModelDetails = async (modelId: string): Promise<HFModel> => {
269
269
  // Example: Mixtral-8x22B-v0.1.IQ3_XS-00001-of-00005.gguf
270
270
  const ggufSplitPattern = /-(\d{5})-of-(\d{5})\.gguf$/
271
271
 
272
+ export const buildGGUFSplitFiles = (
273
+ filename: string,
274
+ splitTotal: string,
275
+ siblings: HFSibling[],
276
+ ): HFSibling[] => {
277
+ const siblingByFilename = new Map<string, HFSibling>()
278
+ for (const sibling of siblings) {
279
+ if (!siblingByFilename.has(sibling.rfilename)) siblingByFilename.set(sibling.rfilename, sibling)
280
+ }
281
+
282
+ return Array.from({ length: Number(splitTotal) }, (_, i) => {
283
+ const split = String(i + 1).padStart(5, '0')
284
+ const splitRFilename = filename.replace(ggufSplitPattern, `-${split}-of-${splitTotal}.gguf`)
285
+ const sibling = siblingByFilename.get(splitRFilename)
286
+ return {
287
+ rfilename: splitRFilename,
288
+ size: sibling?.size,
289
+ lfs: sibling?.lfs,
290
+ }
291
+ })
292
+ }
293
+
272
294
  export function register(server: McpServer) {
273
295
  server.tool(
274
296
  'huggingface_search',
@@ -657,19 +679,7 @@ export function register(server: McpServer) {
657
679
 
658
680
  if (isSplit) {
659
681
  const [, , splitTotal] = matched!
660
- const splitFiles = Array.from({ length: Number(splitTotal) }, (_, i) => {
661
- const split = String(i + 1).padStart(5, '0')
662
- const splitRFilename = filename.replace(
663
- ggufSplitPattern,
664
- `-${split}-of-${splitTotal}.gguf`,
665
- )
666
- const sibling = siblings.find((sb) => sb.rfilename === splitRFilename)
667
- return {
668
- rfilename: splitRFilename,
669
- size: sibling?.size,
670
- lfs: sibling?.lfs,
671
- }
672
- })
682
+ const splitFiles = buildGGUFSplitFiles(filename, splitTotal, siblings)
673
683
 
674
684
  const first = splitFiles[0]
675
685
  const rest = splitFiles.slice(1)
@@ -36,11 +36,31 @@ const iconList = Object.entries(glyphmap as Record<string, number>).map(([name,
36
36
  return { name, code, styles }
37
37
  })
38
38
 
39
- const iconFuse = new Fuse(iconList, {
39
+ const fuseOptions = {
40
40
  keys: ['name'],
41
41
  threshold: 0.3,
42
42
  includeScore: true,
43
- })
43
+ }
44
+
45
+ const iconFuse = new Fuse(iconList, fuseOptions)
46
+ const iconFuseByStyle = new Map<IconStyle, Fuse<(typeof iconList)[number]>>()
47
+
48
+ function getStyleFuse(style: IconStyle) {
49
+ let fuse = iconFuseByStyle.get(style)
50
+ if (!fuse) {
51
+ fuse = new Fuse(
52
+ iconList.filter((icon) => icon.styles.includes(style)),
53
+ fuseOptions,
54
+ )
55
+ iconFuseByStyle.set(style, fuse)
56
+ }
57
+ return fuse
58
+ }
59
+
60
+ export function searchIcons(query: string, limit: number, style?: IconStyle) {
61
+ if (style) return getStyleFuse(style).search(query, { limit })
62
+ return iconFuse.search(query, { limit })
63
+ }
44
64
 
45
65
  export function register(server: McpServer) {
46
66
  server.tool(
@@ -54,11 +74,7 @@ export function register(server: McpServer) {
54
74
  .describe('Filter by icon style'),
55
75
  },
56
76
  async ({ query, limit, style }) => {
57
- let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
58
-
59
- if (style) {
60
- results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
61
- }
77
+ const results = searchIcons(query, limit, style)
62
78
 
63
79
  const icons = results.map((r) => ({
64
80
  name: r.item.name,
@@ -2,9 +2,12 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  import { z } from 'zod'
3
3
  import { sh } from '../_shell'
4
4
 
5
+ // MCP results are rendered as raw text — disable ANSI colors from the child.
6
+ const noColorEnv = { FORCE_COLOR: '0', NO_COLOR: '1' }
7
+
5
8
  const runBricks = async (projectDir: string, ...args: string[]) => {
6
9
  try {
7
- return await sh`bunx bricks ${args}`.cwd(projectDir).text()
10
+ return await sh`bunx bricks ${args}`.cwd(projectDir).env(noColorEnv).text()
8
11
  } catch (err: any) {
9
12
  throw new Error(err.stderr?.toString() || err.message)
10
13
  }
@@ -69,8 +69,15 @@ const projectMcpServer = {
69
69
  args: [`${cwd}/node_modules/@fugood/bricks-ctor/tools/mcp-server.ts`],
70
70
  }
71
71
 
72
+ // Codex cancels MCP tool calls it cannot prompt approval for (e.g. `codex exec`),
73
+ // so the project-local server's tools must be pre-approved in its config entry.
74
+ const codexProjectMcpServer = {
75
+ ...projectMcpServer,
76
+ default_tools_approval_mode: 'approve',
77
+ }
78
+
72
79
  type CodexMcpConfig = {
73
- mcp_servers: Record<string, typeof projectMcpServer>
80
+ mcp_servers: Record<string, typeof codexProjectMcpServer | typeof projectMcpServer>
74
81
  }
75
82
 
76
83
  // Claude Code and AGENTS.md projects both use the shared project .mcp.json file.
@@ -196,11 +203,81 @@ if (hasClaudeCode || hasAgentsMd) {
196
203
  await setupSkills()
197
204
  }
198
205
 
206
+ type ClaudeSettings = {
207
+ autoMode?: {
208
+ environment?: string[]
209
+ allow?: string[]
210
+ soft_deny?: string[]
211
+ hard_deny?: string[]
212
+ }
213
+ [key: string]: unknown
214
+ }
215
+
216
+ // Trusted infrastructure for auto mode's classifier. `$defaults` keeps the
217
+ // built-in environment (the working repo and its git remotes); the extra
218
+ // entries stop routine syncs to the BRICKS backend from being treated as
219
+ // external exfiltration. See https://code.claude.com/docs/en/auto-mode-config
220
+ const autoModeEnvironment = [
221
+ '$defaults',
222
+ 'Organization: BRICKS (bricks.tools). Primary use: building BRICKS apps/modules with the bricks CLI and the local bricks-ctor MCP server.',
223
+ 'Trusted internal domains: all *.bricks.tools services — api.bricks.tools (project GraphQL API), bank.bricks.tools (config & asset Bank API), cdn.bricks.tools (asset CDN), plus the control/display/activity services. This project syncs its config and assets to these endpoints.',
224
+ ]
225
+
226
+ // `.claude/settings.local.json` is per-developer local config; keep it untracked.
227
+ const ensureSettingsLocalGitignored = async () => {
228
+ const gitignorePath = path.join(cwd, '.gitignore')
229
+ const entry = '.claude/settings.local.json'
230
+ const coveredBy = new Set([entry, '.claude', '.claude/', '.claude/*', '*.local.json'])
231
+
232
+ let content = ''
233
+ if (await exists(gitignorePath)) {
234
+ content = await readFile(gitignorePath, 'utf-8')
235
+ if (content.split('\n').some((line) => coveredBy.has(line.trim()))) return
236
+ }
237
+
238
+ const separator = content.length === 0 ? '' : content.endsWith('\n') ? '\n' : '\n\n'
239
+ await writeFile(gitignorePath, `${content}${separator}# Claude Code local settings\n${entry}\n`)
240
+ console.log(`Added ${entry} to .gitignore`)
241
+ }
242
+
243
+ // Pre-configure auto mode once, on initial setup. We only seed the classifier's
244
+ // trusted infrastructure — not `permissions.defaultMode: 'auto'`, which Claude
245
+ // Code ignores from project/local settings (a repo can't grant itself auto mode;
246
+ // it only takes effect from ~/.claude/settings.json). An existing autoMode block
247
+ // is left untouched so reinstalls never clobber a developer's customizations.
248
+ const setupClaudeAutoMode = async () => {
249
+ const settingsPath = path.join(cwd, '.claude', 'settings.local.json')
250
+
251
+ let settings: ClaudeSettings = {}
252
+ if (await exists(settingsPath)) {
253
+ try {
254
+ settings = JSON.parse(await readFile(settingsPath, 'utf-8'))
255
+ } catch {
256
+ console.warn(`Skipping auto mode setup; ${settingsPath} is not valid JSON`)
257
+ return
258
+ }
259
+ if (settings.autoMode) return
260
+ }
261
+
262
+ settings.autoMode = { environment: autoModeEnvironment }
263
+
264
+ await mkdir(path.dirname(settingsPath), { recursive: true })
265
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
266
+ console.log(`Set up auto mode in ${settingsPath}`)
267
+
268
+ await ensureSettingsLocalGitignored()
269
+ }
270
+
271
+ if (hasClaudeCode) {
272
+ // Pre-configure auto mode's trusted infrastructure for Claude Code projects.
273
+ await setupClaudeAutoMode()
274
+ }
275
+
199
276
  if (hasAgentsMd) {
200
277
  // Codex stores its project-local MCP config in .codex/config.toml.
201
278
  const defaultCodexMcpConfig = {
202
279
  mcp_servers: {
203
- 'bricks-ctor': projectMcpServer,
280
+ 'bricks-ctor': codexProjectMcpServer,
204
281
  },
205
282
  }
206
283
 
@@ -212,7 +289,7 @@ if (hasAgentsMd) {
212
289
  const parsed = TOML.parse(configStr) as Partial<CodexMcpConfig>
213
290
  if (!parsed?.mcp_servers) throw new Error('mcp_servers is not defined')
214
291
  mcpConfig = { mcp_servers: parsed.mcp_servers }
215
- mcpConfig.mcp_servers['bricks-ctor'] = projectMcpServer
292
+ mcpConfig.mcp_servers['bricks-ctor'] = codexProjectMcpServer
216
293
  delete mcpConfig.mcp_servers['bricks-project']
217
294
  } catch {
218
295
  mcpConfig = defaultCodexMcpConfig
package/tools/pull.ts CHANGED
@@ -1,7 +1,32 @@
1
- import { readFile, writeFile } from 'node:fs/promises'
1
+ import { readdir, readFile, unlink, writeFile } from 'node:fs/promises'
2
+ import { existsSync } from 'node:fs'
3
+ import { join, relative } from 'node:path'
2
4
  import { format } from 'oxfmt'
3
5
  import { sh } from './_shell'
6
+ import { extractCliErrorMessage } from './_cli-error'
4
7
  import { buildCommitArgs } from './_git-author'
8
+ import { readLastPushedCommit, writeLastPushedCommit } from './_last-pushed-commit'
9
+
10
+ // Directories whose .ts contents are entirely owned by the generator.
11
+ // Anything under these dirs not present in the freshly pulled file list is an orphan.
12
+ const ownedTsDirs = ['subspaces', 'automation-tests']
13
+
14
+ async function walkTsFiles(dir: string, baseDir: string): Promise<string[]> {
15
+ if (!existsSync(dir)) return []
16
+ const result: string[] = []
17
+ const entries = await readdir(dir, { withFileTypes: true })
18
+ await Promise.all(
19
+ entries.map(async (entry) => {
20
+ const full = join(dir, entry.name)
21
+ if (entry.isDirectory()) {
22
+ result.push(...(await walkTsFiles(full, baseDir)))
23
+ } else if (entry.isFile() && entry.name.endsWith('.ts')) {
24
+ result.push(relative(baseDir, full))
25
+ }
26
+ }),
27
+ )
28
+ return result
29
+ }
5
30
 
6
31
  const cwd = process.cwd()
7
32
  const args = process.argv.slice(2)
@@ -44,38 +69,43 @@ const result = await sh`bricks ${command} project-pull ${app.id} --json`.quiet()
44
69
 
45
70
  if (result.exitCode !== 0) {
46
71
  const output = result.stderr.toString() || result.stdout.toString()
47
- try {
48
- const json = JSON.parse(output)
49
- throw new Error(json.error || 'Pull failed')
50
- } catch {
51
- throw new Error(output || 'Pull failed')
52
- }
72
+ throw new Error(extractCliErrorMessage(output, 'Pull failed'))
53
73
  }
54
74
 
55
- const { files, lastCommitId } = JSON.parse(result.stdout.toString())
75
+ const { files, lastCommitId: serverLastCommitId } = JSON.parse(result.stdout.toString())
76
+
77
+ // The locally-saved commit (recorded by deploy/push-config) is the
78
+ // authoritative merge base for THIS clone — the server's value can be an
79
+ // opaque nanoid (config-only updates) or point to a commit other clients
80
+ // produced. Fall back to the server's value only if we have no local
81
+ // record yet.
82
+ const savedLocalCommitId = await readLastPushedCommit(cwd)
83
+ const baseCommitId = savedLocalCommitId || serverLastCommitId
84
+
85
+ const branchName = isModule
86
+ ? 'BRICKS_PROJECT_try-pull-module'
87
+ : 'BRICKS_PROJECT_try-pull-application'
56
88
 
57
- let useMain = false
58
89
  if (isGitRepo && !force) {
59
- console.log(`Checking commit ${lastCommitId}...`)
60
- const found = (await sh`cd ${cwd} && git rev-list -1 ${lastCommitId}`.nothrow().text())
90
+ console.log(`Checking commit ${baseCommitId}...`)
91
+ const found = (await sh`cd ${cwd} && git rev-list -1 ${baseCommitId}`.nothrow().text())
61
92
  .trim()
62
93
  .match(/^[\da-f]{40}$/)
63
94
 
64
- const commitId = (await sh`cd ${cwd} && git rev-parse HEAD`.text()).trim()
65
-
66
- if (commitId === lastCommitId) throw new Error('Commit not changed')
95
+ const headCommitId = (await sh`cd ${cwd} && git rev-parse HEAD`.text()).trim()
67
96
 
68
- const branchName = isModule
69
- ? 'BRICKS_PROJECT_try-pull-module'
70
- : 'BRICKS_PROJECT_try-pull-application'
97
+ if (headCommitId === serverLastCommitId) throw new Error('Commit not changed')
71
98
 
72
99
  await sh`cd ${cwd} && git branch -D ${branchName}`.nothrow()
73
100
 
101
+ // When the base commit isn't reachable in this clone (server stored a
102
+ // nanoid, or the commit was pruned), fall back to forking from current
103
+ // HEAD. The downstream merge into main collapses both paths into the
104
+ // same result, just with different merge bases.
74
105
  if (found) {
75
- await sh`cd ${cwd} && git checkout -b ${branchName} ${lastCommitId}`.nothrow()
106
+ await sh`cd ${cwd} && git checkout -b ${branchName} ${baseCommitId}`.nothrow()
76
107
  } else {
77
108
  await sh`cd ${cwd} && git checkout -b ${branchName}`
78
- useMain = true
79
109
  }
80
110
  }
81
111
 
@@ -89,6 +119,23 @@ const oxfmtConfig = await readFile(`${cwd}/.oxfmtrc.json`, 'utf8')
89
119
  printWidth: 100,
90
120
  }))
91
121
 
122
+ const expectedFiles = new Set(files.map((file: { name: string }) => file.name))
123
+
124
+ // Remove orphan .ts files under generator-owned directories before writing.
125
+ // File paths are produced by buildApplicationFiles and use forward slashes,
126
+ // so normalise the walked paths the same way for comparison.
127
+ const orphans: string[] = []
128
+ await Promise.all(
129
+ ownedTsDirs.map(async (dir) => {
130
+ const existing = await walkTsFiles(join(cwd, dir), cwd)
131
+ for (const file of existing) {
132
+ const normalized = file.split(/[\\/]/).join('/')
133
+ if (!expectedFiles.has(normalized)) orphans.push(normalized)
134
+ }
135
+ }),
136
+ )
137
+ await Promise.all(orphans.map((name) => unlink(`${cwd}/${name}`)))
138
+
92
139
  await Promise.all(
93
140
  files.map(async (file: { name: string; input: string; formatable?: boolean }) => {
94
141
  let content = file.input
@@ -112,11 +159,35 @@ if (isGitRepo) {
112
159
  const commitArgs = await buildCommitArgs(cwd, [commitMsg])
113
160
  await sh`cd ${cwd} && git ${commitArgs}`
114
161
  }
115
- if (!force && !useMain) {
116
- await sh`cd ${cwd} && git merge main`
162
+ if (!force) {
163
+ // Land the pulled commits on main with a single 3-way merge using
164
+ // baseCommit as the merge base. The user doesn't have to manage a side
165
+ // branch, and conflicts (if any) land in the working tree on main where
166
+ // auto-compile surfaces them as typecheck errors to resolve in-place.
167
+ await sh`cd ${cwd} && git checkout main`
168
+ const mergeResult = await sh`cd ${cwd} && git merge ${branchName} --no-edit`.nothrow()
169
+ if (mergeResult.exitCode !== 0) {
170
+ // Conflict markers are in the working tree — commit them so the tree
171
+ // is clean for auto-compile to detect, leaving the resolution to the
172
+ // user. Pre-commit hooks would reject markers (lint/format fail on
173
+ // invalid syntax), so bypass them for this controlled case.
174
+ await sh`cd ${cwd} && git add .`
175
+ const conflictArgs = await buildCommitArgs(
176
+ cwd,
177
+ ['chore(project): merge with conflicts (resolve in main)'],
178
+ ['--no-verify'],
179
+ )
180
+ await sh`cd ${cwd} && git ${conflictArgs}`
181
+ }
182
+ // The try-pull branch served its purpose; delete it so `git branch`
183
+ // stays tidy. The next pull recreates it anyway (line 103).
184
+ await sh`cd ${cwd} && git branch -D ${branchName}`.nothrow()
117
185
  }
186
+ // Record the new sync point so a follow-up pull starts from the right base.
187
+ const newHead = (await sh`cd ${cwd} && git rev-parse HEAD`.nothrow().text()).trim()
188
+ if (newHead) await writeLastPushedCommit(cwd, newHead)
118
189
  }
119
190
 
120
191
  console.log(
121
- `${isModule ? 'Module' : 'App'} project pulled: ${files.length} files${force ? ' (force)' : ''}`,
192
+ `${isModule ? 'Module' : 'App'} project pulled: ${files.length} files${orphans.length ? `, removed ${orphans.length} orphan .ts file${orphans.length === 1 ? '' : 's'}` : ''}${force ? ' (force)' : ''}`,
122
193
  )
@@ -0,0 +1,114 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import { parseArgs } from 'util'
3
+ import { sh } from './_shell'
4
+ import { extractCliErrorMessage } from './_cli-error'
5
+ import { buildCommitArgs } from './_git-author'
6
+ import { writeLastPushedCommit } from './_last-pushed-commit'
7
+
8
+ const cwd = process.cwd()
9
+
10
+ const readJson = async (p: string) => JSON.parse(await readFile(p, 'utf8'))
11
+
12
+ const {
13
+ values: { 'auto-commit': autoCommit, 'no-check': noCheck, 'no-validate': noValidate, yes, help },
14
+ } = parseArgs({
15
+ args: process.argv.slice(2),
16
+ options: {
17
+ 'auto-commit': { type: 'boolean' },
18
+ 'no-check': { type: 'boolean' },
19
+ 'no-validate': { type: 'boolean' },
20
+ yes: { type: 'boolean', short: 'y' },
21
+ help: { type: 'boolean', short: 'h' },
22
+ },
23
+ allowPositionals: true,
24
+ })
25
+
26
+ if (help) {
27
+ console.log(`Push compiled config to BRICKS without creating a release.
28
+
29
+ Options:
30
+ --auto-commit Auto-commit unstaged changes before pushing
31
+ --no-check Skip the conflict guard (don't pass --last-commit-id)
32
+ --no-validate Skip server-side config schema validation
33
+ -y, --yes Skip all prompts
34
+ -h, --help Show this help message`)
35
+ process.exit(0)
36
+ }
37
+
38
+ // Detect git repo (mirrors deploy.ts)
39
+ const { exitCode } = await sh`cd ${cwd} && git status`.quiet().nothrow()
40
+ const isGitRepo = exitCode === 0
41
+
42
+ if (!isGitRepo && !yes) {
43
+ const confirmContinue = prompt('No git repository found, continue? (y/n)')
44
+ if (confirmContinue !== 'y') throw new Error('Update cancelled')
45
+ }
46
+
47
+ // Read application.json + compiled config
48
+ const app = await readJson(`${cwd}/application.json`)
49
+ const config = await readJson(`${cwd}/.bricks/build/application-config.json`)
50
+
51
+ // Handle unstaged changes the same way deploy.ts does.
52
+ let commitId = ''
53
+ let parentCommitId = ''
54
+ if (isGitRepo) {
55
+ const unstagedChanges = await sh`cd ${cwd} && git diff --name-only --diff-filter=ACMR`.text()
56
+ if (unstagedChanges) {
57
+ if (autoCommit) {
58
+ // Capture the pre-commit HEAD so we can use it as the conflict-check
59
+ // baseline (the server should still hold it from the prior deploy).
60
+ parentCommitId = (await sh`cd ${cwd} && git rev-parse HEAD`.nothrow().text()).trim()
61
+ await sh`cd ${cwd} && git add -A`
62
+ const commitArgs = await buildCommitArgs(cwd, ['chore: update bricks config'])
63
+ await sh`cd ${cwd} && git ${commitArgs}`
64
+ } else {
65
+ throw new Error('Unstaged changes found, please commit or stash your changes before updating')
66
+ }
67
+ }
68
+ commitId = (await sh`cd ${cwd} && git rev-parse HEAD`.text()).trim()
69
+ }
70
+
71
+ // Auto-derive --last-commit-id for the server-side conflict guard.
72
+ // - parent of the auto-commit (server still holds it from the prior deploy)
73
+ // - current HEAD (clean tree — server should be at the same commit too)
74
+ let lastCommitId: string | undefined
75
+ if (!noCheck) {
76
+ if (parentCommitId) lastCommitId = parentCommitId
77
+ else if (commitId) lastCommitId = commitId
78
+ }
79
+
80
+ if (!yes) {
81
+ const confirm = prompt('Are you sure you want to push the new config? (y/n)')
82
+ if (confirm !== 'y') throw new Error('Update cancelled')
83
+ }
84
+
85
+ const isModule = app.type === 'module'
86
+ const command = isModule ? 'module' : 'app'
87
+
88
+ const updateConfig = {
89
+ ...config,
90
+ bricks_project_last_commit_id: commitId || undefined,
91
+ }
92
+ const configPath = `${cwd}/.bricks/build/push-config.json`
93
+ await writeFile(configPath, JSON.stringify(updateConfig))
94
+
95
+ const args = ['bricks', command, 'update', app.id, '-f', configPath, '--json']
96
+ if (noValidate) args.push('--no-validate')
97
+ if (lastCommitId) args.push('--last-commit-id', lastCommitId)
98
+
99
+ const result = await sh`${args}`.quiet().nothrow()
100
+
101
+ if (result.exitCode !== 0) {
102
+ const output = result.stderr.toString() || result.stdout.toString()
103
+ throw new Error(extractCliErrorMessage(output, 'Update failed'))
104
+ }
105
+
106
+ const output = JSON.parse(result.stdout.toString())
107
+
108
+ // Record the commit we just pushed from so a later pull can use it as the
109
+ // merge base regardless of what the server stores in
110
+ // bricks_project_last_commit_id (which may be an opaque nanoid for
111
+ // content-only edits).
112
+ if (commitId) await writeLastPushedCommit(cwd, commitId)
113
+
114
+ console.log(`${isModule ? 'Module' : 'App'} config updated: ${output.target?.name || app.name}`)