@owear/cli 0.1.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owear/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI de Owear: create, dev, build",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,12 +8,14 @@
8
8
  "owear-build-native": "./src/owear-build-native.js"
9
9
  },
10
10
  "optionalDependencies": {
11
- "@owear/win32-x64": "0.1.2",
12
- "@owear/linux-x64-gnu": "0.1.2"
11
+ "@owear/linux-x64-gnu": "0.1.3",
12
+ "@owear/win32-x64": "0.1.3"
13
13
  },
14
14
  "files": [
15
15
  "src",
16
- "template"
16
+ "tools",
17
+ "template",
18
+ "template-installer"
17
19
  ],
18
20
  "license": "Apache-2.0",
19
21
  "homepage": "https://owear.dev",
@@ -29,6 +31,6 @@
29
31
  "node": ">=20"
30
32
  },
31
33
  "scripts": {
32
- "typecheck": "node --check src/ow.js && node --check src/owear-build-native.js"
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"
33
35
  }
34
36
  }
package/src/ow.js CHANGED
@@ -65,12 +65,16 @@ function runtimePackageDir() {
65
65
  * - monorepo: cada build/<preset>/api/<nombre>/ (el loader NO recursiona)
66
66
  */
67
67
  function stockModulesPath() {
68
- const rt = runtimePackageDir()
69
- if (rt) {
70
- const m = path.join(rt, 'bin', 'modules')
68
+ // Dev: si el usuario fija el kernel (OW_KERNEL_BIN), sus módulos viven junto
69
+ // a él (<exe>/modules). Es la fuente correcta: kernel y módulos del mismo build.
70
+ const kb = process.env.OW_KERNEL_BIN
71
+ if (kb) {
72
+ const m = path.join(path.dirname(kb), 'modules')
71
73
  if (fs.existsSync(m)) return m
72
74
  }
73
- const api = path.resolve(__dirname, '../../../build', platformPreset(), 'api')
75
+ // Dev en el monorepo: preferir el BUILD LOCAL (mismo build que el kernel)
76
+ // ANTES del runtime package, que puede ser un artefacto prebuilt antiguo.
77
+ const api = path.resolve(__dirname, '../../../build', platformPreset(), 'src', 'api')
74
78
  if (fs.existsSync(api)) {
75
79
  const dirs = fs
76
80
  .readdirSync(api, { withFileTypes: true })
@@ -78,6 +82,11 @@ function stockModulesPath() {
78
82
  .map((d) => path.join(api, d.name))
79
83
  if (dirs.length) return dirs.join(path.delimiter)
80
84
  }
85
+ const rt = runtimePackageDir()
86
+ if (rt) {
87
+ const m = path.join(rt, 'bin', 'modules')
88
+ if (fs.existsSync(m)) return m
89
+ }
81
90
  return ''
82
91
  }
83
92
 
@@ -148,11 +157,16 @@ function printHelp() {
148
157
  console.log(`
149
158
  ${C.cyan}owear${C.reset} — framework desktop nativo
150
159
 
151
- ${C.green}ow create <dir>${C.reset} crea una app nueva
152
- ${C.green}ow dev${C.reset} desarrollo: vite + kernel + sidecar node
153
- ${C.green}ow build${C.reset} build de producción
154
- ${C.green}ow api list${C.reset} lista las APIs del repo y sus manifiestos
155
- ${C.green}ow api new <nombre>${C.reset} scaffoldea una API (api/<nombre>/ + manifiesto)
160
+ ${C.green}ow create <dir>${C.reset} crea una app nueva
161
+ ${C.green}ow create installer <dir>${C.reset} scaffoldea el instalador
162
+ ${C.green}ow create uninstaller <dir>${C.reset} scaffoldea el desinstalador
163
+ ${C.green}ow dev${C.reset} desarrollo: vite + kernel + sidecar node
164
+ ${C.green}ow build${C.reset} build de producción (dist/)
165
+ ${C.green}ow build app${C.reset} payload de la app (--format binary|deb|appimage)
166
+ ${C.green}ow build installer${C.reset} binario instalador (linux) / .exe (win)
167
+ ${C.green}ow build uninstaller${C.reset} binario desinstalador
168
+ ${C.green}ow api list${C.reset} lista las APIs del repo y sus manifiestos
169
+ ${C.green}ow api new <nombre>${C.reset} scaffoldea una API (api/<nombre>/ + manifiesto)
156
170
 
157
171
  Variables útiles:
158
172
  OW_KERNEL_BIN ruta al binario owear
@@ -160,23 +174,36 @@ Variables útiles:
160
174
  }
161
175
 
162
176
  function cmdCreate(args) {
163
- const dir = args[0]
164
- if (!dir) die('uso: ow create <dir>')
177
+ const sub = args[0]
178
+ if (sub === 'installer') {
179
+ return scaffold('template-installer', args[1] ?? 'installer', 'instalador')
180
+ }
181
+ if (sub === 'uninstaller') {
182
+ return scaffold('template-installer/uninstaller', args[1] ?? 'uninstaller', 'desinstalador')
183
+ }
184
+ const dir = sub
185
+ if (!dir) die('uso: ow create [installer|uninstaller] <dir>')
186
+ const target = scaffold('template', dir, 'app')
187
+ log('siguientes pasos:')
188
+ console.log(` cd ${path.basename(target)}`)
189
+ console.log(' npm install')
190
+ console.log(' npm run dev')
191
+ }
192
+
193
+ /** Copia un template del CLI a `dir` y sustituye placeholders. */
194
+ function scaffold(templateRel, dir, what) {
165
195
  const target = path.resolve(dir)
166
196
  if (fs.existsSync(target) && fs.readdirSync(target).length) {
167
197
  die(`el directorio ya existe y no está vacío: ${target}`)
168
198
  }
169
- const templateDir = path.resolve(__dirname, '../template')
199
+ const templateDir = path.resolve(__dirname, '..', templateRel)
170
200
  if (!fs.existsSync(templateDir)) {
171
201
  die(`template no encontrado en ${templateDir} (instala @owear/cli completo)`)
172
202
  }
173
203
  fs.cpSync(templateDir, target, { recursive: true })
174
204
  applyAppName(target)
175
- log(`app creada en ${target}`)
176
- log('siguientes pasos:')
177
- console.log(` cd ${path.basename(target)}`)
178
- console.log(' npm install')
179
- console.log(' npm run dev')
205
+ log(`${what} en ${target}`)
206
+ return target
180
207
  }
181
208
 
182
209
  /**
@@ -228,7 +255,7 @@ function cmdApi(args) {
228
255
  }
229
256
 
230
257
  function readApiManifests(root) {
231
- const apiDir = path.join(root, 'api')
258
+ const apiDir = path.join(root, 'src', 'api')
232
259
  const out = []
233
260
  for (const d of fs.readdirSync(apiDir, { withFileTypes: true })) {
234
261
  if (!d.isDirectory()) continue
@@ -260,7 +287,7 @@ function cmdApiList(root) {
260
287
  function cmdApiNew(root, name) {
261
288
  if (!name) die('uso: ow api new <nombre>')
262
289
  if (!/^[a-z][a-z0-9-]*$/.test(name)) die('nombre inválido (minúsculas, dígitos y guiones; debe empezar por letra)')
263
- const dir = path.join(root, 'api', name)
290
+ const dir = path.join(root, 'src', 'api', name)
264
291
  if (fs.existsSync(dir) && fs.readdirSync(dir).length) die(`api/${name}/ ya existe y no está vacío`)
265
292
 
266
293
  fs.mkdirSync(path.join(dir, 'src'), { recursive: true })
@@ -376,6 +403,59 @@ function prepareMain(cwd, outDir) {
376
403
  return out
377
404
  }
378
405
 
406
+ /**
407
+ * Entries de worker de la app: `app/workers/**\/*.{ts,mts,js,mjs}`.
408
+ * Se compilan a <outDir>/workers/ conservando la ruta relativa, y el kernel
409
+ * expone el directorio al main vía OW_APP_WORKERS para `app.forkWorker()`.
410
+ */
411
+ function findWorkerEntries(cwd) {
412
+ const dir = path.join(cwd, 'app', 'workers')
413
+ if (!fs.existsSync(dir)) return []
414
+ const out = []
415
+ const walk = (d) => {
416
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
417
+ const p = path.join(d, e.name)
418
+ if (e.isDirectory()) walk(p)
419
+ else if (/\.(ts|mts|js|mjs)$/.test(e.name)) out.push(p)
420
+ }
421
+ }
422
+ walk(dir)
423
+ return out
424
+ }
425
+
426
+ function prepareWorkers(cwd, outDir) {
427
+ const entries = findWorkerEntries(cwd)
428
+ if (!entries.length) return null
429
+
430
+ const root = path.join(cwd, 'app', 'workers')
431
+ const workersDir = path.join(outDir, 'workers')
432
+ const built = []
433
+
434
+ for (const entry of entries) {
435
+ const rel = path.relative(root, entry)
436
+ const out = path.join(workersDir, rel.replace(/\.(ts|mts)$/, '.js'))
437
+ fs.mkdirSync(path.dirname(out), { recursive: true })
438
+
439
+ if (entry.endsWith('.js') || entry.endsWith('.mjs')) {
440
+ fs.copyFileSync(entry, out)
441
+ } else {
442
+ log(`compilando worker ${path.relative(cwd, entry)}…`)
443
+ const r = spawnSync(
444
+ 'npx',
445
+ [
446
+ 'esbuild', entry,
447
+ '--bundle', '--platform=node', '--format=esm',
448
+ '--packages=external', `--outfile=${out}`, '--log-level=warning',
449
+ ],
450
+ { cwd, stdio: 'inherit', shell: process.platform === 'win32' }
451
+ )
452
+ if (r.status !== 0) die(`no se pudo compilar el worker ${path.relative(cwd, entry)}`)
453
+ }
454
+ built.push(out)
455
+ }
456
+ return workersDir
457
+ }
458
+
379
459
  async function cmdDev() {
380
460
  const cwd = process.cwd()
381
461
  if (!fs.existsSync(path.join(cwd, 'package.json'))) die('ejecuta dentro de tu app')
@@ -411,6 +491,9 @@ async function cmdDev() {
411
491
  const mainJs = prepareMain(cwd, path.join(cwd, '.owear'))
412
492
  if (mainJs) log(`proceso principal: ${path.relative(cwd, mainJs)}`)
413
493
 
494
+ const workersDir = prepareWorkers(cwd, path.join(cwd, '.owear'))
495
+ if (workersDir) log(`workers: ${path.relative(cwd, workersDir)}`)
496
+
414
497
  log(`lanzando kernel: ${kernelBin}`)
415
498
  const kernel = spawn(kernelBin, [], {
416
499
  stdio: 'inherit',
@@ -418,7 +501,8 @@ async function cmdDev() {
418
501
  ...process.env,
419
502
  OW_APP_NAME: JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).name ?? 'Owear App',
420
503
  OW_DEV_SERVER_URL: 'http://localhost:5173/',
421
- ...(mainJs ? { OW_APP_MAIN: mainJs } : {}),
504
+ ...(mainJs ? { OW_APP_MAIN: mainJs } : { OW_START_URL: 'http://localhost:5173/' }),
505
+ ...(workersDir ? { OW_APP_WORKERS: workersDir } : {}),
422
506
  ...(() => {
423
507
  // módulos stock (fs/path/…) + los nativos de la app, si los hay
424
508
  const dirs = [stockModulesPath(), modulesDir].filter(Boolean)
@@ -438,14 +522,27 @@ async function cmdDev() {
438
522
  })
439
523
  }
440
524
 
441
- async function cmdBuild() {
442
- const cwd = process.cwd()
525
+ // ── ow build [app|installer|uninstaller] ─────────────────────────────────────
526
+
527
+ async function cmdBuild(args) {
528
+ const sub = args[0]
529
+ if (sub === 'installer') return cmdBuildInstaller(process.cwd(), args.slice(1), false)
530
+ if (sub === 'uninstaller') return cmdBuildInstaller(process.cwd(), args.slice(1), true)
531
+ if (sub === 'app') return cmdBuildApp(process.cwd(), args.slice(1))
532
+ return cmdBuildBundle(process.cwd())
533
+ }
534
+
535
+ /** Build de la app → dist/ (vite + main.js + workers + módulos nativos). */
536
+ async function cmdBuildBundle(cwd) {
443
537
  log('vite build…')
444
538
  const code = await runProc('npx', ['vite', 'build'], { cwd, shell: process.platform === 'win32' })
445
539
  if (code !== 0) die('vite build falló')
446
540
 
447
541
  const mainJs = prepareMain(cwd, path.join(cwd, 'dist'))
448
542
 
543
+ const workersDir = prepareWorkers(cwd, path.join(cwd, 'dist'))
544
+ if (workersDir) log(`workers: ${path.relative(cwd, workersDir)}`)
545
+
449
546
  const nativeDir = path.join(cwd, 'native')
450
547
  if (fs.existsSync(nativeDir)) {
451
548
  const out = path.join(cwd, 'dist', 'modules')
@@ -466,8 +563,355 @@ async function cmdBuild() {
466
563
  .join(path.delimiter)
467
564
  console.log(
468
565
  ` OW_ASSETS_DIR=dist${mainJs ? ' OW_APP_MAIN=dist/main.js' : ''}` +
566
+ `${workersDir ? ' OW_APP_WORKERS=dist/workers' : ''}` +
469
567
  `${runMods ? ` OW_MODULES_DIR="${runMods}"` : ''} ./owear`
470
568
  )
569
+ return { dist: path.join(cwd, 'dist'), mainJs, workersDir }
570
+ }
571
+
572
+ // ── empaquetado (D1) ─────────────────────────────────────────────────────────
573
+
574
+ /** Ruta a un script de empaquetado incluido en el paquete @owear/cli. */
575
+ function packTool(name) {
576
+ const local = path.resolve(__dirname, '..', 'tools', name)
577
+ if (fs.existsSync(local)) return local
578
+ const root = owearRepoRoot()
579
+ if (root && fs.existsSync(path.join(root, 'tools', name))) {
580
+ return path.join(root, 'tools', name)
581
+ }
582
+ die(`no encuentro ${name} (reinstala @owear/cli)`)
583
+ }
584
+
585
+ /** Plataforma del destino (para elegir target del bridge). */
586
+ function platformKey() {
587
+ return process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux'
588
+ }
589
+
590
+ /** Slug del proyecto (nombre de package.json sin scope). */
591
+ function appSlug(cwd) {
592
+ try {
593
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
594
+ return (pkg.name ?? 'app').replace(/^@[^/]+\//, '')
595
+ } catch {
596
+ return path.basename(cwd)
597
+ }
598
+ }
599
+
600
+ function flag(args, name, def = null) {
601
+ return args.includes(name) ? args[args.indexOf(name) + 1] : def
602
+ }
603
+
604
+ function readPackConfig(cwd) {
605
+ const p = path.join(cwd, 'owear.pack.json')
606
+ if (!fs.existsSync(p)) return {}
607
+ try {
608
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
609
+ } catch (e) {
610
+ die(`owear.pack.json inválido: ${e.message}`)
611
+ }
612
+ }
613
+
614
+ /** Ensambla el bundle `{app/,modules/,manifest.json}` que espera el kernel. */
615
+ function assembleAppBundle(cwd, built) {
616
+ const bundle = path.join(cwd, '.owear', 'app-bundle')
617
+ fs.rmSync(bundle, { recursive: true, force: true })
618
+ fs.mkdirSync(bundle, { recursive: true })
619
+ fs.cpSync(built.dist, path.join(bundle, 'app'), { recursive: true })
620
+ const mods = path.join(built.dist, 'modules')
621
+ if (fs.existsSync(mods)) fs.cpSync(mods, path.join(bundle, 'modules'), { recursive: true })
622
+ fs.writeFileSync(
623
+ path.join(bundle, 'manifest.json'),
624
+ JSON.stringify({ name: appSlug(cwd), createdAt: Date.now() }, null, 2) + '\n'
625
+ )
626
+ return bundle
627
+ }
628
+
629
+ function packDir(kernel, inDir, out) {
630
+ const r = spawnSync(
631
+ process.execPath,
632
+ [packTool('owear-pack.mjs'), '--kernel', kernel, '--in', inDir, '--out', out],
633
+ { stdio: 'inherit' }
634
+ )
635
+ if (r.status !== 0) die('falló owear-pack')
636
+ }
637
+
638
+ /** Compila `owear.bridge.ts` → `.owear/bridge.json` y devuelve el objeto. */
639
+ async function buildBridge(cwd, optional = false) {
640
+ const { pathToFileURL } = await import('node:url')
641
+ const src = ['owear.bridge.ts', 'owear.bridge.mts', 'owear.bridge.js', 'owear.bridge.mjs']
642
+ .map((n) => path.join(cwd, n))
643
+ .find((p) => fs.existsSync(p))
644
+ if (!src) {
645
+ if (optional) return null
646
+ die('no encuentro owear.bridge.ts — el instalador necesita el bridge de la app')
647
+ }
648
+ const outMjs = path.join(cwd, '.owear', 'bridge.mjs')
649
+ fs.mkdirSync(path.dirname(outMjs), { recursive: true })
650
+ log(`compilando ${path.basename(src)}…`)
651
+ const r = spawnSync(
652
+ 'npx',
653
+ ['esbuild', src, '--bundle', '--platform=node', '--format=esm', '--packages=external',
654
+ `--outfile=${outMjs}`, '--log-level=warning'],
655
+ { cwd, stdio: 'inherit', shell: process.platform === 'win32' }
656
+ )
657
+ if (r.status !== 0) die('no se pudo compilar owear.bridge')
658
+ const mod = await import(`${pathToFileURL(outMjs).href}?t=${Date.now()}`)
659
+ const obj = mod.default ?? mod
660
+ const jsonPath = path.join(cwd, '.owear', 'bridge.json')
661
+ fs.writeFileSync(jsonPath, JSON.stringify(obj, null, 2) + '\n')
662
+ return { obj, jsonPath }
663
+ }
664
+
665
+ /** Compila la UI del instalador/desinstalador → `ui-dist/` (+ sidecar main.js). */
666
+ async function buildInstallerUI(cwd, which) {
667
+ const dir =
668
+ which === 'uninstaller' ? path.join(cwd, 'installer', 'uninstaller') : path.join(cwd, 'installer')
669
+ if (!fs.existsSync(dir)) {
670
+ die(`no existe ${path.relative(cwd, dir)} — créalo con: ow create installer`)
671
+ }
672
+ log(`vite build (${which})…`)
673
+ const code = await runProc('npx', ['vite', 'build'], { cwd: dir, shell: process.platform === 'win32' })
674
+ if (code !== 0) die(`vite build del ${which} falló`)
675
+ const uiDist = path.join(dir, 'ui-dist')
676
+
677
+ // sidecar del instalador (Node): se empaqueta dentro del propio ui/
678
+ const entry = path.join(dir, 'app', 'main.ts')
679
+ if (fs.existsSync(entry)) {
680
+ const out = path.join(uiDist, 'main.js')
681
+ log(`compilando ${which}/app/main.ts…`)
682
+ const r = spawnSync(
683
+ 'npx',
684
+ ['esbuild', entry, '--bundle', '--platform=node', '--format=esm', `--outfile=${out}`, '--log-level=warning'],
685
+ { cwd: dir, stdio: 'inherit', shell: process.platform === 'win32' }
686
+ )
687
+ if (r.status !== 0) die(`no se pudo compilar ${which}/app/main.ts`)
688
+ }
689
+ return uiDist
690
+ }
691
+
692
+ /** Ejecuta un script de tools/ del repo con Node. */
693
+ function runTool(name, args) {
694
+ const r = spawnSync(process.execPath, [packTool(name), ...args], { stdio: 'inherit' })
695
+ if (r.status !== 0) die(`falló ${name}`)
696
+ }
697
+
698
+ function readPkg(cwd) {
699
+ try {
700
+ return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
701
+ } catch {
702
+ return {}
703
+ }
704
+ }
705
+
706
+ async function appMeta(cwd) {
707
+ const bridge = await buildBridge(cwd, true)
708
+ const pkg = readPkg(cwd)
709
+ const slug = appSlug(cwd)
710
+ return {
711
+ bridge,
712
+ slug,
713
+ appId: bridge?.obj?.app?.id ?? slug,
714
+ appName: bridge?.obj?.app?.name ?? pkg.name ?? slug,
715
+ version: bridge?.obj?.app?.version ?? pkg.version ?? '0.0.0',
716
+ publisher: bridge?.obj?.app?.publisher ?? 'unknown',
717
+ icon: bridge?.obj?.app?.icon ?? null,
718
+ }
719
+ }
720
+
721
+ /** Copia kernel + dist + modules + main/workers a `dir` (layout de app). */
722
+ function stageAppLayout(cwd, built, dir, kernel) {
723
+ fs.rmSync(dir, { recursive: true, force: true })
724
+ fs.mkdirSync(dir, { recursive: true })
725
+ const exe = process.platform === 'win32' ? 'owear.exe' : 'owear'
726
+ fs.copyFileSync(kernel, path.join(dir, exe))
727
+ fs.chmodSync(path.join(dir, exe), 0o755)
728
+ fs.cpSync(built.dist, path.join(dir, 'app'), { recursive: true })
729
+ const mods = path.join(built.dist, 'modules')
730
+ if (fs.existsSync(mods)) fs.cpSync(mods, path.join(dir, 'modules'), { recursive: true })
731
+ if (built.mainJs) fs.copyFileSync(built.mainJs, path.join(dir, 'main.js'))
732
+ const workers = path.join(built.dist, 'workers')
733
+ if (fs.existsSync(workers)) fs.cpSync(workers, path.join(dir, 'workers'), { recursive: true })
734
+ return exe
735
+ }
736
+
737
+ function desktopEntry(meta, execLine, iconName) {
738
+ return (
739
+ `[Desktop Entry]\nType=Application\nName=${meta.appName}\n` +
740
+ `Comment=${meta.appName}\nExec=${execLine}\n` +
741
+ (iconName ? `Icon=${iconName}\n` : '') +
742
+ `Terminal=false\nCategories=Utility;\n`
743
+ )
744
+ }
745
+
746
+ function copyIcon(cwd, meta, dstBase) {
747
+ if (!meta.icon) return null
748
+ const src = path.isAbsolute(meta.icon) ? meta.icon : path.join(cwd, meta.icon)
749
+ if (!fs.existsSync(src)) return null
750
+ const ext = path.extname(src) || '.png'
751
+ const dst = dstBase + ext
752
+ fs.mkdirSync(path.dirname(dst), { recursive: true })
753
+ fs.copyFileSync(src, dst)
754
+ return path.basename(dst)
755
+ }
756
+
757
+ /** `ow build app --format binary|deb|appimage|msi` (payload de la app). */
758
+ async function cmdBuildApp(cwd, args) {
759
+ const fmt = flag(args, '--format', 'binary')
760
+ const built = await cmdBuildBundle(cwd)
761
+ const meta = await appMeta(cwd)
762
+ const slug = meta.slug
763
+ const outDir = path.join(cwd, 'release')
764
+ fs.mkdirSync(outDir, { recursive: true })
765
+ const kernel = ensureKernelBuilt(cwd)
766
+ const isWin = process.platform === 'win32'
767
+
768
+ if (fmt === 'binary') {
769
+ const bundle = assembleAppBundle(cwd, built)
770
+ const exe = isWin ? `${slug}.exe` : slug
771
+ const out = path.join(outDir, exe)
772
+ packDir(kernel, bundle, out)
773
+ log(`binario único: ${out}`)
774
+ return
775
+ }
776
+
777
+ if (fmt === 'deb') {
778
+ const stage = path.join(cwd, '.owear', 'linux-stage')
779
+ const opt = path.join(stage, 'opt', slug)
780
+ stageAppLayout(cwd, built, opt, kernel)
781
+ const bin = path.join(stage, 'usr', 'bin', slug)
782
+ fs.mkdirSync(path.dirname(bin), { recursive: true })
783
+ fs.writeFileSync(
784
+ bin,
785
+ `#!/bin/sh\nAPPDIR="/opt/${slug}"\n` +
786
+ `export OW_ASSETS_DIR="$APPDIR/app"\n` +
787
+ `[ -f "$APPDIR/main.js" ] && export OW_APP_MAIN="$APPDIR/main.js"\n` +
788
+ `[ -d "$APPDIR/workers" ] && export OW_APP_WORKERS="$APPDIR/workers"\n` +
789
+ `[ -d "$APPDIR/modules" ] && export OW_MODULES_DIR="$APPDIR/modules"\n` +
790
+ `exec "$APPDIR/owear" "$@"\n`,
791
+ )
792
+ fs.chmodSync(bin, 0o755)
793
+ const apps = path.join(stage, 'usr', 'share', 'applications')
794
+ fs.mkdirSync(apps, { recursive: true })
795
+ const icons = path.join(stage, 'usr', 'share', 'icons', 'hicolor', '256x256', 'apps')
796
+ const iconName = copyIcon(cwd, meta, path.join(icons, meta.appId))
797
+ fs.writeFileSync(path.join(apps, `${meta.appId}.desktop`), desktopEntry(meta, slug, iconName ? meta.appId : null))
798
+ const out = path.join(outDir, `${slug}_${meta.version}_amd64.deb`)
799
+ runTool('owear-linux.mjs', ['deb', '--stage', stage, '--out', out, '--name', slug,
800
+ '--version', meta.version, '--maintainer', meta.publisher, '--description', meta.appName])
801
+ log(`.deb: ${out}`)
802
+ return
803
+ }
804
+
805
+ if (fmt === 'appimage') {
806
+ const appDir = path.join(cwd, '.owear', `${slug}.AppDir`)
807
+ fs.rmSync(appDir, { recursive: true, force: true })
808
+ fs.mkdirSync(appDir, { recursive: true })
809
+ const usr = path.join(appDir, 'usr')
810
+ stageAppLayout(cwd, built, usr, kernel)
811
+ fs.writeFileSync(
812
+ path.join(appDir, 'AppRun'),
813
+ `#!/bin/sh\nAPPDIR="$(dirname "$(readlink -f "$0")")"\n` +
814
+ `export OW_ASSETS_DIR="$APPDIR/usr/app"\n` +
815
+ `[ -f "$APPDIR/usr/main.js" ] && export OW_APP_MAIN="$APPDIR/usr/main.js"\n` +
816
+ `[ -d "$APPDIR/usr/workers" ] && export OW_APP_WORKERS="$APPDIR/usr/workers"\n` +
817
+ `[ -d "$APPDIR/usr/modules" ] && export OW_MODULES_DIR="$APPDIR/usr/modules"\n` +
818
+ `exec "$APPDIR/usr/owear" "$@"\n`,
819
+ )
820
+ fs.chmodSync(path.join(appDir, 'AppRun'), 0o755)
821
+ const iconName = copyIcon(cwd, meta, path.join(appDir, meta.appId))
822
+ fs.writeFileSync(path.join(appDir, `${meta.appId}.desktop`), desktopEntry(meta, slug, iconName ? meta.appId : null))
823
+ const out = path.join(outDir, `${slug}-${meta.version}-x86_64.AppImage`)
824
+ runTool('owear-linux.mjs', ['appimage', '--appdir', appDir, '--out', out, '--arch', 'x86_64'])
825
+ log(`AppImage: ${out}`)
826
+ return
827
+ }
828
+
829
+ if (fmt === 'msi') {
830
+ const stage = path.join(cwd, '.owear', 'win-stage')
831
+ stageAppLayout(cwd, built, stage, kernel)
832
+ const out = path.join(outDir, `${slug}-${meta.version}.msi`)
833
+ runTool('owear-msi.mjs', ['--stage', stage, '--out', out, '--name', slug,
834
+ '--version', meta.version, '--publisher', meta.publisher, '--appId', meta.appId])
835
+ log(`.MSI: ${out}`)
836
+ return
837
+ }
838
+
839
+ die(`formato desconocido: ${fmt} (binary|deb|appimage|msi)`)
840
+ }
841
+
842
+ /** `ow build installer|uninstaller` → binario instalador/desinstalador. */
843
+ async function cmdBuildInstaller(cwd, args, isUninstaller) {
844
+ const kernel = ensureKernelBuilt(cwd)
845
+ const slug = appSlug(cwd)
846
+ const pack = readPackConfig(cwd)
847
+ const bridge = await buildBridge(cwd)
848
+ const target = bridge.obj?.targets?.[platformKey()] ?? {}
849
+ const mode = flag(args, '--mode', target.layout ?? pack.modes?.default ?? 'minimal')
850
+
851
+ const exe = process.platform === 'win32' ? '.exe' : ''
852
+ const outDir = path.join(cwd, 'release')
853
+ fs.mkdirSync(outDir, { recursive: true })
854
+
855
+ // 1) payload de la app (sólo para el instalador)
856
+ let payloadDir = null
857
+ if (!isUninstaller) {
858
+ const built = await cmdBuildBundle(cwd)
859
+ payloadDir = path.join(cwd, '.owear', 'installer-payload', 'payload')
860
+ fs.rmSync(payloadDir, { recursive: true, force: true })
861
+ const bundle = assembleAppBundle(cwd, built)
862
+ if (mode === 'minimal') {
863
+ fs.mkdirSync(payloadDir, { recursive: true })
864
+ packDir(kernel, bundle, path.join(payloadDir, `${slug}${exe}`))
865
+ } else {
866
+ fs.cpSync(bundle, payloadDir, { recursive: true })
867
+ }
868
+
869
+ // 1b) desinstalador (si existe installer/uninstaller) → se instala junto a la app
870
+ if (fs.existsSync(path.join(cwd, 'installer', 'uninstaller'))) {
871
+ log('build del desinstalador…')
872
+ await cmdBuildInstaller(cwd, [], true)
873
+ const un = path.join(outDir, `${slug}-uninstaller${exe}`)
874
+ if (fs.existsSync(un)) fs.copyFileSync(un, path.join(payloadDir, path.basename(un)))
875
+ }
876
+ }
877
+
878
+ // 2) UI del instalador/desinstalador
879
+ const ui = await buildInstallerUI(cwd, isUninstaller ? 'uninstaller' : 'installer')
880
+
881
+ // 3) metadatos (installer.json / uninstaller.json) — el kernel los detecta
882
+ const meta = {
883
+ appId: bridge.obj?.app?.id ?? slug,
884
+ appName: bridge.obj?.app?.name ?? slug,
885
+ version: bridge.obj?.app?.version ?? '0.0.0',
886
+ publisher: bridge.obj?.app?.publisher ?? null,
887
+ mode: isUninstaller ? 'uninstaller' : mode,
888
+ layout: target.layout ?? mode,
889
+ order: bridge.obj?.order ?? null,
890
+ protect: bridge.obj?.protect ?? null,
891
+ hooks: bridge.obj?.hooks ?? null,
892
+ node: bridge.obj?.node ?? null,
893
+ targets: bridge.obj?.targets ?? null,
894
+ }
895
+ const metaPath = path.join(cwd, '.owear', isUninstaller ? 'uninstaller.json' : 'installer.json')
896
+ fs.mkdirSync(path.dirname(metaPath), { recursive: true })
897
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n')
898
+
899
+ // 4) ensambla el binario (kernel + payload + footer OWPK1)
900
+ const kind = isUninstaller ? 'uninstaller' : 'installer'
901
+ const out = path.join(outDir, `${slug}-${kind}${exe}`)
902
+ const toolArgs = [
903
+ packTool('owear-installer.mjs'),
904
+ '--kernel', kernel,
905
+ '--out', out,
906
+ '--ui', ui,
907
+ '--bridge', bridge.jsonPath,
908
+ '--meta', metaPath,
909
+ '--name', slug,
910
+ ]
911
+ if (payloadDir) toolArgs.push('--payload', payloadDir)
912
+ const r = spawnSync(process.execPath, toolArgs, { stdio: 'inherit' })
913
+ if (r.status !== 0) die(`falló owear-installer (${kind})`)
914
+ log(`${kind}: ${out}`)
471
915
  }
472
916
 
473
917
  main().catch((e) => die(e?.stack ?? String(e)))
@@ -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
+ })