@fugood/bricks-ctor 2.25.0-beta.48 → 2.25.0-beta.49
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/package.json +3 -3
- package/tools/__tests__/_cli-error.test.ts +35 -0
- package/tools/_cli-error.ts +17 -0
- package/tools/deploy.ts +2 -6
- package/tools/mcp-tools/_editing-helpers.ts +58 -0
- package/tools/mcp-tools/data-calc-editing.ts +10 -56
- package/tools/mcp-tools/entry-editing.ts +11 -56
- package/tools/pull.ts +2 -6
- package/tools/push-config.ts +2 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fugood/bricks-ctor",
|
|
3
|
-
"version": "2.25.0-beta.
|
|
3
|
+
"version": "2.25.0-beta.49",
|
|
4
4
|
"main": "index.ts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"@babel/parser": "7.28.5",
|
|
12
12
|
"@babel/traverse": "7.28.5",
|
|
13
13
|
"@babel/types": "7.28.5",
|
|
14
|
-
"@fugood/bricks-cli": "^2.25.0-beta.
|
|
14
|
+
"@fugood/bricks-cli": "^2.25.0-beta.49",
|
|
15
15
|
"@huggingface/gguf": "^0.3.2",
|
|
16
16
|
"@iarna/toml": "^3.0.0",
|
|
17
17
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"oxfmt": "^0.36.0"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "e7da261fd97feda0ee059ff04070f0068cad9d29"
|
|
33
33
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { extractCliErrorMessage } from '../_cli-error'
|
|
2
|
+
|
|
3
|
+
// bricks-project's tsconfig has no @types/jest, so declare the globals this test
|
|
4
|
+
// uses (mirrors tools/mcp-tools/__tests__/huggingface.test.ts).
|
|
5
|
+
declare const describe: (name: string, fn: () => void) => void
|
|
6
|
+
declare const it: (name: string, fn: () => void) => void
|
|
7
|
+
declare const expect: (actual: unknown) => { toBe: (expected: unknown) => void }
|
|
8
|
+
|
|
9
|
+
describe('extractCliErrorMessage', () => {
|
|
10
|
+
// Regression: the tools used to build this message inside the same try that
|
|
11
|
+
// wrapped JSON.parse, so the throw was caught by its own catch and replaced
|
|
12
|
+
// with the raw JSON blob. The human-readable message must survive.
|
|
13
|
+
it('extracts error.message from a JSON error body', () => {
|
|
14
|
+
const output = JSON.stringify({ error: { message: 'Conflict: config was modified' } })
|
|
15
|
+
expect(extractCliErrorMessage(output, 'Update failed')).toBe('Conflict: config was modified')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('extracts a string error from a JSON error body', () => {
|
|
19
|
+
expect(extractCliErrorMessage('{"error":"Boom"}', 'Pull failed')).toBe('Boom')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns the raw output when it is not JSON', () => {
|
|
23
|
+
expect(extractCliErrorMessage('plain text failure', 'Release failed')).toBe(
|
|
24
|
+
'plain text failure',
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('falls back to the raw output when the JSON has no error field', () => {
|
|
29
|
+
expect(extractCliErrorMessage('{"ok":true}', 'Update failed')).toBe('{"ok":true}')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('falls back to the generic message when output is empty', () => {
|
|
33
|
+
expect(extractCliErrorMessage('', 'Update failed')).toBe('Update failed')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -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
|
+
}
|
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())
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as t from '@babel/types'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const oxfmtOptions = {
|
|
5
|
+
trailingComma: 'all',
|
|
6
|
+
tabWidth: 2,
|
|
7
|
+
semi: false,
|
|
8
|
+
singleQuote: true,
|
|
9
|
+
printWidth: 100,
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
13
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
14
|
+
|
|
15
|
+
export const isIdentifierName = (value: string) => /^[$A-Z_a-z][$\w]*$/.test(value)
|
|
16
|
+
|
|
17
|
+
export const normalizeRelPath = (file: string) => file.replace(/\\/g, '/').replace(/^\.\/+/, '')
|
|
18
|
+
|
|
19
|
+
export const projectRelativePath = (projectDir: string, absPath: string) =>
|
|
20
|
+
normalizeRelPath(path.relative(projectDir, absPath))
|
|
21
|
+
|
|
22
|
+
export const getPropertyKeyName = (key: t.Expression | t.PrivateName) => {
|
|
23
|
+
if (t.isIdentifier(key)) return key.name
|
|
24
|
+
if (t.isStringLiteral(key) || t.isNumericLiteral(key)) return String(key.value)
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const makeObjectKey = (key: string) =>
|
|
29
|
+
isIdentifierName(key) ? t.identifier(key) : t.stringLiteral(key)
|
|
30
|
+
|
|
31
|
+
export const getObjectProperty = (object: t.ObjectExpression, key: string) =>
|
|
32
|
+
object.properties.find((property): property is t.ObjectProperty => {
|
|
33
|
+
if (!t.isObjectProperty(property)) return false
|
|
34
|
+
return getPropertyKeyName(property.key) === key
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const getStringProperty = (object: t.ObjectExpression, key: string) => {
|
|
38
|
+
const property = getObjectProperty(object, key)
|
|
39
|
+
if (!property || !t.isStringLiteral(property.value)) return undefined
|
|
40
|
+
return property.value.value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const setObjectProperty = (object: t.ObjectExpression, key: string, value: t.Expression) => {
|
|
44
|
+
const existing = getObjectProperty(object, key)
|
|
45
|
+
if (existing) {
|
|
46
|
+
existing.value = value
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
object.properties.push(t.objectProperty(makeObjectKey(key), value))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const removeObjectProperty = (object: t.ObjectExpression, key: string) => {
|
|
53
|
+
const index = object.properties.findIndex((property) => {
|
|
54
|
+
if (!t.isObjectProperty(property)) return false
|
|
55
|
+
return getPropertyKeyName(property.key) === key
|
|
56
|
+
})
|
|
57
|
+
if (index >= 0) object.properties.splice(index, 1)
|
|
58
|
+
}
|
|
@@ -9,18 +9,20 @@ import path from 'node:path'
|
|
|
9
9
|
import { z } from 'zod'
|
|
10
10
|
|
|
11
11
|
import { verifyProject } from './_verify'
|
|
12
|
+
import {
|
|
13
|
+
getObjectProperty,
|
|
14
|
+
getStringProperty,
|
|
15
|
+
isRecord,
|
|
16
|
+
makeObjectKey,
|
|
17
|
+
oxfmtOptions,
|
|
18
|
+
projectRelativePath,
|
|
19
|
+
removeObjectProperty,
|
|
20
|
+
setObjectProperty,
|
|
21
|
+
} from './_editing-helpers'
|
|
12
22
|
import { appendEditRecord, editProvenance } from '../_edits-log'
|
|
13
23
|
|
|
14
24
|
const generate = (generateModule as any).default || generateModule
|
|
15
25
|
|
|
16
|
-
const oxfmtOptions = {
|
|
17
|
-
trailingComma: 'all',
|
|
18
|
-
tabWidth: 2,
|
|
19
|
-
semi: false,
|
|
20
|
-
singleQuote: true,
|
|
21
|
-
printWidth: 100,
|
|
22
|
-
} as const
|
|
23
|
-
|
|
24
26
|
type ParsedFile = {
|
|
25
27
|
ast: t.File
|
|
26
28
|
source: string
|
|
@@ -79,16 +81,6 @@ class DataCalcEditingError extends Error {
|
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
83
|
-
Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
84
|
-
|
|
85
|
-
const isIdentifierName = (value: string) => /^[$A-Z_a-z][$\w]*$/.test(value)
|
|
86
|
-
|
|
87
|
-
const normalizeRelPath = (file: string) => file.replace(/\\/g, '/').replace(/^\.\/+/, '')
|
|
88
|
-
|
|
89
|
-
const projectRelativePath = (projectDir: string, absPath: string) =>
|
|
90
|
-
normalizeRelPath(path.relative(projectDir, absPath))
|
|
91
|
-
|
|
92
84
|
const resolveProjectPath = (projectDir: string, file: string) => {
|
|
93
85
|
if (path.isAbsolute(file)) {
|
|
94
86
|
throw new DataCalcEditingError('invalid_file', 'File must be project-relative', { file })
|
|
@@ -132,44 +124,6 @@ const readParsedFile = async (projectDir: string, absPath: string): Promise<Pars
|
|
|
132
124
|
}
|
|
133
125
|
}
|
|
134
126
|
|
|
135
|
-
const getPropertyKeyName = (key: t.Expression | t.PrivateName) => {
|
|
136
|
-
if (t.isIdentifier(key)) return key.name
|
|
137
|
-
if (t.isStringLiteral(key) || t.isNumericLiteral(key)) return String(key.value)
|
|
138
|
-
return null
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const makeObjectKey = (key: string) =>
|
|
142
|
-
isIdentifierName(key) ? t.identifier(key) : t.stringLiteral(key)
|
|
143
|
-
|
|
144
|
-
const getObjectProperty = (object: t.ObjectExpression, key: string) =>
|
|
145
|
-
object.properties.find((property): property is t.ObjectProperty => {
|
|
146
|
-
if (!t.isObjectProperty(property)) return false
|
|
147
|
-
return getPropertyKeyName(property.key) === key
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
const getStringProperty = (object: t.ObjectExpression, key: string) => {
|
|
151
|
-
const property = getObjectProperty(object, key)
|
|
152
|
-
if (!property || !t.isStringLiteral(property.value)) return undefined
|
|
153
|
-
return property.value.value
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const setObjectProperty = (object: t.ObjectExpression, key: string, value: t.Expression) => {
|
|
157
|
-
const existing = getObjectProperty(object, key)
|
|
158
|
-
if (existing) {
|
|
159
|
-
existing.value = value
|
|
160
|
-
return
|
|
161
|
-
}
|
|
162
|
-
object.properties.push(t.objectProperty(makeObjectKey(key), value))
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const removeObjectProperty = (object: t.ObjectExpression, key: string) => {
|
|
166
|
-
const index = object.properties.findIndex((property) => {
|
|
167
|
-
if (!t.isObjectProperty(property)) return false
|
|
168
|
-
return getPropertyKeyName(property.key) === key
|
|
169
|
-
})
|
|
170
|
-
if (index >= 0) object.properties.splice(index, 1)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
127
|
const getExportEntries = (ast: t.File): ExportEntry[] =>
|
|
174
128
|
ast.program.body.flatMap((statement) => {
|
|
175
129
|
if (!t.isExportNamedDeclaration(statement)) return []
|
|
@@ -10,19 +10,22 @@ import path from 'node:path'
|
|
|
10
10
|
import { z } from 'zod'
|
|
11
11
|
|
|
12
12
|
import { verifyProject } from './_verify'
|
|
13
|
+
import {
|
|
14
|
+
getObjectProperty,
|
|
15
|
+
getPropertyKeyName,
|
|
16
|
+
getStringProperty,
|
|
17
|
+
isRecord,
|
|
18
|
+
makeObjectKey,
|
|
19
|
+
oxfmtOptions,
|
|
20
|
+
projectRelativePath,
|
|
21
|
+
removeObjectProperty,
|
|
22
|
+
setObjectProperty,
|
|
23
|
+
} from './_editing-helpers'
|
|
13
24
|
import { appendEditRecord, editProvenance } from '../_edits-log'
|
|
14
25
|
|
|
15
26
|
const generate = (generateModule as any).default || generateModule
|
|
16
27
|
const traverse = (traverseModule as any).default || traverseModule
|
|
17
28
|
|
|
18
|
-
const oxfmtOptions = {
|
|
19
|
-
trailingComma: 'all',
|
|
20
|
-
tabWidth: 2,
|
|
21
|
-
semi: false,
|
|
22
|
-
singleQuote: true,
|
|
23
|
-
printWidth: 100,
|
|
24
|
-
} as const
|
|
25
|
-
|
|
26
29
|
const entryKinds = {
|
|
27
30
|
'bricks.ts': {
|
|
28
31
|
kind: 'brick',
|
|
@@ -129,16 +132,6 @@ class EntryEditingError extends Error {
|
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
133
|
-
Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
134
|
-
|
|
135
|
-
const isIdentifierName = (value: string) => /^[$A-Z_a-z][$\w]*$/.test(value)
|
|
136
|
-
|
|
137
|
-
const normalizeRelPath = (file: string) => file.replace(/\\/g, '/').replace(/^\.\/+/, '')
|
|
138
|
-
|
|
139
|
-
const projectRelativePath = (projectDir: string, absPath: string) =>
|
|
140
|
-
normalizeRelPath(path.relative(projectDir, absPath))
|
|
141
|
-
|
|
142
135
|
const resolveProjectPath = (projectDir: string, file: string) => {
|
|
143
136
|
if (path.isAbsolute(file)) {
|
|
144
137
|
throw new EntryEditingError('invalid_file', 'File must be project-relative', { file })
|
|
@@ -195,44 +188,6 @@ const readParsedFile = async (projectDir: string, absPath: string): Promise<Pars
|
|
|
195
188
|
}
|
|
196
189
|
}
|
|
197
190
|
|
|
198
|
-
const getPropertyKeyName = (key: t.Expression | t.PrivateName) => {
|
|
199
|
-
if (t.isIdentifier(key)) return key.name
|
|
200
|
-
if (t.isStringLiteral(key) || t.isNumericLiteral(key)) return String(key.value)
|
|
201
|
-
return null
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const makeObjectKey = (key: string) =>
|
|
205
|
-
isIdentifierName(key) ? t.identifier(key) : t.stringLiteral(key)
|
|
206
|
-
|
|
207
|
-
const getObjectProperty = (object: t.ObjectExpression, key: string) =>
|
|
208
|
-
object.properties.find((property): property is t.ObjectProperty => {
|
|
209
|
-
if (!t.isObjectProperty(property)) return false
|
|
210
|
-
return getPropertyKeyName(property.key) === key
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
const getStringProperty = (object: t.ObjectExpression, key: string) => {
|
|
214
|
-
const property = getObjectProperty(object, key)
|
|
215
|
-
if (!property || !t.isStringLiteral(property.value)) return undefined
|
|
216
|
-
return property.value.value
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const setObjectProperty = (object: t.ObjectExpression, key: string, value: t.Expression) => {
|
|
220
|
-
const existing = getObjectProperty(object, key)
|
|
221
|
-
if (existing) {
|
|
222
|
-
existing.value = value
|
|
223
|
-
return
|
|
224
|
-
}
|
|
225
|
-
object.properties.push(t.objectProperty(makeObjectKey(key), value))
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const removeObjectProperty = (object: t.ObjectExpression, key: string) => {
|
|
229
|
-
const index = object.properties.findIndex((property) => {
|
|
230
|
-
if (!t.isObjectProperty(property)) return false
|
|
231
|
-
return getPropertyKeyName(property.key) === key
|
|
232
|
-
})
|
|
233
|
-
if (index >= 0) object.properties.splice(index, 1)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
191
|
const getExportEntries = (ast: t.File): ExportEntry[] =>
|
|
237
192
|
ast.program.body.flatMap((statement) => {
|
|
238
193
|
if (!t.isExportNamedDeclaration(statement)) return []
|
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())
|