@owear/cli 0.1.2 → 0.1.4

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 (34) hide show
  1. package/package.json +10 -8
  2. package/src/ow.js +548 -25
  3. package/template/README.md +34 -28
  4. package/template/app/main.ts +273 -4
  5. package/template/index.html +117 -28
  6. package/template/package.json +1 -1
  7. package/template/public/favicon.svg +4 -0
  8. package/template/src/assets/dancing-script.woff2 +0 -0
  9. package/template/src/assets/jetbrains-mono.woff2 +0 -0
  10. package/template/src/ow.d.ts +9 -1
  11. package/template/src/renderer.ts +489 -1
  12. package/template/src/style.css +280 -56
  13. package/template-installer/README.md +38 -0
  14. package/template-installer/app/main.ts +22 -0
  15. package/template-installer/index.html +70 -0
  16. package/template-installer/package.json +22 -0
  17. package/template-installer/src/assets/dancing-script.woff2 +0 -0
  18. package/template-installer/src/assets/jetbrains-mono.woff2 +0 -0
  19. package/template-installer/src/installer.ts +171 -0
  20. package/template-installer/src/style.css +442 -0
  21. package/template-installer/tsconfig.json +13 -0
  22. package/template-installer/uninstaller/app/main.ts +16 -0
  23. package/template-installer/uninstaller/index.html +54 -0
  24. package/template-installer/uninstaller/package.json +22 -0
  25. package/template-installer/uninstaller/src/uninstaller.ts +101 -0
  26. package/template-installer/uninstaller/vite.config.ts +10 -0
  27. package/template-installer/vite.config.ts +15 -0
  28. package/tools/owear-installer.mjs +86 -0
  29. package/tools/owear-linux.mjs +158 -0
  30. package/tools/owear-msi.mjs +113 -0
  31. package/tools/owear-pack.mjs +93 -0
  32. package/tools/owear-sign.mjs +204 -0
  33. package/tools/owear-update.mjs +150 -0
  34. package/LICENSE +0 -201
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "__PKG_NAME__-uninstaller",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Desinstalador de __APP_NAME__ — app Owear en modo uninstaller.",
7
+ "scripts": {
8
+ "build": "ow build uninstaller",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@owear/core": "^0.1.0"
13
+ },
14
+ "devDependencies": {
15
+ "@owear/cli": "^0.1.0",
16
+ "@owear/vite-plugin": "^0.1.0",
17
+ "@types/node": "^20.14.0",
18
+ "esbuild": "^0.25.0",
19
+ "typescript": "^5.6.0",
20
+ "vite": "^6.0.0"
21
+ }
22
+ }
@@ -0,0 +1,101 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ // src/uninstaller.ts — UI del desinstalador (vanilla). Mismo bridge, pero
5
+ // invoca `installer.uninstall`. Detecta la instalación por el REGISTRO de Owear
6
+ // (no hace falta escribir la ruta ni usar módulos .owm).
7
+ // Estilos: tokens del Starter (ver ../../src/style.css).
8
+
9
+ import '../../src/style.css'
10
+
11
+ interface OwBridge {
12
+ invoke<T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T>
13
+ }
14
+ const ow = (window as unknown as { ow: OwBridge }).ow
15
+ const wid = (window as unknown as { __owWindowId: number }).__owWindowId
16
+
17
+ const $ = <T extends HTMLElement = HTMLElement>(sel: string): T =>
18
+ document.querySelector(sel) as T
19
+ const invoke = <T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> =>
20
+ ow.invoke<T>(module, fn, ...args)
21
+
22
+ interface StateResult {
23
+ installed: boolean
24
+ version?: string
25
+ app?: string
26
+ dir?: string
27
+ }
28
+ interface Info {
29
+ appName?: string
30
+ version?: string
31
+ dir?: string
32
+ installed?: boolean
33
+ }
34
+
35
+ function setStatus(msg: string, kind: 'ok' | 'err' | '' = ''): void {
36
+ const el = $('#status')
37
+ el.textContent = msg
38
+ el.dataset.kind = kind
39
+ }
40
+
41
+ async function boot(): Promise<void> {
42
+ $('#win-min').addEventListener('click', () => void invoke('ow-window', 'minimize', wid))
43
+ $('#win-close').addEventListener('click', () => void invoke('ow-window', 'close', wid))
44
+
45
+ const info = await invoke<Info>('installer', 'info').catch(() => ({}))
46
+ const st = await invoke<StateResult>('installer', 'state', {}).catch(() => ({
47
+ installed: false,
48
+ }))
49
+ const name = st.app ?? info.appName ?? 'La app'
50
+ const dir = st.dir ?? info.dir ?? ''
51
+
52
+ if (st.installed) {
53
+ const v = st.version ?? info.version ?? ''
54
+ $('#summary').textContent = `${name}${v ? ' ' + v : ''} está instalada${
55
+ dir ? ' en ' + dir : ''
56
+ }.`
57
+ } else {
58
+ $('#summary').textContent =
59
+ 'No se detectó ninguna instalación registrada. Elige la carpeta donde se instaló.'
60
+ }
61
+ ;($('#dir') as HTMLInputElement).value = dir
62
+ $('#uninstall').removeAttribute('disabled')
63
+ }
64
+
65
+ async function pickDir(): Promise<void> {
66
+ const current = ($('#dir') as HTMLInputElement).value.trim()
67
+ try {
68
+ // Selector NATIVO del builtin installer (no usa el módulo `dialog`).
69
+ const dir = await invoke<string | null>('installer', 'chooseDir', {
70
+ title: 'Elegir carpeta de instalación',
71
+ defaultPath: current,
72
+ })
73
+ if (dir) ($('#dir') as HTMLInputElement).value = dir
74
+ } catch {
75
+ /* cancelado */
76
+ }
77
+ }
78
+
79
+ async function doUninstall(): Promise<void> {
80
+ const dir = ($('#dir') as HTMLInputElement).value.trim()
81
+ const keepData = ($('#keepData') as HTMLInputElement).checked
82
+ $('#uninstall').setAttribute('disabled', 'true')
83
+ setStatus('Desinstalando…')
84
+ try {
85
+ // Si no hay dir, el builtin usa el registrado.
86
+ const res = await invoke<{ removed: number }>(
87
+ 'installer',
88
+ 'uninstall',
89
+ dir ? { dir, keepData } : { keepData },
90
+ )
91
+ setStatus(`Eliminados ${res.removed} ficheros.`, 'ok')
92
+ setTimeout(() => void invoke('ow-window', 'close', wid), 900)
93
+ } catch (e) {
94
+ setStatus(e instanceof Error ? e.message : String(e), 'err')
95
+ $('#uninstall').removeAttribute('disabled')
96
+ }
97
+ }
98
+
99
+ $('#pick').addEventListener('click', () => void pickDir())
100
+ $('#uninstall').addEventListener('click', () => void doUninstall())
101
+ void boot()
@@ -0,0 +1,10 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { defineConfig } from 'vite'
4
+ import owear from '@owear/vite-plugin'
5
+
6
+ export default defineConfig({
7
+ plugins: [owear()],
8
+ base: './',
9
+ build: { outDir: 'ui-dist', emptyOutDir: true },
10
+ })
@@ -0,0 +1,15 @@
1
+ // Copyright 2026 Owear Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ //
4
+ import { defineConfig } from 'vite'
5
+ import owear from '@owear/vite-plugin'
6
+
7
+ export default defineConfig({
8
+ plugins: [owear()],
9
+ base: './',
10
+ build: {
11
+ // `ow build installer` recoge este directorio como `ui/` del instalador.
12
+ outDir: 'ui-dist',
13
+ emptyOutDir: true,
14
+ },
15
+ })
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ // Copyright 2026 Owear Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // tools/owear-installer.mjs — ensambla el payload del INSTALADOR y lo empaqueta
6
+ // dentro del binario instalador (kernel + footer OWPK1).
7
+ //
8
+ // El instalador es un binario Owear aparte: al arrancar detecta `installer.json`
9
+ // (o `uninstaller.json`) y entra en modo installer/uninstaller (ver
10
+ // src/Core/App/Internal.cpp), sirviendo `ui/` como assets y exponiendo la API
11
+ // `installer` (src/api/installer).
12
+ //
13
+ // Layout del payload del instalador:
14
+ // installer.json metadatos de la app + modo/orden (del bridge)
15
+ // bridge.json contrato embebido (owear.bridge.ts compilado)
16
+ // ui/ renderer + sidecar del instalador
17
+ // payload/ lo que se instala (binario único [minimal] o árbol [layout])
18
+ // modules/ módulos .owm extra (opcional)
19
+ //
20
+ // Uso:
21
+ // node tools/owear-installer.mjs --kernel <owear> --out <MiAppInstaller> \
22
+ // --ui <distUI> --payload <dir> [--bridge <bridge.json>] [--meta <installer.json>] \
23
+ // [--modules <dir>] [--name <slug>]
24
+ //
25
+ import * as fs from 'node:fs'
26
+ import * as os from 'node:os'
27
+ import * as path from 'node:path'
28
+ import { spawnSync } from 'node:child_process'
29
+ import { fileURLToPath } from 'node:url'
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
32
+
33
+ function die(m) {
34
+ console.error(`[owear-installer] ✗ ${m}`)
35
+ process.exit(1)
36
+ }
37
+ const argv = process.argv.slice(2)
38
+ const arg = (n, def = null) => (argv.includes(n) ? argv[argv.indexOf(n) + 1] : def)
39
+
40
+ const kernel = arg('--kernel')
41
+ const out = arg('--out')
42
+ const ui = arg('--ui')
43
+ const payload = arg('--payload')
44
+ const bridge = arg('--bridge')
45
+ const meta = arg('--meta')
46
+ const modules = arg('--modules')
47
+ const icon = arg('--icon')
48
+ const name = arg('--name', 'app')
49
+
50
+ if (!kernel || !out || !ui) die('uso: --kernel <owear> --out <bin> --ui <distUI> [--payload <dir>]')
51
+ if (!fs.existsSync(kernel)) die(`no existe el kernel: ${kernel}`)
52
+ if (!fs.existsSync(ui)) die(`no existe el ui: ${ui}`)
53
+
54
+ const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'owear-installer-'))
55
+ const copyInto = (src, dstRel) => {
56
+ const dst = path.join(stage, dstRel)
57
+ fs.mkdirSync(path.dirname(dst), { recursive: true })
58
+ fs.cpSync(src, dst, { recursive: true })
59
+ }
60
+
61
+ copyInto(ui, 'ui')
62
+ if (payload && fs.existsSync(payload)) copyInto(payload, 'payload')
63
+ if (bridge && fs.existsSync(bridge)) fs.copyFileSync(bridge, path.join(stage, 'bridge.json'))
64
+ if (meta && fs.existsSync(meta)) fs.copyFileSync(meta, path.join(stage, 'installer.json'))
65
+ if (modules && fs.existsSync(modules)) copyInto(modules, 'modules')
66
+ // icono de la app (para el .desktop / acceso directo del instalado)
67
+ if (icon && fs.existsSync(icon)) {
68
+ fs.copyFileSync(icon, path.join(stage, 'icon' + path.extname(icon)))
69
+ }
70
+
71
+ // manifiesto mínimo del payload del instalador (informativo)
72
+ fs.writeFileSync(
73
+ path.join(stage, 'manifest.json'),
74
+ JSON.stringify({ kind: 'installer', name, createdAt: Date.now() }, null, 2) + '\n',
75
+ )
76
+
77
+ // Empaqueta con el empaquetador existente (mismo formato OWPK1).
78
+ const packer = path.join(__dirname, 'owear-pack.mjs')
79
+ const r = spawnSync(
80
+ process.execPath,
81
+ [packer, '--kernel', kernel, '--in', stage, '--out', out],
82
+ { stdio: 'inherit' },
83
+ )
84
+ fs.rmSync(stage, { recursive: true, force: true })
85
+ if (r.status !== 0) die('falló owear-pack')
86
+ console.log(`[owear-installer] instalador: ${out}`)
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ // Copyright 2026 Owear Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // tools/owear-linux.mjs — empaquetado Linux de la app (D1).
6
+ //
7
+ // deb → .deb (ar + control.tar.gz + data.tar.gz), sin deps externas.
8
+ // appimage → AppDir + appimagetool (si está en PATH).
9
+ //
10
+ // Uso:
11
+ // node tools/owear-linux.mjs deb --stage <dataRoot> --out <x.deb> \
12
+ // --name <slug> --version <v> [--arch amd64] [--maintainer <m>] [--description <d>]
13
+ // node tools/owear-linux.mjs appimage --appdir <AppDir> --out <x.AppImage>
14
+ //
15
+ import * as fs from 'node:fs'
16
+ import * as path from 'node:path'
17
+ import * as zlib from 'node:zlib'
18
+ import { spawnSync } from 'node:child_process'
19
+
20
+ function die(m) {
21
+ console.error(`[owear-linux] ✗ ${m}`)
22
+ process.exit(1)
23
+ }
24
+ const argv = process.argv.slice(2)
25
+ const cmd = argv[0]
26
+ const arg = (n, def = null) => (argv.includes(n) ? argv[argv.indexOf(n) + 1] : def)
27
+
28
+ // ── tar (ustar) + gzip ──────────────────────────────────────────────────────
29
+ function oct(n, len) {
30
+ return n.toString(8).padStart(len - 1, '0') + '\0'
31
+ }
32
+ function tarHeader(name, size, mode, typeflag, mtime = 0) {
33
+ const h = Buffer.alloc(512, 0)
34
+ const nm = name.length > 100 ? name.slice(0, 100) : name
35
+ h.write(nm, 0, 100, 'utf8')
36
+ h.write(oct(mode, 8), 100)
37
+ h.write(oct(0, 8), 108)
38
+ h.write(oct(0, 8), 116)
39
+ h.write(oct(size, 12), 124)
40
+ h.write(oct(mtime, 12), 136)
41
+ h.write(' ', 148)
42
+ h.write(typeflag, 156)
43
+ h.write('ustar\0', 257)
44
+ h.write('00', 263)
45
+ let sum = 0
46
+ for (const b of h) sum += b
47
+ h.write(sum.toString(8).padStart(6, '0') + '\0 ', 148)
48
+ return h
49
+ }
50
+ function tarDir(dir) {
51
+ const chunks = []
52
+ const walk = (d, base) => {
53
+ for (const e of fs.readdirSync(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
54
+ const p = path.join(d, e.name)
55
+ const rel = base ? `${base}/${e.name}` : e.name
56
+ const st = fs.lstatSync(p)
57
+ if (st.isSymbolicLink()) {
58
+ chunks.push(tarHeader(rel, 0, 0o777, '2', Math.floor(st.mtimeMs / 1000)))
59
+ const link = fs.readlinkSync(p)
60
+ const b = Buffer.from(link)
61
+ chunks.push(b)
62
+ const pad = (512 - (b.length % 512)) % 512
63
+ if (pad) chunks.push(Buffer.alloc(pad, 0))
64
+ } else if (e.isDirectory()) {
65
+ chunks.push(tarHeader(rel + '/', 0, st.mode & 0o777, '5', Math.floor(st.mtimeMs / 1000)))
66
+ walk(p, rel)
67
+ } else if (e.isFile()) {
68
+ const data = fs.readFileSync(p)
69
+ chunks.push(tarHeader(rel, data.length, st.mode & 0o777, '0', Math.floor(st.mtimeMs / 1000)), data)
70
+ const pad = (512 - (data.length % 512)) % 512
71
+ if (pad) chunks.push(Buffer.alloc(pad, 0))
72
+ }
73
+ }
74
+ }
75
+ walk(dir, '')
76
+ chunks.push(Buffer.alloc(1024, 0))
77
+ return Buffer.concat(chunks)
78
+ }
79
+ const tarGz = (dir) => zlib.gzipSync(tarDir(dir), { level: 9 })
80
+
81
+ // ── ar (formato .deb) ───────────────────────────────────────────────────────
82
+ function ar(entries) {
83
+ const parts = [Buffer.from('!<arch>\n')]
84
+ for (const e of entries) {
85
+ const data = e.data
86
+ const h = Buffer.alloc(60, 0x20)
87
+ const name = (e.name + '/').slice(0, 16)
88
+ h.write(name, 0)
89
+ h.write(String(Math.floor(Date.now() / 1000)).padEnd(12), 16)
90
+ h.write('0'.padEnd(6), 28)
91
+ h.write('0'.padEnd(6), 34)
92
+ h.write('100644'.padEnd(8), 40)
93
+ h.write(String(data.length).padEnd(10), 48)
94
+ h.write('`\n', 58)
95
+ parts.push(h, data)
96
+ if (data.length % 2) parts.push(Buffer.from('\n'))
97
+ }
98
+ return Buffer.concat(parts)
99
+ }
100
+
101
+ function buildDeb() {
102
+ const stage = arg('--stage')
103
+ const out = arg('--out')
104
+ const name = arg('--name', 'app')
105
+ const version = arg('--version', '0.0.0')
106
+ const arch = arg('--arch', 'amd64')
107
+ const maintainer = arg('--maintainer', 'unknown <unknown@example.com>')
108
+ const description = arg('--description', name)
109
+ if (!stage || !out) die('uso: deb --stage <dataRoot> --out <x.deb> --name <slug> --version <v>')
110
+ if (!fs.existsSync(stage)) die(`no existe el stage: ${stage}`)
111
+
112
+ const controlDir = fs.mkdtempSync('/tmp/owear-deb-')
113
+ fs.writeFileSync(
114
+ path.join(controlDir, 'control'),
115
+ `Package: ${name}\nVersion: ${version}\nArchitecture: ${arch}\n` +
116
+ `Maintainer: ${maintainer}\nDescription: ${description}\n`,
117
+ )
118
+ fs.writeFileSync(
119
+ path.join(controlDir, 'postinst'),
120
+ `#!/bin/sh\nset -e\nif command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q || true; fi\nexit 0\n`,
121
+ )
122
+ fs.chmodSync(path.join(controlDir, 'postinst'), 0o755)
123
+
124
+ const deb = ar([
125
+ { name: 'debian-binary', data: Buffer.from('2.0\n') },
126
+ { name: 'control.tar.gz', data: tarGz(controlDir) },
127
+ { name: 'data.tar.gz', data: tarGz(stage) },
128
+ ])
129
+ fs.mkdirSync(path.dirname(out), { recursive: true })
130
+ fs.writeFileSync(out, deb)
131
+ fs.rmSync(controlDir, { recursive: true, force: true })
132
+ const mb = (deb.length / 1024 / 1024).toFixed(2)
133
+ console.log(`[owear-linux] .deb: ${out} (${mb} MB)`)
134
+ }
135
+
136
+ function buildAppImage() {
137
+ const appDir = arg('--appdir')
138
+ const out = arg('--out')
139
+ if (!appDir || !out) die('uso: appimage --appdir <AppDir> --out <x.AppImage>')
140
+ if (!fs.existsSync(appDir)) die(`no existe el AppDir: ${appDir}`)
141
+ const tool = ['appimagetool', 'appimagetool-x86_64.AppImage'].find((t) => {
142
+ const r = spawnSync('sh', ['-c', `command -v ${t}`], { stdio: 'ignore' })
143
+ return r.status === 0
144
+ })
145
+ if (!tool) {
146
+ die(
147
+ 'appimagetool no está en PATH. Instálalo (https://github.com/AppImage/appimagetool) ' +
148
+ `y reintenta. El AppDir queda listo en: ${appDir}`,
149
+ )
150
+ }
151
+ const r = spawnSync(tool, [appDir, out], { stdio: 'inherit', env: { ...process.env, ARCH: arg('--arch', 'x86_64') } })
152
+ if (r.status !== 0) die('appimagetool falló')
153
+ console.log(`[owear-linux] AppImage: ${out}`)
154
+ }
155
+
156
+ if (cmd === 'deb') buildDeb()
157
+ else if (cmd === 'appimage') buildAppImage()
158
+ else die('uso: owear-linux <deb|appimage> …')
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ // Copyright 2026 Owear Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // tools/owear-msi.mjs — genera un .MSI de la app (D1) desde un stage dir.
6
+ //
7
+ // Genera un fuente WiX v3 y lo compila con `wixl` (msitools, Linux) o
8
+ // `wix`/`candle`+`light` (Windows). Si no hay toolchain, deja el .wxs listo.
9
+ //
10
+ // Uso:
11
+ // node tools/owear-msi.mjs --stage <dir> --out <x.msi> --name <slug> \
12
+ // --version <v> [--publisher <p>] [--appId <id>]
13
+ //
14
+ import * as fs from 'node:fs'
15
+ import * as path from 'node:path'
16
+ import * as crypto from 'node:crypto'
17
+ import { spawnSync } from 'node:child_process'
18
+
19
+ function die(m) {
20
+ console.error(`[owear-msi] ✗ ${m}`)
21
+ process.exit(1)
22
+ }
23
+ const argv = process.argv.slice(2)
24
+ const arg = (n, def = null) => (argv.includes(n) ? argv[argv.indexOf(n) + 1] : def)
25
+ const stage = arg('--stage')
26
+ const out = arg('--out')
27
+ const name = arg('--name', 'app')
28
+ const version = arg('--version', '0.0.0')
29
+ const publisher = arg('--publisher', 'unknown')
30
+ const appId = arg('--appId', name)
31
+ if (!stage || !out) die('uso: --stage <dir> --out <x.msi> --name <slug> --version <v>')
32
+ if (!fs.existsSync(stage)) die(`no existe el stage: ${stage}`)
33
+
34
+ /** GUID estable a partir de un string (UpgradeCode). */
35
+ function guidFrom(seed) {
36
+ const h = crypto.createHash('sha1').update(seed).digest()
37
+ const b = Buffer.from(h.subarray(0, 16))
38
+ b[6] = (b[6] & 0x0f) | 0x40 // version 4
39
+ b[8] = (b[8] & 0x3f) | 0x80 // variant
40
+ const s = b.toString('hex')
41
+ return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`.toUpperCase()
42
+ }
43
+
44
+ const xmlEsc = (s) =>
45
+ String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
46
+
47
+ // ── construye el árbol WiX ──────────────────────────────────────────────────
48
+ let idSeq = 0
49
+ const nextId = (p) => `${p}${++idSeq}`
50
+
51
+ function buildTree(dir, dirId, indent) {
52
+ let xml = ''
53
+ for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
54
+ const p = path.join(dir, e.name)
55
+ if (e.isDirectory()) {
56
+ const childId = nextId('dir')
57
+ xml += `${indent}<Directory Id="${childId}" Name="${xmlEsc(e.name)}">\n`
58
+ xml += buildTree(p, childId, indent + ' ')
59
+ xml += `${indent}</Directory>\n`
60
+ } else if (e.isFile()) {
61
+ const compId = nextId('cmp')
62
+ const fileId = nextId('fil')
63
+ xml +=
64
+ `${indent}<Component Id="${compId}" Guid="*">\n` +
65
+ `${indent} <File Id="${fileId}" Source="${xmlEsc(p)}" KeyPath="yes" />\n` +
66
+ `${indent}</Component>\n`
67
+ }
68
+ }
69
+ return xml
70
+ }
71
+
72
+ const wxsPath = out.replace(/\.msi$/, '.wxs')
73
+ const wxs =
74
+ `<?xml version="1.0" encoding="UTF-8"?>\n` +
75
+ `<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">\n` +
76
+ ` <Product Id="*" Name="${xmlEsc(name)}" Language="1033" Version="${xmlEsc(version)}"\n` +
77
+ ` Manufacturer="${xmlEsc(publisher)}" UpgradeCode="${guidFrom(appId)}">\n` +
78
+ ` <Package InstallerVersion="500" Compressed="yes" InstallScope="perUser" />\n` +
79
+ ` <MajorUpgrade DowngradeErrorMessage="Ya hay una version mas reciente instalada." />\n` +
80
+ ` <MediaTemplate EmbedCab="yes" />\n` +
81
+ ` <Directory Id="TARGETDIR" Name="SourceDir">\n` +
82
+ ` <Directory Id="LocalAppDataFolder">\n` +
83
+ ` <Directory Id="INSTALLFOLDER" Name="${xmlEsc(name)}">\n` +
84
+ buildTree(stage, 'INSTALLFOLDER', ' ') +
85
+ ` </Directory>\n` +
86
+ ` </Directory>\n` +
87
+ ` </Directory>\n` +
88
+ ` </Product>\n` +
89
+ `</Wix>\n`
90
+
91
+ fs.mkdirSync(path.dirname(wxsPath), { recursive: true })
92
+ fs.writeFileSync(wxsPath, wxs)
93
+
94
+ const has = (cmd) => spawnSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'ignore' }).status === 0
95
+
96
+ let ok = false
97
+ if (has('wixl')) {
98
+ const r = spawnSync('wixl', ['-o', out, wxsPath], { stdio: 'inherit' })
99
+ ok = r.status === 0
100
+ } else if (has('wix')) {
101
+ const r = spawnSync('wix', ['build', wxsPath, '-o', out], { stdio: 'inherit' })
102
+ ok = r.status === 0
103
+ }
104
+
105
+ if (!ok) {
106
+ console.error(
107
+ `[owear-msi] sin toolchain (wixl/wix). Fuente WiX lista en: ${wxsPath}\n` +
108
+ ` · Linux: sudo apt install msitools → wixl -o "${out}" "${wxsPath}"\n` +
109
+ ` · Windows: dotnet tool install --global wix → wix build "${wxsPath}" -o "${out}"`,
110
+ )
111
+ process.exit(0) // no es un error fatal: el .wxs queda generado
112
+ }
113
+ console.log(`[owear-msi] MSI: ${out}`)
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ // Copyright 2026 Owear Contributors
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+ // tools/owear-pack.mjs — empaqueta un directorio (bundle) DENTRO del binario del
6
+ // kernel → single binary. Formato:
7
+ // [ kernel ][ payload.tar.gz ][ "OWPK1\0\0\0" + offset(u64) + size(u64) ]
8
+ //
9
+ // node tools/owear-pack.mjs --kernel <owear> --in <bundleDir> --out <MiApp>
10
+ //
11
+ import * as fs from 'node:fs'
12
+ import * as path from 'node:path'
13
+ import * as zlib from 'node:zlib'
14
+
15
+ function die(m) {
16
+ console.error(`[owear-pack] ✗ ${m}`)
17
+ process.exit(1)
18
+ }
19
+ const argv = process.argv.slice(2)
20
+ const arg = (n) => (argv.includes(n) ? argv[argv.indexOf(n) + 1] : null)
21
+ const kernel = arg('--kernel')
22
+ const inDir = arg('--in')
23
+ const out = arg('--out')
24
+ if (!kernel || !inDir || !out) die('uso: --kernel <owear> --in <bundle> --out <MiApp>')
25
+
26
+ // ── tar (ustar) mínimo ──────────────────────────────────────────────────────
27
+ function oct(n, len) {
28
+ return n.toString(8).padStart(len - 1, '0') + '\0'
29
+ }
30
+ function tarHeader(name, size, mode, typeflag) {
31
+ const h = Buffer.alloc(512, 0)
32
+ h.write(name.slice(0, 100), 0, 100, 'utf8')
33
+ h.write(oct(mode, 8), 100)
34
+ h.write(oct(0, 8), 108) // uid
35
+ h.write(oct(0, 8), 116) // gid
36
+ h.write(oct(size, 12), 124)
37
+ h.write(oct(0, 12), 136) // mtime
38
+ h.write(' ', 148) // checksum (espacios)
39
+ h.write(typeflag, 156)
40
+ h.write('ustar\0', 257)
41
+ h.write('00', 263)
42
+ let sum = 0
43
+ for (const b of h) sum += b
44
+ h.write(sum.toString(8).padStart(6, '0') + '\0 ', 148)
45
+ return h
46
+ }
47
+ function walk(dir, base = '') {
48
+ const out = []
49
+ for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
50
+ const p = path.join(dir, e.name)
51
+ const rel = base ? `${base}/${e.name}` : e.name
52
+ if (e.isDirectory()) {
53
+ out.push({ name: rel + '/', dir: true })
54
+ out.push(...walk(p, rel))
55
+ } else if (e.isFile()) {
56
+ out.push({ name: rel, dir: false, path: p })
57
+ }
58
+ }
59
+ return out
60
+ }
61
+ function makeTar(dir) {
62
+ const chunks = []
63
+ for (const e of walk(dir)) {
64
+ if (e.dir) {
65
+ chunks.push(tarHeader(e.name, 0, 0o755, '5'))
66
+ } else {
67
+ const data = fs.readFileSync(e.path)
68
+ chunks.push(tarHeader(e.name, data.length, 0o644, '0'), data)
69
+ const pad = (512 - (data.length % 512)) % 512
70
+ if (pad) chunks.push(Buffer.alloc(pad, 0))
71
+ }
72
+ }
73
+ chunks.push(Buffer.alloc(1024, 0)) // 2 bloques fin
74
+ return Buffer.concat(chunks)
75
+ }
76
+
77
+ // ── ensambla ────────────────────────────────────────────────────────────────
78
+ if (!fs.existsSync(kernel)) die(`no existe el kernel: ${kernel}`)
79
+ if (!fs.existsSync(inDir)) die(`no existe el bundle: ${inDir}`)
80
+
81
+ const payload = zlib.gzipSync(makeTar(inDir), { level: 9 })
82
+ const ker = fs.readFileSync(kernel)
83
+ const footer = Buffer.alloc(24)
84
+ footer.write('OWPK1\0\0\0', 0, 'binary')
85
+ footer.writeBigUInt64LE(BigInt(ker.length), 8)
86
+ footer.writeBigUInt64LE(BigInt(payload.length), 16)
87
+ fs.writeFileSync(out, Buffer.concat([ker, payload, footer]))
88
+ fs.chmodSync(out, 0o755)
89
+
90
+ const mb = (b) => (b / 1024 / 1024).toFixed(2)
91
+ console.log(
92
+ `[owear-pack] ${out} kernel=${mb(ker.length)}MB + payload=${mb(payload.length)}MB (${fs.readdirSync(inDir).length} items)`,
93
+ )