@owear/cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +10 -8
  2. package/src/ow.js +548 -25
  3. package/template/README.md +34 -28
  4. package/template/app/main.ts +273 -4
  5. package/template/index.html +117 -28
  6. package/template/package.json +1 -1
  7. package/template/public/favicon.svg +4 -0
  8. package/template/src/assets/dancing-script.woff2 +0 -0
  9. package/template/src/assets/jetbrains-mono.woff2 +0 -0
  10. package/template/src/ow.d.ts +9 -1
  11. package/template/src/renderer.ts +489 -1
  12. package/template/src/style.css +280 -56
  13. package/template-installer/README.md +38 -0
  14. package/template-installer/app/main.ts +22 -0
  15. package/template-installer/index.html +70 -0
  16. package/template-installer/package.json +22 -0
  17. package/template-installer/src/assets/dancing-script.woff2 +0 -0
  18. package/template-installer/src/assets/jetbrains-mono.woff2 +0 -0
  19. package/template-installer/src/installer.ts +171 -0
  20. package/template-installer/src/style.css +442 -0
  21. package/template-installer/tsconfig.json +13 -0
  22. package/template-installer/uninstaller/app/main.ts +16 -0
  23. package/template-installer/uninstaller/index.html +54 -0
  24. package/template-installer/uninstaller/package.json +22 -0
  25. package/template-installer/uninstaller/src/uninstaller.ts +101 -0
  26. package/template-installer/uninstaller/vite.config.ts +10 -0
  27. package/template-installer/vite.config.ts +15 -0
  28. package/tools/owear-installer.mjs +86 -0
  29. package/tools/owear-linux.mjs +158 -0
  30. package/tools/owear-msi.mjs +113 -0
  31. package/tools/owear-pack.mjs +93 -0
  32. package/tools/owear-sign.mjs +204 -0
  33. package/tools/owear-update.mjs +150 -0
  34. package/LICENSE +0 -201
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
 
@@ -139,6 +148,7 @@ async function main() {
139
148
  case 'dev': return cmdDev(rest)
140
149
  case 'build': return cmdBuild(rest)
141
150
  case 'api': return cmdApi(rest)
151
+ case 'update': return cmdUpdate(rest)
142
152
  default:
143
153
  die(`comando desconocido: ${cmd} (usa --help)`)
144
154
  }
@@ -148,35 +158,64 @@ function printHelp() {
148
158
  console.log(`
149
159
  ${C.cyan}owear${C.reset} — framework desktop nativo
150
160
 
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)
161
+ ${C.green}ow create <dir>${C.reset} crea una app nueva
162
+ ${C.green}ow create installer <dir>${C.reset} scaffoldea el instalador
163
+ ${C.green}ow create uninstaller <dir>${C.reset} scaffoldea el desinstalador
164
+ ${C.green}ow dev${C.reset} desarrollo: vite + kernel + sidecar node
165
+ ${C.green}ow build${C.reset} build de producción (dist/)
166
+ ${C.green}ow build app${C.reset} payload de la app (--format binary|deb|appimage)
167
+ ${C.green}ow build installer${C.reset} binario instalador (linux) / .exe (win)
168
+ ${C.green}ow build uninstaller${C.reset} binario desinstalador
169
+ ${C.green}ow api list${C.reset} lista las APIs del repo y sus manifiestos
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
156
173
 
157
174
  Variables útiles:
158
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
159
185
  `)
160
186
  }
161
187
 
