@owear/cli 0.1.0 → 0.1.1

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.0",
3
+ "version": "0.1.1",
4
4
  "description": "CLI de Owear: create, dev, build",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "owear-build-native": "./src/owear-build-native.js"
9
9
  },
10
10
  "optionalDependencies": {
11
- "@owear/linux-x64-gnu": "0.1.0"
11
+ "@owear/linux-x64-gnu": "0.1.1"
12
12
  },
13
13
  "files": [
14
14
  "src",
package/src/ow.js CHANGED
@@ -90,9 +90,9 @@ function findKernelBin(cwd = process.cwd()) {
90
90
  const rt = runtimePackageDir()
91
91
  const candidates = [
92
92
  path.join(cwd, '.owear', 'bin', exe),
93
- ...(rt ? [path.join(rt, 'bin', exe)] : []),
94
- // monorepo en desarrollo
93
+ // monorepo en desarrollo: el kernel recién compilado manda sobre el paquete npm
95
94
  path.resolve(__dirname, '../../../build', platformPreset(), 'src', exe),
95
+ ...(rt ? [path.join(rt, 'bin', exe)] : []),
96
96
  ]
97
97
  for (const c of candidates) if (fs.existsSync(c)) return c
98
98
  return null
@@ -171,11 +171,41 @@ function cmdCreate(args) {
171
171
  die(`template no encontrado en ${templateDir} (instala @owear/cli completo)`)
172
172
  }
173
173
  fs.cpSync(templateDir, target, { recursive: true })
174
+ applyAppName(target)
174
175
  log(`app creada en ${target}`)
175
176
  log('siguientes pasos:')
176
177
  console.log(` cd ${path.basename(target)}`)
177
- console.log(' pnpm install')
178
- console.log(' pnpm dev')
178
+ console.log(' npm install')
179
+ console.log(' npm run dev')
180
+ }
181
+
182
+ /**
183
+ * Sustituye los placeholders del template por el nombre de la app
184
+ * (__APP_NAME__ legible, __PKG_NAME__ válido como nombre de paquete npm).
185
+ */
186
+ function applyAppName(target) {
187
+ const appName = path.basename(target)
188
+ const pkgName =
189
+ appName
190
+ .toLowerCase()
191
+ .replace(/[^a-z0-9._-]+/g, '-')
192
+ .replace(/^[-._]+|[-._]+$/g, '') || 'owear-app'
193
+ const extRe = /\.(ts|tsx|js|mjs|json|html|css|md|txt|ya?ml)$/
194
+ const walk = (dir) => {
195
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
196
+ if (entry.name === 'node_modules') continue
197
+ const p = path.join(dir, entry.name)
198
+ if (entry.isDirectory()) walk(p)
199
+ else if (extRe.test(entry.name)) {
200
+ const src = fs.readFileSync(p, 'utf8')
201
+ const out = src
202
+ .replaceAll('__APP_NAME__', appName)
203
+ .replaceAll('__PKG_NAME__', pkgName)
204
+ if (out !== src) fs.writeFileSync(p, out)
205
+ }
206
+ }
207
+ }
208
+ walk(target)
179
209
  }
180
210
 
181
211
  // ── ow api — gestión de las APIs del framework ──────────────────────────────
