@fugood/bricks-project 2.24.0 → 2.24.1-beta.0

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 CHANGED
@@ -1274,6 +1274,6 @@ export const compile = async (app: Application) => {
1274
1274
  }
1275
1275
 
1276
1276
  export const checkConfig = async (configPath: string) => {
1277
- const { $ } = await import('bun')
1278
- await $`bricks app check-config ${configPath}`
1277
+ const { sh } = await import('../tools/_shell')
1278
+ await sh`bricks app check-config ${configPath}`
1279
1279
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fugood/bricks-project",
3
- "version": "2.24.0",
3
+ "version": "2.24.1-beta.0",
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.0",
10
+ "@fugood/bricks-cli": "^2.24.1-beta.0",
11
11
  "@huggingface/gguf": "^0.3.2",
12
12
  "@iarna/toml": "^3.0.0",
13
13
  "@modelcontextprotocol/sdk": "^1.15.0",
@@ -18,6 +18,7 @@
18
18
  "acorn": "^8.13.0",
19
19
  "escodegen": "2.1.0",
20
20
  "fuse.js": "^7.0.0",
21
+ "json5": "^2.0.1",
21
22
  "lodash": "^4.17.4",
22
23
  "uuid": "^8.3.1"
23
24
  },
package/package.json.bak CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fugood/bricks-ctor",
3
- "version": "2.24.0",
3
+ "version": "2.24.1-beta.0",
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.0",
10
+ "@fugood/bricks-cli": "^2.24.1-beta.0",
11
11
  "@huggingface/gguf": "^0.3.2",
12
12
  "@iarna/toml": "^3.0.0",
13
13
  "@modelcontextprotocol/sdk": "^1.15.0",
@@ -18,6 +18,7 @@
18
18
  "acorn": "^8.13.0",
19
19
  "escodegen": "2.1.0",
20
20
  "fuse.js": "^7.0.0",
21
+ "json5": "^2.0.1",
21
22
  "lodash": "^4.17.4",
22
23
  "uuid": "^8.3.1"
23
24
  },
@@ -27,7 +27,8 @@ const calculation: DataCalculationScript = {
27
27
  }).format(price)
