@fugood/bricks-ctor 2.25.0-beta.6 → 2.25.0-beta.60
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 +461 -0
- package/compile/__tests__/util.test.js +450 -0
- package/compile/action-name-map.ts +64 -0
- package/compile/config-diff.ts +155 -0
- package/compile/index.ts +668 -352
- package/compile/util.ts +134 -10
- 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 +19 -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 +252 -0
- package/skills/bricks-ctor/{rules → references}/media-flow.md +7 -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/__tests__/_mcp-config.test.ts +67 -0
- package/tools/__tests__/pull.test.ts +108 -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/_mcp-config.ts +42 -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 +98 -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 +1311 -0
- package/tools/mcp-tools/entry-editing.ts +2297 -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 +95 -38
- package/tools/pull.ts +100 -23
- 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} +11 -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} +10 -8
- 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} +80 -13
- 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/{HttpServer.ts → HttpServer.d.ts} +56 -2
- 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/{Tick.ts → Tick.d.ts} +1 -1
- 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 +27 -1
- 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}/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/{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/{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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from 'fs/promises'
|
|
13
13
|
import * as path from 'path'
|
|
14
14
|
import TOML from '@iarna/toml'
|
|
15
|
+
import { handleMcpConfigOverride } from './_mcp-config'
|
|
15
16
|
|
|
16
17
|
const cwd = process.cwd()
|
|
17
18
|
const projectSkillsDir = path.join(cwd, '.bricks', 'skills')
|
|
@@ -69,36 +70,15 @@ const projectMcpServer = {
|
|
|
69
70
|
args: [`${cwd}/node_modules/@fugood/bricks-ctor/tools/mcp-server.ts`],
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const defaultMcpConfig = {
|
|
78
|
-
mcpServers: {
|
|
79
|
-
'bricks-ctor': projectMcpServer,
|
|
80
|
-
},
|
|
73
|
+
// Codex cancels MCP tool calls it cannot prompt approval for (e.g. `codex exec`),
|
|
74
|
+
// so the project-local server's tools must be pre-approved in its config entry.
|
|
75
|
+
const codexProjectMcpServer = {
|
|
76
|
+
...projectMcpServer,
|
|
77
|
+
default_tools_approval_mode: 'approve',
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (await exists(mcpConfigPath)) {
|
|
86
|
-
const configStr = await readFile(mcpConfigPath, 'utf-8')
|
|
87
|
-
try {
|
|
88
|
-
mcpConfig = JSON.parse(configStr)
|
|
89
|
-
if (!mcpConfig?.mcpServers) throw new Error('mcpServers is not defined')
|
|
90
|
-
mcpConfig.mcpServers['bricks-ctor'] = projectMcpServer
|
|
91
|
-
delete mcpConfig.mcpServers['bricks-project']
|
|
92
|
-
} catch {
|
|
93
|
-
mcpConfig = defaultMcpConfig
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
mcpConfig = defaultMcpConfig
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
await writeFile(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`)
|
|
100
|
-
|
|
101
|
-
console.log(`Updated ${mcpConfigPath}`)
|
|
80
|
+
type CodexMcpConfig = {
|
|
81
|
+
mcp_servers: Record<string, typeof codexProjectMcpServer | typeof projectMcpServer>
|
|
102
82
|
}
|
|
103
83
|
|
|
104
84
|
const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
|
|
@@ -107,7 +87,7 @@ const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)
|
|
|
107
87
|
if (hasClaudeCode || hasAgentsMd) {
|
|
108
88
|
// Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
|
|
109
89
|
const mcpConfigPath = `${cwd}/.mcp.json`
|
|
110
|
-
await handleMcpConfigOverride(mcpConfigPath)
|
|
90
|
+
await handleMcpConfigOverride(mcpConfigPath, projectMcpServer)
|
|
111
91
|
}
|
|
112
92
|
|
|
113
93
|
const copyMissingSkills = async (sourceDir: string, targetDir: string) => {
|
|
@@ -196,27 +176,104 @@ if (hasClaudeCode || hasAgentsMd) {
|
|
|
196
176
|
await setupSkills()
|
|
197
177
|
}
|
|
198
178
|
|
|
179
|
+
type ClaudeSettings = {
|
|
180
|
+
autoMode?: {
|
|
181
|
+
environment?: string[]
|
|
182
|
+
allow?: string[]
|
|
183
|
+
soft_deny?: string[]
|
|
184
|
+
hard_deny?: string[]
|
|
185
|
+
}
|
|
186
|
+
[key: string]: unknown
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Trusted infrastructure for auto mode's classifier. `$defaults` keeps the
|
|
190
|
+
// built-in environment (the working repo and its git remotes); the extra
|
|
191
|
+
// entries stop routine syncs to the BRICKS backend from being treated as
|
|
192
|
+
// external exfiltration. See https://code.claude.com/docs/en/auto-mode-config
|
|
193
|
+
const autoModeEnvironment = [
|
|
194
|
+
'$defaults',
|
|
195
|
+
'Organization: BRICKS (bricks.tools). Primary use: building BRICKS apps/modules with the bricks CLI and the local bricks-ctor MCP server.',
|
|
196
|
+
'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.',
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
// `.claude/settings.local.json` is per-developer local config; keep it untracked.
|
|
200
|
+
const ensureSettingsLocalGitignored = async () => {
|
|
201
|
+
const gitignorePath = path.join(cwd, '.gitignore')
|
|
202
|
+
const entry = '.claude/settings.local.json'
|
|
203
|
+
const coveredBy = new Set([entry, '.claude', '.claude/', '.claude/*', '*.local.json'])
|
|
204
|
+
|
|
205
|
+
let content = ''
|
|
206
|
+
if (await exists(gitignorePath)) {
|
|
207
|
+
content = await readFile(gitignorePath, 'utf-8')
|
|
208
|
+
if (content.split('\n').some((line) => coveredBy.has(line.trim()))) return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const separator = content.length === 0 ? '' : content.endsWith('\n') ? '\n' : '\n\n'
|
|
212
|
+
await writeFile(gitignorePath, `${content}${separator}# Claude Code local settings\n${entry}\n`)
|
|
213
|
+
console.log(`Added ${entry} to .gitignore`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Pre-configure auto mode once, on initial setup. We only seed the classifier's
|
|
217
|
+
// trusted infrastructure — not `permissions.defaultMode: 'auto'`, which Claude
|
|
218
|
+
// Code ignores from project/local settings (a repo can't grant itself auto mode;
|
|
219
|
+
// it only takes effect from ~/.claude/settings.json). An existing autoMode block
|
|
220
|
+
// is left untouched so reinstalls never clobber a developer's customizations.
|
|
221
|
+
const setupClaudeAutoMode = async () => {
|
|
222
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.local.json')
|
|
223
|
+
|
|
224
|
+
let settings: ClaudeSettings = {}
|
|
225
|
+
if (await exists(settingsPath)) {
|
|
226
|
+
try {
|
|
227
|
+
settings = JSON.parse(await readFile(settingsPath, 'utf-8'))
|
|
228
|
+
} catch {
|
|
229
|
+
console.warn(`Skipping auto mode setup; ${settingsPath} is not valid JSON`)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
if (settings.autoMode) return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
settings.autoMode = { environment: autoModeEnvironment }
|
|
236
|
+
|
|
237
|
+
await mkdir(path.dirname(settingsPath), { recursive: true })
|
|
238
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`)
|
|
239
|
+
console.log(`Set up auto mode in ${settingsPath}`)
|
|
240
|
+
|
|
241
|
+
await ensureSettingsLocalGitignored()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (hasClaudeCode) {
|
|
245
|
+
// Pre-configure auto mode's trusted infrastructure for Claude Code projects.
|
|
246
|
+
await setupClaudeAutoMode()
|
|
247
|
+
}
|
|
248
|
+
|
|
199
249
|
if (hasAgentsMd) {
|
|
200
250
|
// Codex stores its project-local MCP config in .codex/config.toml.
|
|
201
251
|
const defaultCodexMcpConfig = {
|
|
202
252
|
mcp_servers: {
|
|
203
|
-
'bricks-ctor':
|
|
253
|
+
'bricks-ctor': codexProjectMcpServer,
|
|
204
254
|
},
|
|
205
255
|
}
|
|
206
256
|
|
|
207
257
|
const handleCodexMcpConfigOverride = async (mcpConfigPath: string) => {
|
|
208
|
-
let mcpConfig: CodexMcpConfig
|
|
258
|
+
let mcpConfig: CodexMcpConfig
|
|
209
259
|
if (await exists(mcpConfigPath)) {
|
|
210
|
-
|
|
260
|
+
let parsed: unknown
|
|
211
261
|
try {
|
|
212
|
-
|
|
213
|
-
if (!parsed?.mcp_servers) throw new Error('mcp_servers is not defined')
|
|
214
|
-
mcpConfig = { mcp_servers: parsed.mcp_servers }
|
|
215
|
-
mcpConfig.mcp_servers['bricks-ctor'] = projectMcpServer
|
|
216
|
-
delete mcpConfig.mcp_servers['bricks-project']
|
|
262
|
+
parsed = TOML.parse(await readFile(mcpConfigPath, 'utf-8'))
|
|
217
263
|
} catch {
|
|
218
|
-
|
|
264
|
+
// A malformed config is left untouched (with a warning) rather than overwritten with
|
|
265
|
+
// the default — clobbering it would silently delete the user's other server entries.
|
|
266
|
+
// Mirrors handleMcpConfigOverride's handling of a malformed .mcp.json.
|
|
267
|
+
console.warn(`Skipping .codex/config.toml update; ${mcpConfigPath} is not valid TOML`)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
mcpConfig =
|
|
271
|
+
parsed && typeof parsed === 'object' ? (parsed as CodexMcpConfig) : { mcp_servers: {} }
|
|
272
|
+
if (!mcpConfig.mcp_servers || typeof mcpConfig.mcp_servers !== 'object') {
|
|
273
|
+
mcpConfig.mcp_servers = {}
|
|
219
274
|
}
|
|
275
|
+
mcpConfig.mcp_servers['bricks-ctor'] = codexProjectMcpServer
|
|
276
|
+
delete mcpConfig.mcp_servers['bricks-project']
|
|
220
277
|
} else {
|
|
221
278
|
mcpConfig = defaultCodexMcpConfig
|
|
222
279
|
}
|
package/tools/pull.ts
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
1
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { dirname, 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,47 @@ 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
|
|
56
84
|
|
|
57
|
-
|
|
85
|
+
const branchName = isModule
|
|
86
|
+
? 'BRICKS_PROJECT_try-pull-module'
|
|
87
|
+
: 'BRICKS_PROJECT_try-pull-application'
|
|
88
|
+
|
|
89
|
+
let landingBranch = ''
|
|
58
90
|
if (isGitRepo && !force) {
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
landingBranch = (await sh`cd ${cwd} && git branch --show-current`.text()).trim()
|
|
92
|
+
if (!landingBranch) throw new Error('Cannot pull from a detached HEAD')
|
|
93
|
+
|
|
94
|
+
console.log(`Checking commit ${baseCommitId}...`)
|
|
95
|
+
const found = (await sh`cd ${cwd} && git rev-list -1 ${baseCommitId}`.nothrow().text())
|
|
61
96
|
.trim()
|
|
62
97
|
.match(/^[\da-f]{40}$/)
|
|
63
98
|
|
|
64
|
-
const
|
|
99
|
+
const headCommitId = (await sh`cd ${cwd} && git rev-parse HEAD`.text()).trim()
|
|
65
100
|
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
const branchName = isModule
|
|
69
|
-
? 'BRICKS_PROJECT_try-pull-module'
|
|
70
|
-
: 'BRICKS_PROJECT_try-pull-application'
|
|
101
|
+
if (headCommitId === serverLastCommitId) throw new Error('Commit not changed')
|
|
71
102
|
|
|
72
103
|
await sh`cd ${cwd} && git branch -D ${branchName}`.nothrow()
|
|
73
104
|
|
|
105
|
+
// When the base commit isn't reachable in this clone (server stored a
|
|
106
|
+
// nanoid, or the commit was pruned), fall back to forking from current
|
|
107
|
+
// HEAD. The downstream merge into the starting branch collapses both paths
|
|
108
|
+
// into the same result, just with different merge bases.
|
|
74
109
|
if (found) {
|
|
75
|
-
await sh`cd ${cwd} && git checkout -b ${branchName} ${
|
|
110
|
+
await sh`cd ${cwd} && git checkout -b ${branchName} ${baseCommitId}`.nothrow()
|
|
76
111
|
} else {
|
|
77
112
|
await sh`cd ${cwd} && git checkout -b ${branchName}`
|
|
78
|
-
useMain = true
|
|
79
113
|
}
|
|
80
114
|
}
|
|
81
115
|
|
|
@@ -89,6 +123,23 @@ const oxfmtConfig = await readFile(`${cwd}/.oxfmtrc.json`, 'utf8')
|
|
|
89
123
|
printWidth: 100,
|
|
90
124
|
}))
|
|
91
125
|
|
|
126
|
+
const expectedFiles = new Set(files.map((file: { name: string }) => file.name))
|
|
127
|
+
|
|
128
|
+
// Remove orphan .ts files under generator-owned directories before writing.
|
|
129
|
+
// File paths are produced by buildApplicationFiles and use forward slashes,
|
|
130
|
+
// so normalise the walked paths the same way for comparison.
|
|
131
|
+
const orphans: string[] = []
|
|
132
|
+
await Promise.all(
|
|
133
|
+
ownedTsDirs.map(async (dir) => {
|
|
134
|
+
const existing = await walkTsFiles(join(cwd, dir), cwd)
|
|
135
|
+
for (const file of existing) {
|
|
136
|
+
const normalized = file.split(/[\\/]/).join('/')
|
|
137
|
+
if (!expectedFiles.has(normalized)) orphans.push(normalized)
|
|
138
|
+
}
|
|
139
|
+
}),
|
|
140
|
+
)
|
|
141
|
+
await Promise.all(orphans.map((name) => unlink(`${cwd}/${name}`)))
|
|
142
|
+
|
|
92
143
|
await Promise.all(
|
|
93
144
|
files.map(async (file: { name: string; input: string; formatable?: boolean }) => {
|
|
94
145
|
let content = file.input
|
|
@@ -96,7 +147,9 @@ await Promise.all(
|
|
|
96
147
|
const result = await format(file.name, file.input, oxfmtConfig)
|
|
97
148
|
content = result.code
|
|
98
149
|
}
|
|
99
|
-
|
|
150
|
+
const target = join(cwd, file.name)
|
|
151
|
+
await mkdir(dirname(target), { recursive: true })
|
|
152
|
+
return writeFile(target, content)
|
|
100
153
|
}),
|
|
101
154
|
)
|
|
102
155
|
|
|
@@ -112,11 +165,35 @@ if (isGitRepo) {
|
|
|
112
165
|
const commitArgs = await buildCommitArgs(cwd, [commitMsg])
|
|
113
166
|
await sh`cd ${cwd} && git ${commitArgs}`
|
|
114
167
|
}
|
|
115
|
-
if (!force
|
|
116
|
-
|
|
168
|
+
if (!force) {
|
|
169
|
+
// Land the pulled commits on the starting branch with a single 3-way merge using
|
|
170
|
+
// baseCommit as the merge base. The user doesn't have to manage a side
|
|
171
|
+
// branch, and conflicts (if any) land in the working tree on the starting branch where
|
|
172
|
+
// auto-compile surfaces them as typecheck errors to resolve in-place.
|
|
173
|
+
await sh`cd ${cwd} && git checkout ${landingBranch}`
|
|
174
|
+
const mergeResult = await sh`cd ${cwd} && git merge ${branchName} --no-edit`.nothrow()
|
|
175
|
+
if (mergeResult.exitCode !== 0) {
|
|
176
|
+
// Conflict markers are in the working tree — commit them so the tree
|
|
177
|
+
// is clean for auto-compile to detect, leaving the resolution to the
|
|
178
|
+
// user. Pre-commit hooks would reject markers (lint/format fail on
|
|
179
|
+
// invalid syntax), so bypass them for this controlled case.
|
|
180
|
+
await sh`cd ${cwd} && git add .`
|
|
181
|
+
const conflictArgs = await buildCommitArgs(
|
|
182
|
+
cwd,
|
|
183
|
+
[`chore(project): merge with conflicts (resolve in ${landingBranch})`],
|
|
184
|
+
['--no-verify'],
|
|
185
|
+
)
|
|
186
|
+
await sh`cd ${cwd} && git ${conflictArgs}`
|
|
187
|
+
}
|
|
188
|
+
// The try-pull branch served its purpose; delete it so `git branch`
|
|
189
|
+
// stays tidy. The next pull recreates it anyway (line 103).
|
|
190
|
+
await sh`cd ${cwd} && git branch -D ${branchName}`.nothrow()
|
|
117
191
|
}
|
|
192
|
+
// Record the new sync point so a follow-up pull starts from the right base.
|
|
193
|
+
const newHead = (await sh`cd ${cwd} && git rev-parse HEAD`.nothrow().text()).trim()
|
|
194
|
+
if (newHead) await writeLastPushedCommit(cwd, newHead)
|
|
118
195
|
}
|
|
119
196
|
|
|
120
197
|
console.log(
|
|
121
|
-
`${isModule ? 'Module' : 'App'} project pulled: ${files.length} files${force ? ' (force)' : ''}`,
|
|
198
|
+
`${isModule ? 'Module' : 'App'} project pulled: ${files.length} files${orphans.length ? `, removed ${orphans.length} orphan .ts file${orphans.length === 1 ? '' : 's'}` : ''}${force ? ' (force)' : ''}`,
|
|
122
199
|
)
|
|
@@ -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}`)
|