@owear/cli 0.1.3 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owear/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "CLI de Owear: create, dev, build",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,8 +8,8 @@
8
8
  "owear-build-native": "./src/owear-build-native.js"
9
9
  },
10
10
  "optionalDependencies": {
11
- "@owear/linux-x64-gnu": "0.1.3",
12
- "@owear/win32-x64": "0.1.3"
11
+ "@owear/linux-x64-gnu": "workspace:*",
12
+ "@owear/win32-x64": "workspace:*"
13
13
  },
14
14
  "files": [
15
15
  "src",
@@ -17,6 +17,9 @@
17
17
  "template",
18
18
  "template-installer"
19
19
  ],
20
+ "scripts": {
21
+ "typecheck": "node --check src/ow.js && node --check src/owear-build-native.js && node --check tools/owear-pack.mjs && node --check tools/owear-installer.mjs && node --check tools/owear-linux.mjs && node --check tools/owear-msi.mjs && node --check tools/owear-update.mjs && node --check tools/owear-sign.mjs"
22
+ },
20
23
  "license": "Apache-2.0",
21
24
  "homepage": "https://owear.dev",
22
25
  "repository": {
@@ -29,8 +32,5 @@
29
32
  },
30
33
  "engines": {
31
34
  "node": ">=20"
32
- },
33
- "scripts": {
34
- "typecheck": "node --check src/ow.js && node --check src/owear-build-native.js && node --check tools/owear-pack.mjs && node --check tools/owear-installer.mjs && node --check tools/owear-linux.mjs && node --check tools/owear-msi.mjs"
35
35
  }
36
- }
36
+ }
package/src/ow.js CHANGED
@@ -148,6 +148,7 @@ async function main() {
148
148
  case 'dev': return cmdDev(rest)
149
149
  case 'build': return cmdBuild(rest)
150
150
  case 'api': return cmdApi(rest)
151
+ case 'update': return cmdUpdate(rest)
151
152
  default:
152
153
  die(`comando desconocido: ${cmd} (usa --help)`)
153
154
  }
@@ -167,9 +168,20 @@ ${C.cyan}owear${C.reset} — framework desktop nativo
167
168
  ${C.green}ow build uninstaller${C.reset} binario desinstalador
168
169
  ${C.green}ow api list${C.reset} lista las APIs del repo y sus manifiestos
169
170
  ${C.green}ow api new <nombre>${C.reset} scaffoldea una API (api/<nombre>/ + manifiesto)
171
+ ${C.green}ow update --file <bin> --version <v>${C.reset} publica un update (blockmap + manifiesto YAML firmado)
172
+ ${C.green}ow update --gen-key <ruta>${C.reset} genera un par de claves Ed25519 para firmar
170
173
 
171
174
  Variables útiles:
172
175
  OW_KERNEL_BIN ruta al binario owear
