metaowl 0.4.1 → 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 (80) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -0
  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 +28 -10
  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/eslint.config.js +0 -3
  39. package/index.js +0 -328
  40. package/modules/app-mounter.js +0 -104
  41. package/modules/auto-import.js +0 -225
  42. package/modules/cache.js +0 -59
  43. package/modules/composables.js +0 -600
  44. package/modules/error-boundary.js +0 -228
  45. package/modules/fetch.js +0 -51
  46. package/modules/file-router.js +0 -478
  47. package/modules/forms.js +0 -353
  48. package/modules/i18n.js +0 -333
  49. package/modules/layouts.js +0 -431
  50. package/modules/link.js +0 -255
  51. package/modules/meta.js +0 -119
  52. package/modules/odoo-rpc.js +0 -511
  53. package/modules/pwa.js +0 -515
  54. package/modules/router.js +0 -769
  55. package/modules/seo.js +0 -501
  56. package/modules/store.js +0 -409
  57. package/modules/templates-manager.js +0 -89
  58. package/modules/test-utils.js +0 -532
  59. package/test/auto-import.test.js +0 -110
  60. package/test/cache.test.js +0 -55
  61. package/test/composables.test.js +0 -103
  62. package/test/dynamic-routes.test.js +0 -469
  63. package/test/error-boundary.test.js +0 -126
  64. package/test/fetch.test.js +0 -100
  65. package/test/file-router.test.js +0 -55
  66. package/test/forms.test.js +0 -203
  67. package/test/i18n.test.js +0 -188
  68. package/test/layouts.test.js +0 -395
  69. package/test/link.test.js +0 -189
  70. package/test/meta.test.js +0 -146
  71. package/test/odoo-rpc.test.js +0 -547
  72. package/test/pwa.test.js +0 -154
  73. package/test/router-guards.test.js +0 -229
  74. package/test/router.test.js +0 -77
  75. package/test/seo.test.js +0 -353
  76. package/test/store.test.js +0 -476
  77. package/test/templates-manager.test.js +0 -83
  78. package/test/test-utils.test.js +0 -314
  79. package/vite/plugin.js +0 -290
  80. 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
- }
package/eslint.config.js DELETED
@@ -1,3 +0,0 @@
1
- import { eslintConfig } from './eslint.js'
2
-
3
- export default eslintConfig