@brickflow/cli 0.0.6 → 0.0.7

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.
@@ -0,0 +1,105 @@
1
+ import { exec } from 'child_process'
2
+ import fs from 'fs'
3
+ import { globSync } from 'glob'
4
+ import path from 'path'
5
+ import { fileURLToPath } from 'url'
6
+
7
+ const args = process.argv.slice(3)
8
+
9
+ if (args.includes('--help') || args.includes('-h')) {
10
+ printHelp()
11
+ process.exit(0)
12
+ }
13
+
14
+ if (args.length > 0) {
15
+ printHelp()
16
+ process.exit(1)
17
+ }
18
+
19
+ const currentDir = path.dirname(fileURLToPath(import.meta.url))
20
+ const workspaceRoot = path.resolve(currentDir, '../../../..')
21
+ const packageJsonPaths = globSync('**/package.json', {
22
+ absolute: true,
23
+ cwd: workspaceRoot,
24
+ ignore: ['**/node_modules/**'],
25
+ }).sort()
26
+
27
+ console.log('\x1b[32m', '\rPackage update')
28
+
29
+ let index = 0
30
+
31
+ for await (const packageJsonPath of packageJsonPaths) {
32
+ index += 1
33
+
34
+ const packageJson = JSON.parse(
35
+ fs.readFileSync(packageJsonPath, {
36
+ encoding: 'utf-8',
37
+ }),
38
+ )
39
+
40
+ await updateJson(packageJson)
41
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`)
42
+
43
+ const percent = Math.round((index * 100) / packageJsonPaths.length)
44
+ process.stdout.write(`\ršŸ’Ž Updating: [${'šŸ”©'.repeat((percent * 10) / 100)}] ${percent}%`)
45
+ }
46
+
47
+ process.stdout.write('\n')
48
+ console.log('\x1b[0m', '')
49
+
50
+ function isWorkspaceVersion(value) {
51
+ return value === 'workspace:*' || value === 'workspace: *'
52
+ }
53
+
54
+ function printHelp() {
55
+ console.log(`brick latest
56
+
57
+ Usage:
58
+ brick latest
59
+
60
+ Notes:
61
+ Scans all package.json files in the repository, including the root one
62
+ Updates dependencies and devDependencies to the latest npm versions
63
+ Skips workspace:* versions`)
64
+ }
65
+
66
+ function readLatestVersion(packageName) {
67
+ return new Promise((resolve) => {
68
+ exec(`npm view ${packageName} version`, (error, stdout) => {
69
+ if (error) {
70
+ console.info(error.message)
71
+ resolve(null)
72
+ return
73
+ }
74
+
75
+ resolve(String(stdout).trim().replace('\n', ''))
76
+ })
77
+ })
78
+ }
79
+
80
+ async function updateJson(jsonData) {
81
+ await Promise.all([
82
+ ...Object.entries(jsonData.dependencies || {}).map(async ([key, value]) => {
83
+ if (isWorkspaceVersion(value)) {
84
+ return
85
+ }
86
+
87
+ const version = await readLatestVersion(key)
88
+
89
+ if (version) {
90
+ jsonData.dependencies[key] = version
91
+ }
92
+ }),
93
+ ...Object.entries(jsonData.devDependencies || {}).map(async ([key, value]) => {
94
+ if (isWorkspaceVersion(value)) {
95
+ return
96
+ }
97
+
98
+ const version = await readLatestVersion(key)
99
+
100
+ if (version) {
101
+ jsonData.devDependencies[key] = version
102
+ }
103
+ }),
104
+ ])
105
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'fs'
2
+ import { globSync } from 'glob'
3
+
4
+ const args = process.argv.slice(3)
5
+
6
+ if (args.includes('--help') || args.includes('-h')) {
7
+ printHelp()
8
+ process.exit(0)
9
+ }
10
+
11
+ if (args.length > 0) {
12
+ printHelp()
13
+ process.exit(1)
14
+ }
15
+
16
+ console.log('\x1b[32m', '\rAssets analyse')
17
+ const imgList = globSync('./**/*.{webp,svg}')
18
+
19
+ if (imgList.length === 0) {
20
+ console.log('\nNothing find!')
21
+ }
22
+
23
+ function bytesToSize(bytes, decimals = 2) {
24
+ if (!Number(bytes)) {
25
+ return '0 Bytes'
26
+ }
27
+
28
+ const kbToBytes = 1000
29
+ const dm = decimals < 0 ? 0 : decimals
30
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
31
+
32
+ const index = Math.floor(Math.log(bytes) / Math.log(kbToBytes))
33
+
34
+ return `${parseFloat((bytes / kbToBytes ** index).toFixed(dm))} ${sizes[index]}`
35
+ }
36
+
37
+ let result = ''
38
+ for (let i = 0; i < imgList.length; i++) {
39
+ const imgPath = imgList[i]
40
+ const size = fs.statSync(imgPath).size
41
+ if (size > 400000 && !/-animation.webp$/.test(imgPath)) {
42
+ result = `${result}
43
+ 🐢 ${bytesToSize(size)} šŸ—ŗļø ${imgPath}
44
+ `
45
+ }
46
+ }
47
+ if (result.length > 0) {
48
+ console.log('\x1b[0m', '')
49
+ throw result
50
+ }
51
+
52
+ console.log('\x1b[0m', '')
53
+
54
+ function printHelp() {
55
+ console.log(`brick size
56
+
57
+ Usage:
58
+ brick size
59
+
60
+ Notes:
61
+ Scans .webp and .svg files under the current working directory
62
+ Fails when a file is larger than 400000 bytes
63
+ Ignores files ending with -animation.webp`)
64
+ }
@@ -0,0 +1,97 @@
1
+ import fs from 'fs'
2
+ import { globSync } from 'glob'
3
+ import { optimize } from 'svgo'
4
+
5
+ const args = process.argv.slice(3)
6
+
7
+ if (args.includes('--help') || args.includes('-h')) {
8
+ printHelp()
9
+ process.exit(0)
10
+ }
11
+
12
+ if (args.length > 0) {
13
+ printHelp()
14
+ process.exit(1)
15
+ }
16
+
17
+ console.log('\x1b[32m', '\rSVG Optimize')
18
+ const svgList = globSync('./**/*.svg')
19
+
20
+ for (let i = 0; i < svgList.length; i++) {
21
+ const svgPath = svgList[i]
22
+ const svg = fs.readFileSync(svgPath, 'utf8')
23
+ const svgOptimized = optimize(svg, {
24
+ js2svg: {
25
+ indent: '',
26
+ pretty: true,
27
+ },
28
+ path: svgPath,
29
+ plugins: [
30
+ // "addAttributesToSVGElement", // adds attributes to an outer <svg> element
31
+ // "addClassesToSVGElement", // add classnames to an outer <svg> element
32
+ 'cleanupAttrs', // cleanup attributes from newlines, trailing, and repeating spaces Yes
33
+ 'cleanupEnableBackground', // remove or cleanup enable-background attribute when possible Yes
34
+ 'cleanupIds', // remove unused and minify used IDs Yes
35
+ 'cleanupListOfValues', // round numeric values in attributes that take a list of numbers (like viewBox or enable-background)
36
+ 'cleanupNumericValues', // round numeric values to the fixed precision, remove default px units Yes
37
+ 'collapseGroups', // collapse useless groups Yes
38
+ 'convertColors', // convert colors (from rgb() to #rrggbb, from #rrggbb to #rgb) Yes
39
+ 'convertEllipseToCircle', // convert non-eccentric <ellipse> to <circle> Yes
40
+ 'convertPathData', // convert Path data to relative or absolute (whichever is shorter), convert one segment to another, trim useless delimiters, smart rounding, and much more Yes
41
+ 'convertShapeToPath', // convert some basic shapes to <path> Yes
42
+ 'convertStyleToAttrs', // convert styles into attributes
43
+ 'convertTransform', // collapse multiple transforms into one, convert matrices to the short aliases, and much more Yes
44
+ 'inlineStyles', // move and merge styles from <style> elements to element style attributes Yes
45
+ 'mergePaths', // merge multiple Paths into one Yes
46
+ 'mergeStyles', // merge multiple style elements into one Yes
47
+ 'minifyStyles', // minify <style> elements content with CSSO Yes
48
+ 'moveElemsAttrsToGroup', // move elements' attributes to their enclosing group Yes
49
+ 'moveGroupAttrsToElems', // move some group attributes to the contained elements Yes
50
+ 'prefixIds', // prefix IDs and classes with the SVG filename or an arbitrary string
51
+ // "removeAttributesBySelector", // removes attributes of elements that match a CSS selector
52
+ // "removeAttrs", // remove attributes by pattern
53
+ 'removeComments', // remove comments Yes
54
+ 'removeDesc', // remove <desc> Yes
55
+ 'removeDimensions', // remove width/height and add viewBox if it's missing (opposite to removeViewBox, disable it first)
56
+ 'removeDoctype', // remove doctype declaration Yes
57
+ 'removeEditorsNSData', // remove editors namespaces, elements, and attributes Yes
58
+ 'removeElementsByAttr', // remove arbitrary elements by ID or className
59
+ 'removeEmptyAttrs', // remove empty attributes Yes
60
+ 'removeEmptyContainers', // remove empty Container elements Yes
61
+ 'removeEmptyText', // remove empty Text elements Yes
62
+ 'removeHiddenElems', // remove hidden elements Yes
63
+ 'removeMetadata', // remove <metadata> Yes
64
+ 'removeNonInheritableGroupAttrs', // remove non-inheritable group's "presentation" attributes Yes
65
+ 'removeOffCanvasPaths', // removes elements that are drawn outside of the viewbox
66
+ 'removeRasterImages', // remove raster images
67
+ 'removeScripts', // remove <script> elements
68
+ // "removeStyleElement", // remove <style> elements
69
+ 'removeTitle', // remove <title> Yes
70
+ 'removeUnknownsAndDefaults', // remove unknown elements content and attributes, remove attributes with default values Yes
71
+ 'removeUnusedNS', // remove unused namespaces declaration Yes
72
+ 'removeUselessDefs', // remove elements of <defs> without id Yes
73
+ 'removeUselessStrokeAndFill', // remove useless stroke and fill attributes Yes
74
+ // "removeViewBox", // remove viewBox attribute when possible Yes
75
+ // "removeXMLNS", // removes the xmlns attribute (for inline SVG)
76
+ 'removeXMLProcInst', // remove XML processing instructions Yes
77
+ 'reusePaths', // Find duplicated elements and replace them with links
78
+ 'sortAttrs', // sort element attributes for epic readability Yes
79
+ 'sortDefsChildren', // sort children of <defs> in order to improve compression Yes
80
+ ],
81
+ })
82
+ fs.writeFileSync(svgPath, svgOptimized.data)
83
+ const percent = Math.round(((i + 1) * 100) / svgList.length)
84
+ process.stdout.write(`\ršŸ”… Optimizing: [${'🌹'.repeat((percent * 10) / 100)}] ${percent}%`)
85
+ }
86
+ console.log('\x1b[0m', '')
87
+
88
+ function printHelp() {
89
+ console.log(`brick svg
90
+
91
+ Usage:
92
+ brick svg
93
+
94
+ Notes:
95
+ Scans .svg files under the current working directory
96
+ Optimizes files in place with svgo`)
97
+ }
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs'
4
+ import { dirname, join, relative, resolve } from 'path'
5
+ import { fileURLToPath } from 'url'
6
+
7
+ import { generateContentWithLimits } from './gemini.js'
8
+ import {
9
+ buildTranslateHelp,
10
+ DEFAULT_CONTEXT_MODEL,
11
+ getTranslateRuntimeConfig,
12
+ parseTranslateRuntimeArgs,
13
+ setTranslateRuntimeConfig,
14
+ } from './runtime-config.js'
15
+ import { getTranslationPaths, listWorkspaceFiles, stringifySortedJson } from './utils.js'
16
+
17
+ const currentDir = dirname(fileURLToPath(import.meta.url))
18
+ const workspaceRoot = resolve(currentDir, '../../../..')
19
+ const CONTEXT_FILE_NAME = 'ai-context.json'
20
+ const CHANGE_THRESHOLD = readFloat('TRANSLATE_CONTEXT_MIN_CHANGE', 0.3)
21
+ const SOURCE_MAX_CHARS = readPositiveInt('TRANSLATE_CONTEXT_SOURCE_MAX_CHARS', 16000)
22
+
23
+ export function calculateChangeRatio(previousSource, nextSource) {
24
+ const before = normalizeSource(previousSource)
25
+ const after = normalizeSource(nextSource)
26
+
27
+ if (before === after) {
28
+ return 0
29
+ }
30
+
31
+ const beforeLines = before.split('\n')
32
+ const afterLines = after.split('\n')
33
+ const commonLines = longestCommonSubsequenceLength(beforeLines, afterLines)
34
+ const maxLines = Math.max(beforeLines.length, afterLines.length, 1)
35
+
36
+ return 1 - commonLines / maxLines
37
+ }
38
+
39
+ export async function ensureAiContext({ force = false, sample, samplePath, sourceFilePath }) {
40
+ const state = getAiContextState({ force, samplePath, sourceFilePath })
41
+
42
+ if (!state.shouldRegenerate) {
43
+ return state.description
44
+ }
45
+
46
+ const description = await generateContextDescription({
47
+ sample,
48
+ samplePath,
49
+ sourceCode: state.sourceCode,
50
+ sourceFilePath: state.sourceFilePath,
51
+ })
52
+
53
+ const next = {
54
+ changeRatio: state.changeRatio,
55
+ description,
56
+ samplePath: relative(workspaceRoot, samplePath),
57
+ sourceFilePath: relative(workspaceRoot, state.sourceFilePath),
58
+ sourceSnapshot: state.sourceCode,
59
+ updatedAt: new Date().toISOString(),
60
+ version: 1,
61
+ }
62
+
63
+ writeTextPreservingEol(state.contextPath, stringifySortedJson(next))
64
+
65
+ return description
66
+ }
67
+
68
+ export function getAiContextState({ force = false, samplePath, sourceFilePath }) {
69
+ if (!samplePath || !sourceFilePath || !fs.existsSync(sourceFilePath)) {
70
+ return {
71
+ changeRatio: 0,
72
+ contextPath: samplePath ? getContextFilePath(samplePath) : null,
73
+ description: null,
74
+ existing: null,
75
+ reason: 'missing_source',
76
+ shouldRegenerate: false,
77
+ sourceCode: null,
78
+ sourceFilePath,
79
+ }
80
+ }
81
+
82
+ const contextPath = getContextFilePath(samplePath)
83
+ const sourceCode = fs.readFileSync(sourceFilePath, 'utf-8')
84
+ const existing = readContextFile(contextPath)
85
+ const changeRatio = existing?.sourceSnapshot ? calculateChangeRatio(existing.sourceSnapshot, sourceCode) : 1
86
+ const hasDescription = typeof existing?.description === 'string' && existing.description.length > 0
87
+ const shouldRegenerate = force || !hasDescription || changeRatio >= CHANGE_THRESHOLD
88
+
89
+ let reason = 'reuse'
90
+
91
+ if (force) {
92
+ reason = 'force'
93
+ } else if (!hasDescription) {
94
+ reason = 'missing_context'
95
+ } else if (changeRatio >= CHANGE_THRESHOLD) {
96
+ reason = 'changed_30_percent'
97
+ }
98
+
99
+ return {
100
+ changeRatio,
101
+ contextPath,
102
+ description: hasDescription ? existing.description : null,
103
+ existing,
104
+ reason,
105
+ shouldRegenerate,
106
+ sourceCode,
107
+ sourceFilePath,
108
+ }
109
+ }
110
+
111
+ export function getContextFilePath(samplePath) {
112
+ return join(dirname(samplePath), CONTEXT_FILE_NAME)
113
+ }
114
+
115
+ function buildContextContents({ sampleEntries, samplePath, sourceCode, sourceFilePath }) {
116
+ return [
117
+ `Sample path: ${samplePath}`,
118
+ `Source file: ${sourceFilePath}`,
119
+ '',
120
+ 'Known translation keys and sample texts:',
121
+ JSON.stringify(sampleEntries, null, 2),
122
+ '',
123
+ 'Source code:',
124
+ sourceCode,
125
+ ].join('\n')
126
+ }
127
+
128
+ function buildContextSystemInstruction() {
129
+ const { productContext, terminology, tone } = getTranslateRuntimeConfig()
130
+
131
+ return [
132
+ 'You are generating translation context for a UI component.',
133
+ 'Write a compact but informative description for translators.',
134
+ 'Product context:',
135
+ productContext,
136
+ 'Terminology:',
137
+ terminology,
138
+ 'Tone:',
139
+ tone,
140
+ 'Focus on:',
141
+ '- what the component or page does',
142
+ '- main user actions',
143
+ '- important entities and domain meaning',
144
+ '- what the shown strings likely refer to',
145
+ '- tone or UX intent if obvious',
146
+ 'Rules:',
147
+ '- Return plain text only.',
148
+ '- Write 4 to 8 short sentences.',
149
+ '- Be concrete, not generic.',
150
+ '- Do not repeat raw code.',
151
+ '- Mention adult-content context only if it is actually visible in the code or strings.',
152
+ ].join('\n')
153
+ }
154
+
155
+ function cleanText(text) {
156
+ return text
157
+ .replace(/```text/gi, '')
158
+ .replace(/```/g, '')
159
+ .trim()
160
+ }
161
+
162
+ function detectEol(filePath) {
163
+ if (!fs.existsSync(filePath)) {
164
+ return '\n'
165
+ }
166
+
167
+ const text = fs.readFileSync(filePath, 'utf-8')
168
+ return text.includes('\r\n') ? '\r\n' : '\n'
169
+ }
170
+
171
+ async function generateContextDescription({ sample, samplePath, sourceCode, sourceFilePath }) {
172
+ const sampleEntries = Object.entries(sample ?? {})
173
+ .slice(0, 40)
174
+ .map(([key, text]) => ({
175
+ key,
176
+ text,
177
+ }))
178
+ const responseText = await generateContentWithLimits({
179
+ apiKey: getTranslateRuntimeConfig().apiKey,
180
+ config: {
181
+ systemInstruction: buildContextSystemInstruction(),
182
+ },
183
+ contents: buildContextContents({
184
+ sampleEntries,
185
+ samplePath: relative(workspaceRoot, samplePath),
186
+ sourceCode: sourceCode.slice(0, SOURCE_MAX_CHARS),
187
+ sourceFilePath: relative(workspaceRoot, sourceFilePath),
188
+ }),
189
+ model: getTranslateRuntimeConfig().contextModel || DEFAULT_CONTEXT_MODEL,
190
+ })
191
+
192
+ return cleanText(responseText)
193
+ }
194
+
195
+ function longestCommonSubsequenceLength(left, right) {
196
+ const rows = left.length + 1
197
+ const cols = right.length + 1
198
+ const dp = Array.from({ length: rows }, () => Array(cols).fill(0))
199
+
200
+ for (let i = 1; i < rows; i++) {
201
+ for (let j = 1; j < cols; j++) {
202
+ if (left[i - 1] === right[j - 1]) {
203
+ dp[i][j] = dp[i - 1][j - 1] + 1
204
+ } else {
205
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])
206
+ }
207
+ }
208
+ }
209
+
210
+ return dp[left.length][right.length]
211
+ }
212
+
213
+ function normalizeSource(source) {
214
+ return String(source)
215
+ .replace(/\r\n/g, '\n')
216
+ .replace(/[ \t]+$/gm, '')
217
+ .trim()
218
+ }
219
+
220
+ function readContextFile(filePath) {
221
+ if (!fs.existsSync(filePath)) {
222
+ return null
223
+ }
224
+
225
+ try {
226
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'))
227
+ } catch {
228
+ return null
229
+ }
230
+ }
231
+
232
+ function readFloat(name, fallback) {
233
+ const raw = process.env[name]
234
+
235
+ if (!raw) {
236
+ return fallback
237
+ }
238
+
239
+ const value = Number.parseFloat(raw)
240
+ return Number.isFinite(value) && value >= 0 ? value : fallback
241
+ }
242
+
243
+ function readPositiveInt(name, fallback) {
244
+ const raw = process.env[name]
245
+
246
+ if (!raw) {
247
+ return fallback
248
+ }
249
+
250
+ const value = Number.parseInt(raw, 10)
251
+ return Number.isFinite(value) && value > 0 ? value : fallback
252
+ }
253
+
254
+ async function runCli() {
255
+ const rawArgs = process.argv.slice(2)
256
+
257
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
258
+ console.log(`${buildTranslateHelp('node packages/cli/src/translate/ai-context.js')}\n\nExtra:\n --force`)
259
+ process.exit(0)
260
+ }
261
+
262
+ const force = rawArgs.includes('--force')
263
+ const filteredArgs = rawArgs.filter((arg) => arg !== '--force')
264
+ const { options, positional } = parseTranslateRuntimeArgs(filteredArgs)
265
+
266
+ try {
267
+ setTranslateRuntimeConfig(options)
268
+ } catch (error) {
269
+ console.error(error instanceof Error ? error.message : String(error))
270
+ console.error('')
271
+ console.error(`${buildTranslateHelp('node packages/cli/src/translate/ai-context.js')}\n\nExtra:\n --force`)
272
+ process.exit(1)
273
+ }
274
+
275
+ const sourceFiles = listWorkspaceFiles(workspaceRoot).filter(
276
+ (filePath) => /\.(?:js|ts|vue)$/.test(filePath) && !filePath.endsWith('.d.ts'),
277
+ )
278
+ const sampleToSource = new Map()
279
+
280
+ for (const sourceFilePath of sourceFiles) {
281
+ const samplePath = getTranslationPaths(sourceFilePath)?.samplePath
282
+
283
+ if (samplePath && fs.existsSync(samplePath) && !sampleToSource.has(samplePath)) {
284
+ sampleToSource.set(samplePath, sourceFilePath)
285
+ }
286
+ }
287
+
288
+ const targetSamplePaths =
289
+ positional.length > 0
290
+ ? positional.map((samplePath) => resolve(workspaceRoot, samplePath))
291
+ : [...sampleToSource.keys()].sort()
292
+
293
+ for (const samplePath of targetSamplePaths) {
294
+ const sourceFilePath = sampleToSource.get(samplePath)
295
+
296
+ if (!sourceFilePath || !fs.existsSync(samplePath)) {
297
+ continue
298
+ }
299
+
300
+ const description = await ensureAiContext({
301
+ force,
302
+ sample: JSON.parse(fs.readFileSync(samplePath, 'utf-8')),
303
+ samplePath,
304
+ sourceFilePath,
305
+ })
306
+
307
+ if (description) {
308
+ console.log(`🧠 Context updated: ${relative(workspaceRoot, samplePath)}`)
309
+ }
310
+ }
311
+ }
312
+
313
+ function writeTextPreservingEol(filePath, content) {
314
+ const eol = detectEol(filePath)
315
+ const normalized = String(content).replace(/\r?\n/g, eol)
316
+ fs.writeFileSync(filePath, normalized, 'utf-8')
317
+ }
318
+
319
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
320
+ await runCli()
321
+ }