@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fugood/bricks-ctor",
3
- "version": "2.25.0-beta.48",
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.48",
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": "6403fb9aece6b9ee45a72a963ddc75aec5ae3e04"
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
- try {
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
- try {
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())
@@ -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
- try {
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())