176
+ OW_SIGN_KEY clave privada Ed25519 para firmar artefactos (→ <file>.sig)
177
+ OW_SIGN_PFX certificado PKCS#12 para Authenticode (Windows PE/MSI)
178
+ OW_SIGN_PFX_PASSWORD contraseña del PFX (o --pfx-password-env)
179
+
180
+ Firma (ow build app|installer|uninstaller):
181
+ --sign-key <pem> firma Ed25519 desprendida del artefacto
182
+ --pfx <p12> Authenticode (requiere osslsigncode/signtool)
183
+ --timestamp <url> sellado de tiempo Authenticode
184
+ --require-sign falla el build si no se firma
173
185
  `)
174
186
  }
175
187
 
@@ -380,7 +392,7 @@ function findMainEntry(cwd) {
380
392
  * así que compilamos con esbuild (viene con vite). De este modo el runtime del
381
393
  * sistema sirve y el arranque no depende de la versión que tenga el usuario.
382
394
  */
383
- function prepareMain(cwd, outDir) {
395
+ function prepareMain(cwd, outDir, bundle = false) {
384
396
  const entry = findMainEntry(cwd)
385
397
  if (!entry) return null
386
398
  if (entry.endsWith('.js') || entry.endsWith('.mjs')) return entry
@@ -393,7 +405,11 @@ function prepareMain(cwd, outDir) {
393
405
  [
394
406
  'esbuild', entry,
395
407
  '--bundle', '--platform=node', '--format=esm',
396
- '--packages=external', `--outfile=${out}`, '--log-level=warning',
408
+ // En producción se EMBEBE @owear/core (y deps) en main.js: el payload de
409
+ // un single-binary/instalador no lleva node_modules. En dev se dejan
410
+ // externas (node_modules está presente).
411
+ ...(bundle ? [] : ['--packages=external']),
412
+ `--outfile=${out}`, '--log-level=warning',
397
413
  ],
398
414
  { cwd, stdio: 'inherit', shell: process.platform === 'win32' }
399
415
  )
@@ -538,7 +554,7 @@ async function cmdBuildBundle(cwd) {
538
554
  const code = await runProc('npx', ['vite', 'build'], { cwd, shell: process.platform === 'win32' })
539
555
  if (code !== 0) die('vite build falló')
540
556
 
541
- const mainJs = prepareMain(cwd, path.join(cwd, 'dist'))
557
+ const mainJs = prepareMain(cwd, path.join(cwd, 'dist'), true)
542
558
 
543
559
  const workersDir = prepareWorkers(cwd, path.join(cwd, 'dist'))
544
560
  if (workersDir) log(`workers: ${path.relative(cwd, workersDir)}`)
@@ -695,6 +711,52 @@ function runTool(name, args) {
695
711
  if (r.status !== 0) die(`falló ${name}`)
696
712
  }
697
713
 
714
+ /**
715
+ * Firma un artefacto recién construido. Authenticode si hay `--pfx`
716
+ * (`OW_SIGN_PFX`), Ed25519 desprendida (`<file>.sig`) si hay `--sign-key`
717
+ * (`OW_SIGN_KEY`). Nunca rompe el build: sin material de firma no hace nada;
718
+ * con `--require-sign` la firma pasa a ser obligatoria.
719
+ */
720
+ function signArtifact(args, file) {
721
+ const key = flag(args, '--sign-key', process.env.OW_SIGN_KEY ?? '')
722
+ const pfx = flag(args, '--pfx', process.env.OW_SIGN_PFX ?? '')
723
+ if (!key && !pfx) return
724
+ const extra = []
725
+ if (key) extra.push('--ed25519-key', key)
726
+ if (pfx) {
727
+ extra.push('--pfx', pfx, '--pfx-password-env', flag(args, '--pfx-password-env', 'OW_SIGN_PFX_PASSWORD'))
728
+ }
729
+ const ts = flag(args, '--timestamp', process.env.OW_SIGN_TIMESTAMP ?? '')
730
+ if (ts) extra.push('--timestamp', ts)
731
+ if (args.includes('--require-sign')) extra.push('--require')
732
+ runTool('owear-sign.mjs', ['--file', file, ...extra])
733
+ }
734
+
735
+ /**
736
+ * ow update — publica un update del auto-updater: calcula el blockmap (delta
737
+ * por bloques), genera el manifiesto YAML del canal y lo firma (Ed25519).
738
+ * Delega en tools/owear-update.mjs (mismos flags).
739
+ */
740
+ function cmdUpdate(args) {
741
+ if (!args.length) {
742
+ console.log(`
743
+ ${C.cyan}ow update${C.reset} — publica un update (delta + manifiesto YAML firmado)
744
+
745
+ ${C.green}ow update --gen-key owear-signing${C.reset}
746
+ genera el par de claves Ed25519 (privada <ruta>.pem; pública <ruta>.pub.b64,
747
+ que va en el bridge como ${C.dim}updater.publicKey${C.reset})
748
+
749
+ ${C.green}ow update --file release/MiApp-1.4.0 --version 1.4.0 --key owear-signing.pem${C.reset} \\
750
+ [--channel latest] [--url https://up.miapp.dev] [--notes "…"|--notes @notas.md] \\
751
+ [--block-size 262144] [--mandatory] [--out-dir release]
752
+
753
+ Salidas: ${C.dim}<file>.blockmap${C.reset} (bloques sha256) y ${C.dim}<channel>.yml${C.reset} (manifiesto YAML).
754
+ `)
755
+ return
756
+ }
757
+ runTool('owear-update.mjs', args)
758
+ }
759
+
698
760
  function readPkg(cwd) {
699
761
  try {
700
762
  return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
@@ -771,6 +833,7 @@ async function cmdBuildApp(cwd, args) {
771
833
  const out = path.join(outDir, exe)
772
834
  packDir(kernel, bundle, out)
773
835
  log(`binario único: ${out}`)
836
+ signArtifact(args, out)
774
837
  return
775
838
  }
776
839
 
@@ -799,6 +862,7 @@ async function cmdBuildApp(cwd, args) {
799
862
  runTool('owear-linux.mjs', ['deb', '--stage', stage, '--out', out, '--name', slug,
800
863
  '--version', meta.version, '--maintainer', meta.publisher, '--description', meta.appName])
801
864
  log(`.deb: ${out}`)
865
+ signArtifact(args, out)
802
866
  return
803
867
  }
804
868
 
@@ -823,6 +887,7 @@ async function cmdBuildApp(cwd, args) {
823
887
  const out = path.join(outDir, `${slug}-${meta.version}-x86_64.AppImage`)
824
888
  runTool('owear-linux.mjs', ['appimage', '--appdir', appDir, '--out', out, '--arch', 'x86_64'])
825
889
  log(`AppImage: ${out}`)
890
+ signArtifact(args, out)
826
891
  return
827
892
  }
828
893
 
@@ -833,6 +898,7 @@ async function cmdBuildApp(cwd, args) {
833
898
  runTool('owear-msi.mjs', ['--stage', stage, '--out', out, '--name', slug,
834
899
  '--version', meta.version, '--publisher', meta.publisher, '--appId', meta.appId])
835
900
  log(`.MSI: ${out}`)
901
+ signArtifact(args, out)
836
902
  return
837
903
  }
838
904
 
@@ -869,7 +935,14 @@ async function cmdBuildInstaller(cwd, args, isUninstaller) {
869
935
  // 1b) desinstalador (si existe installer/uninstaller) → se instala junto a la app
870
936
  if (fs.existsSync(path.join(cwd, 'installer', 'uninstaller'))) {
871
937
  log('build del desinstalador…')
872
- await cmdBuildInstaller(cwd, [], true)
938
+ // propaga solo los flags de firma (no --mode/--format, que no aplican)
939
+ const signArgs = []
940
+ for (const f of ['--sign-key', '--pfx', '--pfx-password-env', '--timestamp']) {
941
+ const v = flag(args, f, null)
942
+ if (v) signArgs.push(f, v)
943
+ }
944
+ if (args.includes('--require-sign')) signArgs.push('--require-sign')
945
+ await cmdBuildInstaller(cwd, signArgs, true)
873
946
  const un = path.join(outDir, `${slug}-uninstaller${exe}`)
874
947
  if (fs.existsSync(un)) fs.copyFileSync(un, path.join(payloadDir, path.basename(un)))
875
948
  }
@@ -879,11 +952,15 @@ async function cmdBuildInstaller(cwd, args, isUninstaller) {
879
952
  const ui = await buildInstallerUI(cwd, isUninstaller ? 'uninstaller' : 'installer')
880
953
 
881
954
  // 3) metadatos (installer.json / uninstaller.json) — el kernel los detecta
955
+ const iconRel = bridge.obj?.app?.icon
956
+ const iconAbs = iconRel ? path.resolve(cwd, iconRel) : ''
957
+ const iconOk = Boolean(iconAbs) && fs.existsSync(iconAbs)
882
958
  const meta = {
883
959
  appId: bridge.obj?.app?.id ?? slug,
884
960
  appName: bridge.obj?.app?.name ?? slug,
885
961
  version: bridge.obj?.app?.version ?? '0.0.0',
886
962
  publisher: bridge.obj?.app?.publisher ?? null,
963
+ icon: iconOk ? 'icon' + path.extname(iconAbs) : null,
887
964
  mode: isUninstaller ? 'uninstaller' : mode,
888
965
  layout: target.layout ?? mode,
889
966
  order: bridge.obj?.order ?? null,
@@ -909,9 +986,11 @@ async function cmdBuildInstaller(cwd, args, isUninstaller) {
909
986
  '--name', slug,
910
987
  ]
911
988
  if (payloadDir) toolArgs.push('--payload', payloadDir)
989
+ if (iconOk) toolArgs.push('--icon', iconAbs)
912
990
  const r = spawnSync(process.execPath, toolArgs, { stdio: 'inherit' })
913
991
  if (r.status !== 0) die(`falló owear-installer (${kind})`)
914
992
  log(`${kind}: ${out}`)
993
+ signArtifact(args, out)
915
994
  }
916
995
 
917
996
  main().catch((e) => die(e?.stack ?? String(e)))
@@ -3,54 +3,54 @@
3
3
 
4
4
  # __APP_NAME__
5
5
 
6
- App de escritorio construida con **[Owear](https://owear.dev)**: el WebView del
7
- sistema renderiza tu frontend web y el kernel nativo te da el sistema operativo.
6
+ Desktop app built with **[Owear](https://owear.dev)**: the system WebView renders
7
+ your web frontend and the native kernel gives you the operating system.
8
8
 
9
- ## Empezar
9
+ ## Get started
10
10
 
11
11
  ```bash
12
12
  npm install
13
13
  npm run dev # ≡ ow dev
14
14
  ```
15
15
 
16
- Se abre una ventana con el WebView del sistema. Edita `src/renderer.ts` y el
17
- hot-reload de Vite lo refleja al instante.
16
+ A window with the system WebView opens. Edit `src/renderer.ts` and Vite's
17
+ hot-reload reflects it instantly.
18
18
 
19
19
  ```bash
20
20
  npm run build # ≡ ow build → dist/
21
21
  ```
22
22
 
23
- ## Estructura
23
+ ## Structure
24
24
 
25
25
  ```
26
- ├── index.html UI (titlebar propia, hero, tarjetas)
26
+ ├── index.html UI (custom title bar, hero, cards)
27
27
  ├── src/
28
- │ ├── renderer.ts lógica del renderer + llamadas a módulos nativos
29
- │ ├── style.css estilos
30
- │ └── ow.d.ts tipos de window.ow
28
+ │ ├── renderer.ts renderer logic + calls to native modules
29
+ │ ├── style.css styles
30
+ │ └── ow.d.ts types for window.ow
31
31
  └── app/
32
- └── main.ts proceso principal (sidecar Node): sólo la ventana
32
+ └── main.ts main process (Node sidecar): just the window
33
33
  ```
34
34
 
35
- ## Cómo se llama al sistema
35
+ ## How it talks to the system
36
36
 
37
- Sin `ipcRenderer` ni `ipcMain`: el renderer invoca los módulos nativos del
38
- kernel directo.
37
+ No `ipcRenderer` and no `ipcMain`: the renderer calls the kernel's native modules
38
+ directly.
39
39
 
40
40
  ```ts
41
- const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
41
+ const path = await ow.invoke('dialog', 'open', 'open', 'Open a file')
42
42
  const text = await ow.invoke('fs', 'readText', path)
43
- await ow.invoke('notification', 'show', 'Listo', `Leídos ${text.length} bytes`)
43
+ await ow.invoke('notification', 'show', 'Done', `Read ${text.length} bytes`)
44
44
  ```
45
45
 
46
- Módulos incluidos (stock): `fs` · `path` · `process` (PTY) · `dialog` ·
47
- `clipboard` · `shell` · `screen` · `net` · `notification` · `power` · `menu` ·
48
- `globalshortcut` · `updater` · `capturer`, más los builtins `ow-window` (ventana),
49
- `window` (extras del WebView), `session`, `app` y `crashreporter`.
46
+ Stock modules: `fs` · `path` · `process` (PTY) · `dialog` · `clipboard` ·
47
+ `shell` · `screen` · `net` · `notification` · `power` · `menu` ·
48
+ `globalshortcut` · `updater` · `capturer`, plus the builtins `ow-window`
49
+ (window), `window` (WebView extras), `session`, `app`, and `crashreporter`.
50
50
 
51
- ## Añadir un módulo nativo propio (C++)
51
+ ## Adding your own native module (C++)
52
52
 
53
- 1. Crea `native/mi_modulo.cpp`:
53
+ 1. Create `native/my_module.cpp`:
54
54
 
55
55
  ```cpp
56
56
  #include <ow/Json.h>
@@ -61,17 +61,23 @@ Módulos incluidos (stock): `fs` · `path` · `process` (PTY) · `dialog` ·
61
61
  ow::Module::RespondOk(res, "\"pong\"");
62
62
  }
