@fugood/bricks-ctor 2.25.0-beta.47 → 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/compile/__tests__/index.test.js +24 -2
- package/compile/index.ts +7 -2
- package/package.json +3 -3
- package/skills/bricks-ctor/references/verification-toolchain.md +19 -0
- 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
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
jest.mock('../../tools/_shell', () => ({
|
|
2
|
-
sh: jest.fn(
|
|
2
|
+
sh: jest.fn(),
|
|
3
3
|
}))
|
|
4
4
|
|
|
5
|
+
// Mirrors the chainable `sh` result (supports `.nothrow()` like the real helper).
|
|
6
|
+
const shResult = (over = {}) => {
|
|
7
|
+
const result = Promise.resolve({ exitCode: 0, stdout: '', stderr: '', ...over })
|
|
8
|
+
result.nothrow = () => result
|
|
9
|
+
return result
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
6
13
|
import os from 'node:os'
|
|
7
14
|
import path from 'node:path'
|
|
@@ -57,7 +64,8 @@ const commandOf = ([strings, ...values]) =>
|
|
|
57
64
|
|
|
58
65
|
describe('checkConfig', () => {
|
|
59
66
|
beforeEach(() => {
|
|
60
|
-
sh.
|
|
67
|
+
sh.mockReset()
|
|
68
|
+
sh.mockImplementation(() => shResult())
|
|
61
69
|
})
|
|
62
70
|
|
|
63
71
|
test('runs doctor after check-config', async () => {
|
|
@@ -68,6 +76,20 @@ describe('checkConfig', () => {
|
|
|
68
76
|
'bricks app doctor --validate-automation .bricks/build/application-config.json',
|
|
69
77
|
])
|
|
70
78
|
})
|
|
79
|
+
|
|
80
|
+
test('skips doctor when the CLI lacks the command', async () => {
|
|
81
|
+
sh.mockReturnValueOnce(shResult()) // check-config
|
|
82
|
+
sh.mockReturnValueOnce(shResult({ exitCode: 1, stderr: "error: unknown command 'doctor'" }))
|
|
83
|
+
|
|
84
|
+
await expect(checkConfig('config.json')).resolves.toBeUndefined()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('throws when doctor reports config errors', async () => {
|
|
88
|
+
sh.mockReturnValueOnce(shResult()) // check-config
|
|
89
|
+
sh.mockReturnValueOnce(shResult({ exitCode: 1, stderr: 'DATA_RACE: conflicting writes' }))
|
|
90
|
+
|
|
91
|
+
await expect(checkConfig('config.json')).rejects.toThrow()
|
|
92
|
+
})
|
|
71
93
|
})
|
|
72
94
|
|
|
73
95
|
describe('compile animations', () => {
|
package/compile/index.ts
CHANGED
|
@@ -1494,6 +1494,11 @@ export const checkConfig = async (configPath: string) => {
|
|
|
1494
1494
|
// which catches agent-authored automations that reference deleted bricks.
|
|
1495
1495
|
await sh`bricks app check-config --validate-automation ${configPath}`
|
|
1496
1496
|
// Doctor adds semantic lint checks after structural validation. Warnings are
|
|
1497
|
-
// surfaced in the compile log, but only errors fail by default.
|
|
1498
|
-
|
|
1497
|
+
// surfaced in the compile log, but only errors fail by default. Older published
|
|
1498
|
+
// bricks-cli builds lack `app doctor` — skip rather than fail the compile.
|
|
1499
|
+
const doctor = await sh`bricks app doctor --validate-automation ${configPath}`.nothrow()
|
|
1500
|
+
if (doctor.exitCode !== 0) {
|
|
1501
|
+
if (/unknown command/i.test(doctor.stderr?.toString() ?? '')) return
|
|
1502
|
+
throw new Error(`bricks app doctor failed with exit ${doctor.exitCode}`)
|
|
1503
|
+
}
|
|
1499
1504
|
}
|
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
|
}
|
|
@@ -122,6 +122,25 @@ Once DevTools is on, ask the user how they want to drive the verification — Ch
|
|
|
122
122
|
|
|
123
123
|
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.
|
|
124
124
|
|
|
125
|
+
### Running real-device Automations from an agent
|
|
126
|
+
|
|
127
|
+
There is no `bricks devtools automation` subcommand. Use the DevTools runtime helpers exposed inside the app:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "Object.getOwnPropertyNames(automation).sort()" -j
|
|
131
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "automation.list()" -j
|
|
132
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "automation.run('<TEST_id>', { updateScreenshot: false })" --await -j
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`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:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
bricks devtools runtime eval -a <ip> -p 19851 --passcode <pc> "JSON.stringify({ result: system.data.property('<S_xxxx>', '<resultAlias>')?.value })" -j
|
|
139
|
+
bricks devtools screenshot -a <ip> -p 19851 --passcode <pc> -o /tmp/device-automation.png
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
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.
|
|
143
|
+
|
|
125
144
|
### Real-device side-effects warning
|
|
126
145
|
|
|
127
146
|
Real devices fire real peripherals. Payment terminals charge. MQTT broadcasts on shared topics. BLE advertises to bystanders. Use a **staging** device for verification cycles; never iterate on a production-deployed device unless the user explicitly approves.
|
|
@@ -0,0 +1,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())
|