@fugood/bricks-ctor 2.25.0-beta.22 → 2.25.0-beta.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compile/__tests__/util.test.js +3 -3
- package/compile/index.ts +4 -2
- package/package.json +3 -3
- package/skills/bricks-ctor/rules/automations.md +11 -0
- package/skills/bricks-ctor/rules/data-calculation.md +2 -0
- package/tools/_shell.ts +8 -1
- package/tools/mcp-tools/compile.ts +15 -7
- package/tools/mcp-tools/media.ts +4 -1
- package/types/common.ts +2 -2
- package/types/data-calc-command/base.ts +57 -0
- package/types/data-calc-command/collection.ts +418 -0
- package/types/data-calc-command/color.ts +432 -0
- package/types/data-calc-command/constant.ts +50 -0
- package/types/data-calc-command/datetime.ts +147 -0
- package/types/data-calc-command/file.ts +129 -0
- package/types/data-calc-command/index.ts +13 -0
- package/types/data-calc-command/iteratee.ts +23 -0
- package/types/data-calc-command/logictype.ts +190 -0
- package/types/data-calc-command/math.ts +275 -0
- package/types/data-calc-command/object.ts +119 -0
- package/types/data-calc-command/sandbox.ts +58 -0
- package/types/data-calc-command/string.ts +407 -0
- package/types/data.ts +1 -1
- package/utils/__tests__/id.test.js +106 -10
- package/utils/data.ts +1 -1
- package/utils/id.ts +78 -27
- package/types/data-calc-command.ts +0 -7005
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
1
4
|
import { makeId } from '../id'
|
|
2
5
|
|
|
3
|
-
const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{
|
|
6
|
+
const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
|
|
4
7
|
const UUID_ONLY_RE = new RegExp(`^${UUID}$`)
|
|
5
8
|
|
|
9
|
+
const withProjectDir = (app, fn) => {
|
|
10
|
+
const previousCwd = process.cwd()
|
|
11
|
+
const dir = mkdtempSync(join(tmpdir(), 'make-id-'))
|
|
12
|
+
try {
|
|
13
|
+
if (app) writeFileSync(join(dir, 'application.json'), JSON.stringify(app))
|
|
14
|
+
process.chdir(dir)
|
|
15
|
+
return fn()
|
|
16
|
+
} finally {
|
|
17
|
+
process.chdir(previousCwd)
|
|
18
|
+
rmSync(dir, { recursive: true, force: true })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
describe('makeId', () => {
|
|
7
23
|
describe('type prefixes', () => {
|
|
8
24
|
const cases = [
|
|
@@ -25,8 +41,23 @@ describe('makeId', () => {
|
|
|
25
41
|
expect(makeId(type)).toMatch(new RegExp(`^${prefix}${UUID}$`))
|
|
26
42
|
})
|
|
27
43
|
|
|
28
|
-
test('
|
|
29
|
-
|
|
44
|
+
test('uses generated count fallback when no alias is provided', () => {
|
|
45
|
+
const first = withProjectDir({ id: 'count-fallback-app' }, () => {
|
|
46
|
+
jest.resetModules()
|
|
47
|
+
const { makeId: makeFreshId } = require('../id')
|
|
48
|
+
return [makeFreshId('brick'), makeFreshId('brick')]
|
|
49
|
+
})
|
|
50
|
+
const second = withProjectDir({ id: 'count-fallback-app' }, () => {
|
|
51
|
+
jest.resetModules()
|
|
52
|
+
const { makeId: makeFreshId } = require('../id')
|
|
53
|
+
return [makeFreshId('brick'), makeFreshId('brick')]
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
expect(first[0]).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
57
|
+
expect(first[1]).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
58
|
+
expect(first[0]).not.toBe(first[1])
|
|
59
|
+
expect(second).toEqual(first)
|
|
60
|
+
jest.resetModules()
|
|
30
61
|
})
|
|
31
62
|
|
|
32
63
|
test('unknown type falls through to an unprefixed uuid', () => {
|
|
@@ -41,18 +72,83 @@ describe('makeId', () => {
|
|
|
41
72
|
})
|
|
42
73
|
|
|
43
74
|
describe('snapshotMode', () => {
|
|
44
|
-
test('produces
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
test('produces deterministic v4 uuids from the generated count', () => {
|
|
76
|
+
const previousCwd = process.cwd()
|
|
77
|
+
const dir = mkdtempSync(join(tmpdir(), 'make-id-snapshot-'))
|
|
78
|
+
writeFileSync(join(dir, 'application.json'), JSON.stringify({ id: 'snapshot-app' }))
|
|
79
|
+
try {
|
|
80
|
+
process.chdir(dir)
|
|
81
|
+
jest.resetModules()
|
|
82
|
+
const { makeId: makeFreshId } = require('../id')
|
|
83
|
+
const a = makeFreshId('brick', { snapshotMode: true })
|
|
84
|
+
const b = makeFreshId('brick', { snapshotMode: true })
|
|
85
|
+
|
|
86
|
+
jest.resetModules()
|
|
87
|
+
const { makeId: makeFreshIdAgain } = require('../id')
|
|
88
|
+
expect(makeFreshIdAgain('brick', { snapshotMode: true })).toBe(a)
|
|
89
|
+
expect(makeFreshIdAgain('brick', { snapshotMode: true })).toBe(b)
|
|
90
|
+
expect(a).not.toBe(b)
|
|
91
|
+
expect(a).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
92
|
+
expect(b).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
93
|
+
} finally {
|
|
94
|
+
process.chdir(previousCwd)
|
|
95
|
+
rmSync(dir, { recursive: true, force: true })
|
|
96
|
+
jest.resetModules()
|
|
97
|
+
}
|
|
50
98
|
})
|
|
51
99
|
|
|
52
100
|
test('snapshotMode false falls back to random uuid', () => {
|
|
53
101
|
const id = makeId('canvas', { snapshotMode: false })
|
|
54
102
|
expect(id).toMatch(new RegExp(`^CANVAS_${UUID}$`))
|
|
55
|
-
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('stable aliases', () => {
|
|
107
|
+
test('produces the same id for the same application id, type, and alias', () => {
|
|
108
|
+
const first = withProjectDir({ id: 'stable-app' }, () => {
|
|
109
|
+
jest.resetModules()
|
|
110
|
+
return require('../id').makeId('brick', 'stable-hero')
|
|
111
|
+
})
|
|
112
|
+
const second = withProjectDir({ id: 'stable-app' }, () => {
|
|
113
|
+
jest.resetModules()
|
|
114
|
+
return require('../id').makeId('brick', 'stable-hero')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
expect(second).toBe(first)
|
|
118
|
+
expect(first).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
119
|
+
jest.resetModules()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('uses the application id when hashing stable aliases', () => {
|
|
123
|
+
const idA = withProjectDir({ id: 'app-a' }, () => makeId('brick', 'hero'))
|
|
124
|
+
const idB = withProjectDir({ id: 'app-b' }, () => makeId('brick', 'hero'))
|
|
125
|
+
|
|
126
|
+
expect(idA).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
127
|
+
expect(idB).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
128
|
+
expect(idA).not.toBe(idB)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('rejects duplicate aliases for the same application id and entry type', () => {
|
|
132
|
+
withProjectDir({ id: 'duplicate-app' }, () => {
|
|
133
|
+
expect(makeId('brick', 'shared')).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
134
|
+
expect(() => makeId('brick', 'shared')).toThrow(/Duplicate makeId alias 'shared'/)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('allows the same alias for different entry types', () => {
|
|
139
|
+
withProjectDir({ id: 'shared-alias-app' }, () => {
|
|
140
|
+
const brick = makeId('brick', 'shared')
|
|
141
|
+
const canvas = makeId('canvas', 'shared')
|
|
142
|
+
|
|
143
|
+
expect(brick).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
144
|
+
expect(canvas).toMatch(new RegExp(`^CANVAS_${UUID}$`))
|
|
145
|
+
expect(brick).not.toBe(canvas)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('falls back when application.json is not present', () => {
|
|
150
|
+
const id = withProjectDir(null, () => makeId('brick', 'fallback-app-id'))
|
|
151
|
+
expect(id).toMatch(new RegExp(`^BRICK_${UUID}$`))
|
|
56
152
|
})
|
|
57
153
|
})
|
|
58
154
|
})
|
package/utils/data.ts
CHANGED
|
@@ -85,7 +85,7 @@ type SystemDataName =
|
|
|
85
85
|
type SystemDataInfo = {
|
|
86
86
|
name: SystemDataName
|
|
87
87
|
id: string
|
|
88
|
-
type: 'string' | 'number' | 'bool' | 'array' | 'object' | 'any'
|
|
88
|
+
type: 'string' | 'number' | 'bool' | 'boolean' | 'array' | 'object' | 'any'
|
|
89
89
|
title?: string
|
|
90
90
|
description?: string
|
|
91
91
|
schema?: object
|
package/utils/id.ts
CHANGED
|
@@ -1,38 +1,83 @@
|
|
|
1
1
|
import { v4 as uuid } from 'uuid'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
2
5
|
|
|
3
6
|
let count = 0
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
type IdType =
|
|
9
|
+
| 'animation'
|
|
10
|
+
| 'brick'
|
|
11
|
+
| 'canvas'
|
|
12
|
+
| 'generator'
|
|
13
|
+
| 'data'
|
|
14
|
+
| 'switch'
|
|
15
|
+
| 'property_bank_command'
|
|
16
|
+
| 'property_bank_calc'
|
|
17
|
+
| 'dynamic-brick'
|
|
18
|
+
| 'automation_map'
|
|
19
|
+
| 'test'
|
|
20
|
+
| 'test_case'
|
|
21
|
+
| 'test_var'
|
|
22
|
+
| 'subspace'
|
|
23
|
+
|
|
24
|
+
type IdOptions = {
|
|
25
|
+
snapshotMode?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const APPLICATION_ID_FALLBACK = 'unknown-application'
|
|
29
|
+
|
|
30
|
+
const usedStableAliases = new Set<string>()
|
|
31
|
+
let applicationIdCache: { cwd: string; id: string } | null = null
|
|
32
|
+
|
|
33
|
+
const readApplicationId = () => {
|
|
34
|
+
const cwd = process.cwd()
|
|
35
|
+
if (applicationIdCache?.cwd === cwd) return applicationIdCache.id
|
|
36
|
+
|
|
37
|
+
let id = APPLICATION_ID_FALLBACK
|
|
38
|
+
try {
|
|
39
|
+
const app = JSON.parse(readFileSync(join(cwd, 'application.json'), 'utf8'))
|
|
40
|
+
if (typeof app.id === 'string' && app.id) id = app.id
|
|
41
|
+
} catch {
|
|
42
|
+
// `makeId` is also used from tests and utilities outside project roots.
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
applicationIdCache = { cwd, id }
|
|
46
|
+
return id
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hashToRandomBytes = (parts: string[]) => {
|
|
50
|
+
const hash = createHash('sha256').update(JSON.stringify(parts)).digest()
|
|
51
|
+
return new Uint8Array(hash.subarray(0, 16))
|
|
10
52
|
}
|
|
11
53
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
) => {
|
|
54
|
+
const makeStableUuid = (type: string, alias?: string) => {
|
|
55
|
+
const applicationId = readApplicationId()
|
|
56
|
+
const seed = alias ?? String(count)
|
|
57
|
+
if (alias === undefined) count += 1
|
|
58
|
+
|
|
59
|
+
if (alias !== undefined) {
|
|
60
|
+
const aliasKey = JSON.stringify([applicationId, type, alias])
|
|
61
|
+
if (usedStableAliases.has(aliasKey)) {
|
|
62
|
+
throw new Error(`Duplicate makeId alias '${alias}' for type '${type}'`)
|
|
63
|
+
}
|
|
64
|
+
usedStableAliases.add(aliasKey)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return uuid({
|
|
68
|
+
random: hashToRandomBytes([applicationId, type, seed]),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Make stable ids by default; explicit snapshotMode: false preserves the random escape hatch.
|
|
73
|
+
export const makeId = (type: IdType, aliasOrOpts?: string | IdOptions, opts?: IdOptions) => {
|
|
33
74
|
if (type === 'subspace') {
|
|
34
75
|
throw new Error('Currently subspace is not supported for ID generation, please use a fixed ID')
|
|
35
76
|
}
|
|
77
|
+
|
|
78
|
+
const alias = typeof aliasOrOpts === 'string' ? aliasOrOpts : undefined
|
|
79
|
+
const options = typeof aliasOrOpts === 'string' ? opts : (aliasOrOpts ?? opts)
|
|
80
|
+
|
|
36
81
|
let prefix = ''
|
|
37
82
|
switch (type) {
|
|
38
83
|
case 'animation':
|
|
@@ -76,5 +121,11 @@ export const makeId = (
|
|
|
76
121
|
break
|
|
77
122
|
default:
|
|
78
123
|
}
|
|
79
|
-
|
|
124
|
+
|
|
125
|
+
const useCountFallback = aliasOrOpts === undefined && opts === undefined
|
|
126
|
+
const id =
|
|
127
|
+
alias !== undefined || options?.snapshotMode || useCountFallback
|
|
128
|
+
? makeStableUuid(type, alias)
|
|
129
|
+
: uuid()
|
|
130
|
+
return `${prefix}${id}`
|
|
80
131
|
}
|