metaowl 0.4.0 → 0.5.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -15
  3. package/build/runtime/bin/metaowl-build.js +10 -0
  4. package/{bin → build/runtime/bin}/metaowl-create.js +96 -177
  5. package/build/runtime/bin/metaowl-dev.js +10 -0
  6. package/build/runtime/bin/metaowl-generate.js +231 -0
  7. package/build/runtime/bin/metaowl-lint.js +58 -0
  8. package/build/runtime/bin/utils.js +68 -0
  9. package/build/runtime/index.js +141 -0
  10. package/build/runtime/modules/app-mounter.js +65 -0
  11. package/build/runtime/modules/auto-import.js +140 -0
  12. package/build/runtime/modules/cache.js +49 -0
  13. package/build/runtime/modules/composables.js +353 -0
  14. package/build/runtime/modules/error-boundary.js +116 -0
  15. package/build/runtime/modules/fetch.js +31 -0
  16. package/build/runtime/modules/file-router.js +205 -0
  17. package/build/runtime/modules/forms.js +193 -0
  18. package/build/runtime/modules/i18n.js +167 -0
  19. package/build/runtime/modules/layouts.js +163 -0
  20. package/build/runtime/modules/link.js +141 -0
  21. package/build/runtime/modules/meta.js +117 -0
  22. package/build/runtime/modules/odoo-rpc.js +264 -0
  23. package/build/runtime/modules/pwa.js +262 -0
  24. package/build/runtime/modules/router.js +389 -0
  25. package/build/runtime/modules/seo.js +186 -0
  26. package/build/runtime/modules/store.js +196 -0
  27. package/build/runtime/modules/templates-manager.js +52 -0
  28. package/build/runtime/modules/test-utils.js +238 -0
  29. package/build/runtime/vite/plugin.js +183 -0
  30. package/eslint.js +29 -0
  31. package/package.json +29 -11
  32. package/CONTRIBUTING.md +0 -49
  33. package/bin/metaowl-build.js +0 -12
  34. package/bin/metaowl-dev.js +0 -12
  35. package/bin/metaowl-generate.js +0 -339
  36. package/bin/metaowl-lint.js +0 -71
  37. package/bin/utils.js +0 -82
  38. package/index.js +0 -328
  39. package/modules/app-mounter.js +0 -104
  40. package/modules/auto-import.js +0 -225
  41. package/modules/cache.js +0 -59
  42. package/modules/composables.js +0 -600
  43. package/modules/error-boundary.js +0 -228
  44. package/modules/fetch.js +0 -51
  45. package/modules/file-router.js +0 -478
  46. package/modules/forms.js +0 -353
  47. package/modules/i18n.js +0 -333
  48. package/modules/layouts.js +0 -431
  49. package/modules/link.js +0 -255
  50. package/modules/meta.js +0 -119
  51. package/modules/odoo-rpc.js +0 -511
  52. package/modules/pwa.js +0 -515
  53. package/modules/router.js +0 -769
  54. package/modules/seo.js +0 -501
  55. package/modules/store.js +0 -409
  56. package/modules/templates-manager.js +0 -89
  57. package/modules/test-utils.js +0 -532
  58. package/test/auto-import.test.js +0 -110
  59. package/test/cache.test.js +0 -55
  60. package/test/composables.test.js +0 -103
  61. package/test/dynamic-routes.test.js +0 -469
  62. package/test/error-boundary.test.js +0 -126
  63. package/test/fetch.test.js +0 -100
  64. package/test/file-router.test.js +0 -55
  65. package/test/forms.test.js +0 -203
  66. package/test/i18n.test.js +0 -188
  67. package/test/layouts.test.js +0 -395
  68. package/test/link.test.js +0 -189
  69. package/test/meta.test.js +0 -146
  70. package/test/odoo-rpc.test.js +0 -547
  71. package/test/pwa.test.js +0 -154
  72. package/test/router-guards.test.js +0 -229
  73. package/test/router.test.js +0 -77
  74. package/test/seo.test.js +0 -353
  75. package/test/store.test.js +0 -476
  76. package/test/templates-manager.test.js +0 -83
  77. package/test/test-utils.test.js +0 -314
  78. package/vite/plugin.js +0 -277
  79. package/vitest.config.js +0 -8
