@fugood/bricks-ctor 2.24.8 → 2.24.10
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/index.ts +49 -25
- package/compile/util.ts +34 -10
- package/package.json +2 -2
- package/skills/bricks-ctor/references/architecture-patterns.md +1 -0
- package/skills/bricks-ctor/references/data-calculation.md +13 -0
- package/skills/bricks-ctor/references/verification-toolchain.md +21 -0
- package/tools/_cli-error.ts +17 -0
- package/tools/_mcp-config.ts +42 -0
- package/tools/deploy.ts +2 -6
- package/tools/mcp-tools/huggingface.ts +23 -13
- package/tools/mcp-tools/icons.ts +23 -7
- package/tools/postinstall.ts +17 -37
- package/tools/pull.ts +17 -15
- package/tools/push-config.ts +2 -6
- package/types/bricks/Sketch.d.ts +4 -2
- package/types/data-calc-command/datetime.d.ts +2 -2
- package/types/generators/Tick.d.ts +1 -1
- package/utils/calc.ts +5 -1
- package/utils/data.ts +1 -1
- package/utils/event-props.ts +3 -1
- package/utils/id.ts +39 -37
package/compile/index.ts
CHANGED
|
@@ -4,9 +4,9 @@ import upperFirst from 'lodash/upperFirst'
|
|
|
4
4
|
import snakeCase from 'lodash/snakeCase'
|
|
5
5
|
import omit from 'lodash/omit'
|
|
6
6
|
import { parse as parseAST } from 'acorn'
|
|
7
|
-
import type { ExportNamedDeclaration, FunctionDeclaration } from 'acorn'
|
|
7
|
+
import type { BlockStatement, ExportNamedDeclaration, FunctionDeclaration } from 'acorn'
|
|
8
8
|
import escodegen from 'escodegen'
|
|
9
|
-
import {
|
|
9
|
+
import { makeSeededId } from '../utils/id'
|
|
10
10
|
import { generateCalulationMap } from './util'
|
|
11
11
|
import { templateActionNameMap } from './action-name-map'
|
|
12
12
|
import { templateEventPropsMap } from '../utils/event-props'
|
|
@@ -97,17 +97,28 @@ const compileProperty = (property, errorReference: string, result = {}) => {
|
|
|
97
97
|
const compileScriptCalculationCode = (code = '') => {
|
|
98
98
|
try {
|
|
99
99
|
const program = parseAST(code, { sourceType: 'module', ecmaVersion: 2020 })
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
// The stored config holds the bare function body, which codegen re-wraps as
|
|
101
|
+
// `export function main() { <code> }`. Unwrap it back to that body here.
|
|
102
|
+
let block = ((program.body[0] as ExportNamedDeclaration).declaration as FunctionDeclaration)
|
|
103
|
+
?.body as BlockStatement | undefined
|
|
104
|
+
if (!block) return code || ''
|
|
105
|
+
// Earlier versions emitted the whole BlockStatement (braces included), so every
|
|
106
|
+
// compile -> codegen round-trip nested the body in one more `{ }`. Emit the inner
|
|
107
|
+
// statements instead, collapsing any wrapper blocks previous round-trips added so
|
|
108
|
+
// existing over-wrapped sandboxes heal on the next compile.
|
|
109
|
+
while (block.body.length === 1 && block.body[0].type === 'BlockStatement') {
|
|
110
|
+
block = block.body[0] as BlockStatement
|
|
111
|
+
}
|
|
112
|
+
return escodegen.generate(
|
|
113
|
+
{ type: 'Program', body: block.body },
|
|
114
|
+
{
|
|
115
|
+
format: {
|
|
116
|
+
indent: { style: ' ' },
|
|
117
|
+
semicolons: false,
|
|
118
|
+
},
|
|
119
|
+
comment: true,
|
|
108
120
|
},
|
|
109
|
-
|
|
110
|
-
})
|
|
121
|
+
)
|
|
111
122
|
} catch {
|
|
112
123
|
return code || ''
|
|
113
124
|
}
|
|
@@ -181,10 +192,17 @@ const compileEvents = (
|
|
|
181
192
|
|
|
182
193
|
let handlerKey
|
|
183
194
|
let handlerTemplateKey
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
if (typeof handler === 'string') {
|
|
196
|
+
// Only the literal 'system' handler is normalized to the SYSTEM template key.
|
|
197
|
+
// SubspaceID (SUBSPACE_*) and ItemBrickID handlers are kept verbatim: the runtime
|
|
198
|
+
// resolves them case-sensitively (see mapEventMapHandlersWithNewId), so uppercasing
|
|
199
|
+
// a mixed-case ItemBrickID would break handler-to-item event wiring.
|
|
200
|
+
if (handler === 'system') {
|
|
201
|
+
handlerKey = 'SYSTEM'
|
|
202
|
+
handlerTemplateKey = 'SYSTEM'
|
|
203
|
+
} else {
|
|
204
|
+
handlerKey = handler
|
|
205
|
+
}
|
|
188
206
|
} else if (typeof handler === 'function') {
|
|
189
207
|
let instance = handler()
|
|
190
208
|
if (instance?.id) {
|
|
@@ -482,6 +500,7 @@ const preloadTypes = [
|
|
|
482
500
|
'media-resource-audio',
|
|
483
501
|
'media-resource-file',
|
|
484
502
|
'lottie-file-uri',
|
|
503
|
+
'rive-file-uri',
|
|
485
504
|
'ggml-model-asset',
|
|
486
505
|
'gguf-model-asset',
|
|
487
506
|
'binary-asset',
|
|
@@ -570,7 +589,7 @@ function compileRunArray(run: unknown[]): unknown[] {
|
|
|
570
589
|
/**
|
|
571
590
|
* Compile typed TestCase to raw format (strips __typename)
|
|
572
591
|
*/
|
|
573
|
-
const compileTestCase = (testCase: TestCase) => ({
|
|
592
|
+
export const compileTestCase = (testCase: TestCase) => ({
|
|
574
593
|
id: testCase.id,
|
|
575
594
|
name: testCase.name,
|
|
576
595
|
hide_short_ref: testCase.hideShortRef,
|
|
@@ -592,7 +611,10 @@ const compileTestCase = (testCase: TestCase) => ({
|
|
|
592
611
|
variable: cond.variable,
|
|
593
612
|
operator: cond.operator,
|
|
594
613
|
value: cond.value,
|
|
595
|
-
jump_to
|
|
614
|
+
// `jump_to` may be a getter (() => TestCase) for dynamic case ids (the project generator
|
|
615
|
+
// emits this form). Resolve it to its id like the `run` array does — otherwise the function
|
|
616
|
+
// is JSON-serialized to nothing and the conditional jump silently vanishes from the config.
|
|
617
|
+
jump_to: compileRunElement(cond.jump_to),
|
|
596
618
|
}
|
|
597
619
|
}),
|
|
598
620
|
})
|
|
@@ -719,7 +741,14 @@ export const compile = async (app: Application) => {
|
|
|
719
741
|
// validation (root_canvas_id is required before the conditional
|
|
720
742
|
// schema fix is published).
|
|
721
743
|
if (subspace.module?.link) {
|
|
722
|
-
|
|
744
|
+
// Seed the placeholder id from the (stable) subspace id. `makeId('canvas')` would take
|
|
745
|
+
// the count-fallback branch (a process-global counter that is never reset), so the
|
|
746
|
+
// placeholder id depended on how many prior count-fallback ids had been minted — making
|
|
747
|
+
// it differ between recompiles and breaking compile's byte-stable-output contract
|
|
748
|
+
// (phantom config-change ops). `makeSeededId` keeps no global state, so identical source
|
|
749
|
+
// recompiles to an identical id. (`makeId('canvas', alias)` would instead throw
|
|
750
|
+
// "Duplicate makeId alias" on the second compile in a long-lived process.)
|
|
751
|
+
const placeholderCanvasId = makeSeededId('canvas', `${subspaceId}:module-placeholder`)
|
|
723
752
|
subspaceMap[subspaceId] = {
|
|
724
753
|
title: subspace.title,
|
|
725
754
|
description: subspace.description,
|
|
@@ -1412,12 +1441,7 @@ export const compile = async (app: Application) => {
|
|
|
1412
1441
|
: null,
|
|
1413
1442
|
}
|
|
1414
1443
|
|
|
1415
|
-
Object.assign(
|
|
1416
|
-
calc,
|
|
1417
|
-
generateCalulationMap(calc.script_config, {
|
|
1418
|
-
snapshotMode: process.env.BRICKS_SNAPSHOT_MODE === '1',
|
|
1419
|
-
}),
|
|
1420
|
-
)
|
|
1444
|
+
Object.assign(calc, generateCalulationMap(calc.script_config, dataCalcId))
|
|
1421
1445
|
}
|
|
1422
1446
|
map[dataCalcId] = calc
|
|
1423
1447
|
return map
|
package/compile/util.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeSeededId } from '../utils/id'
|
|
2
2
|
|
|
3
3
|
type ScriptConfig = {
|
|
4
4
|
title?: string
|
|
@@ -18,30 +18,54 @@ const errorMsg = 'Not allow duplicate set property id between inputs / outputs /
|
|
|
18
18
|
export const validateConfig = (config: ScriptConfig) => {
|
|
19
19
|
// Skip input/output overlap validation in manual mode (allows same data node in both)
|
|
20
20
|
if (config.trigger_mode === 'manual') return
|
|
21
|
-
|
|
21
|
+
// `inputs` is keyed by data-node id with the OBJECT_SET path as the *value*. The overlap
|
|
22
|
+
// check is "is this id also an input?" — a key-existence question — so test key presence,
|
|
23
|
+
// not the path's truthiness. An empty path ('' = OBJECT_SET's default "set whole object")
|
|
24
|
+
// is a legitimate input value that would otherwise slip the overlap guard.
|
|
25
|
+
const isInput = (id: string) => Object.prototype.hasOwnProperty.call(config.inputs, id)
|
|
26
|
+
if (config.error && isInput(config.error)) {
|
|
22
27
|
throw new Error(`${errorMsg}. key: error`)
|
|
23
28
|
}
|
|
24
|
-
if (config.output && config.
|
|
29
|
+
if (config.output && isInput(config.output)) {
|
|
25
30
|
throw new Error(`${errorMsg}. key: output`)
|
|
26
31
|
}
|
|
27
|
-
if (Object.values(config.outputs).some((value) => value.some(
|
|
32
|
+
if (Object.values(config.outputs).some((value) => value.some(isInput))) {
|
|
28
33
|
throw new Error(`${errorMsg}. key: outputs`)
|
|
29
34
|
}
|
|
35
|
+
// The same data-node id reused across the output-side targets (output / error / outputs)
|
|
36
|
+
// also collides: generateCalulationMap spreads their node objects last-wins, so the later
|
|
37
|
+
// one silently overwrites the earlier wiring (e.g. error == output drops the error change
|
|
38
|
+
// link). The checks above only compare against `inputs`, so guard the output-side pairs
|
|
39
|
+
// too. (The same id appearing in multiple `outputs` entries is a supported last-wins case
|
|
40
|
+
// and stays allowed.)
|
|
41
|
+
if (config.error && config.output && config.error === config.output) {
|
|
42
|
+
throw new Error(`${errorMsg}. key: error/output`)
|
|
43
|
+
}
|
|
44
|
+
const outputsIds = Object.values(config.outputs).flat()
|
|
45
|
+
if (config.output && outputsIds.includes(config.output)) {
|
|
46
|
+
throw new Error(`${errorMsg}. key: output/outputs`)
|
|
47
|
+
}
|
|
48
|
+
if (config.error && outputsIds.includes(config.error)) {
|
|
49
|
+
throw new Error(`${errorMsg}. key: error/outputs`)
|
|
50
|
+
}
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
const padding = 15
|
|
33
54
|
const layerXInterval = 300
|
|
34
55
|
const layerYInterval = 150
|
|
35
56
|
|
|
36
|
-
|
|
57
|
+
// `calcId` (the owning script calc's stable id) seeds every derived command-node id, so an
|
|
58
|
+
// unchanged calc recompiles to identical ids and editing one calc never shifts another's —
|
|
59
|
+
// keeping the compiled config byte-stable for change detection. See makeSeededId in utils/id.
|
|
60
|
+
export const generateCalulationMap = (config: ScriptConfig, calcId: string) => {
|
|
37
61
|
validateConfig(config)
|
|
38
|
-
const sandboxId =
|
|
39
|
-
const sandboxErrorId =
|
|
40
|
-
const sandboxResultId =
|
|
62
|
+
const sandboxId = makeSeededId('property_bank_command', `${calcId}:sandbox-run`)
|
|
63
|
+
const sandboxErrorId = makeSeededId('property_bank_command', `${calcId}:sandbox-error`)
|
|
64
|
+
const sandboxResultId = makeSeededId('property_bank_command', `${calcId}:sandbox-result`)
|
|
41
65
|
|
|
42
66
|
const inputs = Object.entries(config.inputs).reduce(
|
|
43
67
|
(acc, [key, value], index) => {
|
|
44
|
-
const commandId =
|
|
68
|
+
const commandId = makeSeededId('property_bank_command', `${calcId}:input:${key}`)
|
|
45
69
|
acc.map[key] = {
|
|
46
70
|
type: 'data-node',
|
|
47
71
|
properties: {},
|
|
@@ -123,7 +147,7 @@ export const generateCalulationMap = (config: ScriptConfig, opts?: { snapshotMod
|
|
|
123
147
|
let y = 0
|
|
124
148
|
const outputs = Object.entries(config.outputs).reduce(
|
|
125
149
|
(acc, [key, pbList], index) => {
|
|
126
|
-
const commandId =
|
|
150
|
+
const commandId = makeSeededId('property_bank_command', `${calcId}:output:${key}`)
|
|
127
151
|
acc.commandIdList.push(commandId)
|
|
128
152
|
acc.map[commandId] = {
|
|
129
153
|
type: 'command-node-object',
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.10",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
7
7
|
"build": "bun scripts/build.js"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@fugood/bricks-cli": "^2.24.
|
|
10
|
+
"@fugood/bricks-cli": "^2.24.10",
|
|
11
11
|
"@huggingface/gguf": "^0.3.2",
|
|
12
12
|
"@iarna/toml": "^3.0.0",
|
|
13
13
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -25,6 +25,7 @@ The primary way to orchestrate multi-step flows. A single event can contain an a
|
|
|
25
25
|
- Use `waitAsync: true` to await async actions before the next step
|
|
26
26
|
- Use `dataParams` + `mapping` to pass event data downstream
|
|
27
27
|
- This is the "glue" that wires generators, state, and UI together
|
|
28
|
+
- For Generator result UI: await Generator, write done/version Data; route/current Data stays identity.
|
|
28
29
|
|
|
29
30
|
Sequential `PROPERTY_BANK` / `PROPERTY_BANK_EXPRESSION` actions in one chain read the data values that existed when the chain started. If a later action needs to read what an earlier action wrote, set `waitAsync: true` on the earlier action.
|
|
30
31
|
|
|
@@ -87,6 +87,17 @@ const triggerCalc: EventAction = {
|
|
|
87
87
|
- When a **later action reads the calc's outputs**, set `waitAsync: true` on the `PROPERTY_BANK_COMMAND` action itself — it awaits the full calc chain including output writes.
|
|
88
88
|
- A dataParam's `value` acts as an execution gate: `{ input: () => d, value: false }` skips that trigger (combine with `mapping` for conditional runs).
|
|
89
89
|
|
|
90
|
+
## One trigger source per result panel
|
|
91
|
+
|
|
92
|
+
A panel that shows the result of an async action (Generator HTTP/LLM response, request telemetry) reads from a calc whose **trigger should come from a single source**: either the live async outlet, or a completion marker the action chain writes — not both.
|
|
93
|
+
|
|
94
|
+
- **Outlet-triggered** (`{ data: () => dResponse, trigger: true }`): the calc reruns as outlets land. Use when each outlet is only written once its value is final.
|
|
95
|
+
- **Marker-triggered**: run the generator with `waitAsync: true`, then write a `done` Data the calc triggers on. Use when several outlets settle separately and the panel must wait for all of them.
|
|
96
|
+
|
|
97
|
+
Wiring both at once — triggering on the live outlet _and_ gating on a separately-written marker — lets the calc run before the outlets have settled (it renders `undefined`/stale) while the marker path masks it intermittently. A panel that updates "sometimes" usually has two competing triggers: pick one, and order it with `waitAsync` so the trigger fires after the values it reads are written.
|
|
98
|
+
|
|
99
|
+
Scope the panel's calc to the **single value it derives** (the formatted display string), and write sibling status labels — running/done, progress, selected route — imperatively in the event chain with `PROPERTY_BANK`. A calc rewrites _every_ one of its outputs on each run, so a return that omits a key writes `undefined` to that output Data (see [Field Rules](#field-rules-defaults-and-constraints)): folding status labels into a multi-output result calc resets them to `undefined` on any run that returns only the derived value. If a calc genuinely must drive several outputs, return all of their keys every run.
|
|
100
|
+
|
|
90
101
|
## Script Sandbox
|
|
91
102
|
|
|
92
103
|
Scripts run in `use strict` mode as a function body — top-level `return` returns the calc result. No `fetch`, `XMLHttpRequest`, or `require` in any mode: I/O belongs to Generators.
|
|
@@ -220,6 +231,8 @@ const appendHistory: DataCalculationScript = {
|
|
|
220
231
|
| `PROPERTY_BANK_COMMAND` does nothing | Auto calc + `trigger: false` input, or `input` doesn't reference an input Data of the calc | Command a `trigger: true` input (auto) or any input (manual) |
|
|
221
232
|
| Compile error `Not allow duplicate set property id...` | Auto mode with same Data as input and output | Use `triggerMode: 'manual'`, or split into separate Data |
|
|
222
233
|
| Calc reads stale value written earlier in the same chain | Missing `waitAsync: true` on the preceding write | Set `waitAsync: true` on the write action |
|
|
234
|
+
| Result/telemetry panel shows `undefined` or stale data intermittently | Calc triggered by two sources at once (live outlet + a separately-written marker) | Trigger from one source — see [One trigger source per result panel](#one-trigger-source-per-result-panel) |
|
|
235
|
+
| Sibling status Data (selected/progress) resets to `undefined` after a calc runs | Multi-output result calc whose return omitted those keys on that run | Scope the calc to one output and write status labels imperatively, or return every output key each run — see [One trigger source per result panel](#one-trigger-source-per-result-panel) |
|
|
223
236
|
| Works in Simulator, fails on device | V8 vs Hermes/JSC engine difference | Verify on device (Path 2); avoid engine-sensitive parsing |
|
|
224
237
|
| `console.log` shows nothing | Console only emits during DevTools debug sessions | Attach DevTools, or write debug values to an output Data |
|
|
225
238
|
| `Promise`/`setTimeout` undefined, or `Async mode is required` error | `enableAsync: false` | Set `enableAsync: true` |
|
|
@@ -69,6 +69,8 @@ Useful flags:
|
|
|
69
69
|
|
|
70
70
|
For ad-hoc CDP inspection against this local preview, connect any CDP client to `localhost:19852` — Chrome DevTools front-end works directly. For an agent-friendly CLI over CDP (screenshot, brick tree/query, input emulation, storage reads, runtime eval, network capture), the `bricks-cli` skill documents the `bricks devtools` command surface — read that skill if it is installed in this workspace. If it is not installed, run `bricks --help` and `bricks devtools --help`; the CLI's own help output is authoritative.
|
|
71
71
|
|
|
72
|
+
To inspect Data / Property Bank or storage state, prefer the dedicated `bricks devtools storage` subcommands — `storage data-bank get <S_xxxx>` (saved Data values), `storage system persist|memory`, `storage system get <key>` — over hand-written `runtime eval`. Reach for `runtime eval` only for *live* store internals that aren't persisted to a data bank (e.g. a current transient value: `runtime eval "system.data.property('S_xxxx', '<alias>')"`); don't reverse-engineer the `system.*` globals.
|
|
73
|
+
|
|
72
74
|
### Project Automations
|
|
73
75
|
|
|
74
76
|
E2E tests authored in TypeScript inside the project (`AutomationTest` / `TestCase`). Test cases include:
|
|
@@ -118,6 +120,25 @@ Once DevTools is on, ask the user how they want to drive the verification — Ch
|
|
|
118
120
|
|
|
119
121
|
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.
|
|
120
122
|
|
|
123
|
+
### Running real-device Automations from an agent
|
|
124
|
+
|
|
125
|
+
There is no `bricks devtools automation` subcommand. Use the DevTools runtime helpers exposed inside the app:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "Object.getOwnPropertyNames(automation).sort()" -j
|
|
129
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "automation.list()" -j
|
|
130
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "automation.run('<TEST_id>', { updateScreenshot: false })" --await -j
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`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:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "JSON.stringify({ result: system.data.property('<S_xxxx>', '<resultAlias>')?.value })" -j
|
|
137
|
+
bricks devtools screenshot -a <ip> -p 19851 --passcode <pc> -o /tmp/device-automation.png
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
121
142
|
### Real-device side-effects warning
|
|
122
143
|
|
|
123
144
|
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,17 @@
|
|
|
1
|
+
// Extract a human-readable message from a `bricks ... --json` failure payload.
|
|
2
|
+
//
|
|
3
|
+
// On failure the CLI prints `{ "error": { "message": "..." } }` (or the older
|
|
4
|
+
// `{ "error": "..." }`) to stdout/stderr. Earlier call sites built that message
|
|
5
|
+
// inside the same `try` that wrapped `JSON.parse`, so the `throw` was caught by
|
|
6
|
+
// its own `catch` and replaced with the raw JSON blob — the human-readable
|
|
7
|
+
// message never surfaced. Parsing here, outside any throw, avoids that trap.
|
|
8
|
+
export function extractCliErrorMessage(output: string, fallback: string): string {
|
|
9
|
+
try {
|
|
10
|
+
const { error } = JSON.parse(output)
|
|
11
|
+
const message = error?.message ?? error
|
|
12
|
+
if (typeof message === 'string' && message) return message
|
|
13
|
+
} catch {
|
|
14
|
+
// output is not JSON — fall through to the raw output below
|
|
15
|
+
}
|
|
16
|
+
return output || fallback
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFile, writeFile, stat } from 'fs/promises'
|
|
2
|
+
|
|
3
|
+
const exists = async (f: string) => {
|
|
4
|
+
try {
|
|
5
|
+
await stat(f)
|
|
6
|
+
return true
|
|
7
|
+
} catch {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Refresh the `bricks-ctor` entry in a project's `.mcp.json` while preserving every other MCP
|
|
13
|
+
// server the user configured. A malformed `.mcp.json` is left untouched (with a warning)
|
|
14
|
+
// rather than overwritten with the default — clobbering it would silently delete the user's
|
|
15
|
+
// other server entries. This mirrors `setupClaudeAutoMode`'s handling of a malformed
|
|
16
|
+
// `settings.local.json` in postinstall.ts.
|
|
17
|
+
export const handleMcpConfigOverride = async (mcpConfigPath: string, projectMcpServer: object) => {
|
|
18
|
+
let mcpConfig: { mcpServers: Record<string, unknown> }
|
|
19
|
+
if (await exists(mcpConfigPath)) {
|
|
20
|
+
let parsed: unknown
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(await readFile(mcpConfigPath, 'utf-8'))
|
|
23
|
+
} catch {
|
|
24
|
+
console.warn(`Skipping .mcp.json update; ${mcpConfigPath} is not valid JSON`)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
mcpConfig =
|
|
28
|
+
parsed && typeof parsed === 'object'
|
|
29
|
+
? (parsed as { mcpServers: Record<string, unknown> })
|
|
30
|
+
: { mcpServers: {} }
|
|
31
|
+
if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== 'object') {
|
|
32
|
+
mcpConfig.mcpServers = {}
|
|
33
|
+
}
|
|
34
|
+
mcpConfig.mcpServers['bricks-ctor'] = projectMcpServer
|
|
35
|
+
delete mcpConfig.mcpServers['bricks-project']
|
|
36
|
+
} else {
|
|
37
|
+
mcpConfig = { mcpServers: { 'bricks-ctor': projectMcpServer } }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await writeFile(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`)
|
|
41
|
+
console.log(`Updated ${mcpConfigPath}`)
|
|
42
|
+
}
|
package/tools/deploy.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, readFile, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { parseArgs } from 'util'
|
|
3
3
|
import { sh } from './_shell'
|
|
4
|
+
import { extractCliErrorMessage } from './_cli-error'
|
|
4
5
|
import { buildCommitArgs } from './_git-author'
|
|
5
6
|
import { writeLastPushedCommit } from './_last-pushed-commit'
|
|
6
7
|
|
|
@@ -162,12 +163,7 @@ const result = await sh`${args}`.quiet().nothrow()
|
|
|
162
163
|
|
|
163
164
|
if (result.exitCode !== 0) {
|
|
164
165
|
const output = result.stderr.toString() || result.stdout.toString()
|
|
165
|
-
|
|
166
|
-
const json = JSON.parse(output)
|
|
167
|
-
throw new Error(json.error || 'Release failed')
|
|
168
|
-
} catch {
|
|
169
|
-
throw new Error(output || 'Release failed')
|
|
170
|
-
}
|
|
166
|
+
throw new Error(extractCliErrorMessage(output, 'Release failed'))
|
|
171
167
|
}
|
|
172
168
|
|
|
173
169
|
const output = JSON.parse(result.stdout.toString())
|
|
@@ -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/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')
|
|
@@ -80,41 +81,13 @@ type CodexMcpConfig = {
|
|
|
80
81
|
mcp_servers: Record<string, typeof codexProjectMcpServer | typeof projectMcpServer>
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
// Claude Code and AGENTS.md projects both use the shared project .mcp.json file.
|
|
84
|
-
const defaultMcpConfig = {
|
|
85
|
-
mcpServers: {
|
|
86
|
-
'bricks-ctor': projectMcpServer,
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const handleMcpConfigOverride = async (mcpConfigPath: string) => {
|
|
91
|
-
let mcpConfig: { mcpServers: Record<string, typeof projectMcpServer> } | null = null
|
|
92
|
-
if (await exists(mcpConfigPath)) {
|
|
93
|
-
const configStr = await readFile(mcpConfigPath, 'utf-8')
|
|
94
|
-
try {
|
|
95
|
-
mcpConfig = JSON.parse(configStr)
|
|
96
|
-
if (!mcpConfig?.mcpServers) throw new Error('mcpServers is not defined')
|
|
97
|
-
mcpConfig.mcpServers['bricks-ctor'] = projectMcpServer
|
|
98
|
-
delete mcpConfig.mcpServers['bricks-project']
|
|
99
|
-
} catch {
|
|
100
|
-
mcpConfig = defaultMcpConfig
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
mcpConfig = defaultMcpConfig
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
await writeFile(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`)
|
|
107
|
-
|
|
108
|
-
console.log(`Updated ${mcpConfigPath}`)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
84
|
const hasClaudeCode = await exists(`${cwd}/CLAUDE.md`)
|
|
112
85
|
const hasAgentsMd = await exists(`${cwd}/AGENTS.md`)
|
|
113
86
|
|
|
114
87
|
if (hasClaudeCode || hasAgentsMd) {
|
|
115
88
|
// Keep the workspace-level JSON MCP config aligned for tools that read .mcp.json.
|
|
116
89
|
const mcpConfigPath = `${cwd}/.mcp.json`
|
|
117
|
-
await handleMcpConfigOverride(mcpConfigPath)
|
|
90
|
+
await handleMcpConfigOverride(mcpConfigPath, projectMcpServer)
|
|
118
91
|
}
|
|
119
92
|
|
|
120
93
|
const copyMissingSkills = async (sourceDir: string, targetDir: string) => {
|
|
@@ -282,18 +255,25 @@ if (hasAgentsMd) {
|
|
|
282
255
|
}
|
|
283
256
|
|
|
284
257
|
const handleCodexMcpConfigOverride = async (mcpConfigPath: string) => {
|
|
285
|
-
let mcpConfig: CodexMcpConfig
|
|
258
|
+
let mcpConfig: CodexMcpConfig
|
|
286
259
|
if (await exists(mcpConfigPath)) {
|
|
287
|
-
|
|
260
|
+
let parsed: unknown
|
|
288
261
|
try {
|
|
289
|
-
|
|
290
|
-
if (!parsed?.mcp_servers) throw new Error('mcp_servers is not defined')
|
|
291
|
-
mcpConfig = { mcp_servers: parsed.mcp_servers }
|
|
292
|
-
mcpConfig.mcp_servers['bricks-ctor'] = codexProjectMcpServer
|
|
293
|
-
delete mcpConfig.mcp_servers['bricks-project']
|
|
262
|
+
parsed = TOML.parse(await readFile(mcpConfigPath, 'utf-8'))
|
|
294
263
|
} catch {
|
|
295
|
-
|
|
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 = {}
|
|
296
274
|
}
|
|
275
|
+
mcpConfig.mcp_servers['bricks-ctor'] = codexProjectMcpServer
|
|
276
|
+
delete mcpConfig.mcp_servers['bricks-project']
|
|
297
277
|
} else {
|
|
298
278
|
mcpConfig = defaultCodexMcpConfig
|
|
299
279
|
}
|
package/tools/pull.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { readdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
1
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
-
import { join, relative } from 'node:path'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
4
|
import { format } from 'oxfmt'
|
|
5
5
|
import { sh } from './_shell'
|
|
6
|
+
import { extractCliErrorMessage } from './_cli-error'
|
|
6
7
|
import { buildCommitArgs } from './_git-author'
|
|
7
8
|
import { readLastPushedCommit, writeLastPushedCommit } from './_last-pushed-commit'
|
|
8
9
|
|
|
@@ -68,12 +69,7 @@ const result = await sh`bricks ${command} project-pull ${app.id} --json`.quiet()
|
|
|
68
69
|
|
|
69
70
|
if (result.exitCode !== 0) {
|
|
70
71
|
const output = result.stderr.toString() || result.stdout.toString()
|
|
71
|
-
|
|
72
|
-
const json = JSON.parse(output)
|
|
73
|
-
throw new Error(json.error || 'Pull failed')
|
|
74
|
-
} catch {
|
|
75
|
-
throw new Error(output || 'Pull failed')
|
|
76
|
-
}
|
|
72
|
+
throw new Error(extractCliErrorMessage(output, 'Pull failed'))
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
const { files, lastCommitId: serverLastCommitId } = JSON.parse(result.stdout.toString())
|
|
@@ -90,7 +86,11 @@ const branchName = isModule
|
|
|
90
86
|
? 'BRICKS_PROJECT_try-pull-module'
|
|
91
87
|
: 'BRICKS_PROJECT_try-pull-application'
|
|
92
88
|
|
|
89
|
+
let landingBranch = ''
|
|
93
90
|
if (isGitRepo && !force) {
|
|
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
94
|
console.log(`Checking commit ${baseCommitId}...`)
|
|
95
95
|
const found = (await sh`cd ${cwd} && git rev-list -1 ${baseCommitId}`.nothrow().text())
|
|
96
96
|
.trim()
|
|
@@ -104,8 +104,8 @@ if (isGitRepo && !force) {
|
|
|
104
104
|
|
|
105
105
|
// When the base commit isn't reachable in this clone (server stored a
|
|
106
106
|
// nanoid, or the commit was pruned), fall back to forking from current
|
|
107
|
-
// HEAD. The downstream merge into
|
|
108
|
-
// same result, just with different merge bases.
|
|
107
|
+
// HEAD. The downstream merge into the starting branch collapses both paths
|
|
108
|
+
// into the same result, just with different merge bases.
|
|
109
109
|
if (found) {
|
|
110
110
|
await sh`cd ${cwd} && git checkout -b ${branchName} ${baseCommitId}`.nothrow()
|
|
111
111
|
} else {
|
|
@@ -147,7 +147,9 @@ await Promise.all(
|
|
|
147
147
|
const result = await format(file.name, file.input, oxfmtConfig)
|
|
148
148
|
content = result.code
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
const target = join(cwd, file.name)
|
|
151
|
+
await mkdir(dirname(target), { recursive: true })
|
|
152
|
+
return writeFile(target, content)
|
|
151
153
|
}),
|
|
152
154
|
)
|
|
153
155
|
|
|
@@ -164,11 +166,11 @@ if (isGitRepo) {
|
|
|
164
166
|
await sh`cd ${cwd} && git ${commitArgs}`
|
|
165
167
|
}
|
|
166
168
|
if (!force) {
|
|
167
|
-
// Land the pulled commits on
|
|
169
|
+
// Land the pulled commits on the starting branch with a single 3-way merge using
|
|
168
170
|
// baseCommit as the merge base. The user doesn't have to manage a side
|
|
169
|
-
// branch, and conflicts (if any) land in the working tree on
|
|
171
|
+
// branch, and conflicts (if any) land in the working tree on the starting branch where
|
|
170
172
|
// auto-compile surfaces them as typecheck errors to resolve in-place.
|
|
171
|
-
await sh`cd ${cwd} && git checkout
|
|
173
|
+
await sh`cd ${cwd} && git checkout ${landingBranch}`
|
|
172
174
|
const mergeResult = await sh`cd ${cwd} && git merge ${branchName} --no-edit`.nothrow()
|
|
173
175
|
if (mergeResult.exitCode !== 0) {
|
|
174
176
|
// Conflict markers are in the working tree — commit them so the tree
|
|
@@ -178,7 +180,7 @@ if (isGitRepo) {
|
|
|
178
180
|
await sh`cd ${cwd} && git add .`
|
|
179
181
|
const conflictArgs = await buildCommitArgs(
|
|
180
182
|
cwd,
|
|
181
|
-
[
|
|
183
|
+
[`chore(project): merge with conflicts (resolve in ${landingBranch})`],
|
|
182
184
|
['--no-verify'],
|
|
183
185
|
)
|
|
184
186
|
await sh`cd ${cwd} && git ${conflictArgs}`
|
package/tools/push-config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { parseArgs } from 'util'
|
|
3
3
|
import { sh } from './_shell'
|
|
4
|
+
import { extractCliErrorMessage } from './_cli-error'
|
|
4
5
|
import { buildCommitArgs } from './_git-author'
|
|
5
6
|
import { writeLastPushedCommit } from './_last-pushed-commit'
|
|
6
7
|
|
|
@@ -99,12 +100,7 @@ const result = await sh`${args}`.quiet().nothrow()
|
|
|
99
100
|
|
|
100
101
|
if (result.exitCode !== 0) {
|
|
101
102
|
const output = result.stderr.toString() || result.stdout.toString()
|
|
102
|
-
|
|
103
|
-
const json = JSON.parse(output)
|
|
104
|
-
throw new Error(json.error?.message || json.error || 'Update failed')
|
|
105
|
-
} catch {
|
|
106
|
-
throw new Error(output || 'Update failed')
|
|
107
|
-
}
|
|
103
|
+
throw new Error(extractCliErrorMessage(output, 'Update failed'))
|
|
108
104
|
}
|
|
109
105
|
|
|
110
106
|
const output = JSON.parse(result.stdout.toString())
|
package/types/bricks/Sketch.d.ts
CHANGED
|
@@ -191,9 +191,11 @@ Default property:
|
|
|
191
191
|
/* A stroke was just committed (drawn or added programmatically) */
|
|
192
192
|
onStrokeEnd?: Array<EventAction<string & keyof TemplateEventPropsMap['Sketch']['onStrokeEnd']>>
|
|
193
193
|
/* The canvas was cleared */
|
|
194
|
-
onClear?: Array<EventAction
|
|
194
|
+
onClear?: Array<EventAction<string & keyof TemplateEventPropsMap['Sketch']['onClear']>>
|
|
195
195
|
/* Sketch state changed (any commit, undo, redo, clear, or import) */
|
|
196
|
-
onStateChange?: Array<
|
|
196
|
+
onStateChange?: Array<
|
|
197
|
+
EventAction<string & keyof TemplateEventPropsMap['Sketch']['onStateChange']>
|
|
198
|
+
>
|
|
197
199
|
/* Active tool changed */
|
|
198
200
|
onToolChange?: Array<
|
|
199
201
|
EventAction<string & keyof TemplateEventPropsMap['Sketch']['onToolChange']>
|
|
@@ -22,7 +22,7 @@ export type DataCommandDatetimeDate = DataCommand & {
|
|
|
22
22
|
outputs?: Array<DataCalcOutput<'result'> /* target: number */>
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/* Day — Get day (Sunday is
|
|
25
|
+
/* Day — Get day of week (0-based: Sunday is 0, Saturday is 6) */
|
|
26
26
|
export type DataCommandDatetimeDay = DataCommand & {
|
|
27
27
|
__commandName: 'DATETIME_DAY'
|
|
28
28
|
inputs?: Array<
|
|
@@ -64,7 +64,7 @@ export type DataCommandDatetimeMinute = DataCommand & {
|
|
|
64
64
|
outputs?: Array<DataCalcOutput<'result'> /* target: number */>
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/* Month — Get month (January is
|
|
67
|
+
/* Month — Get month (0-based: January is 0, December is 11) */
|
|
68
68
|
export type DataCommandDatetimeMonth = DataCommand & {
|
|
69
69
|
__commandName: 'DATETIME_MONTH'
|
|
70
70
|
inputs?: Array<
|
|
@@ -33,7 +33,7 @@ Default property:
|
|
|
33
33
|
property?: {
|
|
34
34
|
/* Start tick on generator initialized */
|
|
35
35
|
init?: boolean | DataLink
|
|
36
|
-
/*
|
|
36
|
+
/* Tick interval in milliseconds for countdown */
|
|
37
37
|
interval?: number | DataLink
|
|
38
38
|
/* Initial value of countdown */
|
|
39
39
|
countdownStartValue?: number | DataLink
|
package/utils/calc.ts
CHANGED
|
@@ -28,6 +28,10 @@ export const generateDataCalculationMapEditorInfo = (
|
|
|
28
28
|
DataCalculationData | DataCommand,
|
|
29
29
|
Set<DataCalculationData | DataCommand>
|
|
30
30
|
>()
|
|
31
|
+
const nodeById = new Map<string, DataCalculationData | DataCommand>()
|
|
32
|
+
for (const node of nodes) {
|
|
33
|
+
if ('id' in node) nodeById.set(node.id, node)
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
// Analyze node connections
|
|
33
37
|
nodes.forEach((node) => {
|
|
@@ -48,7 +52,7 @@ export const generateDataCalculationMapEditorInfo = (
|
|
|
48
52
|
if (!connectedTo.has(node)) {
|
|
49
53
|
connectedTo.set(node, new Set())
|
|
50
54
|
}
|
|
51
|
-
const sourceNode =
|
|
55
|
+
const sourceNode = nodeById.get(conn.id)
|
|
52
56
|
if (sourceNode) {
|
|
53
57
|
connectedTo.get(node)!.add(sourceNode)
|
|
54
58
|
}
|
package/utils/data.ts
CHANGED
package/utils/event-props.ts
CHANGED
|
@@ -142,7 +142,9 @@ export const templateEventPropsMap = {
|
|
|
142
142
|
},
|
|
143
143
|
},
|
|
144
144
|
Sketch: {
|
|
145
|
-
onStrokeEnd: { BRICK_SKETCH_STROKE_COUNT: 'number' },
|
|
145
|
+
onStrokeEnd: { BRICK_SKETCH_STROKE_COUNT: 'number', BRICK_SKETCH_STATE: '{}' },
|
|
146
|
+
onClear: { BRICK_SKETCH_STATE: '{}' },
|
|
147
|
+
onStateChange: { BRICK_SKETCH_STATE: '{}' },
|
|
146
148
|
onToolChange: { BRICK_SKETCH_TOOL: 'string' },
|
|
147
149
|
onExportImage: { BRICK_SKETCH_IMAGE_URI: 'string' },
|
|
148
150
|
},
|
package/utils/id.ts
CHANGED
|
@@ -69,63 +69,65 @@ const makeStableUuid = (type: string, alias?: string) => {
|
|
|
69
69
|
})
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
export const makeId = (type: IdType, aliasOrOpts?: string | IdOptions, opts?: IdOptions) => {
|
|
74
|
-
if (type === 'subspace') {
|
|
75
|
-
throw new Error('Currently subspace is not supported for ID generation, please use a fixed ID')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const alias = typeof aliasOrOpts === 'string' ? aliasOrOpts : undefined
|
|
79
|
-
const options = typeof aliasOrOpts === 'string' ? opts : (aliasOrOpts ?? opts)
|
|
80
|
-
|
|
81
|
-
let prefix = ''
|
|
72
|
+
const idPrefix = (type: IdType): string => {
|
|
82
73
|
switch (type) {
|
|
83
74
|
case 'animation':
|
|
84
|
-
|
|
85
|
-
break
|
|
75
|
+
return 'ANIMATION_'
|
|
86
76
|
case 'brick':
|
|
87
|
-
|
|
88
|
-
break
|
|
77
|
+
return 'BRICK_'
|
|
89
78
|
case 'dynamic-brick':
|
|
90
|
-
|
|
91
|
-
break
|
|
79
|
+
return 'DYNAMIC_BRICK_'
|
|
92
80
|
case 'canvas':
|
|
93
|
-
|
|
94
|
-
break
|
|
81
|
+
return 'CANVAS_'
|
|
95
82
|
case 'generator':
|
|
96
|
-
|
|
97
|
-
break
|
|
83
|
+
return 'GENERATOR_'
|
|
98
84
|
case 'data':
|
|
99
|
-
|
|
100
|
-
break
|
|
85
|
+
return 'PROPERTY_BANK_DATA_NODE_'
|
|
101
86
|
case 'switch':
|
|
102
|
-
|
|
103
|
-
break
|
|
87
|
+
return 'BRICK_STATE_GROUP_'
|
|
104
88
|
case 'property_bank_command':
|
|
105
|
-
|
|
106
|
-
break
|
|
89
|
+
return 'PROPERTY_BANK_COMMAND_NODE_'
|
|
107
90
|
case 'property_bank_calc':
|
|
108
|
-
|
|
109
|
-
break
|
|
91
|
+
return 'PROPERTY_BANK_COMMAND_MAP_'
|
|
110
92
|
case 'automation_map':
|
|
111
|
-
|
|
112
|
-
break
|
|
93
|
+
return 'AUTOMATION_MAP_'
|
|
113
94
|
case 'test':
|
|
114
|
-
|
|
115
|
-
break
|
|
95
|
+
return 'TEST_'
|
|
116
96
|
case 'test_case':
|
|
117
|
-
|
|
118
|
-
break
|
|
97
|
+
return 'TEST_CASE_'
|
|
119
98
|
case 'test_var':
|
|
120
|
-
|
|
121
|
-
break
|
|
99
|
+
return 'TEST_VAR_'
|
|
122
100
|
default:
|
|
101
|
+
return ''
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Make stable ids by default; explicit snapshotMode: false preserves the random escape hatch.
|
|
106
|
+
export const makeId = (type: IdType, aliasOrOpts?: string | IdOptions, opts?: IdOptions) => {
|
|
107
|
+
if (type === 'subspace') {
|
|
108
|
+
throw new Error('Currently subspace is not supported for ID generation, please use a fixed ID')
|
|
123
109
|
}
|
|
124
110
|
|
|
111
|
+
const alias = typeof aliasOrOpts === 'string' ? aliasOrOpts : undefined
|
|
112
|
+
const options = typeof aliasOrOpts === 'string' ? opts : (aliasOrOpts ?? opts)
|
|
113
|
+
|
|
125
114
|
const useCountFallback = aliasOrOpts === undefined && opts === undefined
|
|
126
115
|
const id =
|
|
127
116
|
alias !== undefined || options?.snapshotMode || useCountFallback
|
|
128
117
|
? makeStableUuid(type, alias)
|
|
129
118
|
: uuid()
|
|
130
|
-
return `${
|
|
119
|
+
return `${idPrefix(type)}${id}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Deterministic id derived solely from a caller-supplied seed. Unlike makeId it keeps no
|
|
123
|
+
// global state — no incrementing counter, no alias registry — so the same (type, seed)
|
|
124
|
+
// always maps to the same id on every compile and in any process. Compiled artifacts seed
|
|
125
|
+
// their ids this way (e.g. generateCalulationMap's command nodes, seeded by their calc id +
|
|
126
|
+
// structural role) so unchanged source recompiles byte-identically and editing one calc
|
|
127
|
+
// never shifts another's ids. The seed must be unique within a single config.
|
|
128
|
+
export const makeSeededId = (type: IdType, seed: string) => {
|
|
129
|
+
if (type === 'subspace') {
|
|
130
|
+
throw new Error('Currently subspace is not supported for ID generation, please use a fixed ID')
|
|
131
|
+
}
|
|
132
|
+
return `${idPrefix(type)}${uuid({ random: hashToRandomBytes([readApplicationId(), type, seed]) })}`
|
|
131
133
|
}
|