@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 +7 -5
- package/src/ow.js +466 -22
- package/template-installer/README.md +38 -0
- package/template-installer/app/main.ts +22 -0
- package/template-installer/index.html +60 -0
- package/template-installer/package.json +22 -0
- package/template-installer/src/installer.ts +152 -0
- package/template-installer/src/style.css +66 -0
- package/template-installer/tsconfig.json +13 -0
- package/template-installer/uninstaller/app/main.ts +16 -0
- package/template-installer/uninstaller/index.html +48 -0
- package/template-installer/uninstaller/package.json +22 -0
- package/template-installer/uninstaller/src/uninstaller.ts +76 -0
- package/template-installer/uninstaller/vite.config.ts +10 -0
- package/template-installer/vite.config.ts +15 -0
- package/tools/owear-installer.mjs +81 -0
- package/tools/owear-linux.mjs +158 -0
- package/tools/owear-msi.mjs +113 -0
- package/tools/owear-pack.mjs +93 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owear/cli",
|
|
3
|
-
"version": "0.1.
|
|
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/
|
|
12
|
-
"@owear/
|
|
11
|
+
"@owear/linux-x64-gnu": "0.1.3",
|
|
12
|
+
"@owear/win32-x64": "0.1.3"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
16
|
-
"
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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}
|
|
152
|
-
${C.green}ow
|
|
153
|
-
${C.green}ow
|
|
154
|
-
${C.green}ow
|
|
155
|
-
${C.green}ow
|
|
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
|
|
164
|
-
if (
|
|
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, '
|
|
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(
|
|
176
|
-
|
|
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
|
-
|
|
442
|
-
|
|
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
|
+
})
|