@@ -0,0 +1,74 @@
1
+ # __APP_NAME__
2
+
3
+ App de escritorio construida con **[Owear](https://owear.dev)**: el WebView del
4
+ sistema renderiza tu frontend web y el kernel nativo te da el sistema operativo.
5
+
6
+ ## Empezar
7
+
8
+ ```bash
9
+ npm install
10
+ npm run dev # ≡ ow dev
11
+ ```
12
+
13
+ Se abre una ventana con el WebView del sistema. Edita `src/renderer.ts` y el
14
+ hot-reload de Vite lo refleja al instante.
15
+
16
+ ```bash
17
+ npm run build # ≡ ow build → dist/
18
+ ```
19
+
20
+ ## Estructura
21
+
22
+ ```
23
+ ├── index.html UI (titlebar propia, hero, tarjetas)
24
+ ├── src/
25
+ │ ├── renderer.ts lógica del renderer + llamadas a módulos nativos
26
+ │ ├── style.css estilos
27
+ │ └── ow.d.ts tipos de window.ow
28
+ └── app/
29
+ └── main.ts proceso principal (sidecar Node): sólo la ventana
30
+ ```
31
+
32
+ ## Cómo se llama al sistema
33
+
34
+ Sin `ipcRenderer` ni `ipcMain`: el renderer invoca los módulos nativos del
35
+ kernel directo.
36
+
37
+ ```ts
38
+ const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
39
+ const text = await ow.invoke('fs', 'readText', path)
40
+ await ow.invoke('notification', 'show', 'Listo', `Leídos ${text.length} bytes`)
41
+ ```
42
+
43
+ Módulos incluidos (stock): `fs` · `path` · `process` (PTY) · `dialog` ·
44
+ `clipboard` · `shell` · `screen` · `net` · `notification` · `power` · `menu` ·
45
+ `globalshortcut` · `updater` · `capturer`, más los builtins `ow-window` (ventana),
46
+ `window` (extras del WebView), `session`, `app` y `crashreporter`.
47
+
48
+ ## Añadir un módulo nativo propio (C++)
49
+
50
+ 1. Crea `native/mi_modulo.cpp`:
51
+
52
+ ```cpp
53
+ #include <ow/Json.h>
54
+ #include <ow/Module.h>
55
+ #include "ow_api.h"
56
+
57
+ static void ping(const ow_request_t*, ow_response_t* res) {
58
+ ow::Module::RespondOk(res, "\"pong\"");
59
+ }
60
+
61
+ OW_MODULE_BEGIN(mi_modulo, "1.0.0")
62
+ OW_FN(ping)
63
+ OW_MODULE_END()
64
+ ```
65
+
66
+ 2. `ow dev` lo compila a `.owm` y genera el binding. Úsalo en el renderer:
67
+
68
+ ```ts
69
+ import { mi_modulo } from '@owear/native'
70
+ await mi_modulo.ping()
71
+ ```
72
+
73
+ > Requiere un compilador de C++ en el sistema. Si no lo tienes, no añadas
74
+ > `native/` y la app sigue funcionando con los módulos stock.
@@ -1,29 +1,20 @@
1
- // Copyright 2026 Owear Contributors
2
- // SPDX-License-Identifier: Apache-2.0
1
+ // app/main.ts — proceso principal (sidecar Node), estilo Electron.
3
2
  //
4
- // main.ts — proceso principal (sidecar Node). Estilo Electron.
5
- //
6
- import { app, BrowserWindow } from '@owear/core'
3
+ // Sólo gestiona el ciclo de vida de la ventana. La app en sí vive en el
4
+ // renderer, que llama a los módulos nativos directo. Aquí podrías añadir
5
+ // lógica privilegiada (por ejemplo, exponerla vía un módulo nativo propio).
7
6
 
8
- let windows = 0
7
+ import { app, BrowserWindow } from '@owear/core'
9
8
 
10
9
  app.whenReady().then(() => {
11
10
  const win = new BrowserWindow({
12
- title: 'Mi App Owear',
13
- width: 1100,
11
+ title: '__APP_NAME__',
12
+ width: 1080,
14
13
  height: 720,
15
- titleBarStyle: 'custom', // titlebar custom + drag regions CSS
14
+ titleBarStyle: 'custom', // titlebar propia + regiones [data-ow-drag]
16
15
  url: process.env.OW_DEV_SERVER_URL ?? 'app://index.html',
17
16
  })
18
- windows++
19
17
 
20
- win.on('closed', () => {
21
- windows--
22
- if (windows === 0) app.quit()
23
- })
24
-
25
- // ejemplo: evaluar JS en la página
26
- win.on('ready-to-show', async () => {
27
- console.log('[main] ventana lista, id =', win.id)
28
- })
18
+ win.on('ready-to-show', () => console.log('[main] ventana lista, id =', win.id))
19
+ win.on('closed', () => app.quit())
29
20
  })
@@ -1,24 +1,82 @@
1
1
  <!doctype html>
2
2
  <html lang="es">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Mi App Owear</title>
7
- </head>
8
- <body>
9
- <header data-ow-drag class="titlebar">
10
- <span>Mi App Owear</span>
11
- <nav>
12
- <button data-ow-no-drag onclick="ow.invoke('ow-window','minimize', window.__owWindowId)">–</button>
13
- <button data-ow-no-drag onclick="ow.invoke('ow-window','maximize', window.__owWindowId, true)">□</button>
14
- <button data-ow-no-drag onclick="ow.invoke('ow-window','close', window.__owWindowId)">✕</button>
15
- </nav>
16
- </header>
17
- <main>
18
- <h1>Funciona 🚀</h1>
19
- <p id="demo">invocando módulo nativo…</p>
20
- <pre id="out"></pre>
21
- </main>
22
- <script type="module" src="/src/renderer.ts"></script>
23
- </body>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__APP_NAME__</title>
7
+ </head>
8
+ <body>
9
+ <!-- Titlebar propia: el kernel convierte [data-ow-drag] en zona de arrastre.
10
+ Los controles van FUERA de esa zona para no iniciar drag al pulsarlos. -->
11
+ <header class="titlebar">
12
+ <div class="titlebar__drag" data-ow-drag>
13
+ <span class="brand__mark" aria-hidden="true"></span>
14
+ <span class="brand__name">__APP_NAME__</span>
15
+ </div>
16
+ <div class="titlebar__controls">
17
+ <button id="win-min" class="winbtn" title="Minimizar" aria-label="Minimizar">
18
+ <svg viewBox="0 0 12 12" width="12" height="12"><path d="M2 6h8" /></svg>
19
+ </button>
20
+ <button id="win-max" class="winbtn" title="Maximizar" aria-label="Maximizar">
21
+ <svg viewBox="0 0 12 12" width="12" height="12"><rect x="2.5" y="2.5" width="7" height="7" rx="1" /></svg>
22
+ </button>
23
+ <button id="win-close" class="winbtn winbtn--close" title="Cerrar" aria-label="Cerrar">
24
+ <svg viewBox="0 0 12 12" width="12" height="12"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7" /></svg>
25
+ </button>
26
+ </div>
27
+ </header>
28
+
29
+ <main class="page">
30
+ <section class="hero">
31
+ <span class="eyebrow">Owear</span>
32
+ <h1>__APP_NAME__</h1>
33
+ <p class="lead">
34
+ Frontend web, sistema nativo. Estos botones llaman <strong>directo</strong> a los
35
+ módulos del kernel — sin <code>ipcRenderer</code> ni <code>ipcMain</code>.
36
+ </p>
37
+ <div class="actions">
38
+ <button class="btn btn--primary" data-action="open">Abrir archivo…</button>
39
+ <button class="btn" data-action="notify">Notificar</button>
40
+ <button class="btn" data-action="copy">Copiar ruta</button>
41
+ <button class="btn" data-action="reveal">Mostrar en carpeta</button>
42
+ <button class="btn btn--ghost" data-action="about">Acerca de</button>
43
+ </div>
44
+ </section>
45
+
46
+ <section class="grid">
47
+ <article class="card">
48
+ <header class="card__head">
49
+ <h2>Entorno</h2>
50
+ <span class="badge" id="env-window">—</span>
51
+ </header>
52
+ <dl class="kv">
53
+ <div><dt>Plataforma</dt><dd id="env-platform">—</dd></div>
54
+ <div><dt>Motor</dt><dd id="env-engine">—</dd></div>
55
+ <div><dt>Escala</dt><dd id="env-dpr">—</dd></div>
56
+ </dl>
57
+ <p class="ua" id="env-ua">—</p>
58
+ </article>
59
+
60
+ <article class="card card--grow">
61
+ <header class="card__head">
62
+ <h2>Consola</h2>
63
+ <button class="linkbtn" data-action="clear">Limpiar</button>
64
+ </header>
65
+ <pre id="console" class="console">Listo. Pulsa «Abrir archivo…» para invocar un módulo nativo.</pre>
66
+ </article>
67
+ </section>
68
+
69
+ <section class="card">
70
+ <header class="card__head"><h2>Cómo funciona</h2></header>
71
+ <p class="hint">
72
+ Cada botón invoca un módulo nativo con <code>ow.invoke()</code>.
73
+ </p>
74
+ <pre class="code"><code>const path = await ow.invoke('dialog', 'open', 'open', 'Abrir archivo')
75
+ const text = await ow.invoke('fs', 'readText', path)
76
+ await ow.invoke('notification', 'show', 'Listo', 'Leídos ' + text.length + ' bytes')</code></pre>
77
+ </section>
78
+ </main>
79
+
80
+ <script type="module" src="/src/renderer.ts"></script>
81
+ </body>
24
82
  </html>
@@ -1,11 +1,13 @@
1
1
  {
2
- "name": "mi-app-owear",
2
+ "name": "__PKG_NAME__",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
+ "description": "App de escritorio construida con Owear.",
6
7
  "scripts": {
7
8
  "dev": "ow dev",
8
- "build": "ow build"
9
+ "build": "ow build",
10
+ "typecheck": "tsc --noEmit"
9
11
  },
10
12
  "dependencies": {
11
13
  "@owear/core": "^0.1.0"
@@ -13,6 +15,7 @@
13
15
  "devDependencies": {
14
16
  "@owear/cli": "^0.1.0",
15
17
  "@owear/vite-plugin": "^0.1.0",
18
+ "@types/node": "^20.14.0",
16
19
  "esbuild": "^0.25.0",
17
20
  "typescript": "^5.6.0",
18
21
  "vite": "^6.0.0"
@@ -0,0 +1,22 @@
1
+ // Tipos de la API que el kernel inyecta en cada documento (`window.ow`).
2
+ // Mantenidos a mano contra docs/API.md §1.
3
+
4
+ export {}
5
+
6
+ declare global {
7
+ interface Window {
8
+ __owWindowId: number
9
+ ow: {
10
+ invoke(module: string, fn: string, ...args: unknown[]): Promise<unknown>
11
+ invokeSync(module: string, fn: string, ...args: unknown[]): unknown
12
+ readShared(handle: { id: string; size: number }): Promise<ArrayBuffer>
13
+ on(name: string, cb: (payload: unknown) => void): () => void
14
+ emit(name: string, payload?: unknown): void
15
+ emitTo(targetWindowId: number, name: string, payload?: unknown): void
16
+ findInPage(
17
+ text: string,
18
+ opts?: { matchCase?: boolean; backwards?: boolean }
19
+ ): { matches: number; active: number }
20
+ }
21
+ }
22
+ }
@@ -1,30 +1,141 @@
1
- // Copyright 2026 Owear Contributors
2
- // SPDX-License-Identifier: Apache-2.0
1
+ // src/renderer.ts — corre dentro del WebView. `ow` lo inyecta el kernel.
3
2
  //
4
- // renderer.ts — corre dentro del WebView. `ow` lo inyecta el kernel.
5
- //
6
- import { files } from '@owear/native'
3
+ // Aquí no hay `ipcRenderer` ni handlers en el main: el renderer llama a los
4
+ // módulos nativos del framework DIRECTO (mismo salto WebView → kernel).
5
+
6
+ import './style.css'
7
+
8
+ const APP = document.title || 'Owear App'
9
+
10
+ const $ = <T extends HTMLElement = HTMLElement>(sel: string): T =>
11
+ document.querySelector(sel) as T
12
+
13
+ const invoke = <T = unknown>(module: string, fn: string, ...args: unknown[]): Promise<T> =>
14
+ window.ow.invoke(module, fn, ...args) as Promise<T>
15
+
16
+ let lastPath: string | null = null
17
+
18
+ // ── Consola ─────────────────────────────────────────────────────────────
19
+ const consoleEl = $('#console')
20
+
21
+ function log(msg: string, kind: 'ok' | 'err' | 'muted' = 'muted'): void {
22
+ const time = new Date().toLocaleTimeString()
23
+ const line = document.createElement('span')
24
+ line.className = `line--${kind}`
25
+ line.textContent = `[${time}] ${msg}\n`
26
+ consoleEl.appendChild(line)
27
+ consoleEl.scrollTop = consoleEl.scrollHeight
28
+ }
29
+
30
+ function logError(err: unknown): void {
31
+ log(err instanceof Error ? err.message : String(err), 'err')
32
+ }
33
+
34
+ // ── Entorno ─────────────────────────────────────────────────────────────
35
+ function renderEnvironment(): void {
36
+ const ua = navigator.userAgent
37
+ const engine = /AppleWebKit\/([\d.]+)/.exec(ua)?.[1]
38
+ $('#env-platform').textContent = navigator.platform || '—'
39
+ $('#env-engine').textContent = engine ? `WebKit ${engine}` : '—'
40
+ $('#env-dpr').textContent = `${window.devicePixelRatio}×`
41
+ $('#env-window').textContent = `#${window.__owWindowId}`
42
+ $('#env-ua').textContent = ua
43
+ }
7
44
 
8
- const out = document.getElementById('out')!
9
- const demo = document.getElementById('demo')!
45
+ // ── Acciones (módulos nativos) ──────────────────────────────────────────
46
+ async function openFile(): Promise<void> {
47
+ const path = await invoke<string | null>('dialog', 'open', 'open', 'Abrir un archivo')
48
+ if (!path) {
49
+ log('dialog.open → cancelado')
50
+ return
51
+ }
52
+ lastPath = path
53
+ const stat = await invoke<{ size: number }>('fs', 'stat', path)
54
+ const text = await invoke<string>('fs', 'readText', path)
55
+ log(`dialog.open → ${path}`, 'ok')
56
+ log(`fs.readText → ${stat.size} bytes`, 'ok')
10
57
 
11
- async function main() {
12
- try {
13
- // módulo nativo PROPIO (native/files.cpp)
14
- const text = await files.readText('/etc/hostname')
15
- demo.textContent = `hostname leído desde C++ nativo: ${text.trim()}`
58
+ const clean = text.replace(/\u0000/g, '')
59
+ const preview = clean.length > 6000 ? `${clean.slice(0, 6000)}\n… (vista truncada)` : clean
60
+ consoleEl.textContent = ''
61
+ log(`── ${path} (${stat.size} bytes) ──`, 'muted')
62
+ const body = document.createElement('span')
63
+ body.style.color = '#c8d4e0'
64
+ body.textContent = preview + '\n'
65
+ consoleEl.appendChild(body)
66
+ consoleEl.scrollTop = 0
67
+ }
16
68
 
17
- // módulo stock fs
18
- const st = await ow.invoke('fs', 'stat', '/etc/hosts')
19
- out.textContent = JSON.stringify(st, null, 2)
69
+ async function notifyUser(): Promise<void> {
70
+ await invoke('notification', 'show', APP, 'Los módulos nativos funcionan sin IPC.', APP)
71
+ log('notification.show → notificación enviada', 'ok')
72
+ }
20
73
 
21
- // eventos de ventana
22
- ow.on('resize', (b) => {
23
- document.title = `${b.width}×${b.height}`
74
+ async function copyPath(): Promise<void> {
75
+ if (!lastPath) {
76
+ log('Abre un archivo antes de copiar.', 'err')
77
+ return
78
+ }
79
+ await invoke('clipboard', 'writeText', lastPath)
80
+ log(`clipboard.writeText → ${lastPath}`, 'ok')
81
+ }
82
+
83
+ async function revealPath(): Promise<void> {
84
+ if (!lastPath) {
85
+ log('Abre un archivo antes de mostrarlo.', 'err')
86
+ return
87
+ }
88
+ await invoke('shell', 'showItemInFolder', lastPath)
89
+ log(`shell.showItemInFolder → ${lastPath}`, 'ok')
90
+ }
91
+
92
+ async function about(): Promise<void> {
93
+ await invoke('dialog', 'messageBox', 'info', `Acerca de ${APP}`, `${APP}\n\nConstruido con Owear.`, [
94
+ 'Cerrar',
95
+ ])
96
+ log('dialog.messageBox → cerrado', 'ok')
97
+ }
98
+
99
+ // ── Titlebar (builtin ow-window) ────────────────────────────────────────
100
+ function wireTitlebar(): void {
101
+ const id = window.__owWindowId
102
+ $('#win-min').addEventListener('click', () => void invoke('ow-window', 'minimize', id))
103
+ $('#win-max').addEventListener('click', async () => {
104
+ const max = await invoke<boolean>('ow-window', 'isMaximized', id)
105
+ void invoke('ow-window', 'maximize', id, !max)
106
+ })
107
+ $('#win-close').addEventListener('click', () => void invoke('ow-window', 'close', id))
108
+ }
109
+
110
+ // ── Arranque ────────────────────────────────────────────────────────────
111
+ const actions: Record<string, () => void | Promise<void>> = {
112
+ open: openFile,
113
+ notify: notifyUser,
114
+ copy: copyPath,
115
+ reveal: revealPath,
116
+ about,
117
+ clear: () => {
118
+ consoleEl.textContent = ''
119
+ log('Consola limpia.')
120
+ },
121
+ }
122
+
123
+ function boot(): void {
124
+ wireTitlebar()
125
+ renderEnvironment()
126
+
127
+ for (const btn of document.querySelectorAll<HTMLElement>('[data-action]')) {
128
+ const action = actions[btn.dataset.action ?? '']
129
+ if (!action) continue
130
+ btn.addEventListener('click', () => {
131
+ Promise.resolve(action()).catch(logError)
24
132
  })
25
- } catch (e) {
26
- demo.textContent = '✗ ' + (e as Error).message
27
133
  }
134
+
135
+ window.addEventListener('resize', renderEnvironment)
136
+ window.ow.on('resize', renderEnvironment)
137
+
138
+ log(`Renderer listo. Ventana ${window.__owWindowId}.`, 'ok')
28
139
  }
29
140
 
30
- main()
141
+ boot()
@@ -0,0 +1,338 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ --bg: #0d1117;
4
+ --panel: #161b22;
5
+ --panel-2: #0f141a;
6
+ --line: #262d38;
7
+ --line-2: #30363d;
8
+ --text: #e6edf3;
9
+ --muted: #8b949e;
10
+ --accent: #3b82f6;
11
+ --accent-hover: #2f6fd0;
12
+ --danger: #e5484d;
13
+ --ok: #3fb950;
14
+ --radius: 12px;
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ html,
22
+ body {
23
+ height: 100%;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ font: 14px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
31
+ display: flex;
32
+ flex-direction: column;
33
+ overflow: hidden;
34
+ user-select: none;
35
+ -webkit-user-select: none;
36
+ }
37
+
38
+ /* ── Titlebar ─────────────────────────────────────────────────────────── */
39
+ .titlebar {
40
+ height: 40px;
41
+ flex: 0 0 auto;
42
+ display: flex;
43
+ align-items: stretch;
44
+ background: #010409;
45
+ border-bottom: 1px solid var(--line);
46
+ }
47
+
48
+ .titlebar__drag {
49
+ flex: 1;
50
+ min-width: 0;
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 10px;
54
+ padding: 0 14px;
55
+ }
56
+
57
+ .brand__mark {
58
+ width: 12px;
59
+ height: 12px;
60
+ border-radius: 3px;
61
+ background: var(--accent);
62
+ }
63
+
64
+ .brand__name {
65
+ font-weight: 600;
66
+ letter-spacing: 0.2px;
67
+ white-space: nowrap;
68
+ overflow: hidden;
69
+ text-overflow: ellipsis;
70
+ }
71
+
72
+ .titlebar__controls {
73
+ display: flex;
74
+ }
75
+
76
+ .winbtn {
77
+ width: 46px;
78
+ border: 0;
79
+ background: transparent;
80
+ color: var(--muted);
81
+ cursor: pointer;
82
+ display: grid;
83
+ place-items: center;
84
+ }
85
+
86
+ .winbtn svg {
87
+ fill: none;
88
+ stroke: currentColor;
89
+ stroke-width: 1.4;
90
+ }
91
+
92
+ .winbtn:hover {
93
+ background: var(--line);
94
+ color: var(--text);
95
+ }
96
+
97
+ .winbtn--close:hover {
98
+ background: var(--danger);
99
+ color: #fff;
100
+ }
101
+
102
+ /* ── Layout ───────────────────────────────────────────────────────────── */
103
+ .page {
104
+ flex: 1;
105
+ min-height: 0;
106
+ overflow: auto;
107
+ padding: 28px clamp(18px, 4vw, 44px) 36px;
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 20px;
111
+ max-width: 1040px;
112
+ width: 100%;
113
+ margin: 0 auto;
114
+ }
115
+
116
+ .hero {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 12px;
120
+ }
121
+
122
+ .eyebrow {
123
+ font-size: 12px;
124
+ font-weight: 600;
125
+ letter-spacing: 0.08em;
126
+ text-transform: uppercase;
127
+ color: var(--muted);
128
+ }
129
+
130
+ h1 {
131
+ margin: 0;
132
+ font-size: clamp(24px, 3vw, 32px);
133
+ line-height: 1.2;
134
+ letter-spacing: -0.01em;
135
+ }
136
+
137
+ .lead {
138
+ margin: 0;
139
+ max-width: 72ch;
140
+ color: var(--muted);
141
+ font-size: 15px;
142
+ }
143
+
144
+ .lead strong {
145
+ color: var(--text);
146
+ }
147
+
148
+ .actions {
149
+ display: flex;
150
+ flex-wrap: wrap;
151
+ gap: 8px;
152
+ margin-top: 4px;
153
+ }
154
+
155
+ .btn {
156
+ font: inherit;
157
+ color: var(--text);
158
+ background: #21262d;
159
+ border: 1px solid var(--line-2);
160
+ border-radius: 8px;
161
+ padding: 8px 14px;
162
+ cursor: pointer;
163
+ }
164
+
165
+ .btn:hover {
166
+ background: #2b323b;
167
+ }
168
+
169
+ .btn:active {
170
+ transform: translateY(1px);
171
+ }
172
+
173
+ .btn--primary {
174
+ background: var(--accent);
175
+ border-color: var(--accent);
176
+ color: #fff;
177
+ font-weight: 600;
178
+ }
179
+
180
+ .btn--primary:hover {
181
+ background: var(--accent-hover);
182
+ border-color: var(--accent-hover);
183
+ }
184
+
185
+ .btn--ghost {
186
+ background: transparent;
187
+ }
188
+
189
+ .grid {
190
+ display: grid;
191
+ grid-template-columns: minmax(240px, 300px) 1fr;
192
+ gap: 16px;
193
+ }
194
+
195
+ @media (max-width: 820px) {
196
+ .grid {
197
+ grid-template-columns: 1fr;
198
+ }
199
+ }
200
+
201
+ .card {
202
+ background: var(--panel);
203
+ border: 1px solid var(--line);
204
+ border-radius: var(--radius);
205
+ padding: 16px 18px;
206
+ }
207
+
208
+ .card--grow {
209
+ min-width: 0;
210
+ display: flex;
211
+ flex-direction: column;
212
+ }
213
+
214
+ .card__head {
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: space-between;
218
+ margin-bottom: 12px;
219
+ }
220
+
221
+ .card__head h2 {
222
+ margin: 0;
223
+ font-size: 12px;
224
+ text-transform: uppercase;
225
+ letter-spacing: 0.07em;
226
+ color: var(--muted);
227
+ }
228
+
229
+ .badge {
230
+ font-size: 12px;
231
+ color: var(--muted);
232
+ background: var(--panel-2);
233
+ border: 1px solid var(--line);
234
+ border-radius: 999px;
235
+ padding: 2px 10px;
236
+ font-variant-numeric: tabular-nums;
237
+ }
238
+
239
+ .kv {
240
+ margin: 0;
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: 8px;
244
+ }
245
+
246
+ .kv > div {
247
+ display: flex;
248
+ justify-content: space-between;
249
+ gap: 12px;
250
+ }
251
+
252
+ .kv dt {
253
+ color: var(--muted);
254
+ }
255
+
256
+ .kv dd {
257
+ margin: 0;
258
+ font-variant-numeric: tabular-nums;
259
+ }
260
+
261
+ .ua {
262
+ margin: 14px 0 0;
263
+ color: #5b6672;
264
+ font-size: 12px;
265
+ word-break: break-word;
266
+ }
267
+
268
+ .console {
269
+ flex: 1;
270
+ min-height: 210px;
271
+ max-height: 46vh;
272
+ margin: 0;
273
+ overflow: auto;
274
+ background: #010409;
275
+ border: 1px solid var(--line);
276
+ border-radius: 8px;
277
+ padding: 12px 14px;
278
+ font: 12.5px/1.6 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
279
+ white-space: pre-wrap;
280
+ word-break: break-word;
281
+ user-select: text;
282
+ -webkit-user-select: text;
283
+ }
284
+
285
+ .console .line--ok {
286
+ color: var(--ok);
287
+ }
288
+
289
+ .console .line--err {
290
+ color: #ff8085;
291
+ }
292
+
293
+ .console .line--muted {
294
+ color: var(--muted);
295
+ }
296
+
297
+ .linkbtn {
298
+ font: inherit;
299
+ background: transparent;
300
+ border: 0;
301
+ color: var(--muted);
302
+ cursor: pointer;
303
+ }
304
+
305
+ .linkbtn:hover {
306
+ color: var(--text);
307
+ }
308
+
309
+ .hint {
310
+ margin: 0 0 12px;
311
+ color: var(--muted);
312
+ }
313
+
314
+ .code {
315
+ margin: 0;
316
+ overflow: auto;
317
+ background: #010409;
318
+ border: 1px solid var(--line);
319
+ border-radius: 8px;
320
+ padding: 14px;
321
+ font: 12.5px/1.7 ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
322
+ color: #c9d1d9;
323
+ user-select: text;
324
+ -webkit-user-select: text;
325
+ }
326
+
327
+ code {
328
+ color: #79c0ff;
329
+ }
330
+
331
+ .hint code,
332
+ .lead code {
333
+ background: #21262d;
334
+ border: 1px solid var(--line);
335
+ border-radius: 6px;
336
+ padding: 1px 6px;
337
+ font-size: 12.5px;
338
+ }
@@ -3,10 +3,11 @@
3
3
  "target": "ES2022",
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "Bundler",
6
- "lib": ["ES2022", "DOM"],
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "types": ["node", "vite/client"],
7
8
  "strict": true,
8
9
  "skipLibCheck": true,
9
- "types": ["vite/client"]
10
+ "noEmit": true
10
11
  },
11
12
  "include": ["src", "app"]
12
13
  }
@@ -1,42 +0,0 @@
1
- // Copyright 2026 Owear Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
- //
4
- // native/files.cpp — módulo nativo PROPIO de la app.
5
- // Se compila a .owm y se invoca desde el renderer con:
6
- // import { files } from '@owear/native'
7
- // await files.readText('/etc/hostname')
8
- //
9
- #include <ow/Json.h>
10
- #include <ow/Module.h>
11
- #include "ow_api.h"
12
-
13
- #include <filesystem>
14
- #include <fstream>
15
- #include <sstream>
16
-
17
- namespace fs = std::filesystem;
18
-
19
- static void readText(const ow_request_t* req, ow_response_t* res) {
20
- auto parsed = ow::json::Parse(std::string_view(req->json, req->json_len));
21
- if (!parsed.value || !parsed.value->IsArray() || !parsed.value->AsArray()[0].IsString())
22
- return ow::Module::RespondError(res, "readText(path): path requerido");
23
- std::ifstream f(parsed.value->AsArray()[0].AsString(), std::ios::binary);
24
- if (!f) return ow::Module::RespondError(res, "no se pudo abrir");
25
- std::ostringstream ss;
26
- ss << f.rdbuf();
27
- ow::Module::RespondOk(res, ow::json::Value(ss.str()).Serialize().c_str());
28
- }
29
-
30
- static void exists(const ow_request_t* req, ow_response_t* res) {
31
- auto parsed = ow::json::Parse(std::string_view(req->json, req->json_len));
32
- if (!parsed.value || !parsed.value->IsArray() || !parsed.value->AsArray()[0].IsString())
33
- return ow::Module::RespondError(res, "exists(path): path requerido");
34
- std::error_code ec;
35
- bool ok = fs::exists(parsed.value->AsArray()[0].AsString(), ec);
36
- ow::Module::RespondOk(res, ok ? "true" : "false");
37
- }
38
-
39
- OW_MODULE_BEGIN(files, "1.0.0")
40
- OW_FN(readText)
41
- OW_FN(exists)
42
- OW_MODULE_END()