162
188
  function cmdCreate(args) {
163
- const dir = args[0]
164
- if (!dir) die('uso: ow create <dir>')
189
+ const sub = args[0]
190
+ if (sub === 'installer') {
191
+ return scaffold('template-installer', args[1] ?? 'installer', 'instalador')
192
+ }
193
+ if (sub === 'uninstaller') {
194
+ return scaffold('template-installer/uninstaller', args[1] ?? 'uninstaller', 'desinstalador')
195
+ }
196
+ const dir = sub
197
+ if (!dir) die('uso: ow create [installer|uninstaller] <dir>')
198
+ const target = scaffold('template', dir, 'app')
199
+ log('siguientes pasos:')
200
+ console.log(` cd ${path.basename(target)}`)
201
+ console.log(' npm install')
202
+ console.log(' npm run dev')
203
+ }
204
+
205
+ /** Copia un template del CLI a `dir` y sustituye placeholders. */
206
+ function scaffold(templateRel, dir, what) {
165
207
  const target = path.resolve(dir)
166
208
  if (fs.existsSync(target) && fs.readdirSync(target).length) {
167
209
  die(`el directorio ya existe y no está vacío: ${target}`)
168
210
  }
169
- const templateDir = path.resolve(__dirname, '../template')
211
+ const templateDir = path.resolve(__dirname, '..', templateRel)
170
212
  if (!fs.existsSync(templateDir)) {
171
213
  die(`template no encontrado en ${templateDir} (instala @owear/cli completo)`)
172
214
  }
173
215
  fs.cpSync(templateDir, target, { recursive: true })
174
216
  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')
217
+ log(`${what} en ${target}`)
218
+ return target
180
219
  }
181
220
 
182
221
  /**
@@ -228,7 +267,7 @@ function cmdApi(args) {
228
267
  }
229
268
 
230
269
  function readApiManifests(root) {
231
- const apiDir = path.join(root, 'api')
270
+ const apiDir = path.join(root, 'src', 'api')
232
271
  const out = []
233
272
  for (const d of fs.readdirSync(apiDir, { withFileTypes: true })) {
234
273
  if (!d.isDirectory()) continue
@@ -260,7 +299,7 @@ function cmdApiList(root) {
260
299
  function cmdApiNew(root, name) {
261
300
  if (!name) die('uso: ow api new <nombre>')
262
301
  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)
302
+ const dir = path.join(root, 'src', 'api', name)
264
303
  if (fs.existsSync(dir) && fs.readdirSync(dir).length) die(`api/${name}/ ya existe y no está vacío`)
265
304
 
266
305
  fs.mkdirSync(path.join(dir, 'src'), { recursive: true })
@@ -353,7 +392,7 @@ function findMainEntry(cwd) {
353
392
  * así que compilamos con esbuild (viene con vite). De este modo el runtime del
354
393
  * sistema sirve y el arranque no depende de la versión que tenga el usuario.
355
394
  */
