@fugood/bricks-ctor 2.24.7 → 2.24.9
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 +27 -14
- package/compile/util.ts +34 -10
- package/package.json +2 -2
- package/skills/bricks-ctor/SKILL.md +1 -1
- package/skills/bricks-ctor/references/data-calculation.md +141 -111
- package/skills/bricks-ctor/references/verification-toolchain.md +21 -0
- package/skills/bricks-design/SKILL.md +14 -24
- package/skills/bricks-ux/SKILL.md +14 -20
- 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 +95 -38
- package/tools/pull.ts +2 -6
- package/tools/push-config.ts +2 -6
- package/types/data-calc-command/datetime.d.ts +2 -2
- package/types/data-calc-command/sandbox.d.ts +10 -2
- package/utils/calc.ts +5 -1
- package/utils/id.ts +39 -37
package/compile/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import omit from 'lodash/omit'
|
|
|
6
6
|
import { parse as parseAST } from 'acorn'
|
|
7
7
|
import type { 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'
|
|
@@ -181,10 +181,17 @@ const compileEvents = (
|
|
|
181
181
|
|
|
182
182
|
let handlerKey
|
|
183
183
|
let handlerTemplateKey
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
if (typeof handler === 'string') {
|
|
185
|
+
// Only the literal 'system' handler is normalized to the SYSTEM template key.
|
|
186
|
+
// SubspaceID (SUBSPACE_*) and ItemBrickID handlers are kept verbatim: the runtime
|
|
187
|
+
// resolves them case-sensitively (see mapEventMapHandlersWithNewId), so uppercasing
|
|
188
|
+
// a mixed-case ItemBrickID would break handler-to-item event wiring.
|
|
189
|
+
if (handler === 'system') {
|
|
190
|
+
handlerKey = 'SYSTEM'
|
|
191
|
+
handlerTemplateKey = 'SYSTEM'
|
|
192
|
+
} else {
|
|
193
|
+
handlerKey = handler
|
|
194
|
+
}
|
|
188
195
|
} else if (typeof handler === 'function') {
|
|
189
196
|
let instance = handler()
|
|
190
197
|
if (instance?.id) {
|
|
@@ -482,6 +489,7 @@ const preloadTypes = [
|
|
|
482
489
|
'media-resource-audio',
|
|
483
490
|
'media-resource-file',
|
|
484
491
|
'lottie-file-uri',
|
|
492
|
+
'rive-file-uri',
|
|
485
493
|
'ggml-model-asset',
|
|
486
494
|
'gguf-model-asset',
|
|
487
495
|
'binary-asset',
|
|
@@ -570,7 +578,7 @@ function compileRunArray(run: unknown[]): unknown[] {
|
|
|
570
578
|
/**
|
|
571
579
|
* Compile typed TestCase to raw format (strips __typename)
|
|
572
580
|
*/
|
|
573
|
-
const compileTestCase = (testCase: TestCase) => ({
|
|
581
|
+
export const compileTestCase = (testCase: TestCase) => ({
|
|
574
582
|
id: testCase.id,
|
|
575
583
|
name: testCase.name,
|
|
576
584
|
hide_short_ref: testCase.hideShortRef,
|
|
@@ -592,7 +600,10 @@ const compileTestCase = (testCase: TestCase) => ({
|
|
|
592
600
|
variable: cond.variable,
|
|
593
601
|
operator: cond.operator,
|
|
594
602
|
value: cond.value,
|
|
595
|
-
jump_to
|
|
603
|
+
// `jump_to` may be a getter (() => TestCase) for dynamic case ids (the project generator
|
|
604
|
+
// emits this form). Resolve it to its id like the `run` array does — otherwise the function
|
|
605
|
+
// is JSON-serialized to nothing and the conditional jump silently vanishes from the config.
|
|
606
|
+
jump_to: compileRunElement(cond.jump_to),
|
|
596
607
|
}
|
|
597
608
|
}),
|
|
598
609
|
})
|
|
@@ -719,7 +730,14 @@ export const compile = async (app: Application) => {
|
|
|
719
730
|
// validation (root_canvas_id is required before the conditional
|
|
720
731
|
// schema fix is published).
|
|
721
732
|
if (subspace.module?.link) {
|
|
722
|
-
|
|
733
|
+
// Seed the placeholder id from the (stable) subspace id. `makeId('canvas')` would take
|
|
734
|
+
// the count-fallback branch (a process-global counter that is never reset), so the
|
|
735
|
+
// placeholder id depended on how many prior count-fallback ids had been minted — making
|
|
736
|
+
// it differ between recompiles and breaking compile's byte-stable-output contract
|
|
737
|
+
// (phantom config-change ops). `makeSeededId` keeps no global state, so identical source
|
|
738
|
+
// recompiles to an identical id. (`makeId('canvas', alias)` would instead throw
|
|
739
|
+
// "Duplicate makeId alias" on the second compile in a long-lived process.)
|
|
740
|
+
const placeholderCanvasId = makeSeededId('canvas', `${subspaceId}:module-placeholder`)
|
|
723
741
|
subspaceMap[subspaceId] = {
|
|
724
742
|
title: subspace.title,
|
|
725
743
|
description: subspace.description,
|
|
@@ -1412,12 +1430,7 @@ export const compile = async (app: Application) => {
|
|
|
1412
1430
|
: null,
|
|
1413
1431
|
}
|
|
1414
1432
|
|
|
1415
|
-
Object.assign(
|
|
1416
|
-
calc,
|
|
1417
|
-
generateCalulationMap(calc.script_config, {
|
|
1418
|
-
snapshotMode: process.env.BRICKS_SNAPSHOT_MODE === '1',
|
|
1419
|
-
}),
|
|
1420
|
-
)
|
|
1433
|
+
Object.assign(calc, generateCalulationMap(calc.script_config, dataCalcId))
|
|
1421
1434
|
}
|
|
1422
1435
|
map[dataCalcId] = calc
|
|
1423
1436
|
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.9",
|
|
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.9",
|
|
11
11
|
"@huggingface/gguf": "^0.3.2",
|
|
12
12
|
"@iarna/toml": "^3.0.0",
|
|
13
13
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -15,7 +15,7 @@ This skill covers advanced BRICKS features not in the main project instructions.
|
|
|
15
15
|
| [Animation](references/animation.md) | Animation system for brick transforms and opacity |
|
|
16
16
|
| [Standby Transition](references/standby-transition.md) | Canvas enter/exit animations |
|
|
17
17
|
| [Automations](references/automations.md) | E2E testing and scheduled tasks |
|
|
18
|
-
| [Data Calculation](references/data-calculation.md) | JS sandbox
|
|
18
|
+
| [Data Calculation](references/data-calculation.md) | Wiring contract, trigger semantics, JS sandbox (25+ libraries) |
|
|
19
19
|
| [Local Sync](references/local-sync.md) | LAN device synchronization |
|
|
20
20
|
| [Remote Data Bank](references/remote-data-bank.md) | Cloud data sync and API access |
|
|
21
21
|
| [Media Flow](references/media-flow.md) | Media asset management |
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# Data Calculation (JS Sandbox)
|
|
2
2
|
|
|
3
|
-
Transform and compute Data Bank values using JavaScript scripts.
|
|
3
|
+
Transform and compute Data Bank values using JavaScript scripts. Calcs are for **pure data transformation only** — see [Architecture Patterns](architecture-patterns.md) for the pattern selection guide.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
| If you need to... | Use instead |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Call an LLM / AI model | Generator (Assistant, LLM, HTTP) |
|
|
8
|
+
| Sequence multiple actions | Event Action Chain |
|
|
9
|
+
| Set a data value directly | PROPERTY_BANK system action |
|
|
10
|
+
| Compute a simple expression | PROPERTY_BANK_EXPRESSION |
|
|
11
|
+
| Transform/format/parse data | Data Calculation (correct use) |
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
## Authoring Contract
|
|
8
14
|
|
|
9
15
|
```typescript
|
|
10
16
|
import { makeId } from 'bricks-ctor'
|
|
17
|
+
import { readFile } from 'node:fs/promises'
|
|
11
18
|
|
|
12
19
|
const calculation: DataCalculationScript = {
|
|
13
20
|
__typename: 'DataCalculationScript',
|
|
@@ -15,9 +22,9 @@ const calculation: DataCalculationScript = {
|
|
|
15
22
|
title: 'Format Price',
|
|
16
23
|
description: 'Formats price with currency symbol',
|
|
17
24
|
note: '',
|
|
18
|
-
triggerMode: 'auto', // 'auto' | 'manual'
|
|
25
|
+
triggerMode: 'auto', // 'auto' (default) | 'manual'
|
|
19
26
|
enableAsync: false,
|
|
20
|
-
// Inline code
|
|
27
|
+
// Inline code for short scripts...
|
|
21
28
|
code: `
|
|
22
29
|
const price = inputs.price || 0
|
|
23
30
|
const currency = inputs.currency || 'USD'
|
|
@@ -26,118 +33,82 @@ const calculation: DataCalculationScript = {
|
|
|
26
33
|
currency,
|
|
27
34
|
}).format(price)
|
|
28
35
|
`,
|
|
29
|
-
//
|
|
30
|
-
// import { readFile } from 'node:fs/promises'
|
|
36
|
+
// ...or load from a file (preferred for longer scripts):
|
|
31
37
|
// code: await readFile(new URL('./format-price.sandbox.js', import.meta.url), 'utf8'),
|
|
32
38
|
inputs: [
|
|
33
39
|
{ key: 'price', data: () => priceData, trigger: true },
|
|
34
40
|
{ key: 'currency', data: () => currencyData, trigger: false },
|
|
35
41
|
],
|
|
36
42
|
output: () => formattedPriceData,
|
|
37
|
-
outputs: [], // Additional named outputs
|
|
43
|
+
outputs: [], // Additional named outputs (see Multiple Outputs)
|
|
38
44
|
error: null, // or () => errorData for error handling
|
|
39
45
|
}
|
|
40
46
|
```
|
|
41
47
|
|
|
48
|
+
### Field Rules (defaults and constraints)
|
|
49
|
+
|
|
50
|
+
- `trigger` **defaults to `false` when omitted** — always set it explicitly. A calc whose inputs are all non-trigger never auto-runs.
|
|
51
|
+
- `triggerMode` defaults to `'auto'` when omitted.
|
|
52
|
+
- `output` receives the **whole return value**. `outputs` entries extract fields by key from a returned object — `key` is a lodash-get path, so deep paths like `'user.name'` work. `output`, `outputs`, and `error` can be combined.
|
|
53
|
+
- `error` receives the error **message string** when the script throws. Both extraction steps run on every execution: a success overwrites the error Data, a failure overwrites the output Data(s) — don't expect stale values to persist.
|
|
54
|
+
- **Auto mode rejects the same Data in both `inputs` and `output`/`outputs`/`error`** — compile fails with `Not allow duplicate set property id between inputs / outputs / output / error`. Manual mode allows the overlap (self-referential updates, see Recipes).
|
|
55
|
+
- `.sandbox.js` files use an `export function main() { ... }` wrapper — compile unwraps it. Raw statements also work, but the wrapper keeps linters happy since script bodies use top-level `return`.
|
|
56
|
+
|
|
42
57
|
## Trigger Modes
|
|
43
58
|
|
|
44
59
|
| Mode | Description |
|
|
45
60
|
|------|-------------|
|
|
46
|
-
| `auto` | Run
|
|
47
|
-
| `manual` |
|
|
61
|
+
| `auto` | Run on every write to a `trigger: true` input (even if the value is unchanged) |
|
|
62
|
+
| `manual` | Never auto-runs; only via `PROPERTY_BANK_COMMAND` action. Allows the same Data as both input and output |
|
|
63
|
+
|
|
64
|
+
Use `manual` to prevent circular dependencies, for explicit control, or when an output must feed back into an input.
|
|
65
|
+
|
|
66
|
+
## Triggering via PROPERTY_BANK_COMMAND
|
|
67
|
+
|
|
68
|
+
`input` references a Data that is an input of the target calc — the system runs the calc(s) that data feeds into. It does NOT reference the DataCalculation itself.
|
|
69
|
+
|
|
70
|
+
- **Manual calc**: ANY input works — `trigger` flags are ignored for manual-mode calcs.
|
|
71
|
+
- **Auto calc**: only `trigger: true` inputs work; commanding a `trigger: false` input is a silent no-op.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const triggerCalc: EventAction = {
|
|
75
|
+
handler: 'system',
|
|
76
|
+
action: {
|
|
77
|
+
__actionName: 'PROPERTY_BANK_COMMAND',
|
|
78
|
+
parent: 'System',
|
|
79
|
+
dataParams: [
|
|
80
|
+
{ input: () => priceData }, // Reference to an input Data of the calc
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
```
|
|
48
85
|
|
|
49
|
-
|
|
86
|
+
- When the same chain **writes a calc input first** (e.g. `PROPERTY_BANK` setting `dLastButton`) then issues `PROPERTY_BANK_COMMAND`, set `waitAsync: true` on the write so the calc reads the new value rather than the pre-chain snapshot. See [Event Action Chains](architecture-patterns.md#event-action-chains-priority-2).
|
|
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
|
+
- A dataParam's `value` acts as an execution gate: `{ input: () => d, value: false }` skips that trigger (combine with `mapping` for conditional runs).
|
|
50
89
|
|
|
51
|
-
## Script Sandbox
|
|
90
|
+
## Script Sandbox
|
|
52
91
|
|
|
53
|
-
Scripts run in `use strict` mode
|
|
92
|
+
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.
|
|
54
93
|
|
|
55
94
|
### Built-in Globals
|
|
56
95
|
|
|
57
96
|
| Global | Description |
|
|
58
97
|
|--------|-------------|
|
|
59
|
-
| `inputs` | Object with input values |
|
|
60
|
-
| `console` | `{ log, error, warn, info }`
|
|
98
|
+
| `inputs` | Object with input values (keyed by input `key`) |
|
|
99
|
+
| `console` | `{ log, error, warn, info }` — output is only visible in DevTools debug sessions; production is a no-op |
|
|
61
100
|
| `Platform` | `{ OS, isTV, isPad, isVision, isElectron }` |
|
|
62
101
|
| `TextEncoder`, `TextDecoder` | Text encoding/decoding |
|
|
63
|
-
| `Buffer` | Node.js Buffer (without `allocUnsafe`) |
|
|
102
|
+
| `Buffer` | Node.js Buffer (without `allocUnsafe`/`allocUnsafeSlow`) |
|
|
64
103
|
| `btoa`, `atob` | Base64 encoding/decoding |
|
|
65
104
|
|
|
66
|
-
### Available Libraries
|
|
67
|
-
|
|
68
|
-
**Utility**
|
|
69
|
-
| Library | Global | Description |
|
|
70
|
-
|---------|--------|-------------|
|
|
71
|
-
| [lodash](https://lodash.com) | `_`, `lodash` | Utility (sync: no debounce/delay/defer) |
|
|
72
|
-
| [voca](https://vocajs.com) | `voca` | String manipulation |
|
|
73
|
-
| [invariant](https://github.com/zertosh/invariant) | `invariant` | Assertions |
|
|
74
|
-
|
|
75
|
-
**Data & Encoding**
|
|
76
|
-
| Library | Global | Description |
|
|
77
|
-
|---------|--------|-------------|
|
|
78
|
-
| [json5](https://github.com/json5/json5) | `json5` | JSON5 parsing |
|
|
79
|
-
| [qs](https://github.com/ljharb/qs) | `qs` | Query string parsing |
|
|
80
|
-
| [url](https://github.com/defunctzombie/node-url) | `url` | URL parsing |
|
|
81
|
-
| [bytes](https://github.com/visionmedia/bytes.js) | `bytes` | Byte parsing/formatting |
|
|
82
|
-
| [ms](https://github.com/vercel/ms) | `ms` | Millisecond conversion |
|
|
83
|
-
| [base45](https://github.com/irony/base45) | `base45` | Base45 encoding |
|
|
84
|
-
| [iconv-lite](https://github.com/ashtuchkin/iconv-lite) | `iconv` | Character encoding |
|
|
85
|
-
|
|
86
|
-
**Math & Color**
|
|
87
|
-
| Library | Global | Description |
|
|
88
|
-
|---------|--------|-------------|
|
|
89
|
-
| [mathjs](https://mathjs.org) | `math`, `mathjs` | Math library |
|
|
90
|
-
| [chroma-js](https://gka.github.io/chroma.js) | `chroma` | Color manipulation |
|
|
91
|
-
|
|
92
|
-
**Date/Time**
|
|
93
|
-
| Library | Global | Description |
|
|
94
|
-
|---------|--------|-------------|
|
|
95
|
-
| [moment](https://momentjs.com) | `moment` | Date/time (auto parseFormat) |
|
|
96
|
-
|
|
97
|
-
**ID & Hash**
|
|
98
|
-
| Library | Global | Description |
|
|
99
|
-
|---------|--------|-------------|
|
|
100
|
-
| [nanoid](https://github.com/ai/nanoid) | `nanoid` | Unique ID generation |
|
|
101
|
-
| [md5](https://github.com/pvorb/node-md5) | `md5` | MD5 hashing |
|
|
102
|
-
|
|
103
|
-
**Crypto**
|
|
104
|
-
| Library | Global | Description |
|
|
105
|
-
|---------|--------|-------------|
|
|
106
|
-
| [crypto-browserify](https://github.com/crypto-browserify/crypto-browserify) | `crypto` | Crypto functions |
|
|
107
|
-
| [jsrsasign](https://github.com/kjur/jsrsasign) | `kjurJWS` | JWT/JWS signing |
|
|
108
|
-
| [cose-js](https://github.com/erdtman/COSE-JS) | `coseVerify` | COSE verification (sync) |
|
|
109
|
-
|
|
110
|
-
**Compression**
|
|
111
|
-
| Library | Global | Description |
|
|
112
|
-
|---------|--------|-------------|
|
|
113
|
-
| [fflate](https://github.com/101arrowz/fflate) | `fflate` | `{ zlibSync, unzlibSync, gzipSync, gunzipSync, compressSync, decompressSync, strFromU8 }` |
|
|
114
|
-
| [cbor](https://github.com/hildjj/node-cbor) | `cbor` | `{ encode, decode, decodeFirstSync, decodeAllSync }` |
|
|
115
|
-
|
|
116
|
-
**File & Document**
|
|
117
|
-
| Library | Global | Description |
|
|
118
|
-
|---------|--------|-------------|
|
|
119
|
-
| fs | `fs` | File system (limited, no download/upload) |
|
|
120
|
-
| [officeparser](https://github.com/nicktang) | `parseDocument` | Office document parsing (async) |
|
|
121
|
-
| [turndown](https://github.com/mixmark-io/turndown) | `TurndownService` | HTML to Markdown |
|
|
122
|
-
| [opencc-js](https://github.com/nk2028/opencc-js) | `OpenCC` | Chinese conversion `{ Converter, ConverterFactory, CustomConverter, Locale }` |
|
|
123
|
-
| [toon-format](https://github.com/nicktang) | `TOON` | TOON format parsing |
|
|
124
|
-
|
|
125
|
-
### Runtime Environment
|
|
126
|
-
|
|
127
|
-
| Platform | Engine |
|
|
128
|
-
|----------|--------|
|
|
129
|
-
| Android | Hermes engine sandbox |
|
|
130
|
-
| iOS | JavaScriptCore sandbox |
|
|
131
|
-
|
|
132
105
|
### Async Mode
|
|
133
106
|
|
|
134
|
-
Enable `enableAsync: true` to unlock
|
|
107
|
+
Sync mode (default) has **no `Promise`, timers, or `await`**. Enable `enableAsync: true` to unlock:
|
|
135
108
|
|
|
136
|
-
|
|
137
|
-
-
|
|
138
|
-
- `
|
|
139
|
-
- `requestAnimationFrame`
|
|
140
|
-
- Full lodash (including `debounce`, `delay`, `defer`)
|
|
109
|
+
- `Promise`, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `requestAnimationFrame`
|
|
110
|
+
- Full lodash (sync mode omits `debounce`, `delay`, `defer`)
|
|
111
|
+
- `await` at the top level of the script
|
|
141
112
|
|
|
142
113
|
```typescript
|
|
143
114
|
code: `
|
|
@@ -149,14 +120,63 @@ code: `
|
|
|
149
120
|
enableAsync: true,
|
|
150
121
|
```
|
|
151
122
|
|
|
123
|
+
### Runtime Environment
|
|
124
|
+
|
|
125
|
+
| Platform | Engine |
|
|
126
|
+
|----------|--------|
|
|
127
|
+
| Android | Hermes engine sandbox |
|
|
128
|
+
| iOS | JavaScriptCore sandbox |
|
|
129
|
+
| Electron desktop | Web Worker (V8) |
|
|
130
|
+
| Web preview / Simulator | Web Worker (V8) |
|
|
131
|
+
|
|
132
|
+
Simulator (Path 1) runs scripts on V8 while devices run Hermes/JSC — engine-sensitive code (date parsing, Intl, regex features) can pass in the Simulator and fail on device.
|
|
133
|
+
|
|
134
|
+
### Available Libraries
|
|
135
|
+
|
|
136
|
+
All exposed as globals. No network/file-download capability in any of them.
|
|
137
|
+
|
|
138
|
+
| Global | Library | Notes |
|
|
139
|
+
|--------|---------|-------|
|
|
140
|
+
| `_`, `lodash` | lodash | Sync mode omits `debounce`/`delay`/`defer` |
|
|
141
|
+
| `voca` | voca | String manipulation |
|
|
142
|
+
| `invariant` | invariant | Assertions |
|
|
143
|
+
| `json5` | json5 | JSON5 parsing |
|
|
144
|
+
| `qs` | qs | Query string parsing |
|
|
145
|
+
| `url` | node-url | URL parsing |
|
|
146
|
+
| `bytes` | bytes | Byte parsing/formatting |
|
|
147
|
+
| `ms` | ms | Millisecond conversion |
|
|
148
|
+
| `base45` | base45 | Base45 encoding |
|
|
149
|
+
| `iconv` | iconv-lite | Character encoding |
|
|
150
|
+
| `math`, `mathjs` | mathjs | Math library |
|
|
151
|
+
| `chroma` | chroma-js | Color manipulation |
|
|
152
|
+
| `moment` | moment | Date/time; auto parseFormat for string args |
|
|
153
|
+
| `nanoid` | nanoid | Unique ID generation |
|
|
154
|
+
| `md5` | md5 | MD5 hashing |
|
|
155
|
+
| `crypto` | crypto-browserify | Crypto functions |
|
|
156
|
+
| `kjurJWS` | jsrsasign | JWT/JWS signing (`KJUR.jws.JWS`) |
|
|
157
|
+
| `coseVerify` | cose-js | COSE verification (sync) |
|
|
158
|
+
| `fflate` | fflate | `{ zlibSync, unzlibSync, gzipSync, gunzipSync, compressSync, decompressSync, strFromU8 }` |
|
|
159
|
+
| `cbor` | cbor | `{ encode, decode, decodeFirstSync, decodeAllSync, addSemanticType }` |
|
|
160
|
+
| `fs` | (in-repo fs-compat) | File system; no download/upload methods |
|
|
161
|
+
| `parseDocument` | officeparser (in-repo fork) | Office document parsing (async) |
|
|
162
|
+
| `TurndownService` | turndown | HTML to Markdown |
|
|
163
|
+
| `OpenCC` | opencc-js | Chinese conversion `{ Converter, ConverterFactory, CustomConverter, Locale }` |
|
|
164
|
+
| `TOON` | @toon-format/toon | TOON format parsing |
|
|
165
|
+
|
|
166
|
+
## Recipes
|
|
167
|
+
|
|
152
168
|
### Multiple Outputs
|
|
153
169
|
|
|
170
|
+
Return an object; each `outputs` entry extracts its `key`:
|
|
171
|
+
|
|
154
172
|
```typescript
|
|
155
173
|
outputs: [
|
|
156
174
|
{ key: 'total', data: () => totalData },
|
|
157
175
|
{ key: 'tax', data: () => taxData },
|
|
158
176
|
{ key: 'subtotal', data: () => subtotalData },
|
|
159
177
|
],
|
|
178
|
+
output: null,
|
|
179
|
+
error: null,
|
|
160
180
|
code: `
|
|
161
181
|
const subtotal = inputs.price * inputs.quantity
|
|
162
182
|
const tax = subtotal * 0.1
|
|
@@ -165,31 +185,51 @@ code: `
|
|
|
165
185
|
`,
|
|
166
186
|
```
|
|
167
187
|
|
|
168
|
-
|
|
188
|
+
### Manual Self-Referential Update
|
|
169
189
|
|
|
170
|
-
|
|
190
|
+
When an update is too complex for `PROPERTY_BANK_EXPRESSION` and must read its own previous value — manual mode allows the same Data as input and output:
|
|
171
191
|
|
|
172
192
|
```typescript
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
]
|
|
181
|
-
|
|
193
|
+
const appendHistory: DataCalculationScript = {
|
|
194
|
+
__typename: 'DataCalculationScript',
|
|
195
|
+
id: makeId('property_bank_calc'),
|
|
196
|
+
title: 'Append History Entry',
|
|
197
|
+
triggerMode: 'manual',
|
|
198
|
+
enableAsync: false,
|
|
199
|
+
code: `
|
|
200
|
+
const history = Array.isArray(inputs.history) ? inputs.history : []
|
|
201
|
+
return [...history, { entry: inputs.entry, at: moment().toISOString() }].slice(-50)
|
|
202
|
+
`,
|
|
203
|
+
inputs: [
|
|
204
|
+
{ key: 'history', data: () => historyData, trigger: true },
|
|
205
|
+
{ key: 'entry', data: () => entryData, trigger: true },
|
|
206
|
+
],
|
|
207
|
+
output: () => historyData, // same Data as input — manual mode only
|
|
208
|
+
outputs: [],
|
|
209
|
+
error: null,
|
|
182
210
|
}
|
|
211
|
+
// Run it from an event chain: PROPERTY_BANK writes entryData (waitAsync: true),
|
|
212
|
+
// then PROPERTY_BANK_COMMAND with input: () => entryData.
|
|
183
213
|
```
|
|
184
214
|
|
|
185
|
-
|
|
215
|
+
## Failure Modes
|
|
216
|
+
|
|
217
|
+
| Symptom | Cause | Fix |
|
|
218
|
+
|---------|-------|-----|
|
|
219
|
+
| Calc never auto-runs | `trigger` omitted on inputs (defaults to `false`) | Set `trigger: true` explicitly |
|
|
220
|
+
| `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
|
+
| 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
|
+
| Calc reads stale value written earlier in the same chain | Missing `waitAsync: true` on the preceding write | Set `waitAsync: true` on the write action |
|
|
223
|
+
| Works in Simulator, fails on device | V8 vs Hermes/JSC engine difference | Verify on device (Path 2); avoid engine-sensitive parsing |
|
|
224
|
+
| `console.log` shows nothing | Console only emits during DevTools debug sessions | Attach DevTools, or write debug values to an output Data |
|
|
225
|
+
| `Promise`/`setTimeout` undefined, or `Async mode is required` error | `enableAsync: false` | Set `enableAsync: true` |
|
|
186
226
|
|
|
187
227
|
## Best Practices
|
|
188
228
|
|
|
189
229
|
1. **Avoid circular deps**: Set non-triggering inputs (`trigger: false`) or use `manual` mode
|
|
190
230
|
2. **Error handling**: Always set `error` output for scripts that might fail
|
|
191
231
|
3. **Keep scripts pure**: Avoid side effects, return computed values
|
|
192
|
-
4. **Debounce rapid updates**: Use `manual` mode + timer for high-frequency inputs
|
|
232
|
+
4. **Debounce rapid updates**: Use `manual` mode + timer for high-frequency inputs (auto calcs re-run on every write, even unchanged)
|
|
193
233
|
|
|
194
234
|
## Anti-Patterns (AVOID)
|
|
195
235
|
|
|
@@ -197,13 +237,3 @@ See [Architecture Patterns](architecture-patterns.md) for the full pattern selec
|
|
|
197
237
|
|
|
198
238
|
### Using Data Calc as an orchestrator
|
|
199
239
|
Scripts that manage state machines, control UI flow, or coordinate multi-step processes belong in Event Action Chains. Symptoms: if/else on "what happens next", mirror `dFooResult` outputs that copy back to `dFoo` via `valueChange`, or a `dLastInput` field set-then-cleared to force an auto calc. See the "user-driven state machine" recipe in [Architecture Patterns](architecture-patterns.md).
|
|
200
|
-
|
|
201
|
-
### Quick reference
|
|
202
|
-
|
|
203
|
-
| If you need to... | Use instead |
|
|
204
|
-
|---|---|
|
|
205
|
-
| Call an LLM / AI model | Generator (Assistant, LLM, HTTP) |
|
|
206
|
-
| Sequence multiple actions | Event Action Chain |
|
|
207
|
-
| Set a data value directly | PROPERTY_BANK system action |
|
|
208
|
-
| Compute a simple expression | PROPERTY_BANK_EXPRESSION |
|
|
209
|
-
| Transform/format/parse data | Data Calculation (correct use) |
|
|
@@ -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.
|
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bricks-design
|
|
3
3
|
description: >-
|
|
4
|
-
Visual design discipline for Applications and Subspaces — type,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
pure usability / flow / journey / affordance / feedback / recovery
|
|
19
|
-
/ accessibility / multilingual audits — those go to bricks-ux. SKIP
|
|
20
|
-
for single-Brick or Generator template work
|
|
21
|
-
(create-brick-or-generator), debugging or refactoring with no
|
|
22
|
-
design intent, or non-display deliverables (CLI, server, tooling).
|
|
23
|
-
Encodes architecture truths, performance and complexity guardrails,
|
|
24
|
-
input-translation rules, visual languages library, Direction
|
|
25
|
-
Advisor for vague briefs, Media Flow protocol for branded work.
|
|
26
|
-
Verification toolchain (compile, preview tool selection, on-device DevTools,
|
|
27
|
-
Path 1/2/3) lives in the bricks-ctor skill.
|
|
4
|
+
Visual design discipline for Applications and Subspaces — type, palette, asset
|
|
5
|
+
acquisition, design language, system commitment, visual rhythm, brand. TRIGGER
|
|
6
|
+
for visual / aesthetic / system / style / brand-asset work even when named in
|
|
7
|
+
product terms — slideshow, pitch deck, explainer, kiosk, signage, menu board,
|
|
8
|
+
lobby, wayfinding, retail, museum, transit; translate or rebuild from Figma /
|
|
9
|
+
HTML / screenshot / website / PDF / brand book; vague creative briefs; branded
|
|
10
|
+
scenes; rework or audit of an existing Subspace's type / palette / motion /
|
|
11
|
+
rhythm. For end-to-end briefs invoke in parallel with bricks-ux (interaction /
|
|
12
|
+
flow layer). SKIP for pure usability / flow / accessibility audits (bricks-
|
|
13
|
+
ux), single-Brick or Generator template work (create-brick-or-generator),
|
|
14
|
+
debugging without design intent, or non-display deliverables. Encodes
|
|
15
|
+
architecture truths, performance guardrails, input-translation rules, visual
|
|
16
|
+
languages, Direction Advisor, Media Flow protocol; verification toolchain
|
|
17
|
+
lives in bricks-ctor.
|
|
28
18
|
---
|
|
29
19
|
|
|
30
20
|
# BRICKS Design
|
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bricks-ux
|
|
3
3
|
description: >-
|
|
4
|
-
Interaction design and end-user experience for any Application or
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
are visual design, not interaction design. Encodes a universal
|
|
19
|
-
user-journey spine, interaction archetypes, pressable composition,
|
|
20
|
-
monitoring-screen discipline, designed flow states, accessibility,
|
|
21
|
-
and a tiered UX critique. Hardware floors are deployment-relative;
|
|
22
|
-
web habits (hover affordances, modal-as-default, scroll, page-submit
|
|
23
|
-
forms) do not transfer.
|
|
4
|
+
Interaction design and end-user experience for any Application or Subspace
|
|
5
|
+
built on Canvases, Bricks, Generators, Data, DataCalculation. TRIGGER on
|
|
6
|
+
usability / flow / interaction / journey / state / affordance / feedback /
|
|
7
|
+
recovery / accessibility audits and design work — "audit this flow", "improve
|
|
8
|
+
usability", "design the wait state", "make the kiosk usable", "design the
|
|
9
|
+
payment flow", "design idle and attractor states", "design for multilingual /
|
|
10
|
+
low vision". Also triggers in parallel with visual-design work for end-to-end
|
|
11
|
+
deliverables (kiosk, signage, dashboard, interactive screen, pitch deck) where
|
|
12
|
+
interaction shape matters as much as look. SKIP for purely visual / aesthetic
|
|
13
|
+
/ style / brand-asset / typography / palette work — that is bricks-design.
|
|
14
|
+
Encodes a universal user-journey spine, interaction archetypes, pressable
|
|
15
|
+
composition, monitoring-screen discipline, designed flow states,
|
|
16
|
+
accessibility, tiered UX critique; hardware floors are deployment-relative and
|
|
17
|
+
web habits do not transfer.
|
|
24
18
|
---
|
|
25
19
|
|
|
26
20
|
# BRICKS UX
|
|
@@ -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')
|
|
@@ -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
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs'
|
|
|
3
3
|
import { 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())
|
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())
|
|
@@ -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<
|
|
@@ -18,7 +18,9 @@ export type DataCommandSandboxGetReturnValue = DataCommand & {
|
|
|
18
18
|
/* Run JS — Run JavaScript code
|
|
19
19
|
|
|
20
20
|
- Default `use strict`
|
|
21
|
-
- Global functions fetch, XMLHttpRequest
|
|
21
|
+
- Global functions fetch, XMLHttpRequest are not available.
|
|
22
|
+
- setTimeout, setInterval, Promise... require Enable Async.
|
|
23
|
+
- Built-in globals: Platform, Buffer, btoa/atob, TextEncoder/TextDecoder
|
|
22
24
|
|
|
23
25
|
##### Available libraries (global)
|
|
24
26
|
|
|
@@ -43,10 +45,16 @@ export type DataCommandSandboxGetReturnValue = DataCommand & {
|
|
|
43
45
|
- fflate (Not support async, callback and stream)
|
|
44
46
|
- iconv (Use iconv-lite)
|
|
45
47
|
- OpenCC (Use opencc-js)
|
|
48
|
+
- fs (Limited, no download/upload)
|
|
49
|
+
- parseDocument (Use officeparser, requires Enable Async)
|
|
50
|
+
- TurndownService (Use turndown, HTML to Markdown)
|
|
51
|
+
- TOON (Use toon-format/toon)
|
|
46
52
|
|
|
47
53
|
Android: Running on the sandbox of Hermes engine
|
|
48
54
|
|
|
49
|
-
iOS: Running on the sandbox of iOS built-in JavaScriptCore
|
|
55
|
+
iOS: Running on the sandbox of iOS built-in JavaScriptCore
|
|
56
|
+
|
|
57
|
+
Desktop / Web: Running on the sandbox of a Web Worker */
|
|
50
58
|
export type DataCommandSandboxRunJavascript = DataCommand & {
|
|
51
59
|
__commandName: 'SANDBOX_RUN_JAVASCRIPT'
|
|
52
60
|
inputs?: Array<
|
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/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
|
}
|