28
28
  `,
29
29
  // Or load from file (preferred for longer scripts)
30
- // code: await Bun.file(`${import.meta.dir}/format-price.sandbox.js`).text(),
30
+ // import { readFile } from 'node:fs/promises'
31
+ // code: await readFile(new URL('./format-price.sandbox.js', import.meta.url), 'utf8'),
31
32
  inputs: [
32
33
  { key: 'price', data: () => priceData, trigger: true },
33
34
  { key: 'currency', data: () => currencyData, trigger: false },
@@ -0,0 +1,29 @@
1
+ import { sh } from './_shell'
2
+
3
+ const CTOR_NAME = 'CTOR'
4
+ const CTOR_EMAIL = 'ctor@bricks.tools'
5
+ const CTOR_COAUTHOR_TRAILER = `Co-authored-by: ${CTOR_NAME} <${CTOR_EMAIL}>`
6
+
7
+ async function hasGitIdentity(cwd: string): Promise<boolean> {
8
+ const out = (await sh`cd ${cwd} && git config user.email`.quiet().nothrow().text()).trim()
9
+ return !!out
10
+ }
11
+
12
+ // Build `git ...` args for a commit attributed to CTOR.
13
+ // If the machine has a git identity configured (any config level), the user
14
+ // remains the main author and CTOR is added via a Co-authored-by trailer.
15
+ // If no identity is configured (typical for non-tech users), CTOR becomes the
16
+ // main author via per-command `-c user.name/-c user.email` overrides so the
17
+ // user's git config is left untouched.
18
+ export async function buildCommitArgs(cwd: string, messages: string[]): Promise<string[]> {
19
+ const hasIdentity = await hasGitIdentity(cwd)
20
+ const prefix = hasIdentity
21
+ ? []
22
+ : ['-c', `user.name=${CTOR_NAME}`, '-c', `user.email=${CTOR_EMAIL}`]
23
+ const msgArgs: string[] = []
24
+ for (const m of messages) msgArgs.push('-m', m)
25
+ if (hasIdentity) msgArgs.push('-m', CTOR_COAUTHOR_TRAILER)
26
+ return [...prefix, 'commit', ...msgArgs]
27
+ }
28
+
29
+ export { CTOR_NAME, CTOR_EMAIL, CTOR_COAUTHOR_TRAILER }
@@ -0,0 +1,173 @@
1
+ import { spawn, type StdioOptions } from 'node:child_process'
2
+
3
+ // Template-literal shell helper replacing `$` from 'bun'. Only covers the
4
+ // ergonomics actually used in this package (see call sites in tools/*.ts).
5
+ //
6
+ // Usage:
7
+ // await sh`git status` // inherit stdio, throw on nonzero
8
+ // await sh`cd ${cwd} && git status`.quiet().nothrow()
9
+ // const out = await sh`git rev-parse HEAD`.text()
10
+ // await sh`bunx bricks ${args}`.cwd(dir).text()
11
+ //
12
+ // Notes:
13
+ // - Interpolated values are treated as single argv tokens (arrays expand to
14
+ // multiple tokens). Whitespace inside interpolated strings is preserved.
15
+ // - A leading `cd <path> && <cmd...>` in the literal is lifted into the
16
+ // subprocess cwd — no shell is spawned, so no quoting concerns.
17
+ // - Commands that chain with `&&` beyond the `cd` prefix aren't supported;
18
+ // split them into multiple `sh` awaits at the call site.
19
+
20
+ export interface ShResult {
21
+ exitCode: number
22
+ stdout: Buffer
23
+ stderr: Buffer
24
+ }
25
+
26
+ type Mode = 'default' | 'text' | 'quiet'
27
+
28
+ class Sh implements PromiseLike<ShResult> {
29
+ private _cwd: string | undefined
30
+ private _mode: Mode = 'default'
31
+ private _throw = true
32
+ private _promise: Promise<ShResult> | null = null
33
+
34
+ constructor(private readonly args: string[]) {}
35
+
36
+ cwd(dir: string): this {
37
+ this._cwd = dir
38
+ return this
39
+ }
40
+
41
+ quiet(): this {
42
+ this._mode = 'quiet'
43
+ return this
44
+ }
45
+
46
+ nothrow(): this {
47
+ this._throw = false
48
+ return this
49
+ }
50
+
51
+ async text(): Promise<string> {
52
+ // If no explicit quiet was requested, capture stdout/stderr without
53
+ // forwarding them to the parent's streams (matches Bun's `.text()`).
54
+ if (this._mode === 'default') this._mode = 'text'
55
+ const r = await this.run()
56
+ return r.stdout.toString('utf8')
57
+ }
58
+
59
+ then<TResult1 = ShResult, TResult2 = never>(
60
+ onfulfilled?: ((value: ShResult) => TResult1 | PromiseLike<TResult1>) | null,
61
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
62
+ ): Promise<TResult1 | TResult2> {
63
+ return this.run().then(onfulfilled, onrejected)
64
+ }
65
+
66
+ private run(): Promise<ShResult> {
67
+ if (!this._promise) this._promise = this.exec()
68
+ return this._promise
69
+ }
70
+
71
+ private async exec(): Promise<ShResult> {
72
+ const [cmd, ...rest] = this.args
73
+ if (!cmd) throw new Error('sh: empty command')
74
+
75
+ const stdio: StdioOptions =
76
+ this._mode === 'default'
77
+ ? ['inherit', 'pipe', 'pipe']
78
+ : this._mode === 'quiet'
79
+ ? ['ignore', 'pipe', 'pipe']
80
+ : ['inherit', 'pipe', 'pipe']
81
+
82
+ const proc = spawn(cmd, rest, { cwd: this._cwd, stdio })
83
+
84
+ const outChunks: Buffer[] = []
85
+ const errChunks: Buffer[] = []
86
+ proc.stdout?.on('data', (c: Buffer) => {
87
+ outChunks.push(c)
88
+ if (this._mode === 'default') process.stdout.write(c)
89
+ })
90
+ proc.stderr?.on('data', (c: Buffer) => {
91
+ errChunks.push(c)
92
+ if (this._mode === 'default') process.stderr.write(c)
93
+ })
94
+
95
+ const exitCode: number = await new Promise((resolve, reject) => {
96
+ proc.on('error', reject)
97
+ proc.on('close', (code) => resolve(code ?? 0))
98
+ })
99
+
100
+ const stdout = Buffer.concat(outChunks)
101
+ const stderr = Buffer.concat(errChunks)
102
+ const result: ShResult = { exitCode, stdout, stderr }
103
+
104
+ if (exitCode !== 0 && this._throw) {
105
+ const err: any = new Error(`sh: command failed with exit ${exitCode}: ${this.args.join(' ')}`)
106
+ err.exitCode = exitCode
107
+ err.stdout = stdout
108
+ err.stderr = stderr
109
+ throw err
110
+ }
111
+
112
+ return result
113
+ }
114
+ }
115
+
116
+ const WHITESPACE = /\s/
117
+
118
+ function tokenize(
119
+ strings: TemplateStringsArray,
120
+ values: unknown[],
121
+ ): { tokens: string[]; initialCwd?: string } {
122
+ const tokens: string[] = []
123
+ let current = ''
124
+ let inToken = false
125
+
126
+ const commit = () => {
127
+ if (inToken) {
128
+ tokens.push(current)
129
+ current = ''
130
+ inToken = false
131
+ }
132
+ }
133
+
134
+ const append = (s: string) => {
135
+ current += s
136
+ inToken = true
137
+ }
138
+
139
+ const appendValue = (val: unknown) => {
140
+ if (val == null) return
141
+ if (Array.isArray(val)) {
142
+ val.forEach((item, i) => {
143
+ if (i > 0) commit()
144
+ append(String(item))
145
+ })
146
+ return
147
+ }
148
+ append(String(val))
149
+ }
150
+
151
+ strings.forEach((literal, i) => {
152
+ for (let c = 0; c < literal.length; c++) {
153
+ const ch = literal[c]
154
+ if (WHITESPACE.test(ch)) commit()
155
+ else append(ch)
156
+ }
157
+ if (i < values.length) appendValue(values[i])
158
+ })
159
+ commit()
160
+
161
+ // Lift `cd <path> && <cmd...>` prefix into subprocess cwd
162
+ if (tokens.length >= 3 && tokens[0] === 'cd' && tokens[2] === '&&') {
163
+ return { tokens: tokens.slice(3), initialCwd: tokens[1] }
164
+ }
165
+ return { tokens }
166
+ }
167
+
168
+ export function sh(strings: TemplateStringsArray, ...values: unknown[]): Sh {
169
+ const { tokens, initialCwd } = tokenize(strings, values)
170
+ const s = new Sh(tokens)
171
+ if (initialCwd) s.cwd(initialCwd)
172
+ return s
173
+ }
package/tools/deploy.ts CHANGED
@@ -1,8 +1,21 @@
1
- import { $ } from 'bun'
1
+ import { access, readFile, writeFile } from 'node:fs/promises'
2
2
  import { parseArgs } from 'util'
3
+ import { sh } from './_shell'
4
+ import { buildCommitArgs } from './_git-author'
3
5
 
4
6
  const cwd = process.cwd()
5
7
 
8
+ const exists = async (p: string) => {
9
+ try {
10
+ await access(p)
11
+ return true
12
+ } catch {
13
+ return false
14
+ }
15
+ }
16
+
17
+ const readJson = async (p: string) => JSON.parse(await readFile(p, 'utf8'))
18
+
6
19
  const {
7
20
  values: {
8
21
  changelogs: changelogsArg,
@@ -14,7 +27,7 @@ const {
14
27
  help,
15
28
  },
16
29
  } = parseArgs({
17
- args: Bun.argv.slice(2),
30
+ args: process.argv.slice(2),
18
31
  options: {
19
32
  changelogs: { type: 'string' },
20
33
  'changelogs-file': { type: 'string' },
@@ -40,7 +53,7 @@ if (help) {
40
53
  }
41
54
 
42
55
  // Check git status
43
- const { exitCode } = await $`cd ${cwd} && git status`.quiet().nothrow()
56
+ const { exitCode } = await sh`cd ${cwd} && git status`.quiet().nothrow()
44
57
  const isGitRepo = exitCode === 0
45
58
 
46
59
  if (!isGitRepo && !yes) {
@@ -49,30 +62,30 @@ if (!isGitRepo && !yes) {
49
62
  }
50
63
 
51
64
  // Read application.json
52
- const app = await Bun.file(`${cwd}/application.json`).json()
53
- const config = await Bun.file(`${cwd}/.bricks/build/application-config.json`).json()
65
+ const app = await readJson(`${cwd}/application.json`)
66
+ const config = await readJson(`${cwd}/.bricks/build/application-config.json`)
54
67
 
55
68
  // Resolve version: explicit flag > auto-bump > package.json
56
- const pkgFile = Bun.file(`${cwd}/package.json`)
57
- const pkgExists = await pkgFile.exists()
69
+ const pkgPath = `${cwd}/package.json`
70
+ const pkgExists = await exists(pkgPath)
58
71
  let version: string | undefined
59
72
 
60
73
  if (versionArg) {
61
74
  version = versionArg
62
75
  if (pkgExists) {
63
- const pkg = await pkgFile.json()
76
+ const pkg = await readJson(pkgPath)
64
77
  pkg.version = version
65
- await Bun.write(`${cwd}/package.json`, JSON.stringify(pkg, null, 2) + '\n')
78
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
66
79
  }
67
80
  } else if (autoVersion && pkgExists) {
68
- const pkg = await pkgFile.json()
81
+ const pkg = await readJson(pkgPath)
69
82
  const parts = (pkg.version || '0.0.0').split('.')
70
83
  parts[2] = String(Number(parts[2] || 0) + 1)
71
84
  version = parts.join('.')
72
85
  pkg.version = version
73
- await Bun.write(`${cwd}/package.json`, JSON.stringify(pkg, null, 2) + '\n')
86
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
74
87
  } else {
75
- version = pkgExists ? (await pkgFile.json()).version : undefined
88
+ version = pkgExists ? (await readJson(pkgPath)).version : undefined
76
89
  }
77
90
 
78
91
  // Get changelog from flag or file
@@ -80,11 +93,10 @@ let changelogs = ''
80
93
  if (changelogsArg) {
81
94
  changelogs = changelogsArg
82
95
  } else if (changelogsFile) {
83
- const file = Bun.file(changelogsFile)
84
- if (!(await file.exists())) {
96
+ if (!(await exists(changelogsFile))) {
85
97
  throw new Error(`Changelogs file not found: ${changelogsFile}`)
86
98
  }
87
- changelogs = await file.text()
99
+ changelogs = await readFile(changelogsFile, 'utf8')
88
100
  } else if (!yes) {
89
101
  changelogs = prompt('Enter changelogs (optional, press Enter to skip):') || ''
90
102
  }
@@ -92,19 +104,21 @@ if (changelogsArg) {
92
104
  // Handle unstaged changes
93
105
  let commitId = ''
94
106
  if (isGitRepo) {
95
- const unstagedChanges = await $`cd ${cwd} && git diff --name-only --diff-filter=ACMR`.text()
107
+ const unstagedChanges = await sh`cd ${cwd} && git diff --name-only --diff-filter=ACMR`.text()
96
108
  if (unstagedChanges) {
97
109
  if (autoCommit) {
98
110
  const commitMsg = `chore: release ${version || 'new version'}`
99
111
  const commitBody = changelogs || '[no changelogs]'
100
- await $`cd ${cwd} && git add -A && git commit -m ${commitMsg} -m ${commitBody}`
112
+ await sh`cd ${cwd} && git add -A`
113
+ const commitArgs = await buildCommitArgs(cwd, [commitMsg, commitBody])
114
+ await sh`cd ${cwd} && git ${commitArgs}`
101
115
  } else {
102
116
  throw new Error(
103
117
  'Unstaged changes found, please commit or stash your changes before deploying',
104
118
  )
105
119
  }
106
120
  }
107
- commitId = (await $`cd ${cwd} && git rev-parse HEAD`.text()).trim()
121
+ commitId = (await sh`cd ${cwd} && git rev-parse HEAD`.text()).trim()
108
122
  }
109
123
 
110
124
  // Ask for confirmation
@@ -123,7 +137,7 @@ const releaseConfig = {
123
137
  bricks_project_last_commit_id: commitId || undefined,
124
138
  }
125
139
  const configPath = `${cwd}/.bricks/build/release-config.json`
126
- await Bun.write(configPath, JSON.stringify(releaseConfig))
140
+ await writeFile(configPath, JSON.stringify(releaseConfig))
127
141
 
128
142
  const args = ['bricks', command, 'release', app.id, '-c', configPath, '--json']
129
143
 
@@ -135,7 +149,7 @@ if (changelogs) {
135
149
  args.push('--changelogs', changelogs)
136
150
  }
137
151
 
138
- const result = await $`${args}`.quiet().nothrow()
152
+ const result = await sh`${args}`.quiet().nothrow()
139
153
 
140
154
  if (result.exitCode !== 0) {
141
155
  const output = result.stderr.toString() || result.stdout.toString()
@@ -1,6 +1,7 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { readFile } from 'node:fs/promises'
2
3
  import { z } from 'zod'
3
- import { $ } from 'bun'
4
+ import { sh } from '../_shell'
4
5
 
5
6
  export function register(server: McpServer, projectDir: string) {
6
7
  const { dirname } = import.meta
@@ -9,7 +10,7 @@ export function register(server: McpServer, projectDir: string) {
9
10
  let log = ''
10
11
  try {
11
12
  log += 'Type checking & Compiling...'
12
- log += await $`bun compile`.cwd(projectDir).text()
13
+ log += await sh`bun compile`.cwd(projectDir).text()
13
14
  } catch (err) {
14
15
  log += `${err.stdout.toString()}\n${err.stderr.toString()}`
15
16
  }
@@ -60,7 +61,7 @@ export function register(server: McpServer, projectDir: string) {
60
61
  ]
61
62
  if (testId) args.push('--test-id', testId)
62
63
  if (testTitleLike) args.push('--test-title-like', testTitleLike)
63
- log = await $`bunx --bun electron ${toolsDir}/preview-main.mjs ${args}`
64
+ log = await sh`bunx --bun electron ${toolsDir}/preview-main.mjs ${args}`
64
65
  .cwd(projectDir)
65
66
  .text()
66
67
  } catch (err) {
@@ -70,8 +71,8 @@ export function register(server: McpServer, projectDir: string) {
70
71
  let screenshotBase64: string | null = null
71
72
  if (!error && responseImage) {
72
73
  const toolsDir = `${dirname}/..`
73
- const screenshot = await Bun.file(`${toolsDir}/screenshot.jpg`).arrayBuffer()
74
- screenshotBase64 = Buffer.from(screenshot).toString('base64')
74
+ const screenshot = await readFile(`${toolsDir}/screenshot.jpg`)
75
+ screenshotBase64 = screenshot.toString('base64')
75
76
  }
76
77
  const content: Array<
77
78
  { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  import { z } from 'zod'
3
- import { JSON5 } from 'bun'
3
+ import JSON5 from 'json5'
4
4
  import * as TOON from '@toon-format/toon'
5
5
  import { gguf } from '@huggingface/gguf'
6
6
 
@@ -1,10 +1,10 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  import { z } from 'zod'
3
- import { $ } from 'bun'
3
+ import { sh } from '../_shell'
4
4
 
5
5
  const runBricks = async (projectDir: string, ...args: string[]) => {
6
6
  try {
7
- return await $`bunx bricks ${args}`.cwd(projectDir).text()
7
+ return await sh`bunx bricks ${args}`.cwd(projectDir).text()
8
8
  } catch (err: any) {
9
9
  throw new Error(err.stderr?.toString() || err.message)
10
10
  }
@@ -1,4 +1,3 @@
1
- import { $ } from 'bun'
2
1
  import {
3
2
  cp,
4
3
  lstat,
@@ -53,8 +52,15 @@ if (skipCopyProject) {
53
52
  } else {
54
53
  const libFiles = ['types', 'utils', 'index.ts']
55
54
 
56
- await $`mkdir -p ${cwd}/ctor`
57
- await Promise.all(libFiles.map((file) => $`cp -r ${__dirname}/../${file} ${cwd}/ctor`))
55
+ const ctorDir = path.join(cwd, 'ctor')
56
+ await mkdir(ctorDir, { recursive: true })
57
+ await Promise.all(
58
+ libFiles.map((file) =>
59
+ cp(path.join(import.meta.dirname, '..', file), path.join(ctorDir, file), {
60
+ recursive: true,
61
+ }),
62
+ ),
63
+ )
58
64
  console.log('Copied files to ctor/')
59
65
  }
60
66
 
@@ -175,7 +181,7 @@ const ensureCompatibilitySkillLink = async (linkPath: string, targetDir: string)
175
181
  }
176
182
 
177
183
  const setupSkills = async () => {
178
- const packageSkillsDir = `${__dirname}/../skills`
184
+ const packageSkillsDir = path.join(import.meta.dirname, '..', 'skills')
179
185
  await mkdir(projectSkillsDir, { recursive: true })
180
186
  await copyMissingSkills(packageSkillsDir, projectSkillsDir)
181
187
 
package/tools/preview.ts CHANGED
@@ -1,11 +1,14 @@
1
- import { $ } from 'bun'
1
+ import { spawn } from 'node:child_process'
2
+ import { readFile, writeFile } from 'node:fs/promises'
2
3
  import { watch, unlinkSync } from 'fs'
3
4
  import type { FSWatcher } from 'fs'
5
+ import { createInterface } from 'node:readline'
4
6
  import { parseArgs } from 'util'
5
7
  import debounce from 'lodash/debounce'
8
+ import { sh } from './_shell'
6
9
 
7
10
  const { values } = parseArgs({
8
- args: Bun.argv,
11
+ args: process.argv,
9
12
  options: {
10
13
  'skip-typecheck': { type: 'boolean' },
11
14
  'clear-cache': { type: 'boolean' },
@@ -28,7 +31,7 @@ const { values } = parseArgs({
28
31
 
29
32
  const cwd = process.cwd()
30
33
 
31
- const app = await Bun.file(`${cwd}/application.json`).json()
34
+ const app = JSON.parse(await readFile(`${cwd}/application.json`, 'utf8'))
32
35
 
33
36
  const args: string[] = []
34
37
  if (values['clear-cache']) args.push('--clear-cache')
@@ -72,8 +75,8 @@ if (!values['no-cdp']) {
72
75
  const useTypecheck = !values['skip-typecheck']
73
76
 
74
77
  const compile = async () => {
75
- if (useTypecheck) await $`bun typecheck`
76
- await $`bun compile.ts`
78
+ if (useTypecheck) await sh`bun typecheck`
79
+ await sh`bun compile.ts`
77
80
  }
78
81
 
79
82
  await compile()
@@ -97,7 +100,7 @@ const cleanupDevtoolsInfo = () => {
97
100
 
98
101
  // Kill existing preview process if devtools.json contains a stale pid
99
102
  try {
100
- const devtoolsInfo = await Bun.file(devtoolsInfoPath).json()
103
+ const devtoolsInfo = JSON.parse(await readFile(devtoolsInfoPath, 'utf8'))
101
104
  if (devtoolsInfo.pid) {
102
105
  try {
103
106
  process.kill(devtoolsInfo.pid)
@@ -106,49 +109,42 @@ try {
106
109
  }
107
110
  } catch {}
108
111
 
109
- const proc = Bun.spawn(['bunx', '--bun', 'electron', `${__dirname}/preview-main.mjs`, ...args], {
110
- env: { ...process.env, BRICKS_STAGE: app.stage || 'production' },
111
- stdout: 'pipe',
112
- stderr: 'inherit',
113
- })
112
+ const proc = spawn(
113
+ 'bunx',
114
+ ['--bun', 'electron', `${import.meta.dirname}/preview-main.mjs`, ...args],
115
+ {
116
+ env: { ...process.env, BRICKS_STAGE: app.stage || 'production' },
117
+ stdio: ['inherit', 'pipe', 'inherit'],
118
+ },
119
+ )
114
120
 
115
- const reader = proc.stdout.getReader()
116
- const decoder = new TextDecoder()
117
-
118
- const processOutput = async () => {
119
- let done = false
120
- while (!done) {
121
- // eslint-disable-next-line no-await-in-loop
122
- const result = await reader.read()
123
- ;({ done } = result)
124
- if (!done) {
125
- const text = decoder.decode(result.value)
126
- text.split('\n').forEach((line) => {
127
- if (line.startsWith('[TEST_RESULT_TOON]')) {
128
- const toonData = line.replace('[TEST_RESULT_TOON]', '')
129
- console.log(toonData)
130
- } else if (line) {
131
- // Detect CDP server startup from preview-main output
132
- const cdpMatch = line.match(/^CDP server: ws:\/\/localhost:(\d+)/)
133
- if (cdpMatch) {
134
- const info = {
135
- port: parseInt(cdpMatch[1], 10),
136
- pid: proc.pid,
137
- address: 'localhost',
138
- name: `${app.name || 'Unnamed'} (CTOR Preview)`,
139
- startedAt: new Date().toISOString(),
140
- }
141
- Bun.write(devtoolsInfoPath, JSON.stringify(info, null, 2))
142
- }
143
- console.log(line)
144
- }
145
- })
121
+ const rl = createInterface({ input: proc.stdout! })
122
+
123
+ rl.on('line', (line) => {
124
+ if (line.startsWith('[TEST_RESULT_TOON]')) {
125
+ const toonData = line.replace('[TEST_RESULT_TOON]', '')
126
+ console.log(toonData)
127
+ return
128
+ }
129
+ if (!line) return
130
+ // Detect CDP server startup from preview-main output
131
+ const cdpMatch = line.match(/^CDP server: ws:\/\/localhost:(\d+)/)
132
+ if (cdpMatch) {
133
+ const info = {
134
+ port: parseInt(cdpMatch[1], 10),
135
+ pid: proc.pid,
136
+ address: 'localhost',
137
+ name: `${app.name || 'Unnamed'} (CTOR Preview)`,
138
+ startedAt: new Date().toISOString(),
146
139
  }
140
+ writeFile(devtoolsInfoPath, JSON.stringify(info, null, 2))
147
141
  }
148
- }
142
+ console.log(line)
143
+ })
149
144
 
150
- await processOutput()
151
- await proc.exited
145
+ await new Promise<void>((resolve) => {
146
+ proc.on('close', () => resolve())
147
+ })
152
148
 
153
149
  cleanupDevtoolsInfo()
154
150
  if (watcher) watcher.close()
package/tools/pull.ts CHANGED
@@ -1,21 +1,26 @@
1
- import { $ } from 'bun'
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
2
  import { format } from 'oxfmt'
3
+ import { sh } from './_shell'
4
+ import { buildCommitArgs } from './_git-author'
3
5
 
4
6
  const cwd = process.cwd()
5
7
  const args = process.argv.slice(2)
6
8
  const force = args.includes('--force') || args.includes('-f')
7
9
 
8
10
  // Check git status
9
- const { exitCode } = await $`cd ${cwd} && git status`.nothrow()
11
+ const { exitCode } = await sh`cd ${cwd} && git status`.nothrow()
10
12
  const isGitRepo = exitCode === 0
11
13
 
12
14
  if (isGitRepo) {
13
- const unstagedChanges = await $`cd ${cwd} && git diff --name-only --diff-filter=ACMR`.text()
15
+ const unstagedChanges = await sh`cd ${cwd} && git diff --name-only --diff-filter=ACMR`.text()
14
16
  if (unstagedChanges) {
15
17
  if (force) {
16
18
  console.log('Force mode: committing unstaged changes before pull...')
17
- await $`cd ${cwd} && git add .`
18
- await $`cd ${cwd} && git commit -m ${'chore(force-pull): saved unstaged changes before pull'}`
19
+ await sh`cd ${cwd} && git add .`
20
+ const preCommitArgs = await buildCommitArgs(cwd, [
21
+ 'chore(force-pull): saved unstaged changes before pull',
22
+ ])
23
+ await sh`cd ${cwd} && git ${preCommitArgs}`
19
24
  } else {
20
25
  throw new Error('Unstaged changes found, please commit or stash your changes before pulling')
21
26
  }
@@ -28,14 +33,14 @@ if (isGitRepo) {
28
33
  }
29
34
 
30
35
  // Read application.json
31
- const app = await Bun.file(`${cwd}/application.json`).json()
36
+ const app = JSON.parse(await readFile(`${cwd}/application.json`, 'utf8'))
32
37
 
33
38
  const isModule = app.type === 'module'
34
39
  const command = isModule ? 'module' : 'app'
35
40
 
36
41
  // Fetch project files using CLI
37
42
  console.log(`Pulling ${command} project (${app.id})...`)
38
- const result = await $`bricks ${command} project-pull ${app.id} --json`.quiet().nothrow()
43
+ const result = await sh`bricks ${command} project-pull ${app.id} --json`.quiet().nothrow()
39
44
 
40
45
  if (result.exitCode !== 0) {
41
46
  const output = result.stderr.toString() || result.stdout.toString()
@@ -52,11 +57,11 @@ const { files, lastCommitId } = JSON.parse(result.stdout.toString())
52
57
  let useMain = false
53
58
  if (isGitRepo && !force) {
54
59
  console.log(`Checking commit ${lastCommitId}...`)
55
- const found = (await $`cd ${cwd} && git rev-list -1 ${lastCommitId}`.nothrow().text())
60
+ const found = (await sh`cd ${cwd} && git rev-list -1 ${lastCommitId}`.nothrow().text())
56
61
  .trim()
57
62
  .match(/^[\da-f]{40}$/)
58
63
 
59
- const commitId = (await $`cd ${cwd} && git rev-parse HEAD`.text()).trim()
64
+ const commitId = (await sh`cd ${cwd} && git rev-parse HEAD`.text()).trim()
60
65
 
61
66
  if (commitId === lastCommitId) throw new Error('Commit not changed')
62
67
 
@@ -64,18 +69,18 @@ if (isGitRepo && !force) {
64
69
  ? 'BRICKS_PROJECT_try-pull-module'
65
70
  : 'BRICKS_PROJECT_try-pull-application'
66
71
 
67
- await $`cd ${cwd} && git branch -D ${branchName}`.nothrow()
72
+ await sh`cd ${cwd} && git branch -D ${branchName}`.nothrow()
68
73
 
69
74
  if (found) {
70
- await $`cd ${cwd} && git checkout -b ${branchName} ${lastCommitId}`.nothrow()
75
+ await sh`cd ${cwd} && git checkout -b ${branchName} ${lastCommitId}`.nothrow()
71
76
  } else {
72
- await $`cd ${cwd} && git checkout -b ${branchName}`
77
+ await sh`cd ${cwd} && git checkout -b ${branchName}`
73
78
  useMain = true
74
79
  }
75
80
  }
76
81
 
77
- const oxfmtConfig = await Bun.file(`${cwd}/.oxfmtrc.json`)
78
- .json()
82
+ const oxfmtConfig = await readFile(`${cwd}/.oxfmtrc.json`, 'utf8')
83
+ .then(JSON.parse)
79
84
  .catch(() => ({
80
85
  trailingComma: 'all',
81
86
  tabWidth: 2,
@@ -91,23 +96,24 @@ await Promise.all(
91
96
  const result = await format(file.name, file.input, oxfmtConfig)
92
97
  content = result.code
93
98
  }
94
- return Bun.write(`${cwd}/${file.name}`, content)
99
+ return writeFile(`${cwd}/${file.name}`, content)
95
100
  }),
96
101
  )
97
102
 
98
103
  if (isGitRepo) {
99
- await $`cd ${cwd} && git add .`
100
- const hasChanges = !!(await $`cd ${cwd} && git diff --cached --name-only`.text()).trim()
104
+ await sh`cd ${cwd} && git add .`
105
+ const hasChanges = !!(await sh`cd ${cwd} && git diff --cached --name-only`.text()).trim()
101
106
  if (hasChanges) {
102
107
  const commitMsg = force
103
108
  ? `chore(force-pull): apply force pull-${command}`
104
109
  : isModule
105
110
  ? 'chore(project): apply file changes from BRICKS module'
106
111
  : 'chore(project): apply file changes from BRICKS application'
107
- await $`cd ${cwd} && git commit -m ${commitMsg}`
112
+ const commitArgs = await buildCommitArgs(cwd, [commitMsg])
113
+ await sh`cd ${cwd} && git ${commitArgs}`
108
114
  }
109
115
  if (!force && !useMain) {
110
- await $`cd ${cwd} && git merge main`
116
+ await sh`cd ${cwd} && git merge main`
111
117
  }
112
118
  }
113
119