356
- function prepareMain(cwd, outDir) {
395
+ function prepareMain(cwd, outDir, bundle = false) {
357
396
  const entry = findMainEntry(cwd)
358
397
  if (!entry) return null
359
398
  if (entry.endsWith('.js') || entry.endsWith('.mjs')) return entry
@@ -366,7 +405,11 @@ function prepareMain(cwd, outDir) {
366
405
  [
367
406
  'esbuild', entry,
368
407
  '--bundle', '--platform=node', '--format=esm',
369
- '--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',
370
413
  ],
371
414
  { cwd, stdio: 'inherit', shell: process.platform === 'win32' }
372
415
  )
@@ -376,6 +419,59 @@ function prepareMain(cwd, outDir) {
376
419
  return out
377
420
  }
378
421
 
422
+ /**
423
+ * Entries de worker de la app: `app/workers/**\/*.{ts,mts,js,mjs}`.
424
+ * Se compilan a <outDir>/workers/ conservando la ruta relativa, y el kernel
425
+ * expone el directorio al main vía OW_APP_WORKERS para `app.forkWorker()`.
426
+ */
427
+ function findWorkerEntries(cwd) {
428
+ const dir = path.join(cwd, 'app', 'workers')
429
+ if (!fs.existsSync(dir)) return []
430
+ const out = []
431
+ const walk = (d) => {
432
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
433
+ const p = path.join(d, e.name)
434
+ if (e.isDirectory()) walk(p)
435
+ else if (/\.(ts|mts|js|mjs)$/.test(e.name)) out.push(p)
436
+ }
437
+ }
438
+ walk(dir)
439
+ return out
440
+ }
441
+
442
+ function prepareWorkers(cwd, outDir) {
443
+ const entries = findWorkerEntries(cwd)
444
+ if (!entries.length) return null
445
+
446
+ const root = path.join(cwd, 'app', 'workers')
447
+ const workersDir = path.join(outDir, 'workers')
448
+ const built = []
449
+
450
+ for (const entry of entries) {
451
+ const rel = path.relative(root, entry)
452
+ const out = path.join(workersDir, rel.replace(/\.(ts|mts)$/, '.js'))
453
+ fs.mkdirSync(path.dirname(out), { recursive: true })
454
+
455
+ if (entry.endsWith('.js') || entry.endsWith('.mjs')) {
456
+ fs.copyFileSync(entry, out)
457
+ } else {
458
+ log(`compilando worker ${path.relative(cwd, entry)}…`)
459
+ const r = spawnSync(
460
+ 'npx',
461
+ [
462
+ 'esbuild', entry,
463
+ '--bundle', '--platform=node', '--format=esm',
464
+ '--packages=external', `--outfile=${out}`, '--log-level=warning',
465
+ ],
466
+ { cwd, stdio: 'inherit', shell: process.platform === 'win32' }
467
+ )
468
+ if (r.status !== 0) die(`no se pudo compilar el worker ${path.relative(cwd, entry)}`)
469
+ }
470
+ built.push(out)
471
+ }
472
+ return workersDir
473
+ }
474
+
379
475
  async function cmdDev() {
380
476
  const cwd = process.cwd()
381
477
  if (!fs.existsSync(path.join(cwd, 'package.json'))) die('ejecuta dentro de tu app')
@@ -411,6 +507,9 @@ async function cmdDev() {
411
507
  const mainJs = prepareMain(cwd, path.join(cwd, '.owear'))
412
508
  if (mainJs) log(`proceso principal: ${path.relative(cwd, mainJs)}`)
413
509
 
510
+ const workersDir = prepareWorkers(cwd, path.join(cwd, '.owear'))
511
+ if (workersDir) log(`workers: ${path.relative(cwd, workersDir)}`)
512
+
414
513
  log(`lanzando kernel: ${kernelBin}`)
415
514
  const kernel = spawn(kernelBin, [], {
416
515
  stdio: 'inherit',
@@ -418,7 +517,8 @@ async function cmdDev() {
418
517
  ...process.env,
419
518
  OW_APP_NAME: JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).name ?? 'Owear App',
420
519
  OW_DEV_SERVER_URL: 'http://localhost:5173/',
421
- ...(mainJs ? { OW_APP_MAIN: mainJs } : {}),
520
+ ...(mainJs ? { OW_APP_MAIN: mainJs } : { OW_START_URL: 'http://localhost:5173/' }),
521
+ ...(workersDir ? { OW_APP_WORKERS: workersDir } : {}),
422
522
  ...(() => {
423
523
  // módulos stock (fs/path/…) + los nativos de la app, si los hay
424
524
  const dirs = [stockModulesPath(), modulesDir].filter(Boolean)
@@ -438,13 +538,26 @@ async function cmdDev() {
438
538
  })
439
539
  }
440
540
 
441
- async function cmdBuild() {
442
- const cwd = process.cwd()
541
+ // ── ow build [app|installer|uninstaller] ─────────────────────────────────────
542
+
543
+ async function cmdBuild(args) {
544
+ const sub = args[0]
545
+ if (sub === 'installer') return cmdBuildInstaller(process.cwd(), args.slice(1), false)
546
+ if (sub === 'uninstaller') return cmdBuildInstaller(process.cwd(), args.slice(1), true)
547
+ if (sub === 'app') return cmdBuildApp(process.cwd(), args.slice(1))
548
+ return cmdBuildBundle(process.cwd())
549
+ }
550
+
551
+ /** Build de la app → dist/ (vite + main.js + workers + módulos nativos). */
552
+ async function cmdBuildBundle(cwd) {
443
553
  log('vite build…')
444
554
  const code = await runProc('npx', ['vite', 'build'], { cwd, shell: process.platform === 'win32' })
445
555
  if (code !== 0) die('vite build falló')
446
556
 
447
- const mainJs = prepareMain(cwd, path.join(cwd, 'dist'))
557
+ const mainJs = prepareMain(cwd, path.join(cwd, 'dist'), true)
558
+
559
+ const workersDir = prepareWorkers(cwd, path.join(cwd, 'dist'))
560
+ if (workersDir) log(`workers: ${path.relative(cwd, workersDir)}`)
448
561
 
449
562
  const nativeDir = path.join(cwd, 'native')
450
563
  if (fs.existsSync(nativeDir)) {
@@ -466,8 +579,418 @@ async function cmdBuild() {
466
579
  .join(path.delimiter)
467
580
  console.log(
468
581
  ` OW_ASSETS_DIR=dist${mainJs ? ' OW_APP_MAIN=dist/main.js' : ''}` +
582
+ `${workersDir ? ' OW_APP_WORKERS=dist/workers' : ''}` +
469
583
  `${runMods ? ` OW_MODULES_DIR="${runMods}"` : ''} ./owear`
470
584
  )
585
+ return { dist: path.join(cwd, 'dist'), mainJs, workersDir }
586
+ }
587
+
588
+ // ── empaquetado (D1) ─────────────────────────────────────────────────────────
589
+
590
+ /** Ruta a un script de empaquetado incluido en el paquete @owear/cli. */
591
+ function packTool(name) {
592
+ const local = path.resolve(__dirname, '..', 'tools', name)
593
+ if (fs.existsSync(local)) return local
594
+ const root = owearRepoRoot()
595
+ if (root && fs.existsSync(path.join(root, 'tools', name))) {
596
+ return path.join(root, 'tools', name)
597
+ }
598
+ die(`no encuentro ${name} (reinstala @owear/cli)`)
599
+ }
600
+
601
+ /** Plataforma del destino (para elegir target del bridge). */
602
+ function platformKey() {
603
+ return process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux'
604
+ }
605
+
606
+ /** Slug del proyecto (nombre de package.json sin scope). */
607
+ function appSlug(cwd) {
608
+ try {
609
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
610
+ return (pkg.name ?? 'app').replace(/^@[^/]+\//, '')
611
+ } catch {
612
+ return path.basename(cwd)
613
+ }
614
+ }
615
+
616
+ function flag(args, name, def = null) {
617
+ return args.includes(name) ? args[args.indexOf(name) + 1] : def
618
+ }
619
+
620
+ function readPackConfig(cwd) {
621
+ const p = path.join(cwd, 'owear.pack.json')
622
+ if (!fs.existsSync(p)) return {}
623
+ try {
624
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
625
+ } catch (e) {
626
+ die(`owear.pack.json inválido: ${e.message}`)
627
+ }
628
+ }
629
+
630
+ /** Ensambla el bundle `{app/,modules/,manifest.json}` que espera el kernel. */
631
+ function assembleAppBundle(cwd, built) {
632
+ const bundle = path.join(cwd, '.owear', 'app-bundle')
633
+ fs.rmSync(bundle, { recursive: true, force: true })
634
+ fs.mkdirSync(bundle, { recursive: true })
635
+ fs.cpSync(built.dist, path.join(bundle, 'app'), { recursive: true })
636
+ const mods = path.join(built.dist, 'modules')
637
+ if (fs.existsSync(mods)) fs.cpSync(mods, path.join(bundle, 'modules'), { recursive: true })
638
+ fs.writeFileSync(
639
+ path.join(bundle, 'manifest.json'),
640
+ JSON.stringify({ name: appSlug(cwd), createdAt: Date.now() }, null, 2) + '\n'
641
+ )
642
+ return bundle
643
+ }
644
+
645
+ function packDir(kernel, inDir, out) {
646
+ const r = spawnSync(
647
+ process.execPath,
648
+ [packTool('owear-pack.mjs'), '--kernel', kernel, '--in', inDir, '--out', out],
649
+ { stdio: 'inherit' }
650
+ )
651
+ if (r.status !== 0) die('falló owear-pack')
652
+ }
653
+
654
+ /** Compila `owear.bridge.ts` → `.owear/bridge.json` y devuelve el objeto. */
655
+ async function buildBridge(cwd, optional = false) {
656
+ const { pathToFileURL } = await import('node:url')
657
+ const src = ['owear.bridge.ts', 'owear.bridge.mts', 'owear.bridge.js', 'owear.bridge.mjs']
658
+ .map((n) => path.join(cwd, n))
659
+ .find((p) => fs.existsSync(p))
660
+ if (!src) {
661
+ if (optional) return null
662
+ die('no encuentro owear.bridge.ts — el instalador necesita el bridge de la app')
663
+ }
664
+ const outMjs = path.join(cwd, '.owear', 'bridge.mjs')
665
+ fs.mkdirSync(path.dirname(outMjs), { recursive: true })
666
+ log(`compilando ${path.basename(src)}…`)
667
+ const r = spawnSync(
668
+ 'npx',
669
+ ['esbuild', src, '--bundle', '--platform=node', '--format=esm', '--packages=external',
670
+ `--outfile=${outMjs}`, '--log-level=warning'],
671
+ { cwd, stdio: 'inherit', shell: process.platform === 'win32' }
672
+ )
673
+ if (r.status !== 0) die('no se pudo compilar owear.bridge')
674
+ const mod = await import(`${pathToFileURL(outMjs).href}?t=${Date.now()}`)
675
+ const obj = mod.default ?? mod
676
+ const jsonPath = path.join(cwd, '.owear', 'bridge.json')
677
+ fs.writeFileSync(jsonPath, JSON.stringify(obj, null, 2) + '\n')
678
+ return { obj, jsonPath }
679
+ }
680
+
681
+ /** Compila la UI del instalador/desinstalador → `ui-dist/` (+ sidecar main.js). */
682
+ async function buildInstallerUI(cwd, which) {
683
+ const dir =
684
+ which === 'uninstaller' ? path.join(cwd, 'installer', 'uninstaller') : path.join(cwd, 'installer')
685
+ if (!fs.existsSync(dir)) {
686
+ die(`no existe ${path.relative(cwd, dir)} — créalo con: ow create installer`)
687
+ }
688
+ log(`vite build (${which})…`)
689
+ const code = await runProc('npx', ['vite', 'build'], { cwd: dir, shell: process.platform === 'win32' })
690
+ if (code !== 0) die(`vite build del ${which} falló`)
691
+ const uiDist = path.join(dir, 'ui-dist')
692
+
693
+ // sidecar del instalador (Node): se empaqueta dentro del propio ui/
694
+ const entry = path.join(dir, 'app', 'main.ts')
695
+ if (fs.existsSync(entry)) {
696
+ const out = path.join(uiDist, 'main.js')
697
+ log(`compilando ${which}/app/main.ts…`)
698
+ const r = spawnSync(
699
+ 'npx',
700
+ ['esbuild', entry, '--bundle', '--platform=node', '--format=esm', `--outfile=${out}`, '--log-level=warning'],
701
+ { cwd: dir, stdio: 'inherit', shell: process.platform === 'win32' }
702
+ )
703
+ if (r.status !== 0) die(`no se pudo compilar ${which}/app/main.ts`)
704
+ }
705
+ return uiDist
706
+ }
707
+
708
+ /** Ejecuta un script de tools/ del repo con Node. */
709
+ function runTool(name, args) {
710
+ const r = spawnSync(process.execPath, [packTool(name), ...args], { stdio: 'inherit' })
711
+ if (r.status !== 0) die(`falló ${name}`)
712
+ }
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
+
760
+ function readPkg(cwd) {
761
+ try {
762
+ return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'))
763
+ } catch {
764
+ return {}
765
+ }
766
+ }
767
+
768
+ async function appMeta(cwd) {
769
+ const bridge = await buildBridge(cwd, true)
770
+ const pkg = readPkg(cwd)
771
+ const slug = appSlug(cwd)
772
+ return {
773
+ bridge,
774
+ slug,
775
+ appId: bridge?.obj?.app?.id ?? slug,
776
+ appName: bridge?.obj?.app?.name ?? pkg.name ?? slug,
777
+ version: bridge?.obj?.app?.version ?? pkg.version ?? '0.0.0',
778
+ publisher: bridge?.obj?.app?.publisher ?? 'unknown',
779
+ icon: bridge?.obj?.app?.icon ?? null,
780
+ }
781
+ }
782
+
783
+ /** Copia kernel + dist + modules + main/workers a `dir` (layout de app). */
784
+ function stageAppLayout(cwd, built, dir, kernel) {
785
+ fs.rmSync(dir, { recursive: true, force: true })
786
+ fs.mkdirSync(dir, { recursive: true })
787
+ const exe = process.platform === 'win32' ? 'owear.exe' : 'owear'
788
+ fs.copyFileSync(kernel, path.join(dir, exe))
789
+ fs.chmodSync(path.join(dir, exe), 0o755)
790
+ fs.cpSync(built.dist, path.join(dir, 'app'), { recursive: true })
791
+ const mods = path.join(built.dist, 'modules')
792
+ if (fs.existsSync(mods)) fs.cpSync(mods, path.join(dir, 'modules'), { recursive: true })
793
+ if (built.mainJs) fs.copyFileSync(built.mainJs, path.join(dir, 'main.js'))
794
+ const workers = path.join(built.dist, 'workers')
795
+ if (fs.existsSync(workers)) fs.cpSync(workers, path.join(dir, 'workers'), { recursive: true })
796
+ return exe
797
+ }
798
+
799
+ function desktopEntry(meta, execLine, iconName) {
800
+ return (
801
+ `[Desktop Entry]\nType=Application\nName=${meta.appName}\n` +
802
+ `Comment=${meta.appName}\nExec=${execLine}\n` +
803
+ (iconName ? `Icon=${iconName}\n` : '') +
804
+ `Terminal=false\nCategories=Utility;\n`
805
+ )
806
+ }
807
+
808
+ function copyIcon(cwd, meta, dstBase) {
809
+ if (!meta.icon) return null
810
+ const src = path.isAbsolute(meta.icon) ? meta.icon : path.join(cwd, meta.icon)
811
+ if (!fs.existsSync(src)) return null
812
+ const ext = path.extname(src) || '.png'
813
+ const dst = dstBase + ext
814
+ fs.mkdirSync(path.dirname(dst), { recursive: true })
815
+ fs.copyFileSync(src, dst)
816
+ return path.basename(dst)
817
+ }
818
+
819
+ /** `ow build app --format binary|deb|appimage|msi` (payload de la app). */
820
+ async function cmdBuildApp(cwd, args) {
821
+ const fmt = flag(args, '--format', 'binary')
822
+ const built = await cmdBuildBundle(cwd)
823
+ const meta = await appMeta(cwd)
824
+ const slug = meta.slug
825
+ const outDir = path.join(cwd, 'release')
826
+ fs.mkdirSync(outDir, { recursive: true })
827
+ const kernel = ensureKernelBuilt(cwd)
828
+ const isWin = process.platform === 'win32'
829
+
830
+ if (fmt === 'binary') {
831
+ const bundle = assembleAppBundle(cwd, built)
832
+ const exe = isWin ? `${slug}.exe` : slug
833
+ const out = path.join(outDir, exe)
834
+ packDir(kernel, bundle, out)
835
+ log(`binario único: ${out}`)
836
+ signArtifact(args, out)
837
+ return
838
+ }
839
+
840
+ if (fmt === 'deb') {
841
+ const stage = path.join(cwd, '.owear', 'linux-stage')
842
+ const opt = path.join(stage, 'opt', slug)
843
+ stageAppLayout(cwd, built, opt, kernel)
844
+ const bin = path.join(stage, 'usr', 'bin', slug)
845
+ fs.mkdirSync(path.dirname(bin), { recursive: true })
846
+ fs.writeFileSync(
847
+ bin,
848
+ `#!/bin/sh\nAPPDIR="/opt/${slug}"\n` +
849
+ `export OW_ASSETS_DIR="$APPDIR/app"\n` +
850
+ `[ -f "$APPDIR/main.js" ] && export OW_APP_MAIN="$APPDIR/main.js"\n` +
851
+ `[ -d "$APPDIR/workers" ] && export OW_APP_WORKERS="$APPDIR/workers"\n` +
852
+ `[ -d "$APPDIR/modules" ] && export OW_MODULES_DIR="$APPDIR/modules"\n` +
853
+ `exec "$APPDIR/owear" "$@"\n`,
854
+ )
855
+ fs.chmodSync(bin, 0o755)
856
+ const apps = path.join(stage, 'usr', 'share', 'applications')
857
+ fs.mkdirSync(apps, { recursive: true })
858
+ const icons = path.join(stage, 'usr', 'share', 'icons', 'hicolor', '256x256', 'apps')
859
+ const iconName = copyIcon(cwd, meta, path.join(icons, meta.appId))
860
+ fs.writeFileSync(path.join(apps, `${meta.appId}.desktop`), desktopEntry(meta, slug, iconName ? meta.appId : null))
861
+ const out = path.join(outDir, `${slug}_${meta.version}_amd64.deb`)
862
+ runTool('owear-linux.mjs', ['deb', '--stage', stage, '--out', out, '--name', slug,
863
+ '--version', meta.version, '--maintainer', meta.publisher, '--description', meta.appName])
864
+ log(`.deb: ${out}`)
865
+ signArtifact(args, out)
866
+ return
867
+ }
868
+
869
+ if (fmt === 'appimage') {
870
+ const appDir = path.join(cwd, '.owear', `${slug}.AppDir`)
871
+ fs.rmSync(appDir, { recursive: true, force: true })
872
+ fs.mkdirSync(appDir, { recursive: true })
873
+ const usr = path.join(appDir, 'usr')
874
+ stageAppLayout(cwd, built, usr, kernel)
875
+ fs.writeFileSync(
876
+ path.join(appDir, 'AppRun'),
877
+ `#!/bin/sh\nAPPDIR="$(dirname "$(readlink -f "$0")")"\n` +
878
+ `export OW_ASSETS_DIR="$APPDIR/usr/app"\n` +
879
+ `[ -f "$APPDIR/usr/main.js" ] && export OW_APP_MAIN="$APPDIR/usr/main.js"\n` +
880
+ `[ -d "$APPDIR/usr/workers" ] && export OW_APP_WORKERS="$APPDIR/usr/workers"\n` +
881
+ `[ -d "$APPDIR/usr/modules" ] && export OW_MODULES_DIR="$APPDIR/usr/modules"\n` +
882
+ `exec "$APPDIR/usr/owear" "$@"\n`,
883
+ )
884
+ fs.chmodSync(path.join(appDir, 'AppRun'), 0o755)
885
+ const iconName = copyIcon(cwd, meta, path.join(appDir, meta.appId))
886
+ fs.writeFileSync(path.join(appDir, `${meta.appId}.desktop`), desktopEntry(meta, slug, iconName ? meta.appId : null))
887
+ const out = path.join(outDir, `${slug}-${meta.version}-x86_64.AppImage`)
888
+ runTool('owear-linux.mjs', ['appimage', '--appdir', appDir, '--out', out, '--arch', 'x86_64'])
889
+ log(`AppImage: ${out}`)
890
+ signArtifact(args, out)
891
+ return
892
+ }
893
+
894
+ if (fmt === 'msi') {
895
+ const stage = path.join(cwd, '.owear', 'win-stage')
896
+ stageAppLayout(cwd, built, stage, kernel)
897
+ const out = path.join(outDir, `${slug}-${meta.version}.msi`)
898
+ runTool('owear-msi.mjs', ['--stage', stage, '--out', out, '--name', slug,
899
+ '--version', meta.version, '--publisher', meta.publisher, '--appId', meta.appId])
900
+ log(`.MSI: ${out}`)
901
+ signArtifact(args, out)
902
+ return
903
+ }
904
+
905
+ die(`formato desconocido: ${fmt} (binary|deb|appimage|msi)`)
906
+ }
907
+
908
+ /** `ow build installer|uninstaller` → binario instalador/desinstalador. */
909
+ async function cmdBuildInstaller(cwd, args, isUninstaller) {
910
+ const kernel = ensureKernelBuilt(cwd)
911
+ const slug = appSlug(cwd)
912
+ const pack = readPackConfig(cwd)
913
+ const bridge = await buildBridge(cwd)
914
+ const target = bridge.obj?.targets?.[platformKey()] ?? {}
915
+ const mode = flag(args, '--mode', target.layout ?? pack.modes?.default ?? 'minimal')
916
+
917
+ const exe = process.platform === 'win32' ? '.exe' : ''
918
+ const outDir = path.join(cwd, 'release')
919
+ fs.mkdirSync(outDir, { recursive: true })
920
+
921
+ // 1) payload de la app (sólo para el instalador)
922
+ let payloadDir = null
923
+ if (!isUninstaller) {
924
+ const built = await cmdBuildBundle(cwd)
925
+ payloadDir = path.join(cwd, '.owear', 'installer-payload', 'payload')
926
+ fs.rmSync(payloadDir, { recursive: true, force: true })
927
+ const bundle = assembleAppBundle(cwd, built)
928
+ if (mode === 'minimal') {
929
+ fs.mkdirSync(payloadDir, { recursive: true })
930
+ packDir(kernel, bundle, path.join(payloadDir, `${slug}${exe}`))
931
+ } else {
932
+ fs.cpSync(bundle, payloadDir, { recursive: true })
933
+ }
934
+
935
+ // 1b) desinstalador (si existe installer/uninstaller) → se instala junto a la app
936
+ if (fs.existsSync(path.join(cwd, 'installer', 'uninstaller'))) {
937
+ log('build del desinstalador…')
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)
946
+ const un = path.join(outDir, `${slug}-uninstaller${exe}`)
947
+ if (fs.existsSync(un)) fs.copyFileSync(un, path.join(payloadDir, path.basename(un)))
948
+ }
949
+ }
950
+
951
+ // 2) UI del instalador/desinstalador
952
+ const ui = await buildInstallerUI(cwd, isUninstaller ? 'uninstaller' : 'installer')
953
+
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)
958
+ const meta = {
959
+ appId: bridge.obj?.app?.id ?? slug,
960
+ appName: bridge.obj?.app?.name ?? slug,
961
+ version: bridge.obj?.app?.version ?? '0.0.0',
962
+ publisher: bridge.obj?.app?.publisher ?? null,
963
+ icon: iconOk ? 'icon' + path.extname(iconAbs) : null,
964
+ mode: isUninstaller ? 'uninstaller' : mode,
965
+ layout: target.layout ?? mode,
966
+ order: bridge.obj?.order ?? null,
967
+ protect: bridge.obj?.protect ?? null,
968
+ hooks: bridge.obj?.hooks ?? null,
969
+ node: bridge.obj?.node ?? null,
970
+ targets: bridge.obj?.targets ?? null,
971
+ }
972
+ const metaPath = path.join(cwd, '.owear', isUninstaller ? 'uninstaller.json' : 'installer.json')
973
+ fs.mkdirSync(path.dirname(metaPath), { recursive: true })
974
+ fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n')
975
+
976
+ // 4) ensambla el binario (kernel + payload + footer OWPK1)
977
+ const kind = isUninstaller ? 'uninstaller' : 'installer'
978
+ const out = path.join(outDir, `${slug}-${kind}${exe}`)
979
+ const toolArgs = [
980
+ packTool('owear-installer.mjs'),
981
+ '--kernel', kernel,
982
+ '--out', out,
983
+ '--ui', ui,
984
+ '--bridge', bridge.jsonPath,
985
+ '--meta', metaPath,
986
+ '--name', slug,
987
+ ]
988
+ if (payloadDir) toolArgs.push('--payload', payloadDir)
989
+ if (iconOk) toolArgs.push('--icon', iconAbs)
990
+ const r = spawnSync(process.execPath, toolArgs, { stdio: 'inherit' })
991
+ if (r.status !== 0) die(`falló owear-installer (${kind})`)
992
+ log(`${kind}: ${out}`)
993
+ signArtifact(args, out)
471
994
  }
472
995
 
473
996
  main().catch((e) => die(e?.stack ?? String(e)))