@brickflow/cli 0.0.11 โ†’ 0.0.13

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @brickflow/cli
2
2
 
3
+ ## 0.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - update multiply file in types
8
+
9
+ ## 0.0.12
10
+
11
+ ### Patch Changes
12
+
13
+ - Fix workspace cli translate
14
+
3
15
  ## 0.0.11
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brickflow/cli",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -4,6 +4,7 @@ import fs from 'fs'
4
4
  import { dirname, join, relative, resolve } from 'path'
5
5
  import { fileURLToPath } from 'url'
6
6
 
7
+ import { resolveWorkspaceRoot } from '../shared/workspace-root.js'
7
8
  import { generateContentWithLimits } from './gemini.js'
8
9
  import {
9
10
  buildTranslateHelp,
@@ -14,8 +15,7 @@ import {
14
15
  } from './runtime-config.js'
15
16
  import { listTranslationTargets, stringifySortedJson } from './utils.js'
16
17
 
17
- const currentDir = dirname(fileURLToPath(import.meta.url))
18
- const workspaceRoot = resolve(currentDir, '../../../..')
18
+ const workspaceRoot = resolveWorkspaceRoot()
19
19
  const CONTEXT_FILE_NAME = 'ai-context.json'
20
20
  const CHANGE_THRESHOLD = readFloat('TRANSLATE_CONTEXT_MIN_CHANGE', 0.3)
21
21
  const SOURCE_MAX_CHARS = readPositiveInt('TRANSLATE_CONTEXT_SOURCE_MAX_CHARS', 16000)
@@ -1,15 +1,14 @@
1
1
  import fs from 'fs'
2
2
  import { globSync } from 'glob'
3
- import { dirname, join, relative, resolve } from 'path'
4
- import { fileURLToPath } from 'url'
3
+ import { dirname, join, relative } from 'path'
5
4
 
5
+ import { resolveWorkspaceRoot } from '../shared/workspace-root.js'
6
6
  import { getAiContextState } from './ai-context.js'
7
7
  import { translateBatch } from './ai.js'
8
8
  import { buildTranslateHelp, parseTranslateRuntimeArgs, setTranslateRuntimeConfig } from './runtime-config.js'
9
9
  import { listTranslationTargets, sortObjectKeys, stringifySortedJson } from './utils.js'
10
10
 
11
- const currentDir = dirname(fileURLToPath(import.meta.url))
12
- const workspaceRoot = resolve(currentDir, '../../../..')
11
+ const workspaceRoot = resolveWorkspaceRoot()
13
12
  const rawArgs = process.argv.slice(3)
14
13
 
15
14
  if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
@@ -1,3 +1,18 @@
1
+ import fs from 'fs'
2
+ import { globSync } from 'glob'
3
+ import path from 'path'
4
+
5
+ import {
6
+ compileVueToJS,
7
+ extractStrings,
8
+ getTranslationPaths,
9
+ listWorkspaceFiles,
10
+ sortObjectKeys,
11
+ stringifySortedJson,
12
+ } from '../translate/utils.js'
13
+
14
+ const WORKSPACE_MARKERS = ['.git', 'pnpm-workspace.yaml', 'lerna.json', 'turbo.json']
15
+
1
16
  const args = process.argv.slice(3)
2
17
 
3
18
  if (args.includes('--help') || args.includes('-h')) {
@@ -10,7 +25,137 @@ if (args.length > 0) {
10
25
  process.exit(1)
11
26
  }
12
27
 
13
- await import('../translate/sync.js')
28
+ const workspaceRoot = resolveWorkspaceRoot()
29
+ const activeGeneratedDirs = new Set()
30
+ const cleanupRoots = new Set()
31
+ const staticCleanupRoots = [
32
+ path.resolve(workspaceRoot, 'packages/brick/global'),
33
+ ...globSync('apps/*/global', {
34
+ absolute: true,
35
+ cwd: workspaceRoot,
36
+ }),
37
+ ]
38
+
39
+ const files = listWorkspaceFiles(workspaceRoot).filter(
40
+ (file) => /\.(?:js|ts|vue)$/.test(file) && !file.endsWith('.d.ts'),
41
+ )
42
+ const filtered = files.filter((file) => getTranslationPaths(file))
43
+
44
+ for (const rootDir of staticCleanupRoots) {
45
+ cleanupRoots.add(rootDir)
46
+ }
47
+
48
+ const progress = createProgress(filtered.length)
49
+
50
+ for (const file of filtered) {
51
+ processFile(file)
52
+ progress(file)
53
+ }
54
+
55
+ cleanupGeneratedDirs()
56
+
57
+ process.stdout.write('\n')
58
+ console.log('โœ… Done')
59
+
60
+ function cleanupGeneratedDirs() {
61
+ for (const rootDir of cleanupRoots) {
62
+ if (!fs.existsSync(rootDir)) {
63
+ continue
64
+ }
65
+
66
+ for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
67
+ if (!entry.isDirectory()) {
68
+ continue
69
+ }
70
+
71
+ const targetDir = path.resolve(rootDir, entry.name)
72
+
73
+ if (!activeGeneratedDirs.has(targetDir)) {
74
+ removeDir(targetDir)
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ function createProgress(total) {
81
+ let done = 0
82
+ const start = Date.now()
83
+
84
+ return function update(currentFile) {
85
+ done += 1
86
+
87
+ const percent = total === 0 ? 100 : Math.round((done * 100) / total)
88
+ const filled = Math.round(percent / 5)
89
+ const empty = 20 - filled
90
+
91
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1)
92
+ const shortName = currentFile.split('/').slice(-3).join('/')
93
+
94
+ process.stdout.write(
95
+ `\rโš™๏ธ Processing: [${'โ–ˆ'.repeat(filled)}${' '.repeat(empty)}] ` +
96
+ `${percent}% (${done}/${total}) ` +
97
+ `โฑ ${elapsed}s ` +
98
+ `\x1b[90m${shortName}\x1b[0m\x1b[K`,
99
+ )
100
+ }
101
+ }
102
+
103
+ function detectEol(filePath) {
104
+ if (!fs.existsSync(filePath)) {
105
+ return '\n'
106
+ }
107
+
108
+ const text = fs.readFileSync(filePath, 'utf-8')
109
+ return text.includes('\r\n') ? '\r\n' : '\n'
110
+ }
111
+
112
+ function ensureSortedJsonFile(filePath) {
113
+ if (!fs.existsSync(filePath)) {
114
+ return false
115
+ }
116
+
117
+ const currentText = fs.readFileSync(filePath, 'utf-8')
118
+ const parsed = JSON.parse(currentText)
119
+ const sortedText = stringifySortedJson(parsed)
120
+
121
+ if (normalizeJsonEol(currentText) === normalizeJsonEol(sortedText)) {
122
+ return false
123
+ }
124
+
125
+ writeTextPreservingEol(filePath, sortedText)
126
+ return true
127
+ }
128
+
129
+ function ensureSortedTranslationJsons(baseDir, samplePath) {
130
+ const filesToCheck = [samplePath, path.resolve(baseDir, 'ai-context.json')]
131
+ const generatedDir = path.resolve(baseDir, 'generated')
132
+
133
+ if (fs.existsSync(generatedDir)) {
134
+ for (const entry of fs.readdirSync(generatedDir, { withFileTypes: true })) {
135
+ if (entry.isFile() && entry.name.endsWith('.json')) {
136
+ filesToCheck.push(path.resolve(generatedDir, entry.name))
137
+ }
138
+ }
139
+ }
140
+
141
+ for (const filePath of filesToCheck) {
142
+ try {
143
+ if (ensureSortedJsonFile(filePath)) {
144
+ console.log('\n๐Ÿ”ค Sorted keys:', filePath)
145
+ }
146
+ } catch (error) {
147
+ console.error('\nโŒ invalid translation json:', filePath, error)
148
+ }
149
+ }
150
+ }
151
+
152
+ function hasAnyFile(directoryPath, fileNames) {
153
+ return fileNames.some((fileName) => fs.existsSync(path.join(directoryPath, fileName)))
154
+ }
155
+
156
+ function normalizeJsonEol(content) {
157
+ return String(content).replace(/\r\n/g, '\n')
158
+ }
14
159
 
15
160
  function printHelp() {
16
161
  console.log(`brick translate-sync
@@ -23,3 +168,109 @@ Notes:
23
168
  Removes obsolete generated translation directories
24
169
  Verifies and normalizes translation JSON key ordering`)
25
170
  }
171
+
172
+ function processFile(filePath) {
173
+ let code = fs.readFileSync(filePath, 'utf-8')
174
+
175
+ try {
176
+ if (filePath.endsWith('.vue')) {
177
+ code = compileVueToJS(code, filePath)
178
+ }
179
+
180
+ const strings = extractStrings(code, filePath)
181
+
182
+ writeTranslations(filePath, strings)
183
+ } catch (error) {
184
+ console.error('\nโŒ error:', filePath, error)
185
+ }
186
+ }
187
+
188
+ function removeDir(dir) {
189
+ if (fs.existsSync(dir)) {
190
+ fs.rmSync(dir, { force: true, recursive: true })
191
+ }
192
+ }
193
+
194
+ function resolveWorkspaceRoot(startDir = process.cwd()) {
195
+ let currentDir = path.resolve(startDir)
196
+ let packageRoot = null
197
+
198
+ while (true) {
199
+ if (hasAnyFile(currentDir, WORKSPACE_MARKERS)) {
200
+ return currentDir
201
+ }
202
+
203
+ if (fs.existsSync(path.join(currentDir, 'package.json'))) {
204
+ packageRoot = currentDir
205
+ }
206
+
207
+ const parentDir = path.dirname(currentDir)
208
+
209
+ if (parentDir === currentDir) {
210
+ return packageRoot || startDir
211
+ }
212
+
213
+ currentDir = parentDir
214
+ }
215
+ }
216
+
217
+ function writeTextPreservingEol(filePath, content) {
218
+ const eol = detectEol(filePath)
219
+ const normalized = String(content).replace(/\r?\n/g, eol)
220
+ fs.writeFileSync(filePath, normalized, 'utf-8')
221
+ }
222
+
223
+ function writeTranslations(id, strings) {
224
+ const paths = getTranslationPaths(id)
225
+
226
+ if (!paths) {
227
+ return
228
+ }
229
+
230
+ const { baseDir, isComponent, isLayout, isPage, isScript, samplePath } = paths
231
+
232
+ if (isPage || isLayout || isScript) {
233
+ cleanupRoots.add(path.dirname(baseDir))
234
+ }
235
+
236
+ if (strings.size === 0) {
237
+ if (isComponent || isPage || isLayout || isScript) {
238
+ removeDir(baseDir)
239
+ }
240
+ return
241
+ }
242
+
243
+ if (isPage || isLayout || isScript) {
244
+ activeGeneratedDirs.add(baseDir)
245
+ }
246
+
247
+ fs.mkdirSync(baseDir, { recursive: true })
248
+
249
+ let prev = {}
250
+
251
+ if (fs.existsSync(samplePath)) {
252
+ try {
253
+ prev = JSON.parse(fs.readFileSync(samplePath, 'utf-8'))
254
+ } catch {
255
+ prev = {}
256
+ }
257
+ }
258
+
259
+ const next = {}
260
+
261
+ for (const [key, value] of strings) {
262
+ next[key] = value
263
+ }
264
+
265
+ const sortedNext = sortObjectKeys(next)
266
+ const isSame =
267
+ Object.keys(prev).length === Object.keys(sortedNext).length &&
268
+ Object.keys(prev).every((key) => prev[key] === sortedNext[key])
269
+
270
+ if (!isSame) {
271
+ writeTextPreservingEol(samplePath, stringifySortedJson(sortedNext))
272
+ console.log('\n๐Ÿงช Updated:', samplePath)
273
+ }
274
+
275
+ ensureSortedTranslationJsons(baseDir, samplePath)
276
+ }
@@ -10,29 +10,41 @@ if (args.includes('--help') || args.includes('-h')) {
10
10
 
11
11
  const options = parseArgs(args)
12
12
 
13
- if (!options.path || !options.typeName || !options.outputFile || !options.replacePattern) {
13
+ if (options.paths.length === 0 || !options.typeName || !options.outputFile || !options.replacePattern) {
14
14
  printHelp()
15
15
  process.exit(1)
16
16
  }
17
17
 
18
- const inputDir = path.resolve(process.cwd(), options.path)
18
+ const inputDirs = options.paths.map((inputPath) => path.resolve(process.cwd(), inputPath))
19
19
  const outputFile = path.resolve(process.cwd(), options.outputFile)
20
20
  const replacePattern = parseReplacePattern(options.replacePattern)
21
- const entries = readdirSync(inputDir)
22
- .filter((entry) => statSync(path.join(inputDir, entry)).isFile())
23
- .sort((first, second) => first.localeCompare(second))
21
+ const entries = inputDirs
22
+ .flatMap((inputDir) =>
23
+ readdirSync(inputDir)
24
+ .filter((entry) => statSync(path.join(inputDir, entry)).isFile())
25
+ .map((entry) => ({ entry, inputDir })),
26
+ )
27
+ .sort((first, second) => {
28
+ const entryComparison = first.entry.localeCompare(second.entry)
29
+
30
+ if (entryComparison !== 0) {
31
+ return entryComparison
32
+ }
33
+
34
+ return first.inputDir.localeCompare(second.inputDir)
35
+ })
24
36
 
25
- const typeValues = new Set(entries.map((entry) => applyReplacePattern(entry, replacePattern)).filter(Boolean))
37
+ const typeValues = new Set(entries.map(({ entry }) => applyReplacePattern(entry, replacePattern)).filter(Boolean))
26
38
 
27
39
  if (typeValues.size === 0) {
28
- throw new Error(`No type values generated from: ${inputDir}`)
40
+ throw new Error(`No type values generated from: ${inputDirs.join(', ')}`)
29
41
  }
30
42
 
31
43
  const declaration = `declare type ${options.typeName} = ${[...typeValues].map((value) => `'${value}'`).join(' | ')}\n`
32
44
  writeFileSync(outputFile, declaration, 'utf8')
33
45
 
34
46
  console.log(`โœ… Types generated for ${options.typeName}`)
35
- console.log(` input: ${inputDir}`)
47
+ console.log(` input: ${inputDirs.join(', ')}`)
36
48
  console.log(` output: ${outputFile}`)
37
49
 
38
50
  function applyReplacePattern(value, pattern) {
@@ -46,7 +58,7 @@ function applyReplacePattern(value, pattern) {
46
58
  function parseArgs(rawArgs) {
47
59
  const parsedOptions = {
48
60
  outputFile: null,
49
- path: null,
61
+ paths: [],
50
62
  replacePattern: null,
51
63
  typeName: null,
52
64
  }
@@ -56,7 +68,12 @@ function parseArgs(rawArgs) {
56
68
  const value = rawArgs[index]
57
69
 
58
70
  if (value === '--path') {
59
- parsedOptions.path = rawArgs[index + 1] ?? null
71
+ const inputPath = rawArgs[index + 1] ?? null
72
+
73
+ if (inputPath) {
74
+ parsedOptions.paths.push(inputPath)
75
+ }
76
+
60
77
  index += 1
61
78
  continue
62
79
  }
@@ -82,8 +99,8 @@ function parseArgs(rawArgs) {
82
99
  positional.push(value)
83
100
  }
84
101
 
85
- if (!parsedOptions.path) {
86
- parsedOptions.path = positional[0] ?? null
102
+ if (parsedOptions.paths.length === 0 && positional[0]) {
103
+ parsedOptions.paths.push(positional[0])
87
104
  }
88
105
 
89
106
  if (!parsedOptions.typeName) {
@@ -118,9 +135,10 @@ Usage:
118
135
  brick types ./path/to/files IconName ./types/icon.d.ts .svg
119
136
  brick types ./path/to/files IconName ./types/icon.d.ts /\\.svg$/
120
137
  brick types --path ./path/to/files --type-name IconName --output-file ./types/icon.d.ts --replace-pattern /\\.svg$/
138
+ brick types --path ./path/to/files --path ./path/to/more-files --type-name IconName --output-file ./types/icon.d.ts --replace-pattern /\\.svg$/
121
139
 
122
140
  Notes:
123
- path: directory with source files
141
+ path: directory with source files, can be passed multiple times
124
142
  typeName: generated TypeScript type name
125
143
  outputFile: file to write declaration into
126
144
  replacePattern: string or regex literal used in replace(..., '') for each filename`)
@@ -1,238 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
4
- import { globSync } from 'glob'
5
- import { dirname, resolve } from 'path'
6
- import { fileURLToPath } from 'url'
7
-
8
- import {
9
- compileVueToJS,
10
- extractStrings,
11
- getTranslationPaths,
12
- listWorkspaceFiles,
13
- sortObjectKeys,
14
- stringifySortedJson,
15
- } from './utils.js'
16
-
17
- const currentDir = dirname(fileURLToPath(import.meta.url))
18
- const workspaceRoot = resolve(currentDir, '../../../..')
19
- const activeGeneratedDirs = new Set()
20
- const cleanupRoots = new Set()
21
- const STATIC_CLEANUP_ROOTS = [
22
- resolve(workspaceRoot, 'packages/brick/global'),
23
- ...globSync('apps/*/global', {
24
- absolute: true,
25
- cwd: workspaceRoot,
26
- }),
27
- ]
28
-
29
- function cleanupGeneratedDirs() {
30
- for (const rootDir of cleanupRoots) {
31
- if (!existsSync(rootDir)) {
32
- continue
33
- }
34
-
35
- for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
36
- if (!entry.isDirectory()) {
37
- continue
38
- }
39
-
40
- const targetDir = resolve(rootDir, entry.name)
41
-
42
- if (!activeGeneratedDirs.has(targetDir)) {
43
- removeDir(targetDir)
44
- }
45
- }
46
- }
47
- }
48
-
49
- function createProgress(total) {
50
- let done = 0
51
- const start = Date.now()
52
-
53
- return function update(currentFile) {
54
- done += 1
55
-
56
- const percent = Math.round((done * 100) / total)
57
- const filled = Math.round(percent / 5)
58
- const empty = 20 - filled
59
-
60
- const elapsed = ((Date.now() - start) / 1000).toFixed(1)
61
-
62
- const shortName = currentFile.split('/').slice(-3).join('/')
63
-
64
- process.stdout.write(
65
- `\rโš™๏ธ Processing: [${'โ–ˆ'.repeat(filled)}${' '.repeat(empty)}] ` +
66
- `${percent}% (${done}/${total}) ` +
67
- `โฑ ${elapsed}s ` +
68
- `\x1b[90m${shortName}\x1b[0m\x1b[K`,
69
- )
70
- }
71
- }
72
-
73
- // ---------- progress ----------
74
-
75
- function detectEol(filePath) {
76
- if (!existsSync(filePath)) {
77
- return '\n'
78
- }
79
-
80
- const text = readFileSync(filePath, 'utf-8')
81
- return text.includes('\r\n') ? '\r\n' : '\n'
82
- }
83
-
84
- // ---------- core ----------
85
-
86
- function ensureSortedJsonFile(filePath) {
87
- if (!existsSync(filePath)) {
88
- return false
89
- }
90
-
91
- const currentText = readFileSync(filePath, 'utf-8')
92
- const parsed = JSON.parse(currentText)
93
- const sortedText = stringifySortedJson(parsed)
94
-
95
- if (normalizeJsonEol(currentText) === normalizeJsonEol(sortedText)) {
96
- return false
97
- }
98
-
99
- writeTextPreservingEol(filePath, sortedText)
100
- return true
101
- }
102
-
103
- function ensureSortedTranslationJsons(baseDir, samplePath) {
104
- const filesToCheck = [samplePath, resolve(baseDir, 'ai-context.json')]
105
- const generatedDir = resolve(baseDir, 'generated')
106
-
107
- if (existsSync(generatedDir)) {
108
- for (const entry of readdirSync(generatedDir, { withFileTypes: true })) {
109
- if (entry.isFile() && entry.name.endsWith('.json')) {
110
- filesToCheck.push(resolve(generatedDir, entry.name))
111
- }
112
- }
113
- }
114
-
115
- for (const filePath of filesToCheck) {
116
- try {
117
- if (ensureSortedJsonFile(filePath)) {
118
- console.log('\n๐Ÿ”ค Sorted keys:', filePath)
119
- }
120
- } catch (error) {
121
- console.error('\nโŒ invalid translation json:', filePath, error)
122
- }
123
- }
124
- }
125
-
126
- function normalizeJsonEol(content) {
127
- return String(content).replace(/\r\n/g, '\n')
128
- }
129
-
130
- // ---------- core ----------
131
-
132
- function processFile(filePath) {
133
- let code = readFileSync(filePath, 'utf-8')
134
-
135
- try {
136
- if (filePath.endsWith('.vue')) {
137
- code = compileVueToJS(code, filePath)
138
- }
139
-
140
- const strings = extractStrings(code, filePath)
141
-
142
- writeTranslations(filePath, strings)
143
- } catch (e) {
144
- console.error('\nโŒ error:', filePath, e)
145
- }
146
- }
147
-
148
- // ---------- translations ----------
149
-
150
- function removeDir(dir) {
151
- if (existsSync(dir)) {
152
- rmSync(dir, { force: true, recursive: true })
153
- }
154
- }
155
-
156
- function writeTextPreservingEol(filePath, content) {
157
- const eol = detectEol(filePath)
158
- const normalized = String(content).replace(/\r?\n/g, eol)
159
- writeFileSync(filePath, normalized, 'utf-8')
160
- }
161
-
162
- function writeTranslations(id, strings) {
163
- const paths = getTranslationPaths(id)
164
-
165
- if (!paths) {
166
- return
167
- }
168
-
169
- const { baseDir, isComponent, isLayout, isPage, isScript, samplePath } = paths
170
-
171
- if (isPage || isLayout || isScript) {
172
- cleanupRoots.add(dirname(baseDir))
173
- }
174
-
175
- if (strings.size === 0) {
176
- if (isComponent || isPage || isLayout || isScript) {
177
- removeDir(baseDir)
178
- }
179
- return
180
- }
181
-
182
- if (isPage || isLayout || isScript) {
183
- activeGeneratedDirs.add(baseDir)
184
- }
185
-
186
- mkdirSync(baseDir, { recursive: true })
187
-
188
- let prev = {}
189
-
190
- if (existsSync(samplePath)) {
191
- try {
192
- prev = JSON.parse(readFileSync(samplePath, 'utf-8'))
193
- } catch {
194
- prev = {}
195
- }
196
- }
197
-
198
- const next = {}
199
-
200
- for (const [k, v] of strings) {
201
- next[k] = v
202
- }
203
-
204
- const sortedNext = sortObjectKeys(next)
205
- const isSame =
206
- Object.keys(prev).length === Object.keys(sortedNext).length &&
207
- Object.keys(prev).every((k) => prev[k] === sortedNext[k])
208
-
209
- if (!isSame) {
210
- writeTextPreservingEol(samplePath, stringifySortedJson(sortedNext))
211
- console.log('\n๐Ÿงช Updated:', samplePath)
212
- }
213
-
214
- ensureSortedTranslationJsons(baseDir, samplePath)
215
- }
216
-
217
- // ---------- run ----------
218
-
219
- const files = listWorkspaceFiles(workspaceRoot).filter(
220
- (file) => /\.(?:js|ts|vue)$/.test(file) && !file.endsWith('.d.ts'),
221
- )
222
-
223
- const filtered = files.filter((file) => getTranslationPaths(file))
224
- for (const rootDir of STATIC_CLEANUP_ROOTS) {
225
- cleanupRoots.add(rootDir)
226
- }
227
-
228
- const progress = createProgress(filtered.length)
229
-
230
- for (const file of filtered) {
231
- processFile(file)
232
- progress(file)
233
- }
234
-
235
- cleanupGeneratedDirs()
236
-
237
- process.stdout.write('\n')
238
- console.log('โœ… Done')