@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.
- package/compile/__tests__/config-diff.test.js +100 -0
- package/compile/__tests__/index.test.js +365 -0
- package/compile/__tests__/util.test.js +317 -0
- package/compile/action-name-map.ts +64 -0
- package/compile/config-diff.ts +155 -0
- package/compile/index.ts +273 -32
- package/compile/util.ts +26 -7
- package/package.json +7 -3
- package/skills/bricks-ctor/SKILL.md +23 -17
- package/skills/bricks-ctor/{rules → references}/animation.md +3 -2
- package/skills/bricks-ctor/{rules → references}/architecture-patterns.md +18 -0
- package/skills/bricks-ctor/{rules → references}/automations.md +11 -0
- package/skills/bricks-ctor/references/buttress.md +245 -0
- package/skills/bricks-ctor/references/data-calculation.md +239 -0
- package/skills/bricks-ctor/references/simulator.md +132 -0
- package/skills/bricks-ctor/references/source-editing-tools.md +81 -0
- package/skills/bricks-ctor/references/verification-toolchain.md +200 -0
- package/skills/bricks-design/SKILL.md +150 -45
- package/skills/bricks-design/references/architecture-truths.md +132 -0
- package/skills/bricks-design/references/avoiding-complexity.md +91 -0
- package/skills/bricks-design/references/design-critique.md +195 -0
- package/skills/bricks-design/references/design-languages.md +265 -0
- package/skills/bricks-design/references/performance.md +116 -0
- package/skills/bricks-design/references/presentation-and-slideshow.md +137 -0
- package/skills/bricks-design/references/translating-inputs.md +152 -0
- package/skills/bricks-design/references/variations-and-tweaks.md +124 -0
- package/skills/bricks-design/references/when-the-brief-is-branded.md +284 -0
- package/skills/bricks-design/references/when-the-brief-is-vague.md +85 -0
- package/skills/bricks-design/references/workflow.md +134 -0
- package/skills/bricks-ux/SKILL.md +114 -0
- package/skills/bricks-ux/references/accessibility.md +162 -0
- package/skills/bricks-ux/references/flow-states.md +175 -0
- package/skills/bricks-ux/references/interaction-archetypes.md +189 -0
- package/skills/bricks-ux/references/monitoring-screens.md +153 -0
- package/skills/bricks-ux/references/pressable-composition.md +126 -0
- package/skills/bricks-ux/references/user-journey.md +168 -0
- package/skills/bricks-ux/references/ux-critique.md +256 -0
- package/tools/__tests__/_cli-error.test.ts +35 -0
- package/tools/_cli-error.ts +17 -0
- package/tools/_edits-log.ts +41 -0
- package/tools/_git-author.ts +10 -2
- package/tools/_last-pushed-commit.ts +28 -0
- package/tools/_shell.ts +8 -1
- package/tools/deploy.ts +17 -6
- package/tools/mcp-env.ts +13 -0
- package/tools/mcp-server.ts +8 -0
- package/tools/mcp-tools/__tests__/data-calc-editing.test.js +516 -0
- package/tools/mcp-tools/__tests__/entry-editing.test.js +866 -0
- package/tools/mcp-tools/__tests__/huggingface.test.ts +49 -0
- package/tools/mcp-tools/__tests__/icons.test.ts +21 -0
- package/tools/mcp-tools/__tests__/mcp-env.test.js +19 -0
- package/tools/mcp-tools/_editing-helpers.ts +58 -0
- package/tools/mcp-tools/_verify.ts +50 -0
- package/tools/mcp-tools/compile.ts +21 -9
- package/tools/mcp-tools/data-calc-editing.ts +1349 -0
- package/tools/mcp-tools/entry-editing.ts +2336 -0
- package/tools/mcp-tools/huggingface.ts +23 -13
- package/tools/mcp-tools/icons.ts +23 -7
- package/tools/mcp-tools/media.ts +4 -1
- package/tools/postinstall.ts +80 -3
- package/tools/pull.ts +93 -22
- package/tools/push-config.ts +114 -0
- package/tools/{preview-main.mjs → simulator-main.mjs} +207 -12
- package/tools/simulator-preload.cjs +16 -0
- package/tools/{preview.ts → simulator.ts} +4 -4
- package/types/{animation.ts → animation.d.ts} +24 -8
- package/types/{automation.ts → automation.d.ts} +16 -20
- package/types/{brick-base.ts → brick-base.d.ts} +1 -1
- package/types/bricks/{Camera.ts → Camera.d.ts} +8 -8
- package/types/bricks/{Chart.ts → Chart.d.ts} +4 -4
- package/types/bricks/{GenerativeMedia.ts → GenerativeMedia.d.ts} +15 -15
- package/types/bricks/{Icon.ts → Icon.d.ts} +7 -7
- package/types/bricks/{Image.ts → Image.d.ts} +21 -9
- package/types/bricks/{Items.ts → Items.d.ts} +7 -7
- package/types/bricks/{Lottie.ts → Lottie.d.ts} +10 -10
- package/types/bricks/{Maps.ts → Maps.d.ts} +11 -11
- package/types/bricks/{QrCode.ts → QrCode.d.ts} +7 -7
- package/types/bricks/{Rect.ts → Rect.d.ts} +7 -7
- package/types/bricks/{RichText.ts → RichText.d.ts} +12 -9
- package/types/bricks/{Rive.ts → Rive.d.ts} +9 -9
- package/types/bricks/Scene3D.d.ts +676 -0
- package/types/bricks/{Sketch.ts → Sketch.d.ts} +6 -6
- package/types/bricks/{Slideshow.ts → Slideshow.d.ts} +7 -7
- package/types/bricks/{Svg.ts → Svg.d.ts} +7 -7
- package/types/bricks/{Text.ts → Text.d.ts} +9 -9
- package/types/bricks/{TextInput.ts → TextInput.d.ts} +10 -10
- package/types/bricks/{Video.ts → Video.d.ts} +12 -12
- package/types/bricks/{VideoStreaming.ts → VideoStreaming.d.ts} +10 -10
- package/types/bricks/{WebRtcStream.ts → WebRtcStream.d.ts} +1 -1
- package/types/bricks/{WebView.ts → WebView.d.ts} +4 -4
- package/types/bricks/{index.ts → index.d.ts} +1 -0
- package/types/{common.ts → common.d.ts} +3 -6
- package/types/data-calc-command/base.d.ts +57 -0
- package/types/data-calc-command/collection.d.ts +418 -0
- package/types/data-calc-command/color.d.ts +432 -0
- package/types/data-calc-command/constant.d.ts +50 -0
- package/types/data-calc-command/datetime.d.ts +147 -0
- package/types/data-calc-command/file.d.ts +129 -0
- package/types/data-calc-command/index.d.ts +13 -0
- package/types/data-calc-command/iteratee.d.ts +23 -0
- package/types/data-calc-command/logictype.d.ts +190 -0
- package/types/data-calc-command/math.d.ts +275 -0
- package/types/data-calc-command/object.d.ts +119 -0
- package/types/data-calc-command/sandbox.d.ts +66 -0
- package/types/data-calc-command/string.d.ts +407 -0
- package/types/{data-calc.ts → data-calc.d.ts} +1 -0
- package/types/{data.ts → data.d.ts} +4 -2
- package/types/generators/{Assistant.ts → Assistant.d.ts} +19 -0
- package/types/generators/{LlmGgml.ts → LlmGgml.d.ts} +43 -1
- package/types/generators/{LlmMlx.ts → LlmMlx.d.ts} +1 -0
- package/types/generators/{RerankerGgml.ts → RerankerGgml.d.ts} +5 -1
- package/types/generators/{SoundRecorder.ts → SoundRecorder.d.ts} +10 -1
- package/types/generators/{SpeechToTextGgml.ts → SpeechToTextGgml.d.ts} +6 -1
- package/types/generators/{SttAppleBuiltin.ts → SttAppleBuiltin.d.ts} +27 -4
- package/types/generators/{ThermalPrinter.ts → ThermalPrinter.d.ts} +9 -7
- package/types/generators/{VadGgml.ts → VadGgml.d.ts} +12 -2
- package/types/{subspace.ts → subspace.d.ts} +1 -1
- package/utils/__tests__/calc.test.js +25 -0
- package/utils/__tests__/id.test.js +154 -0
- package/utils/calc.ts +5 -1
- package/utils/data.ts +5 -7
- package/utils/event-props.ts +17 -0
- package/utils/id.ts +109 -56
- package/skills/bricks-ctor/rules/buttress.md +0 -156
- package/skills/bricks-ctor/rules/data-calculation.md +0 -209
- package/skills/bricks-design/LICENSE.txt +0 -180
- package/types/data-calc-command.ts +0 -7005
- /package/skills/bricks-ctor/{rules → references}/local-sync.md +0 -0
- /package/skills/bricks-ctor/{rules → references}/media-flow.md +0 -0
- /package/skills/bricks-ctor/{rules → references}/remote-data-bank.md +0 -0
- /package/skills/bricks-ctor/{rules → references}/standby-transition.md +0 -0
- /package/types/{canvas.ts → canvas.d.ts} +0 -0
- /package/types/{data-calc-script.ts → data-calc-script.d.ts} +0 -0
- /package/types/generators/{AlarmClock.ts → AlarmClock.d.ts} +0 -0
- /package/types/generators/{BleCentral.ts → BleCentral.d.ts} +0 -0
- /package/types/generators/{BlePeripheral.ts → BlePeripheral.d.ts} +0 -0
- /package/types/generators/{CanvasMap.ts → CanvasMap.d.ts} +0 -0
- /package/types/generators/{CastlesPay.ts → CastlesPay.d.ts} +0 -0
- /package/types/generators/{DataBank.ts → DataBank.d.ts} +0 -0
- /package/types/generators/{File.ts → File.d.ts} +0 -0
- /package/types/generators/{GraphQl.ts → GraphQl.d.ts} +0 -0
- /package/types/generators/{Http.ts → Http.d.ts} +0 -0
- /package/types/generators/{HttpServer.ts → HttpServer.d.ts} +0 -0
- /package/types/generators/{Information.ts → Information.d.ts} +0 -0
- /package/types/generators/{Intent.ts → Intent.d.ts} +0 -0
- /package/types/generators/{Iterator.ts → Iterator.d.ts} +0 -0
- /package/types/generators/{Keyboard.ts → Keyboard.d.ts} +0 -0
- /package/types/generators/{LlmAnthropicCompat.ts → LlmAnthropicCompat.d.ts} +0 -0
- /package/types/generators/{LlmAppleBuiltin.ts → LlmAppleBuiltin.d.ts} +0 -0
- /package/types/generators/{LlmMediaTekNeuroPilot.ts → LlmMediaTekNeuroPilot.d.ts} +0 -0
- /package/types/generators/{LlmOnnx.ts → LlmOnnx.d.ts} +0 -0
- /package/types/generators/{LlmOpenAiCompat.ts → LlmOpenAiCompat.d.ts} +0 -0
- /package/types/generators/{LlmQualcommAiEngine.ts → LlmQualcommAiEngine.d.ts} +0 -0
- /package/types/generators/{Mcp.ts → Mcp.d.ts} +0 -0
- /package/types/generators/{McpServer.ts → McpServer.d.ts} +0 -0
- /package/types/generators/{MediaFlow.ts → MediaFlow.d.ts} +0 -0
- /package/types/generators/{MqttBroker.ts → MqttBroker.d.ts} +0 -0
- /package/types/generators/{MqttClient.ts → MqttClient.d.ts} +0 -0
- /package/types/generators/{Question.ts → Question.d.ts} +0 -0
- /package/types/generators/{RealtimeTranscription.ts → RealtimeTranscription.d.ts} +0 -0
- /package/types/generators/{SerialPort.ts → SerialPort.d.ts} +0 -0
- /package/types/generators/{SoundPlayer.ts → SoundPlayer.d.ts} +0 -0
- /package/types/generators/{SpeechToTextOnnx.ts → SpeechToTextOnnx.d.ts} +0 -0
- /package/types/generators/{SpeechToTextPlatform.ts → SpeechToTextPlatform.d.ts} +0 -0
- /package/types/generators/{SqLite.ts → SqLite.d.ts} +0 -0
- /package/types/generators/{Step.ts → Step.d.ts} +0 -0
- /package/types/generators/{Tcp.ts → Tcp.d.ts} +0 -0
- /package/types/generators/{TcpServer.ts → TcpServer.d.ts} +0 -0
- /package/types/generators/{TextToSpeechAppleBuiltin.ts → TextToSpeechAppleBuiltin.d.ts} +0 -0
- /package/types/generators/{TextToSpeechGgml.ts → TextToSpeechGgml.d.ts} +0 -0
- /package/types/generators/{TextToSpeechOnnx.ts → TextToSpeechOnnx.d.ts} +0 -0
- /package/types/generators/{TextToSpeechOpenAiLike.ts → TextToSpeechOpenAiLike.d.ts} +0 -0
- /package/types/generators/{Tick.ts → Tick.d.ts} +0 -0
- /package/types/generators/{Udp.ts → Udp.d.ts} +0 -0
- /package/types/generators/{VadOnnx.ts → VadOnnx.d.ts} +0 -0
- /package/types/generators/{VadTraditional.ts → VadTraditional.d.ts} +0 -0
- /package/types/generators/{VectorStore.ts → VectorStore.d.ts} +0 -0
- /package/types/generators/{Watchdog.ts → Watchdog.d.ts} +0 -0
- /package/types/generators/{WebCrawler.ts → WebCrawler.d.ts} +0 -0
- /package/types/generators/{WebRtc.ts → WebRtc.d.ts} +0 -0
- /package/types/generators/{WebSocket.ts → WebSocket.d.ts} +0 -0
- /package/types/generators/{index.ts → index.d.ts} +0 -0
- /package/types/{index.ts → index.d.ts} +0 -0
- /package/types/{switch.ts → switch.d.ts} +0 -0
- /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 =
|
|
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)
|
package/tools/mcp-tools/icons.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
package/tools/mcp-tools/media.ts
CHANGED
|
@@ -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
|
}
|
package/tools/postinstall.ts
CHANGED
|
@@ -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':
|
|
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'] =
|
|
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
|
-
|
|
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 ${
|
|
60
|
-
const found = (await sh`cd ${cwd} && git rev-list -1 ${
|
|
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
|
|
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
|
-
|
|
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} ${
|
|
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
|
|
116
|
-
|
|
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}`)
|