@owear/cli 0.1.1 → 0.1.3
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/package.json +8 -5
- package/src/ow.js +466 -22
- package/template/README.md +3 -0
- package/template/app/main.ts +3 -0
- package/template/index.html +3 -0
- package/template/src/ow.d.ts +3 -0
- package/template/src/renderer.ts +3 -0
- package/template/src/style.css +4 -0
- package/template/vite.config.ts +3 -0
- package/template-installer/README.md +38 -0
- package/template-installer/app/main.ts +22 -0
- package/template-installer/index.html +60 -0
- package/template-installer/package.json +22 -0
- package/template-installer/src/installer.ts +152 -0
- package/template-installer/src/style.css +66 -0
- package/template-installer/tsconfig.json +13 -0
- package/template-installer/uninstaller/app/main.ts +16 -0
- package/template-installer/uninstaller/index.html +48 -0
- package/template-installer/uninstaller/package.json +22 -0
- package/template-installer/uninstaller/src/uninstaller.ts +76 -0
- package/template-installer/uninstaller/vite.config.ts +10 -0
- package/template-installer/vite.config.ts +15 -0
- package/tools/owear-installer.mjs +81 -0
- package/tools/owear-linux.mjs +158 -0
- package/tools/owear-msi.mjs +113 -0
- package/tools/owear-pack.mjs +93 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!-- Copyright 2026 Owear Contributors
|
|
2
|
+
SPDX-License-Identifier: Apache-2.0 -->
|
|
3
|
+
|
|
4
|
+
# Instalador de Owear (`ow create installer`)
|
|
5
|
+
|
|
6
|
+
Este proyecto es una **app Owear aparte** que instala tu app. Se compila con
|
|
7
|
+
`ow build installer` a un **binario instalador** (Linux: binario; Windows: `.exe`)
|
|
8
|
+
que lleva embebido el payload de la app y el **bridge** (`owear.bridge.ts`).
|
|
9
|
+
|
|
10
|
+
## Estructura
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
installer/
|
|
14
|
+
index.html # UI del instalador (vanilla por defecto)
|
|
15
|
+
src/installer.ts # lógica de la UI (usa la API nativa `installer`)
|
|
16
|
+
app/main.ts # sidecar Node (crea la ventana del instalador)
|
|
17
|
+
vite.config.ts # outDir: ui-dist (ow build installer lo recoge como ui/)
|
|
18
|
+
uninstaller/ # OTRO template: mismo bridge, pero desinstala
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Cómo funciona
|
|
22
|
+
|
|
23
|
+
1. `ow build installer` compila el bridge (`owear.bridge.ts`), la app (payload),
|
|
24
|
+
esta UI y (si existe) el desinstalador.
|
|
25
|
+
2. Se ensambla el payload del instalador (`ui/`, `payload/`, `bridge.json`,
|
|
26
|
+
`installer.json`) y se empaqueta dentro del kernel (footer `OWPK1`).
|
|
27
|
+
3. Al ejecutar el instalador, el kernel detecta `installer.json`, entra en
|
|
28
|
+
**modo installer** (`OW_MODE=installer`), sirve `ui/` como assets y expone la
|
|
29
|
+
API nativa `installer`.
|
|
30
|
+
4. La UI (`src/installer.ts`) usa esa API para planificar y desplegar la app.
|
|
31
|
+
|
|
32
|
+
## API disponible (renderer y main)
|
|
33
|
+
|
|
34
|
+
`installer.info / bridge / payloadList / payloadRead / plan / install /
|
|
35
|
+
uninstall / verify / state / shortcuts / launch / elevate / mode`.
|
|
36
|
+
|
|
37
|
+
Puedes personalizar la UI como quieras (React, Vue, Tailwind…): sigue siendo
|
|
38
|
+
una app Owear normal.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright 2026 Owear Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// app/main.ts — proceso principal del INSTALADOR (sidecar Node).
|
|
5
|
+
//
|
|
6
|
+
// El instalador es una app Owear normal: aquí creas su ventana y, si quieres,
|
|
7
|
+
// lógica privilegiada. La UI (renderer) usa la API nativa `installer`.
|
|
8
|
+
|
|
9
|
+
import { app, BrowserWindow } from '@owear/core'
|
|
10
|
+
|
|
11
|
+
app.whenReady().then(() => {
|
|
12
|
+
const win = new BrowserWindow({
|
|
13
|
+
title: 'Instalador',
|
|
14
|
+
width: 820,
|
|
15
|
+
height: 640,
|
|
16
|
+
resizable: true,
|
|
17
|
+
titleBarStyle: 'custom', // titlebar propia + regiones [data-ow-drag]
|
|
18
|
+
url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
win.on('closed', () => app.quit())
|
|
22
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- Copyright 2026 Owear Contributors
|
|
3
|
+
SPDX-License-Identifier: Apache-2.0 -->
|
|
4
|
+
|
|
5
|
+
<html lang="es">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<title>Instalador — __APP_NAME__</title>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<header class="titlebar">
|
|
13
|
+
<div class="titlebar__drag" data-ow-drag>
|
|
14
|
+
<span class="brand__name">Instalador · __APP_NAME__</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="titlebar__controls">
|
|
17
|
+
<button id="win-close" class="winbtn winbtn--close" title="Cerrar" aria-label="Cerrar">
|
|
18
|
+
<svg viewBox="0 0 12 12" width="12" height="12"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" /></svg>
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<main class="page">
|
|
24
|
+
<section class="hero">
|
|
25
|
+
<span class="eyebrow">Owear Installer</span>
|
|
26
|
+
<h1>Instalar __APP_NAME__</h1>
|
|
27
|
+
<p class="lead" id="summary">Cargando metadatos…</p>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<section class="card">
|
|
31
|
+
<header class="card__head"><h2>Destino</h2></header>
|
|
32
|
+
<div class="row">
|
|
33
|
+
<input id="dir" class="input" type="text" spellcheck="false" />
|
|
34
|
+
<button id="pick" class="btn">Elegir…</button>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="row">
|
|
37
|
+
<label class="opt"><input type="checkbox" id="shortcut" checked /> Acceso directo</label>
|
|
38
|
+
<label class="opt"><input type="checkbox" id="launch" checked /> Abrir al terminar</label>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<section class="card card--grow">
|
|
43
|
+
<header class="card__head">
|
|
44
|
+
<h2>Contenido</h2>
|
|
45
|
+
<span class="badge" id="count">—</span>
|
|
46
|
+
</header>
|
|
47
|
+
<pre id="plan" class="console">—</pre>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<footer class="actions">
|
|
51
|
+
<progress id="bar" max="100" value="0" hidden></progress>
|
|
52
|
+
<span id="status" class="status"></span>
|
|
53
|
+
<button id="install" class="btn btn--primary" disabled>Instalar</button>
|
|
54
|
+
<button id="cancel" class="btn btn--ghost" hidden>Cancelar</button>
|
|
55
|
+
</footer>
|
|
56
|
+
</main>
|
|
57
|
+
|
|
58
|
+
<script type="module" src="/src/installer.ts"></script>
|
|
59
|
+
</body>
|
|
60
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PKG_NAME__-installer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Instalador de __APP_NAME__ — app Owear en modo installer.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "ow build installer",
|
|
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,152 @@
|
|
|
1
|
+
// Copyright 2026 Owear Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// src/installer.ts — UI del instalador (vanilla). Corre en el renderer y usa
|
|
5
|
+
// la API nativa `installer` vía `window.ow` (inyectado por el kernel).
|
|
6
|
+
//
|
|
7
|
+
// Personalízalo a gusto (React, Vue, Tailwind…): esto es sólo el default.
|
|
8
|
+
|
|
9
|
+
import './style.css'
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
__owWindowId: number
|
|
14
|
+
ow: { invoke<T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const $ = <T extends HTMLElement = HTMLElement>(sel: string): T =>
|
|
19
|
+
document.querySelector(sel) as T
|
|
20
|
+
const invoke = <T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> =>
|
|
21
|
+
window.ow.invoke(module, fn, ...args) as Promise<T>
|
|
22
|
+
|
|
23
|
+
interface PayloadEntry {
|
|
24
|
+
rel: string
|
|
25
|
+
dst: string
|
|
26
|
+
size: number
|
|
27
|
+
dir: boolean
|
|
28
|
+
}
|
|
29
|
+
interface InstallerInfo {
|
|
30
|
+
appId?: string
|
|
31
|
+
appName?: string
|
|
32
|
+
version?: string
|
|
33
|
+
publisher?: string
|
|
34
|
+
}
|
|
35
|
+
interface Target {
|
|
36
|
+
layout?: 'minimal' | 'layout'
|
|
37
|
+
dir?: string
|
|
38
|
+
shortcuts?: string[]
|
|
39
|
+
}
|
|
40
|
+
interface Bridge {
|
|
41
|
+
app?: { id?: string; name?: string; version?: string; publisher?: string }
|
|
42
|
+
targets?: Record<string, Target>
|
|
43
|
+
order?: string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const platform: string = (() => {
|
|
47
|
+
const p = navigator.userAgent.toLowerCase()
|
|
48
|
+
return p.includes('win') ? 'win' : p.includes('mac') ? 'mac' : 'linux'
|
|
49
|
+
})()
|
|
50
|
+
|
|
51
|
+
let mode: 'minimal' | 'layout' = 'minimal'
|
|
52
|
+
let plan: PayloadEntry[] = []
|
|
53
|
+
let busy = false
|
|
54
|
+
|
|
55
|
+
function setStatus(msg: string, kind: 'ok' | 'err' | '' = ''): void {
|
|
56
|
+
const el = $('#status')
|
|
57
|
+
el.textContent = msg
|
|
58
|
+
el.dataset.kind = kind
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function boot(): Promise<void> {
|
|
62
|
+
// controles de ventana (titlebar propia)
|
|
63
|
+
const id = window.__owWindowId
|
|
64
|
+
$('#win-close').addEventListener('click', () => void invoke('ow-window', 'close', id))
|
|
65
|
+
|
|
66
|
+
const info = await invoke<InstallerInfo>('installer', 'info').catch(() => ({}))
|
|
67
|
+
const bridge = await invoke<Bridge | null>('installer', 'bridge').catch(() => null)
|
|
68
|
+
const t: Target = bridge?.targets?.[platform] ?? {}
|
|
69
|
+
mode = t.layout ?? 'minimal'
|
|
70
|
+
|
|
71
|
+
const name = info.appName ?? bridge?.app?.name ?? 'la app'
|
|
72
|
+
$('#summary').textContent =
|
|
73
|
+
`${name} ${info.version ?? bridge?.app?.version ?? ''} — modo ${mode} · ${platform}`
|
|
74
|
+
|
|
75
|
+
const value = t.dir ?? (info.appName ? `~/opt/${info.appName.toLowerCase()}` : '')
|
|
76
|
+
;($('#dir') as HTMLInputElement).value = value
|
|
77
|
+
|
|
78
|
+
await refreshPlan()
|
|
79
|
+
$('#install').removeAttribute('disabled')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function refreshPlan(): Promise<void> {
|
|
83
|
+
plan = await invoke<PayloadEntry[]>('installer', 'plan', {
|
|
84
|
+
mode,
|
|
85
|
+
layout: mode === 'minimal' ? 'flat' : 'tree',
|
|
86
|
+
order: (await invoke<Bridge | null>('installer', 'bridge').catch(() => null))?.order,
|
|
87
|
+
})
|
|
88
|
+
const total = plan.reduce((n, e) => n + e.size, 0)
|
|
89
|
+
$('#count').textContent = `${plan.length} entradas · ${(total / 1024).toFixed(1)} KiB`
|
|
90
|
+
$('#plan').textContent = plan
|
|
91
|
+
.slice(0, 300)
|
|
92
|
+
.map((e) => `${e.dir ? '📁' : '📄'} ${e.dst}${e.size ? ` (${e.size} B)` : ''}`)
|
|
93
|
+
.join('\n')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function pickDir(): Promise<void> {
|
|
97
|
+
try {
|
|
98
|
+
const dir = await invoke<string | null>('dialog', 'open', 'openDirectory', 'Elegir carpeta')
|
|
99
|
+
if (dir) ($('#dir') as HTMLInputElement).value = dir
|
|
100
|
+
} catch {
|
|
101
|
+
/* cancelado o sin picker */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function doInstall(): Promise<void> {
|
|
106
|
+
if (busy) return
|
|
107
|
+
busy = true
|
|
108
|
+
const dir = ($('#dir') as HTMLInputElement).value.trim()
|
|
109
|
+
if (!dir) {
|
|
110
|
+
setStatus('Elige un directorio de instalación', 'err')
|
|
111
|
+
busy = false
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
$('#install').setAttribute('disabled', 'true')
|
|
115
|
+
$('#cancel').removeAttribute('hidden')
|
|
116
|
+
$('#bar').removeAttribute('hidden')
|
|
117
|
+
setStatus('Instalando…')
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const res = await invoke<{ files: number; dir: string }>('installer', 'install', {
|
|
121
|
+
dir,
|
|
122
|
+
mode,
|
|
123
|
+
layout: mode === 'minimal' ? 'flat' : 'tree',
|
|
124
|
+
})
|
|
125
|
+
$('#bar').value = 100
|
|
126
|
+
setStatus(`Instalado en ${res.dir} (${res.files} ficheros)`, 'ok')
|
|
127
|
+
|
|
128
|
+
if (($('#shortcut') as HTMLInputElement).checked) {
|
|
129
|
+
const exec = plan.find((e) => !e.dir && !e.dst.includes('/'))?.dst ?? plan[0]?.dst
|
|
130
|
+
await invoke('installer', 'shortcuts', { execPath: `${dir}/${exec ?? ''}` }).catch(() => false)
|
|
131
|
+
}
|
|
132
|
+
if (($('#launch') as HTMLInputElement).checked) {
|
|
133
|
+
const exec = plan.find((e) => !e.dir && !e.dst.includes('/'))?.dst ?? plan[0]?.dst
|
|
134
|
+
await invoke('installer', 'launch', { path: `${dir}/${exec ?? ''}` }).catch(() => false)
|
|
135
|
+
const v = await invoke<{ elevated: boolean }>('installer', 'elevate').catch(() => ({ elevated: false }))
|
|
136
|
+
void v
|
|
137
|
+
await invoke('ow-window', 'close', window.__owWindowId)
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
setStatus(e instanceof Error ? e.message : String(e), 'err')
|
|
141
|
+
$('#install').removeAttribute('disabled')
|
|
142
|
+
} finally {
|
|
143
|
+
busy = false
|
|
144
|
+
$('#cancel').setAttribute('hidden', 'true')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$('#pick').addEventListener('click', () => void pickDir())
|
|
149
|
+
$('#install').addEventListener('click', () => void doInstall())
|
|
150
|
+
$('#cancel').addEventListener('click', () => void invoke('ow-window', 'close', window.__owWindowId))
|
|
151
|
+
|
|
152
|
+
void boot()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* Copyright 2026 Owear Contributors
|
|
2
|
+
SPDX-License-Identifier: Apache-2.0 */
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #0f1115;
|
|
5
|
+
--panel: #171a21;
|
|
6
|
+
--fg: #e8eaed;
|
|
7
|
+
--muted: #9aa0aa;
|
|
8
|
+
--accent: #4c8bf5;
|
|
9
|
+
--ok: #3fb950;
|
|
10
|
+
--err: #f85149;
|
|
11
|
+
color-scheme: dark;
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
html, body { height: 100%; margin: 0; }
|
|
15
|
+
body {
|
|
16
|
+
background: var(--bg);
|
|
17
|
+
color: var(--fg);
|
|
18
|
+
font: 14px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.titlebar {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: space-between;
|
|
25
|
+
height: 38px;
|
|
26
|
+
padding: 0 8px 0 14px;
|
|
27
|
+
background: #12141a;
|
|
28
|
+
border-bottom: 1px solid #232733;
|
|
29
|
+
}
|
|
30
|
+
.titlebar__drag { flex: 1; -webkit-app-region: drag; user-select: none; }
|
|
31
|
+
.brand__name { font-weight: 600; }
|
|
32
|
+
.winbtn {
|
|
33
|
+
width: 30px; height: 30px; border: 0; border-radius: 6px;
|
|
34
|
+
background: transparent; color: var(--muted); cursor: pointer;
|
|
35
|
+
}
|
|
36
|
+
.winbtn:hover { background: #232733; color: var(--fg); }
|
|
37
|
+
.winbtn--close:hover { background: #7d2c2c; color: #fff; }
|
|
38
|
+
.winbtn svg { fill: none; stroke: currentColor; stroke-width: 1.3; }
|
|
39
|
+
|
|
40
|
+
.page { max-width: 780px; margin: 0 auto; padding: 22px 18px 90px; display: grid; gap: 14px; }
|
|
41
|
+
.hero h1 { margin: 6px 0 4px; font-size: 24px; }
|
|
42
|
+
.eyebrow { color: var(--accent); text-transform: uppercase; letter-spacing: .08em; font-size: 11px; }
|
|
43
|
+
.lead { color: var(--muted); margin: 0; }
|
|
44
|
+
|
|
45
|
+
.card { background: var(--panel); border: 1px solid #232733; border-radius: 12px; padding: 14px; }
|
|
46
|
+
.card--grow { min-height: 160px; }
|
|
47
|
+
.card__head { display: flex; align-items: center; justify-content: space-between; }
|
|
48
|
+
.card__head h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 0; }
|
|
49
|
+
.badge { background: #232733; border-radius: 999px; padding: 2px 10px; font-size: 12px; color: var(--muted); }
|
|
50
|
+
|
|
51
|
+
.row { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
|
|
52
|
+
.input { flex: 1; background: #0d0f13; border: 1px solid #2a2f3b; color: var(--fg); border-radius: 8px; padding: 8px 10px; }
|
|
53
|
+
.opt { display: flex; align-items: center; gap: 6px; color: var(--muted); }
|
|
54
|
+
.console { background: #0d0f13; border: 1px solid #232733; border-radius: 8px; padding: 10px; max-height: 260px; overflow: auto; font: 12px/1.5 ui-monospace, monospace; white-space: pre-wrap; color: var(--muted); }
|
|
55
|
+
|
|
56
|
+
.btn { background: #232733; color: var(--fg); border: 0; border-radius: 8px; padding: 8px 14px; cursor: pointer; }
|
|
57
|
+
.btn:hover { background: #2b3040; }
|
|
58
|
+
.btn--primary { background: var(--accent); color: #fff; }
|
|
59
|
+
.btn--primary:disabled { opacity: .5; cursor: default; }
|
|
60
|
+
.btn--ghost { background: transparent; border: 1px solid #2a2f3b; color: var(--muted); }
|
|
61
|
+
|
|
62
|
+
.actions { position: fixed; left: 0; right: 0; bottom: 0; display: flex; gap: 12px; align-items: center; justify-content: flex-end; padding: 12px 18px; background: #12141acc; backdrop-filter: blur(6px); border-top: 1px solid #232733; }
|
|
63
|
+
.status { margin-right: auto; color: var(--muted); }
|
|
64
|
+
.status[data-kind="ok"] { color: var(--ok); }
|
|
65
|
+
.status[data-kind="err"] { color: var(--err); }
|
|
66
|
+
progress { width: 160px; }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"types": ["node", "vite/client"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src", "app"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2026 Owear Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// app/main.ts — proceso principal del DESINSTALADOR (sidecar Node).
|
|
5
|
+
import { app, BrowserWindow } from '@owear/core'
|
|
6
|
+
|
|
7
|
+
app.whenReady().then(() => {
|
|
8
|
+
const win = new BrowserWindow({
|
|
9
|
+
title: 'Desinstalador',
|
|
10
|
+
width: 720,
|
|
11
|
+
height: 460,
|
|
12
|
+
titleBarStyle: 'custom',
|
|
13
|
+
url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
|
|
14
|
+
})
|
|
15
|
+
win.on('closed', () => app.quit())
|
|
16
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- Copyright 2026 Owear Contributors
|
|
3
|
+
SPDX-License-Identifier: Apache-2.0 -->
|
|
4
|
+
<html lang="es">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
+
<title>Desinstalar — __APP_NAME__</title>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="titlebar">
|
|
12
|
+
<div class="titlebar__drag" data-ow-drag>
|
|
13
|
+
<span class="brand__name">Desinstalador · __APP_NAME__</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="titlebar__controls">
|
|
16
|
+
<button id="win-close" class="winbtn winbtn--close" title="Cerrar" aria-label="Cerrar">
|
|
17
|
+
<svg viewBox="0 0 12 12" width="12" height="12"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" /></svg>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
</header>
|
|
21
|
+
|
|
22
|
+
<main class="page">
|
|
23
|
+
<section class="hero">
|
|
24
|
+
<span class="eyebrow">Owear Uninstaller</span>
|
|
25
|
+
<h1>Desinstalar __APP_NAME__</h1>
|
|
26
|
+
<p class="lead" id="summary">Localizando la instalación…</p>
|
|
27
|
+
</section>
|
|
28
|
+
|
|
29
|
+
<section class="card">
|
|
30
|
+
<header class="card__head"><h2>Instalación</h2></header>
|
|
31
|
+
<div class="row">
|
|
32
|
+
<input id="dir" class="input" type="text" spellcheck="false" />
|
|
33
|
+
<button id="pick" class="btn">Elegir…</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="row">
|
|
36
|
+
<label class="opt"><input type="checkbox" id="keepData" checked /> Conservar mis datos</label>
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
40
|
+
<footer class="actions">
|
|
41
|
+
<span id="status" class="status"></span>
|
|
42
|
+
<button id="uninstall" class="btn btn--primary" disabled>Desinstalar</button>
|
|
43
|
+
</footer>
|
|
44
|
+
</main>
|
|
45
|
+
|
|
46
|
+
<script type="module" src="/src/uninstaller.ts"></script>
|
|
47
|
+
</body>
|
|
48
|
+
</html>
|
|
@@ -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,76 @@
|
|
|
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`.
|
|
6
|
+
|
|
7
|
+
import '../../src/style.css'
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface Window {
|
|
11
|
+
__owWindowId: number
|
|
12
|
+
ow: { invoke<T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const $ = <T extends HTMLElement = HTMLElement>(sel: string): T =>
|
|
17
|
+
document.querySelector(sel) as T
|
|
18
|
+
const invoke = <T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> =>
|
|
19
|
+
window.ow.invoke(module, fn, ...args) as Promise<T>
|
|
20
|
+
|
|
21
|
+
function setStatus(msg: string, kind: 'ok' | 'err' | '' = ''): void {
|
|
22
|
+
const el = $('#status')
|
|
23
|
+
el.textContent = msg
|
|
24
|
+
el.dataset.kind = kind
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function boot(): Promise<void> {
|
|
28
|
+
$('#win-close').addEventListener('click', () => void invoke('ow-window', 'close', window.__owWindowId))
|
|
29
|
+
|
|
30
|
+
const info = await invoke<{ appName?: string; dir?: string }>('installer', 'info').catch(() => ({}))
|
|
31
|
+
const st = await invoke<{ installed: boolean; version?: string; app?: string }>(
|
|
32
|
+
'installer',
|
|
33
|
+
'state',
|
|
34
|
+
{ dir: info.dir ?? '' },
|
|
35
|
+
).catch(() => ({ installed: false }))
|
|
36
|
+
|
|
37
|
+
$('#summary').textContent = st.installed
|
|
38
|
+
? `${st.app ?? info.appName ?? 'La app'} ${st.version ?? ''} está instalada.`
|
|
39
|
+
: 'No se detectó una instalación en el directorio por defecto; elígelo.'
|
|
40
|
+
;($('#dir') as HTMLInputElement).value = info.dir ?? ''
|
|
41
|
+
$('#uninstall').removeAttribute('disabled')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function pickDir(): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
const dir = await invoke<string | null>('dialog', 'open', 'openDirectory', 'Elegir carpeta')
|
|
47
|
+
if (dir) ($('#dir') as HTMLInputElement).value = dir
|
|
48
|
+
} catch {
|
|
49
|
+
/* cancelado */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function doUninstall(): Promise<void> {
|
|
54
|
+
const dir = ($('#dir') as HTMLInputElement).value.trim()
|
|
55
|
+
if (!dir) {
|
|
56
|
+
setStatus('Elige la carpeta de instalación', 'err')
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
$('#uninstall').setAttribute('disabled', 'true')
|
|
60
|
+
setStatus('Desinstalando…')
|
|
61
|
+
try {
|
|
62
|
+
const res = await invoke<{ removed: number }>('installer', 'uninstall', {
|
|
63
|
+
dir,
|
|
64
|
+
keepData: ($('#keepData') as HTMLInputElement).checked,
|
|
65
|
+
})
|
|
66
|
+
setStatus(`Eliminados ${res.removed} ficheros.`, 'ok')
|
|
67
|
+
setTimeout(() => void invoke('ow-window', 'close', window.__owWindowId), 900)
|
|
68
|
+
} catch (e) {
|
|
69
|
+
setStatus(e instanceof Error ? e.message : String(e), 'err')
|
|
70
|
+
$('#uninstall').removeAttribute('disabled')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
$('#pick').addEventListener('click', () => void pickDir())
|
|
75
|
+
$('#uninstall').addEventListener('click', () => void doUninstall())
|
|
76
|
+
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,81 @@
|
|
|
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 name = arg('--name', 'app')
|
|
48
|
+
|
|
49
|
+
if (!kernel || !out || !ui) die('uso: --kernel <owear> --out <bin> --ui <distUI> [--payload <dir>]')
|
|
50
|
+
if (!fs.existsSync(kernel)) die(`no existe el kernel: ${kernel}`)
|
|
51
|
+
if (!fs.existsSync(ui)) die(`no existe el ui: ${ui}`)
|
|
52
|
+
|
|
53
|
+
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'owear-installer-'))
|
|
54
|
+
const copyInto = (src, dstRel) => {
|
|
55
|
+
const dst = path.join(stage, dstRel)
|
|
56
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true })
|
|
57
|
+
fs.cpSync(src, dst, { recursive: true })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
copyInto(ui, 'ui')
|
|
61
|
+
if (payload && fs.existsSync(payload)) copyInto(payload, 'payload')
|
|
62
|
+
if (bridge && fs.existsSync(bridge)) fs.copyFileSync(bridge, path.join(stage, 'bridge.json'))
|
|
63
|
+
if (meta && fs.existsSync(meta)) fs.copyFileSync(meta, path.join(stage, 'installer.json'))
|
|
64
|
+
if (modules && fs.existsSync(modules)) copyInto(modules, 'modules')
|
|
65
|
+
|
|
66
|
+
// manifiesto mínimo del payload del instalador (informativo)
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(stage, 'manifest.json'),
|
|
69
|
+
JSON.stringify({ kind: 'installer', name, createdAt: Date.now() }, null, 2) + '\n',
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// Empaqueta con el empaquetador existente (mismo formato OWPK1).
|
|
73
|
+
const packer = path.join(__dirname, 'owear-pack.mjs')
|
|
74
|
+
const r = spawnSync(
|
|
75
|
+
process.execPath,
|
|
76
|
+
[packer, '--kernel', kernel, '--in', stage, '--out', out],
|
|
77
|
+
{ stdio: 'inherit' },
|
|
78
|
+
)
|
|
79
|
+
fs.rmSync(stage, { recursive: true, force: true })
|
|
80
|
+
if (r.status !== 0) die('falló owear-pack')
|
|
81
|
+
console.log(`[owear-installer] instalador: ${out}`)
|