@@ -1,339 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * metaowl generate — SSG production build.
4
- *
5
- * Runs lint + vite build, then generates a static HTML file for every page:
6
- * - <title> and meta tags extracted statically from Meta.*() calls in the page JS
7
- * - Static HTML content auto-extracted from the page's OWL XML template,
8
- * with OWL-specific syntax stripped (best-effort, no JS evaluated)
9
- *
10
- * pagesDir / outDir can be overridden in package.json under "metaowl":
11
- * { "metaowl": { "pagesDir": "src/pages", "outDir": "dist" } }
12
- */
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
14
- import { resolve } from 'node:path'
15
- import { globSync } from 'glob'
16
- import { banner, cwd, metaowlRoot, resolveBin, run, step, success, failure } from './utils.js'
17
-
18
- banner('generate')
19
-
20
- function escapeAttr(str) {
21
- return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;')
22
- }
23
-
24
- /**
25
- * Statically extract Meta.*() call arguments from JS source.
26
- * Only works for string literals — dynamic values are skipped.
27
- *
28
- * Returns an object with the keys:
29
- * title, description, keywords, author, canonical,
30
- * ogTitle, ogDescription, ogImage, ogUrl, ogType, ogSiteName
31
- */
32
- function extractMetaFromJs(src) {
33
- const meta = {}
34
- const fns = [
35
- 'title', 'description', 'keywords', 'author', 'canonical',
36
- 'ogTitle', 'ogDescription', 'ogImage', 'ogUrl', 'ogType', 'ogSiteName',
37
- ]
38
- for (const fn of fns) {
39
- const m = src.match(new RegExp(`Meta\\.${fn}\\s*\\(\\s*(['"\`])([^'"\`]+)\\1\\s*\\)`))
40
- if (m) meta[fn] = m[2]
41
- }
42
- return meta
43
- }
44
-
45
- /**
46
- * Inject extracted meta values into the HTML head.
47
- * Returns the modified HTML string.
48
- */
49
- function injectMeta(html, meta) {
50
- if (meta.title) {
51
- html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeAttr(meta.title)}</title>`)
52
- }
53
- /** @param {string} selector @param {string} tag */
54
- const injectTag = (selector, tag) => {
55
- html = html.replace(new RegExp(`\\s*${selector}[^>]*>\\s*`, 'gi'), '')
56
- html = html.replace('</head>', ` ${tag}\n </head>`)
57
- }
58
- if (meta.description)
59
- injectTag('<meta\\s+name="description"', `<meta name="description" content="${escapeAttr(meta.description)}">`)
60
- if (meta.keywords)
61
- injectTag('<meta\\s+name="keywords"', `<meta name="keywords" content="${escapeAttr(meta.keywords)}">`)
62
- if (meta.author)
63
- injectTag('<meta\\s+name="author"', `<meta name="author" content="${escapeAttr(meta.author)}">`)
64
- if (meta.canonical)
65
- injectTag('<link\\s+rel="canonical"', `<link rel="canonical" href="${escapeAttr(meta.canonical)}">`)
66
- if (meta.ogTitle)
67
- injectTag('<meta\\s+property="og:title"', `<meta property="og:title" content="${escapeAttr(meta.ogTitle)}">`)
68
- if (meta.ogDescription)
69
- injectTag('<meta\\s+property="og:description"', `<meta property="og:description" content="${escapeAttr(meta.ogDescription)}">`)
70
- if (meta.ogImage)
71
- injectTag('<meta\\s+property="og:image"', `<meta property="og:image" content="${escapeAttr(meta.ogImage)}">`)
72
- if (meta.ogUrl)
73
- injectTag('<meta\\s+property="og:url"', `<meta property="og:url" content="${escapeAttr(meta.ogUrl)}">`)
74
- if (meta.ogType)
75
- injectTag('<meta\\s+property="og:type"', `<meta property="og:type" content="${escapeAttr(meta.ogType)}">`)
76
- if (meta.ogSiteName)
77
- injectTag('<meta\\s+property="og:site_name"', `<meta property="og:site_name" content="${escapeAttr(meta.ogSiteName)}">`)
78
- return html
79
- }
80
-
81
- // Read project config
82
- const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf-8'))
83
- const metaowlConfig = pkg.metaowl ?? {}
84
- const pagesDir = metaowlConfig.pagesDir ?? 'src/pages'
85
- const outDir = metaowlConfig.outDir ?? 'dist'
86
-
87
- // Derive URL route from a page file path
88
- function deriveRoute(pageFile) {
89
- const rel = pageFile.replace(new RegExp(`^${pagesDir}[\\/]`), '')
90
- const parts = rel.split('/').slice(0, -1)
91
- if (parts.length === 1 && parts[0] === 'index') return '/'
92
- return '/' + parts.join('/')
93
- }
94
-
95
- /**
96
- * Extracts the layout name from a page component JS file.
97
- * Looks for `static layout = 'layoutName'` or `@layout('layoutName')`.
98
- *
99
- * @param {string} pageFile - Absolute path to the page JS file
100
- * @returns {string|null} Layout name or null if not found
101
- */
102
- function extractLayoutName(pageFile) {
103
- const jsSource = readFileSync(pageFile, 'utf-8')
104
-
105
- // Match: static layout = 'layoutName' or static layout = "layoutName"
106
- let match = jsSource.match(/static\s+layout\s*=\s*['"]([^'"]+)['"]/)
107
- if (match) return match[1]
108
-
109
- // Match: @layout('layoutName') or @layout("layoutName")
110
- match = jsSource.match(/@layout\s*\(\s*['"]([^'"]+)['"]\s*\)/)
111
- if (match) return match[1]
112
-
113
- // Match: @defineLayout('layoutName', ...)
114
- match = jsSource.match(/@defineLayout\s*\(\s*['"]([^'"]+)['"]/)
115
- if (match) return match[1]
116
-
117
- return 'default'
118
- }
119
-
120
- /**
121
- * Converts an OWL XML template to best-effort static HTML.
122
- * - Strips t-name on the root element
123
- * - Strips all t-* attributes
124
- * - Unwraps bare <t> elements (replaces with their inner content)
125
- * - Replaces PascalCase component tags with their template content
126
- * - Replaces t-slot="default" with provided page content
127
- * - Removes <templates> wrapper and root <t> elements
128
- *
129
- * @param {string} xml - OWL XML template
130
- * @param {string} [pageContent] - Optional page content to inject into t-slot="default"
131
- * @param {object} [options] - Options object
132
- * @param {Map<string, string>} [options.templateCache] - Cache of template name -> content
133
- */
134
- function xmlToStaticHtml(xml, pageContent = '', options = {}) {
135
- const { templateCache } = options
136
- let html = xml
137
-
138
- // Remove <templates> wrapper
139
- html = html.replace(/<templates>/g, '').replace(/<\/templates>/g, '')
140
-
141
- // Remove root <t> elements (self-closing or with content)
142
- // Match <t ...>...</t> at the start and end
143
- html = html.replace(/^\s*<t[^>]*>/, '').replace(/<\/t>\s*$/, '')
144
-
145
- // Remove t-name attribute from root
146
- html = html.replace(/\s+t-name="[^"]*"/g, '')
147
- // Remove all t-* attributes (handles both t-if="..." and bare t-else/t-else)
148
- html = html.replace(/\s+t-[\w-]+(="[^"]*")?/g, '')
149
- // Unwrap bare <t> wrapper elements (self-closing)
150
- html = html.replace(/<t\s*\/>/g, '')
151
- // Unwrap <t ...> ... </t> blocks — replace opening/closing tags with content
152
- html = html.replace(/<t(?:\s[^>]*)?>([\s\S]*?)<\/t>/g, (_, inner) => inner)
153
- // Replace t-slot="default" with page content (if provided)
154
- if (pageContent) {
155
- html = html.replace(/<t\s+t-slot="default"\s*\/?>/g, pageContent)
156
- html = html.replace(/<t\s+t-slot="default"[^>]*>([\s\S]*?)<\/t>/g, pageContent)
157
- }
158
-
159
- // Replace PascalCase component tags with their template content (if available)
160
- if (templateCache) {
161
- // Keep replacing until no more changes (for nested components)
162
- let previousHtml
163
- do {
164
- previousHtml = html
165
- html = html.replace(/<([A-Z][A-Za-z0-9]*)\s*\/>/g, (match, componentName) => {
166
- // Try different naming conventions
167
- const templateNames = [
168
- componentName,
169
- componentName.charAt(0).toLowerCase() + componentName.slice(1),
170
- componentName + 'Component',
171
- ]
172
- for (const name of templateNames) {
173
- if (templateCache.has(name)) {
174
- return templateCache.get(name)
175
- }
176
- }
177
- return match // Keep original if not found
178
- })
179
- html = html.replace(/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/g, (match, componentName, innerContent) => {
180
- const templateNames = [
181
- componentName,
182
- componentName.charAt(0).toLowerCase() + componentName.slice(1),
183
- componentName + 'Component',
184
- ]
185
- for (const name of templateNames) {
186
- if (templateCache.has(name)) {
187
- return templateCache.get(name)
188
- }
189
- }
190
- return match
191
- })
192
- } while (html !== previousHtml)
193
- } else {
194
- // No cache available, use comment stubs as fallback
195
- html = html.replace(/<([A-Z][A-Za-z0-9]*)\s*\/>/g, '<!-- $1 -->')
196
- html = html.replace(/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g, '<!-- $1 -->')
197
- }
198
-
199
- return html.trim()
200
- }
201
-
202
- // Build final HTML shell for a page
203
- function buildShell(baseHtml, pageFile) {
204
- let html = baseHtml
205
-
206
- // Extract meta tags from JS source (Meta.title(...), Meta.description(...) etc.)
207
- const jsSource = readFileSync(resolve(cwd, pageFile), 'utf-8')
208
- const meta = extractMetaFromJs(jsSource)
209
- html = injectMeta(html, meta)
210
-
211
- // Get the page's layout name
212
- const layoutName = extractLayoutName(resolve(cwd, pageFile))
213
- const layoutsDir = metaowlConfig.layoutsDir ?? 'src/layouts'
214
- const componentsDir = metaowlConfig.componentsDir ?? 'src/components'
215
-
216
- // Build template cache from all XML files (components, pages, layouts)
217
- const templateCache = new Map()
218
-
219
- // Scan component XML files
220
- const componentXmlFiles = globSync(`${componentsDir}/**/*.xml`, { cwd })
221
- for (const componentXmlFile of componentXmlFiles) {
222
- const content = readFileSync(resolve(cwd, componentXmlFile), 'utf-8')
223
- // Extract t-name values and their content: <t t-name="ComponentName">...content...</t>
224
- const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g)
225
- for (const match of tNameMatches) {
226
- const templateName = match[1]
227
- const templateContent = match[2]
228
- templateCache.set(templateName, templateContent)
229
- }
230
- // Also try to get the root element as the template (without t-name)
231
- const rootMatch = content.match(/<templates>\s*<t[^>]*>([\s\S]*?)<\/t>\s*<\/templates>/)
232
- if (rootMatch) {
233
- // Try to derive component name from file path
234
- const fileName = componentXmlFile.replace(/\.xml$/, '').split('/').pop()
235
- if (fileName) {
236
- templateCache.set(fileName, rootMatch[1])
237
- }
238
- }
239
- }
240
-
241
- // Also scan layout XML files for templates
242
- const layoutXmlFiles = globSync(`${layoutsDir}/**/*.xml`, { cwd })
243
- for (const layoutXmlFile of layoutXmlFiles) {
244
- const content = readFileSync(resolve(cwd, layoutXmlFile), 'utf-8')
245
- const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g)
246
- for (const match of tNameMatches) {
247
- templateCache.set(match[1], match[2])
248
- }
249
- }
250
-
251
- // Also scan page XML files for templates
252
- const pageXmlFiles = globSync(`${pagesDir}/**/*.xml`, { cwd })
253
- for (const pageXmlFile of pageXmlFiles) {
254
- const content = readFileSync(resolve(cwd, pageXmlFile), 'utf-8')
255
- const tNameMatches = content.matchAll(/<t\s+t-name="([^"]+)"[^>]*>([\s\S]*?)<\/t>/g)
256
- for (const match of tNameMatches) {
257
- templateCache.set(match[1], match[2])
258
- }
259
- // Try to get the root element as the template (without t-name)
260
- const rootMatch = content.match(/<templates>\s*<t[^>]*>([\s\S]*?)<\/t>\s*<\/templates>/)
261
- if (rootMatch) {
262
- const fileName = pageXmlFile.replace(/\.xml$/, '').split('/').pop()
263
- if (fileName) {
264
- templateCache.set(fileName, rootMatch[1])
265
- }
266
- }
267
- }
268
-
269
- // Try to find and read the layout XML template
270
- let finalContent = ''
271
-
272
- // First, try to read the layout XML
273
- const layoutXmlFile = resolve(cwd, layoutsDir, layoutName, `${layoutName.charAt(0).toUpperCase() + layoutName.slice(1)}Layout.xml`)
274
- const layoutXmlExists = existsSync(layoutXmlFile)
275
-
276
- // Read the page XML template
277
- const pageXmlFile = resolve(cwd, pageFile.replace(/\.js$/, '.xml'))
278
- const pageXmlExists = existsSync(pageXmlFile)
279
-
280
- if (layoutXmlExists && pageXmlExists) {
281
- // Read both layout and page templates
282
- const layoutXmlContent = readFileSync(layoutXmlFile, 'utf-8')
283
- const pageXmlContent = readFileSync(pageXmlFile, 'utf-8')
284
-
285
- // Convert page XML to static HTML first
286
- const pageStaticHtml = xmlToStaticHtml(pageXmlContent, '', { templateCache })
287
-
288
- // Then inject into layout's t-slot="default"
289
- finalContent = xmlToStaticHtml(layoutXmlContent, pageStaticHtml, { templateCache })
290
- } else if (pageXmlExists) {
291
- // No layout found, just use page content
292
- const pageXmlContent = readFileSync(pageXmlFile, 'utf-8')
293
- finalContent = xmlToStaticHtml(pageXmlContent, '', { templateCache })
294
- }
295
-
296
- // Inject the combined layout + page HTML
297
- if (finalContent) {
298
- html = html.replace(/(<div\s+id="metaowl"[^>]*>)(<\/div>)/, `$1${finalContent}$2`)
299
- }
300
-
301
- return html
302
- }
303
-
304
- // 1. Lint
305
- run('Linting', `node "${metaowlRoot}/bin/metaowl-lint.js"`)
306
-
307
- // 2. Vite build
308
- run('Building', `"${resolveBin('vite')}" build`)
309
-
310
- // 3. SSG post-processing
311
- step('Generating static pages...')
312
- console.log()
313
- const baseHtml = readFileSync(resolve(cwd, outDir, 'index.html'), 'utf-8')
314
-
315
- const pageFiles = globSync(`${pagesDir}/**/*.js`, { cwd })
316
-
317
- const seen = new Set()
318
-
319
- for (const pageFile of pageFiles) {
320
- const route = deriveRoute(pageFile)
321
- if (seen.has(route)) continue
322
- seen.add(route)
323
-
324
- const shell = buildShell(baseHtml, pageFile)
325
-
326
- if (route === '/') {
327
- writeFileSync(resolve(cwd, outDir, 'index.html'), shell)
328
- console.log(` /index.html`)
329
- } else {
330
- const destDir = resolve(cwd, outDir, route.slice(1))
331
- mkdirSync(destDir, { recursive: true })
332
- writeFileSync(resolve(destDir, 'index.html'), shell)
333
- console.log(` ${route}/index.html`)
334
- }
335
- }
336
-
337
- console.log()
338
- success(`${seen.size} route(s) generated`)
339
- console.log()
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * metaowl lint — format with Prettier then lint with ESLint.
4
- *
5
- * Lint targets can be configured in package.json under `metaowl.lint`:
6
- *
7
- * "metaowl": {
8
- * "lint": ["src/app.js", "src/pages/**", "src/components/**"]
9
- * }
10
- */
11
- import { execSync } from 'node:child_process'
12
- import { existsSync, readFileSync } from 'node:fs'
13
- import { resolve } from 'node:path'
14
- import { globSync } from 'glob'
15
- import { banner, cwd, resolveBin, step, success, failure } from './utils.js'
16
-
17
- banner('lint')
18
-
19
- let lintTargets = null
20
- try {
21
- const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf8'))
22
- lintTargets = pkg?.metaowl?.lint ?? null
23
- } catch {
24
- // no package.json or no metaowl config
25
- }
26
-
27
- const defaults = [
28
- 'src/metaowl.js',
29
- 'src/css.js',
30
- 'src/pages/**',
31
- 'src/components/**'
32
- ]
33
-
34
- const candidates = lintTargets ?? defaults
35
-
36
- const existing = candidates.filter(pattern => {
37
- if (existsSync(resolve(cwd, pattern))) return true
38
- return globSync(pattern, { cwd }).length > 0
39
- })
40
-
41
- if (existing.length === 0) {
42
- success('No lint targets found — skipping')
43
- console.log()
44
- process.exit(0)
45
- }
46
-
47
- const targets = existing.map(t => `"${t}"`).join(' ')
48
-
49
- step('Formatting with Prettier...')
50
- console.log()
51
- try {
52
- execSync(`"${resolveBin('prettier')}" src --single-quote --no-semi --write`, { stdio: 'inherit', cwd })
53
- } catch {
54
- failure('Prettier failed')
55
- process.exit(1)
56
- }
57
- console.log()
58
-
59
- step('Linting with ESLint...')
60
- console.log()
61
- try {
62
- execSync(`"${resolveBin('eslint')}" ${targets} --fix`, { stdio: 'inherit', cwd })
63
- } catch {
64
- failure('ESLint failed')
65
- process.exit(1)
66
- }
67
- console.log()
68
-
69
- success('Lint complete')
70
- console.log()
71
-
package/bin/utils.js DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * Shared CLI utilities for metaowl bin scripts.
3
- * Uses ANSI escape codes only when stdout is a TTY (no color when piped).
4
- */
5
- import { existsSync, readFileSync } from 'node:fs'
6
- import { resolve, dirname } from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
- import { execSync } from 'node:child_process'
9
-
10
- export const metaowlRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
11
- export const bin = resolve(metaowlRoot, 'node_modules/.bin')
12
- export const cwd = process.cwd()
13
- const cwdBin = resolve(cwd, 'node_modules/.bin')
14
-
15
- const { version } = JSON.parse(readFileSync(resolve(metaowlRoot, 'package.json'), 'utf-8'))
16
- export { version }
17
-
18
- /**
19
- * Resolve an executable path with fallback for hoisted installs.
20
- * Priority:
21
- * 1) metaowl-local node_modules/.bin
22
- * 2) project node_modules/.bin
23
- * 3) command name (PATH lookup by shell)
24
- *
25
- * @param {string} name
26
- * @returns {string}
27
- */
28
- export function resolveBin(name) {
29
- const local = resolve(bin, name)
30
- if (existsSync(local)) return local
31
-
32
- const project = resolve(cwdBin, name)
33
- if (existsSync(project)) return project
34
-
35
- return name
36
- }
37
-
38
- const TTY = Boolean(process.stdout.isTTY)
39
- const a = (str, code) => TTY ? `\x1b[${code}m${str}\x1b[0m` : str
40
-
41
- /** Print a styled header for the current command. */
42
- export function banner(command) {
43
- console.log()
44
- console.log(` ${a('metaowl', '1;36')} ${a(command, '1')} ${a(`v${version}`, '2')}`)
45
- console.log()
46
- }
47
-
48
- /** Print a step indicator: " › message" */
49
- export function step(msg) {
50
- console.log(` ${a('›', '36')} ${msg}`)
51
- }
52
-
53
- /** Print a success line: " ✓ message" */
54
- export function success(msg) {
55
- console.log(` ${a('✓', '32')} ${a(msg, '2')}`)
56
- }
57
-
58
- /** Print an error line: " ✗ message" */
59
- export function failure(msg) {
60
- console.error(` ${a('✗', '31')} ${msg}`)
61
- }
62
-
63
- /**
64
- * Run a shell command, printing a step label before and a blank line after.
65
- * Exits the process with code 1 on failure.
66
- *
67
- * @param {string} label - Human-readable step description.
68
- * @param {string} cmd - Shell command to execute.
69
- * @param {object} [opts] - Additional options forwarded to execSync.
70
- */
71
- export function run(label, cmd, opts = {}) {
72
- step(label)
73
- console.log()
74
- try {
75
- execSync(cmd, { stdio: 'inherit', cwd, ...opts })
76
- } catch {
77
- console.log()
78
- failure(`${label} failed`)
79
- process.exit(1)
80
- }
81
- console.log()
82
- }