@fugood/bricks-ctor 2.25.0-beta.46 → 2.25.0-beta.48
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__/index.test.js +24 -2
- package/compile/__tests__/util.test.js +22 -0
- package/compile/index.ts +7 -2
- package/compile/util.ts +16 -0
- package/package.json +3 -3
- package/skills/bricks-ctor/references/verification-toolchain.md +19 -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/huggingface.ts +23 -13
- package/tools/mcp-tools/icons.ts +23 -7
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
jest.mock('../../tools/_shell', () => ({
|
|
2
|
-
sh: jest.fn(
|
|
2
|
+
sh: jest.fn(),
|
|
3
3
|
}))
|
|
4
4
|
|
|
5
|
+
// Mirrors the chainable `sh` result (supports `.nothrow()` like the real helper).
|
|
6
|
+
const shResult = (over = {}) => {
|
|
7
|
+
const result = Promise.resolve({ exitCode: 0, stdout: '', stderr: '', ...over })
|
|
8
|
+
result.nothrow = () => result
|
|
9
|
+
return result
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
6
13
|
import os from 'node:os'
|
|
7
14
|
import path from 'node:path'
|
|
@@ -57,7 +64,8 @@ const commandOf = ([strings, ...values]) =>
|
|
|
57
64
|
|
|
58
65
|
describe('checkConfig', () => {
|
|
59
66
|
beforeEach(() => {
|
|
60
|
-
sh.
|
|
67
|
+
sh.mockReset()
|
|
68
|
+
sh.mockImplementation(() => shResult())
|
|
61
69
|
})
|
|
62
70
|
|
|
63
71
|
test('runs doctor after check-config', async () => {
|
|
@@ -68,6 +76,20 @@ describe('checkConfig', () => {
|
|
|
68
76
|
'bricks app doctor --validate-automation .bricks/build/application-config.json',
|
|
69
77
|
])
|
|
70
78
|
})
|
|
79
|
+
|
|
80
|
+
test('skips doctor when the CLI lacks the command', async () => {
|
|
81
|
+
sh.mockReturnValueOnce(shResult()) // check-config
|
|
82
|
+
sh.mockReturnValueOnce(shResult({ exitCode: 1, stderr: "error: unknown command 'doctor'" }))
|
|
83
|
+
|
|
84
|
+
await expect(checkConfig('config.json')).resolves.toBeUndefined()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('throws when doctor reports config errors', async () => {
|
|
88
|
+
sh.mockReturnValueOnce(shResult()) // check-config
|
|
89
|
+
sh.mockReturnValueOnce(shResult({ exitCode: 1, stderr: 'DATA_RACE: conflicting writes' }))
|
|
90
|
+
|
|
91
|
+
await expect(checkConfig('config.json')).rejects.toThrow()
|
|
92
|
+
})
|
|
71
93
|
})
|
|
72
94
|
|
|
73
95
|
describe('compile animations', () => {
|
|
@@ -64,6 +64,28 @@ describe('validateConfig', () => {
|
|
|
64
64
|
test('does not throw when error/output are falsy and inputs are empty', () => {
|
|
65
65
|
expect(() => validateConfig(baseConfig())).not.toThrow()
|
|
66
66
|
})
|
|
67
|
+
|
|
68
|
+
test('throws when error and output target the same id', () => {
|
|
69
|
+
const config = baseConfig({ output: 'shared', error: 'shared' })
|
|
70
|
+
expect(() => validateConfig(config)).toThrow(/key: error\/output/)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('throws when output collides with an outputs target id', () => {
|
|
74
|
+
const config = baseConfig({ output: 'shared', outputs: { x: ['shared'] } })
|
|
75
|
+
expect(() => validateConfig(config)).toThrow(/key: output\/outputs/)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('throws when error collides with an outputs target id', () => {
|
|
79
|
+
const config = baseConfig({ error: 'shared', outputs: { x: ['shared'] } })
|
|
80
|
+
expect(() => validateConfig(config)).toThrow(/key: error\/outputs/)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// The same id reused across *different* outputs entries is a supported last-wins case
|
|
84
|
+
// (see generateCalulationMap test below) and must stay allowed.
|
|
85
|
+
test('allows the same id across multiple outputs entries', () => {
|
|
86
|
+
const config = baseConfig({ outputs: { first: ['pb1'], second: ['pb1'] } })
|
|
87
|
+
expect(() => validateConfig(config)).not.toThrow()
|
|
88
|
+
})
|
|
67
89
|
})
|
|
68
90
|
|
|
69
91
|
// generateCalulationMap now seeds command ids from the owning calc id; tests that don't
|
package/compile/index.ts
CHANGED
|
@@ -1494,6 +1494,11 @@ export const checkConfig = async (configPath: string) => {
|
|
|
1494
1494
|
// which catches agent-authored automations that reference deleted bricks.
|
|
1495
1495
|
await sh`bricks app check-config --validate-automation ${configPath}`
|
|
1496
1496
|
// Doctor adds semantic lint checks after structural validation. Warnings are
|
|
1497
|
-
// surfaced in the compile log, but only errors fail by default.
|
|
1498
|
-
|
|
1497
|
+
// surfaced in the compile log, but only errors fail by default. Older published
|
|
1498
|
+
// bricks-cli builds lack `app doctor` — skip rather than fail the compile.
|
|
1499
|
+
const doctor = await sh`bricks app doctor --validate-automation ${configPath}`.nothrow()
|
|
1500
|
+
if (doctor.exitCode !== 0) {
|
|
1501
|
+
if (/unknown command/i.test(doctor.stderr?.toString() ?? '')) return
|
|
1502
|
+
throw new Error(`bricks app doctor failed with exit ${doctor.exitCode}`)
|
|
1503
|
+
}
|
|
1499
1504
|
}
|
package/compile/util.ts
CHANGED
|
@@ -27,6 +27,22 @@ export const validateConfig = (config: ScriptConfig) => {
|
|
|
27
27
|
if (Object.values(config.outputs).some((value) => value.some((id) => config.inputs[id]))) {
|
|
28
28
|
throw new Error(`${errorMsg}. key: outputs`)
|
|
29
29
|
}
|
|
30
|
+
// The same data-node id reused across the output-side targets (output / error / outputs)
|
|
31
|
+
// also collides: generateCalulationMap spreads their node objects last-wins, so the later
|
|
32
|
+
// one silently overwrites the earlier wiring (e.g. error == output drops the error change
|
|
33
|
+
// link). The checks above only compare against `inputs`, so guard the output-side pairs
|
|
34
|
+
// too. (The same id appearing in multiple `outputs` entries is a supported last-wins case
|
|
35
|
+
// and stays allowed.)
|
|
36
|
+
if (config.error && config.output && config.error === config.output) {
|
|
37
|
+
throw new Error(`${errorMsg}. key: error/output`)
|
|
38
|
+
}
|
|
39
|
+
const outputsIds = Object.values(config.outputs).flat()
|
|
40
|
+
if (config.output && outputsIds.includes(config.output)) {
|
|
41
|
+
throw new Error(`${errorMsg}. key: output/outputs`)
|
|
42
|
+
}
|
|
43
|
+
if (config.error && outputsIds.includes(config.error)) {
|
|
44
|
+
throw new Error(`${errorMsg}. key: error/outputs`)
|
|
45
|
+
}
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
const padding = 15
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.25.0-beta.
|
|
3
|
+
"version": "2.25.0-beta.48",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@babel/parser": "7.28.5",
|
|
12
12
|
"@babel/traverse": "7.28.5",
|
|
13
13
|
"@babel/types": "7.28.5",
|
|
14
|
-
"@fugood/bricks-cli": "^2.25.0-beta.
|
|
14
|
+
"@fugood/bricks-cli": "^2.25.0-beta.48",
|
|
15
15
|
"@huggingface/gguf": "^0.3.2",
|
|
16
16
|
"@iarna/toml": "^3.0.0",
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"oxfmt": "^0.36.0"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "6403fb9aece6b9ee45a72a963ddc75aec5ae3e04"
|
|
33
33
|
}
|
|
@@ -122,6 +122,25 @@ Once DevTools is on, ask the user how they want to drive the verification — Ch
|
|
|
122
122
|
|
|
123
123
|
For agent-driven CDP/MCP work against the device (`bricks devtools …` with `-a <ip> --passcode <pc>`, plus bridging the device MCP endpoint into an MCP client), the same `bricks-cli` skill referenced in Path 1 covers the on-device case — read it if installed. If not installed, run `bricks --help` and `bricks devtools --help` for the authoritative command listing.
|
|
124
124
|
|
|
125
|
+
### Running real-device Automations from an agent
|
|
126
|
+
|
|
127
|
+
There is no `bricks devtools automation` subcommand. Use the DevTools runtime helpers exposed inside the app:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "Object.getOwnPropertyNames(automation).sort()" -j
|
|
131
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "automation.list()" -j
|
|
132
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "automation.run('<TEST_id>', { updateScreenshot: false })" --await -j
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`automation.run()` starts the device-side run and may return `null`; treat that as accepted, not as a pass/fail result. Wait for the run timeout/window, then verify completion through the app's own state and a screenshot. For app state, use live runtime reads such as:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "JSON.stringify({ result: system.data.property('<S_xxxx>', '<resultAlias>')?.value })" -j
|
|
139
|
+
bricks devtools screenshot -a <ip> -p 19851 --passcode <pc> -o /tmp/device-automation.png
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
In CTOR Desktop sandboxed sessions, keep these as separate simple commands. Avoid multi-line shell scripts, `for` loops, brace expansion, and command substitution around `bricks devtools`; those can stay sandboxed and lose LAN/device access. After `bun update-app` or a device refresh, the DevTools socket may briefly drop, so wait and probe with one screenshot or one `runtime eval` before running the automation.
|
|
143
|
+
|
|
125
144
|
### Real-device side-effects warning
|
|
126
145
|
|
|
127
146
|
Real devices fire real peripherals. Payment terminals charge. MQTT broadcasts on shared topics. BLE advertises to bystanders. Use a **staging** device for verification cycles; never iterate on a production-deployed device unless the user explicitly approves.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildGGUFSplitFiles } from '../huggingface'
|
|
2
|
+
|
|
3
|
+
declare const describe: (name: string, fn: () => void) => void
|
|
4
|
+
declare const it: (name: string, fn: () => void) => void
|
|
5
|
+
declare const expect: (actual: unknown) => { toEqual: (expected: unknown) => void }
|
|
6
|
+
|
|
7
|
+
describe('buildGGUFSplitFiles', () => {
|
|
8
|
+
it('builds split GGUF files with indexed sibling metadata lookup', () => {
|
|
9
|
+
const siblings = [
|
|
10
|
+
{
|
|
11
|
+
rfilename: 'other-file.txt',
|
|
12
|
+
size: 999,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
rfilename: 'model-00001-of-00003.gguf',
|
|
16
|
+
size: 1,
|
|
17
|
+
lfs: { sha256: 'one' },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
rfilename: 'model-00002-of-00003.gguf',
|
|
21
|
+
size: 2,
|
|
22
|
+
lfs: { sha256: 'two' },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
rfilename: 'model-00002-of-00003.gguf',
|
|
26
|
+
size: 22,
|
|
27
|
+
lfs: { sha256: 'two-late' },
|
|
28
|
+
},
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
expect(buildGGUFSplitFiles('model-00001-of-00003.gguf', '00003', siblings)).toEqual([
|
|
32
|
+
{
|
|
33
|
+
rfilename: 'model-00001-of-00003.gguf',
|
|
34
|
+
size: 1,
|
|
35
|
+
lfs: { sha256: 'one' },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
rfilename: 'model-00002-of-00003.gguf',
|
|
39
|
+
size: 2,
|
|
40
|
+
lfs: { sha256: 'two' },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
rfilename: 'model-00003-of-00003.gguf',
|
|
44
|
+
size: undefined,
|
|
45
|
+
lfs: undefined,
|
|
46
|
+
},
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { searchIcons } from '../icons'
|
|
2
|
+
|
|
3
|
+
declare const describe: (name: string, fn: () => void) => void
|
|
4
|
+
declare const test: (name: string, fn: () => void) => void
|
|
5
|
+
declare const expect: (actual: unknown) => {
|
|
6
|
+
toBe: (expected: unknown) => void
|
|
7
|
+
toHaveLength: (expected: number) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('icon_search helpers', () => {
|
|
11
|
+
test('searches the selected style corpus directly', () => {
|
|
12
|
+
const results = searchIcons('x', 10, 'brands')
|
|
13
|
+
|
|
14
|
+
expect(results).toHaveLength(10)
|
|
15
|
+
expect(results.every((result) => result.item.styles.includes('brands'))).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('keeps unstyled searches capped by limit', () => {
|
|
19
|
+
expect(searchIcons('arrow', 5)).toHaveLength(5)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -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,
|