63
63
 
64
- OW_MODULE_BEGIN(mi_modulo, "1.0.0")
64
+ OW_MODULE_BEGIN(my_module, "1.0.0")
65
65
  OW_FN(ping)
66
66
  OW_MODULE_END()
67
67
  ```
68
68
 
69
- 2. `ow dev` lo compila a `.owm` y genera el binding. Úsalo en el renderer:
69
+ 2. `ow dev` compiles it to `.owm` and generates the binding. Use it in the
70
+ renderer:
70
71
 
71
72
  ```ts
72
- import { mi_modulo } from '@owear/native'
73
- await mi_modulo.ping()
73
+ import { my_module } from '@owear/native'
74
+ await my_module.ping()
74
75
  ```
75
76
 
76
- > Requiere un compilador de C++ en el sistema. Si no lo tienes, no añadas
77
- > `native/` y la app sigue funcionando con los módulos stock.
77
+ > Requires a C++ compiler on the system. If you do not have one, do not add
78
+ > `native/` and the app keeps working with the stock modules.
79
+
80
+ ## Learn more
81
+
82
+ The full documentation lives in the Owear repository under
83
+ [`docs/`](https://github.com/owear/owear/tree/main/docs).
@@ -3,21 +3,290 @@
3
3
  //
4
4
  // app/main.ts — proceso principal (sidecar Node), estilo Electron.
5
5
  //
6
- // Sólo gestiona el ciclo de vida de la ventana. La app en sí vive en el
7
- // renderer, que llama a los módulos nativos directo. Aquí podrías añadir
8
- // lógica privilegiada (por ejemplo, exponerla vía un módulo nativo propio).
6
+ // Sólo gestiona el ciclo de vida de la ventana. La app vive en el renderer,
7
+ // que llama a los módulos nativos directo.
9
8
 
10
- import { app, BrowserWindow } from '@owear/core'
9
+ import { app, BrowserWindow, Menu, Tray, invokeNative, nativeImage, theme, screen, powerMonitor, powerSaveBlocker } from '@owear/core'
10
+ import * as fs from 'node:fs'
11
+ import * as os from 'node:os'
12
+ import * as path from 'node:path'
11
13
 
12
14
  app.whenReady().then(() => {
15
+ // Icono de la app: `favicon.svg` del proyecto (Vite lo copia a dist/ en build).
16
+ // `app.setIcon` fija el icono por defecto de las ventanas creadas después.
17
+ // (API de icono del kernel: `window.setIcon`.)
18
+ const iconPath = [
19
+ path.join(app.getAppPath(), 'favicon.svg'), // empaquetado: dist/ → app/
20
+ path.join(process.cwd(), 'public', 'favicon.svg'), // dev: public/ del proyecto
21
+ ].find((p) => fs.existsSync(p))
22
+ if (iconPath) app.setIcon(iconPath)
23
+
13
24
  const win = new BrowserWindow({
14
25
  title: '__APP_NAME__',
15
26
  width: 1080,
16
27
  height: 720,
17
28
  titleBarStyle: 'custom', // titlebar propia + regiones [data-ow-drag]
29
+ // Botones de ventana nativos (min/max/close) dentro de la titlebar custom.
30
+ // En Linux son los del TEMA (la distro define forma/tamaño/hover).
31
+ // Opcional: color = fondo de la banda · symbolColor = glifo ·
32
+ // buttonColor = fondo interno del círculo (si no, los del tema).
33
+ titleBarOverlay: { height: 40, color: '#21252b' },
18
34
  url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
19
35
  })
20
36
 
21
37
  win.on('ready-to-show', () => console.log('[main] ventana lista, id =', win.id))
22
38
  win.on('closed', () => app.quit())
39
+
40
+ // ── C3: webContents (capturePage, eventos, setWindowOpenHandler) ─────────
41
+ app.handle('starter.capture', async () => {
42
+ const img = await win.webContents.capturePage()
43
+ return { data: img.toPNG().toString('base64') }
44
+ })
45
+ win.webContents.on('did-finish-load', () =>
46
+ void app.send('webContents.event', 'did-finish-load')
47
+ )
48
+ win.webContents.on('before-input-event', (e: { key?: string }) =>
49
+ void app.send('webContents.event', `before-input-event ${e?.key ?? ''}`)
50
+ )
51
+ win.webContents.setWindowOpenHandler(({ url }) => {
52
+ void app.send('webContents.event', `window-open ${url}`)
53
+ return { action: 'deny' }
54
+ })
55
+
56
+ // ── C4: nativeImage (captura → resize/crop/toPNG/dataURL) ────────────────
57
+ app.handle('starter.nativeImage', async () => {
58
+ const shot = await win.webContents.capturePage() // NativeImage
59
+ const small = shot.resize({ width: 64 })
60
+ const fromBuffer = nativeImage.createFromBuffer(shot.toPNG())
61
+ const fromUrl = nativeImage.createFromDataURL(small.toDataURL())
62
+ const cropped = shot.crop({ x: 0, y: 0, width: 16, height: 16 })
63
+ return {
64
+ original: shot.getSize(),
65
+ resized: small.getSize(),
66
+ cropped: cropped.getSize(),
67
+ pngBytes: small.toPNG().length,
68
+ reDecoded: fromBuffer.getSize(),
69
+ fromDataURL: fromUrl.getSize(),
70
+ empty: small.isEmpty(),
71
+ }
72
+ })
73
+
74
+ // ── C5: menu (contextual + menubar) ──────────────────────────────────────
75
+ // Menubar (Windows nativo; noop en Linux/GNOME por diseño).
76
+ Menu.setApplicationMenu(
77
+ Menu.buildFromTemplate([
78
+ {
79
+ label: 'Archivo',
80
+ submenu: [
81
+ { label: 'Nuevo', accelerator: 'CmdOrCtrl+N', click: () => void app.send('menu.event', 'Nuevo') },
82
+ { type: 'separator' },
83
+ { role: 'quit' },
84
+ ],
85
+ },
86
+ {
87
+ label: 'Ver',
88
+ submenu: [
89
+ {
90
+ label: 'Barra lateral',
91
+ type: 'checkbox',
92
+ checked: true,
93
+ click: (mi) => void app.send('menu.event', `sidebar=${mi.checked}`),
94
+ },
95
+ { type: 'separator' },
96
+ { role: 'toggleDevTools' },
97
+ { role: 'reload' },
98
+ ],
99
+ },
100
+ ])
101
+ )
102
+
103
+ // Menú contextual (popup) disparado por un botón del starter.
104
+ app.handle('starter.menu', () => {
105
+ Menu.buildFromTemplate([
106
+ { label: 'Nuevo', click: () => void app.send('menu.event', 'Nuevo') },
107
+ { label: 'Abrir', accelerator: 'CmdOrCtrl+O', click: () => void app.send('menu.event', 'Abrir') },
108
+ { type: 'separator' },
109
+ {
110
+ label: 'Barra lateral',
111
+ type: 'checkbox',
112
+ checked: true,
113
+ click: (mi) => void app.send('menu.event', `checkbox=${mi.checked}`),
114
+ },
115
+ { label: 'Zona A', type: 'radio', checked: true, click: () => void app.send('menu.event', 'radio A') },
116
+ { label: 'Zona B', type: 'radio', click: () => void app.send('menu.event', 'radio B') },
117
+ { type: 'separator' },
118
+ { role: 'copy' },
119
+ { role: 'toggleDevTools' },
120
+ { role: 'quit' },
121
+ ]).popup({ window: win })
122
+ return null
123
+ })
124
+
125
+ // ── C6: tray (icono de bandeja) ──────────────────────────────────────────
126
+ let tray: Tray | null = null
127
+ app.handle('starter.tray', async () => {
128
+ if (tray) return { created: false, id: tray.id }
129
+ const icon = (await win.webContents.capturePage()).resize({ width: 16, height: 16 })
130
+ tray = new Tray(icon)
131
+ tray.setToolTip('__APP_NAME__')
132
+ tray.setTitle('__APP_NAME__')
133
+ tray.setContextMenu(
134
+ Menu.buildFromTemplate([
135
+ { label: 'Mostrar ventana', click: () => void win.show() },
136
+ { label: 'Capturar página', click: () => void app.send('tray.event', 'capturar') },
137
+ { type: 'separator' },
138
+ { role: 'quit' },
139
+ ])
140
+ )
141
+ tray.on('click', () => {
142
+ void win.show()
143
+ void app.send('tray.event', 'click')
144
+ })
145
+ tray.on('right-click', () => void app.send('tray.event', 'right-click'))
146
+ tray.on('double-click', () => void app.send('tray.event', 'double-click'))
147
+ return { created: true, id: tray.id }
148
+ })
149
+ app.handle('starter.trayDestroy', () => {
150
+ tray?.destroy()
151
+ tray = null
152
+ return null
153
+ })
154
+
155
+ // ── C7: nativeTheme (leer/forzar/escuchar) ───────────────────────────────
156
+ app.handle('starter.themeGet', () => theme.get())
157
+ app.handle('starter.themeSet', async (src: string) => {
158
+ await theme.setSource((src as 'system' | 'light' | 'dark') ?? 'system')
159
+ return theme.get()
160
+ })
161
+ void theme.watch() // cambios del sistema → ow.on('theme.changed')
162
+
163
+ // ── C8: print / printToPDF ───────────────────────────────────────────────
164
+ app.handle('starter.printPdf', async () => {
165
+ try {
166
+ const pdf = await win.webContents.printToPDF()
167
+ const out = path.join(os.tmpdir(), 'owear-starter.pdf')
168
+ fs.writeFileSync(out, pdf)
169
+ void invokeNative('shell', 'showItemInFolder', out)
170
+ return { ok: true, bytes: pdf.length, path: out }
171
+ } catch (e) {
172
+ return { ok: false, error: e instanceof Error ? e.message : String(e) }
173
+ }
174
+ })
175
+ app.handle('starter.print', () => {
176
+ win.webContents.print()
177
+ return null
178
+ })
179
+
180
+ // ── C9: BrowserWindow completo ───────────────────────────────────────────
181
+ let child: BrowserWindow | null = null
182
+ app.handle('starter.childWindow', () => {
183
+ if (child) {
184
+ void child.focus()
185
+ return { id: child.id }
186
+ }
187
+ child = new BrowserWindow({
188
+ title: 'Ventana secundaria',
189
+ width: 520,
190
+ height: 360,
191
+ parent: win.id ?? undefined,
192
+ backgroundColor: '#12161c',
193
+ url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
194
+ })
195
+ child.on('closed', () => {
196
+ child = null
197
+ })
198
+ return { id: child.id }
199
+ })
200
+ app.handle('starter.toggleOnTop', async () => {
201
+ const on = await win.isAlwaysOnTop()
202
+ await win.setAlwaysOnTop(!on)
203
+ return { on: !on }
204
+ })
205
+ app.handle('starter.progress', async () => {
206
+ await win.setProgressBar(0.5)
207
+ setTimeout(() => void win.setProgressBar(-1, { mode: 'none' }), 4000)
208
+ return null
209
+ })
210
+ app.handle('starter.ignoreMouse', () => {
211
+ void win.setIgnoreMouseEvents(true, { forward: true })
212
+ setTimeout(() => void win.setIgnoreMouseEvents(false), 3000)
213
+ return null
214
+ })
215
+ app.handle('starter.windows', async () => {
216
+ const focus = await BrowserWindow.getFocusedWindow()
217
+ const all = await BrowserWindow.getAllWindows()
218
+ return { focused: focus?.id ?? null, count: all.length, ids: all.map((w) => w.id) }
219
+ })
220
+
221
+ // ── C10: pantallas (multi-monitor) y energía ─────────────────────────────
222
+ app.handle('starter.screens', async () => {
223
+ await screen.watch()
224
+ const displays = await screen.getAllDisplays()
225
+ const primary = displays.find((d) => d.primary) ?? displays[0] ?? null
226
+ const cursor = await screen.getCursorScreenPoint()
227
+ return { displays, primaryId: primary?.id ?? null, cursor }
228
+ })
229
+ app.handle('starter.nearest', async () => {
230
+ const cursor = await screen.getCursorScreenPoint()
231
+ const display = await screen.getDisplayNearestPoint(cursor)
232
+ return { cursor, display }
233
+ })
234
+ app.handle('starter.powerOn', async () => {
235
+ await powerMonitor.watch()
236
+ const onBattery = await powerMonitor.isOnBatteryPower()
237
+ const idle = await powerMonitor.getIdleTime()
238
+ return { onBattery, idle }
239
+ })
240
+ app.handle('starter.idle', async () => {
241
+ const idleTime = await powerMonitor.getIdleTime()
242
+ const state = await powerMonitor.getIdleState(30)
243
+ return { idleTime, state }
244
+ })
245
+ let blockerId: number | null = null
246
+ app.handle('starter.blocker', () => {
247
+ if (blockerId != null) {
248
+ const ok = powerSaveBlocker.stop(blockerId)
249
+ blockerId = null
250
+ return { on: false, ok }
251
+ }
252
+ blockerId = powerSaveBlocker.start('prevent-display-sleep')
253
+ return { on: true, id: blockerId }
254
+ })
255
+ })
256
+
257
+ // ── C1: exponer `app` al renderer para probarlo en el starter ──────────────
258
+ // El renderer llama `ow.invoke('node','call',{fn:'starter.*'})`.
259
+ app.handle('starter.appInfo', () => ({
260
+ name: app.getName(),
261
+ version: app.getVersion(),
262
+ packaged: app.isPackaged(),
263
+ appPath: app.getAppPath(),
264
+ }))
265
+
266
+ const APP_PATH_NAMES = [
267
+ 'home',
268
+ 'userData',
269
+ 'temp',
270
+ 'logs',
271
+ 'downloads',
272
+ 'documents',
273
+ 'exe',
274
+ 'appPath',
275
+ ] as const
276
+
277
+ app.handle('starter.appPaths', () => {
278
+ const out: Record<string, string> = {}
279
+ for (const name of APP_PATH_NAMES) out[name] = app.getPath(name)
280
+ return out
23
281
  })
282
+
283
+ // Ciclo de vida de la app → renderer (app.send → ow.on('app.event')).
284
+ for (const ev of [
285
+ 'window-all-closed',
286
+ 'before-quit',
287
+ 'will-quit',
288
+ 'second-instance',
289
+ 'child-process-gone',
290
+ ] as const) {
291
+ app.on(ev, () => app.send('app.event', ev))
292
+ }