lampson 0.2.7 → 0.2.9
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/.env.example +10 -1
- package/README.md +9 -3
- package/bin/lampson.js +1 -1
- package/chat.syn +112 -8
- package/cli.syn +143 -7
- package/edge.syn +26 -0
- package/hub.tpl.syn +127 -6
- package/lampson.ps1 +43 -2
- package/lampson.sh +40 -2
- package/lib/agents.syn +30 -9
- package/lib/approvals.syn +2 -2
- package/lib/auth.syn +274 -0
- package/lib/computer.syn +2000 -0
- package/lib/computer_judge.syn +410 -0
- package/lib/fs.syn +242 -0
- package/lib/git.syn +2 -2
- package/lib/judge.syn +78 -0
- package/lib/line.syn +2 -2
- package/lib/loop.syn +41 -8
- package/lib/lsp.syn +4 -4
- package/lib/mcp.syn +20 -4
- package/lib/md.syn +11 -11
- package/lib/origin.syn +67 -0
- package/lib/permission.syn +47 -2
- package/lib/prompt.syn +10 -1
- package/lib/provider.syn +70 -20
- package/lib/schedule.syn +12 -12
- package/lib/session.syn +5 -5
- package/lib/settings.syn +65 -1
- package/lib/skills.syn +22 -6
- package/lib/tools/bash.syn +1 -1
- package/lib/tools/common.syn +1 -1
- package/lib/tools/fetch.syn +600 -0
- package/lib/tools/grep.syn +1 -1
- package/lib/tools/memo.syn +3 -3
- package/lib/tools/proc.syn +38 -20
- package/lib/tools/qr.js +26 -0
- package/lib/tools/url.syn +220 -0
- package/lib/tools.syn +25 -2
- package/lib/trace.syn +2 -2
- package/lib/tree.syn +29 -13
- package/lib/update.syn +1 -1
- package/lib/workspaces.syn +202 -36
- package/package.json +12 -3
- package/public/apple-touch-icon.png +0 -0
- package/public/badge-96.png +0 -0
- package/public/css/code.css +48 -0
- package/public/css/hub.css +9 -0
- package/public/css/login.css +16 -0
- package/public/css/panel.css +30 -0
- package/public/css/sidebar.css +21 -0
- package/public/css/tokens.css +1 -1
- package/public/hub.html +11 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-maskable-512.png +0 -0
- package/public/icons-src/badge.svg +12 -0
- package/public/icons-src/icon-maskable.svg +15 -0
- package/public/icons-src/icon.svg +13 -0
- package/public/index.html +14 -3
- package/public/js/agents.js +1 -1
- package/public/js/chat.js +1 -1
- package/public/js/config.js +145 -2
- package/public/js/core.js +5 -3
- package/public/js/devices.js +80 -0
- package/public/js/editor.js +232 -0
- package/public/js/login.js +78 -0
- package/public/js/procs.js +1 -1
- package/public/js/pwa.js +5 -0
- package/public/js/tree.js +224 -14
- package/public/login.html +43 -0
- package/public/manifest.webmanifest +16 -0
- package/public/offline.html +24 -0
- package/public/sw.js +63 -0
- package/public/vendor/highlight.min.js +1343 -0
- package/public/vendor/qrcode.js +2297 -0
- package/skills/computer-use/SKILL.md +178 -0
- package/skills/lampson/SKILL.md +13 -0
- package/web.syn +82 -7
package/lib/workspaces.syn
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
-- web.syn chat.syn → copias (refrescadas al arrancar) .lampson/ → estado propio (sesiones, tareas…)
|
|
8
8
|
-- .lampson/global → junction a <home>/.lampson (config.json con keys, mcp/lsp globales, skills-*)
|
|
9
9
|
-- con eso `file("workspace/*")`, `use "./lib/x.syn"` y todo el código de siempre funcionan SIN cambios.
|
|
10
|
-
-- * cada workspace corre `synsema serve web.syn --port
|
|
10
|
+
-- * cada workspace corre `synsema serve web.syn --port 471NN --bind 127.0.0.1` DESACOPLADO del hub (start /b,
|
|
11
11
|
-- process_group:false): sobrevive a reinicios del hub y a cerrar terminales. Solo loopback.
|
|
12
12
|
-- * el hub (hub.syn, :8080) se GENERA desde hub.tpl.syn con una ruta proxy por workspace y corre con
|
|
13
13
|
-- `synsema serve hub.syn` desacoplado: al registrar/quitar un workspace se reescribe y un helper lo reinicia.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
use "./tools/bash.syn" as t_bash
|
|
19
19
|
use "./tools/proc.syn" as proc
|
|
20
20
|
use "./settings.syn" as settings
|
|
21
|
+
use "./auth.syn" as auth
|
|
21
22
|
|
|
22
23
|
export let ROOT be ".lampson/ws"
|
|
23
24
|
export let REGISTRY be ".lampson/workspaces.json"
|
|
@@ -47,11 +48,11 @@ export task home()
|
|
|
47
48
|
let r be sh("pwd -W 2>/dev/null || pwd -P", 10)
|
|
48
49
|
let p be trim(text(r["stdout"]))
|
|
49
50
|
when is_win()
|
|
50
|
-
give
|
|
51
|
+
give replace(p, "/", "\\")
|
|
51
52
|
give p
|
|
52
53
|
|
|
53
54
|
export task norm(p)
|
|
54
|
-
let s be lower(
|
|
55
|
+
let s be lower(replace(trim(text(p)), "\\", "/"))
|
|
55
56
|
when matches(s, "/[a-z]/.*")
|
|
56
57
|
set s to slice(s, 1, 2) + ":" + slice(s, 2, length(s))
|
|
57
58
|
while length(s) > 1 and slice(s, length(s) - 1, length(s)) == "/"
|
|
@@ -64,7 +65,7 @@ export task slug_of(path)
|
|
|
64
65
|
let n be norm(path)
|
|
65
66
|
let parts be where(split(n, "/"), (x) => x != "")
|
|
66
67
|
let base be when length(parts) > 0 then parts[length(parts) - 1] otherwise "workspace"
|
|
67
|
-
let runs be
|
|
68
|
+
let runs be regex_find_all(base, "[a-z0-9]+")
|
|
68
69
|
let clean be when length(runs) == 0 then "project" otherwise join(runs, "-")
|
|
69
70
|
give clean + "-" + slice(decode(sha256(n), "hex"), 0, 8)
|
|
70
71
|
|
|
@@ -102,10 +103,7 @@ export task migrate_ports()
|
|
|
102
103
|
each k in keys(doc["workspaces"])
|
|
103
104
|
let w be doc["workspaces"][k]
|
|
104
105
|
when w["port"] < FIRST_PORT
|
|
105
|
-
|
|
106
|
-
each l in proc.listeners()
|
|
107
|
-
when l["port"] == old
|
|
108
|
-
proc.kill_pid(l["pid"])
|
|
106
|
+
kill_ports([w["port"]])
|
|
109
107
|
set w["port"] to doc["next_port"]
|
|
110
108
|
set doc["next_port"] to doc["next_port"] + 1
|
|
111
109
|
set doc["workspaces"][k] to w
|
|
@@ -167,14 +165,14 @@ export task dir_of(slug)
|
|
|
167
165
|
|
|
168
166
|
task link(where_, target)
|
|
169
167
|
when is_win()
|
|
170
|
-
let r be run("cmd", ["/c", "mklink", "/J",
|
|
168
|
+
let r be run("cmd", ["/c", "mklink", "/J", replace(where_, "/", "\\"), replace(target, "/", "\\")], 15)
|
|
171
169
|
give r["exit_code"] == 0
|
|
172
170
|
let r be run("ln", ["-s", target, where_], 10)
|
|
173
171
|
give r["exit_code"] == 0
|
|
174
172
|
|
|
175
173
|
task unlink(where_)
|
|
176
174
|
when is_win()
|
|
177
|
-
run("cmd", ["/c", "rmdir",
|
|
175
|
+
run("cmd", ["/c", "rmdir", replace(where_, "/", "\\")], 10)
|
|
178
176
|
otherwise
|
|
179
177
|
run("rm", ["-f", where_], 10)
|
|
180
178
|
give true
|
|
@@ -187,7 +185,7 @@ task ensure_dir(p)
|
|
|
187
185
|
when is_win()
|
|
188
186
|
-- argumentos separados, como mklink: una línea entera con comillas dentro de `cmd /c` se rompe al citarla.
|
|
189
187
|
-- mkdir de cmd crea los intermedios y falla (sin daño) si ya existe.
|
|
190
|
-
run("cmd", ["/c", "mkdir",
|
|
188
|
+
run("cmd", ["/c", "mkdir", replace(p, "/", "\\")], 10)
|
|
191
189
|
otherwise
|
|
192
190
|
run("mkdir", ["-p", p], 10)
|
|
193
191
|
give true
|
|
@@ -254,7 +252,7 @@ task migrate(w, d)
|
|
|
254
252
|
recover err
|
|
255
253
|
set doc to nothing
|
|
256
254
|
when doc != nothing and contains(doc, "meta") and contains(doc["meta"], "project") and doc["meta"]["project"] == slug
|
|
257
|
-
let id be
|
|
255
|
+
let id be replace(e["name"], ".json", "")
|
|
258
256
|
sh("mkdir -p '" + d + "/.lampson/sessions' '" + d + "/.lampson/trace' && mv -f '.lampson/sessions/" + e["name"] + "' '" + d + "/.lampson/sessions/' && mv -f .lampson/trace/" + id + ".* '" + d + "/.lampson/trace/' 2>/dev/null; true", 20)
|
|
259
257
|
set moved to moved + 1
|
|
260
258
|
recover err
|
|
@@ -313,7 +311,7 @@ export task ensure(path)
|
|
|
313
311
|
let port be doc["next_port"]
|
|
314
312
|
set doc["next_port"] to port + 1
|
|
315
313
|
save_reg(doc)
|
|
316
|
-
let parts be where(split(
|
|
314
|
+
let parts be where(split(replace(text(path), "\\", "/"), "/"), (x) => x != "")
|
|
317
315
|
set w to {"slug": slug, "name": parts[length(parts) - 1], "path": text(path), "port": port, "policy": "auto", "created": now(), "last_used": now()}
|
|
318
316
|
put(w)
|
|
319
317
|
when regen_hub() and hub_alive()
|
|
@@ -350,8 +348,10 @@ export task remove(slug)
|
|
|
350
348
|
task hub_routes(w)
|
|
351
349
|
let base be "http://127.0.0.1:" + text(w["port"])
|
|
352
350
|
let lines be []
|
|
351
|
+
-- el API pide auth en el hub: el proceso del workspace ve 127.0.0.1 y confía en todo lo que le llega.
|
|
352
|
+
-- Los links de aprobación no: llevan su propio token de un solo uso.
|
|
353
353
|
each m in ["GET", "POST"]
|
|
354
|
-
set lines to append(lines, " route \"" + m + " /w/" + w["slug"] + "/api/*path\"\n proxy to \"" + base + "\"")
|
|
354
|
+
set lines to append(lines, " route \"" + m + " /w/" + w["slug"] + "/api/*path\" requires auth\n proxy to \"" + base + "\"")
|
|
355
355
|
set lines to append(lines, " route \"GET /w/" + w["slug"] + "/approve/*path\"\n proxy to \"" + base + "\"")
|
|
356
356
|
give join(lines, "\n")
|
|
357
357
|
|
|
@@ -365,7 +365,10 @@ export task regen_hub()
|
|
|
365
365
|
let blocks be []
|
|
366
366
|
each w in all()
|
|
367
367
|
set blocks to append(blocks, hub_routes(w))
|
|
368
|
-
let body be
|
|
368
|
+
let body be replace(tpl, " -- @workspaces", when length(blocks) == 0 then " -- (sin workspaces registrados todavía)" otherwise join(blocks, "\n"))
|
|
369
|
+
-- con dominio propio, de afuera se entra SOLO por el edge (:443, TLS): el hub deja de escuchar en la red
|
|
370
|
+
when domain_cfg() != nothing
|
|
371
|
+
set body to replace(body, " -- @bind", " bind \"127.0.0.1\" -- dominio activo (lampson --domain): de afuera se entra por el edge HTTPS")
|
|
369
372
|
let cur be ""
|
|
370
373
|
try
|
|
371
374
|
set cur to read_file(HUB_FILE)
|
|
@@ -384,8 +387,8 @@ task spawn_detached(cwd, cmdline, envs)
|
|
|
384
387
|
when is_win()
|
|
385
388
|
-- WorkingDirectory absoluto: el relativo se resolvería contra el cwd del PowerShell (que ya es `cwd`)
|
|
386
389
|
let h be home()
|
|
387
|
-
let abs be when cwd == "." then h otherwise h + "\\" +
|
|
388
|
-
let ps be "Start-Process -FilePath cmd -ArgumentList '/c', '" +
|
|
390
|
+
let abs be when cwd == "." then h otherwise h + "\\" + replace(cwd, "/", "\\")
|
|
391
|
+
let ps be "Start-Process -FilePath cmd -ArgumentList '/c', '" + replace(cmdline, "'", "''") + "' -WorkingDirectory '" + replace(abs, "'", "''") + "' -WindowStyle Hidden"
|
|
389
392
|
let p be proc_spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], {"cwd": cwd, "env": envs, "process_group": false})
|
|
390
393
|
proc_wait(p, 15)
|
|
391
394
|
proc_close(p)
|
|
@@ -414,6 +417,10 @@ export task start_ws(w)
|
|
|
414
417
|
let sep be when is_win() then "\\" otherwise "/"
|
|
415
418
|
let envf be h + sep + ".env"
|
|
416
419
|
let envs be {"LAMPSON_WS": w["slug"], "LAMPSON_WORKSPACE": w["path"], "LAMPSON_PORT": text(w["port"]), "LAMPSON_HOME": h}
|
|
420
|
+
-- el juez de computer use (⚙ → Computer use): el runtime lo lee del entorno al arrancar — por eso aplica al reiniciar
|
|
421
|
+
let je be settings.judge_env()
|
|
422
|
+
each k in keys(je)
|
|
423
|
+
set envs[k] to je[k]
|
|
417
424
|
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
418
425
|
-- el .env de la instalación (cwd = home; la capability es relativa, la ruta que se pasa al hijo es absoluta)
|
|
419
426
|
let cmd be exe + " serve web.syn --port " + text(w["port"]) + " --bind 127.0.0.1" + (when file_exists(".env") then " --env-file \"" + envf + "\"" otherwise "") + " > .lampson/web.log 2>&1"
|
|
@@ -470,16 +477,30 @@ export task pause_ws(w)
|
|
|
470
477
|
summary()
|
|
471
478
|
give true
|
|
472
479
|
|
|
473
|
-
|
|
480
|
+
-- matar lo que escucha en estos puertos: UNA consulta de puertos (proc.port_owners) para todos → cuántos mató
|
|
481
|
+
export task kill_ports(ports)
|
|
474
482
|
require exec
|
|
475
|
-
require time
|
|
476
483
|
require env("LAMPSON_*")
|
|
477
484
|
require env("OS")
|
|
478
|
-
|
|
479
|
-
let
|
|
480
|
-
each l in
|
|
481
|
-
|
|
482
|
-
|
|
485
|
+
let n be 0
|
|
486
|
+
let done be {}
|
|
487
|
+
each l in proc.port_owners()
|
|
488
|
+
when contains(ports, l["port"]) and not contains(done, text(l["pid"]))
|
|
489
|
+
set done[text(l["pid"])] to true
|
|
490
|
+
proc.kill_pid(l["pid"])
|
|
491
|
+
set n to n + 1
|
|
492
|
+
give n
|
|
493
|
+
|
|
494
|
+
export task stop_ws(w)
|
|
495
|
+
give kill_ports([w["port"]]) > 0
|
|
496
|
+
|
|
497
|
+
-- Lampson entero (lampson --hub stop, ⚙ → Apagar o reiniciar): workspaces y hub en una sola pasada, y el edge
|
|
498
|
+
export task stop_all()
|
|
499
|
+
let ports be [HUB_PORT]
|
|
500
|
+
each w in all()
|
|
501
|
+
set ports to append(ports, w["port"])
|
|
502
|
+
stop_edge()
|
|
503
|
+
give kill_ports(ports)
|
|
483
504
|
|
|
484
505
|
-- ---------- hub ----------
|
|
485
506
|
export task hub_alive()
|
|
@@ -508,15 +529,34 @@ export task start_hub()
|
|
|
508
529
|
spawn_detached(".", exe + " serve " + HUB_FILE + " > .lampson/hub.log 2>&1", {"LAMPSON_HOME": home()})
|
|
509
530
|
give true
|
|
510
531
|
|
|
511
|
-
--
|
|
512
|
-
--
|
|
532
|
+
-- un comando de cli.syn en un helper DESACOPLADO: para lo que el propio hub pide y en el medio lo mata a él
|
|
533
|
+
-- (reiniciarse, apagar Lampson entero). Log en .lampson/<cmd>.log
|
|
534
|
+
export task cli_later(cmd)
|
|
535
|
+
require exec
|
|
536
|
+
require time
|
|
537
|
+
require env("LAMPSON_*")
|
|
538
|
+
require env("OS")
|
|
539
|
+
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
540
|
+
spawn_detached(".", exe + " run cli.syn > .lampson/" + cmd + ".log 2>&1", {"LAMPSON_HOME": home(), "LAMPSON_CMD": cmd, "LAMPSON_DETACHED": "1"})
|
|
541
|
+
give true
|
|
542
|
+
|
|
543
|
+
-- hub.syn cambió (workspace nuevo/quitado): cli.syn hub-restart para el hub y lo vuelve a arrancar (los workspaces
|
|
544
|
+
-- siguen) — puede llamarse desde el propio hub o desde cli.syn
|
|
513
545
|
export task restart_hub_later()
|
|
546
|
+
give cli_later("hub-restart")
|
|
547
|
+
|
|
548
|
+
-- reiniciar UN workspace desde adentro (su propio proceso va a morir en el medio): un helper desacoplado
|
|
549
|
+
-- (cli.syn ws-restart) lo para y lo vuelve a arrancar con el entorno nuevo — el juez de computer use se lee al
|
|
550
|
+
-- arrancar el proceso (settings.judge_env), así que cambiarlo en ⚙ necesita esto (2026-09-28)
|
|
551
|
+
export task restart_ws_later(slug)
|
|
514
552
|
require exec
|
|
515
553
|
require time
|
|
516
554
|
require env("LAMPSON_*")
|
|
517
555
|
require env("OS")
|
|
518
556
|
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
519
|
-
|
|
557
|
+
-- cwd = la instalación: en Windows spawn_detached resuelve "." contra home(); en unix hay que dársela absoluta
|
|
558
|
+
let cwd be when is_win() then "." otherwise home()
|
|
559
|
+
spawn_detached(cwd, exe + " run cli.syn > .lampson/ws-restart.log 2>&1", {"LAMPSON_HOME": home(), "LAMPSON_CMD": "ws-restart", "LAMPSON_WS": slug})
|
|
520
560
|
give true
|
|
521
561
|
|
|
522
562
|
export task stop_hub()
|
|
@@ -524,10 +564,135 @@ export task stop_hub()
|
|
|
524
564
|
require time
|
|
525
565
|
require env("LAMPSON_*")
|
|
526
566
|
require env("OS")
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
567
|
+
give kill_ports([HUB_PORT]) > 0
|
|
568
|
+
|
|
569
|
+
-- ---------- dominio propio (opcional): edge HTTPS en :443 ----------
|
|
570
|
+
-- `lampson --domain lampson.midominio.com --email yo@midominio.com` guarda .lampson/domain.json; desde ahí el
|
|
571
|
+
-- supervisor mantiene vivo edge.syn (TLS de Let's Encrypt) y el hub pasa a escuchar solo en 127.0.0.1.
|
|
572
|
+
-- `lampson --domain off` lo quita y todo vuelve a ser como antes. Sin dominio, nada de esto corre.
|
|
573
|
+
export let EDGE_FILE be "edge.syn"
|
|
574
|
+
let DOMAIN_FILE be ".lampson/domain.json"
|
|
575
|
+
-- tras un arranque fallido no reintentar enseguida: Let's Encrypt limita las validaciones fallidas por hora
|
|
576
|
+
let EDGE_RETRY_S be 600
|
|
577
|
+
|
|
578
|
+
task edge_port()
|
|
579
|
+
give int(env("LAMPSON_EDGE_PORT", "443"))
|
|
580
|
+
|
|
581
|
+
export task domain_cfg()
|
|
582
|
+
require file.read(".lampson")
|
|
583
|
+
require file.read(".lampson/*")
|
|
584
|
+
try
|
|
585
|
+
when file_exists(DOMAIN_FILE)
|
|
586
|
+
let d be json_decode(read_file(DOMAIN_FILE))
|
|
587
|
+
-- ojo: este módulo define su propia task `get(slug)`, que tapa el builtin get(mapa, clave, default)
|
|
588
|
+
when type_of(d) == "map" and contains(d, "domain") and text(d["domain"]) != ""
|
|
589
|
+
give d
|
|
590
|
+
recover err
|
|
591
|
+
give nothing
|
|
592
|
+
give nothing
|
|
593
|
+
|
|
594
|
+
export task valid_domain(d)
|
|
595
|
+
give matches(d, "[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+") and not matches(d, "[0-9.]+")
|
|
596
|
+
|
|
597
|
+
-- guarda el dominio; el email (contacto de Let's Encrypt) se pide la primera vez y después se recuerda
|
|
598
|
+
export task set_domain(domain, email)
|
|
599
|
+
require file(".lampson")
|
|
600
|
+
require file(".lampson/*")
|
|
601
|
+
require env("LAMPSON_*")
|
|
602
|
+
let d be lower(trim(text(domain)))
|
|
603
|
+
set d to regex_replace(d, "^https?://", "")
|
|
604
|
+
set d to regex_replace(d, "/.*$", "")
|
|
605
|
+
when not valid_domain(d)
|
|
606
|
+
raise "«" + text(domain) + "» no parece un dominio (ej.: lampson.midominio.com)"
|
|
607
|
+
let prev be domain_cfg()
|
|
608
|
+
let em be trim(text(email))
|
|
609
|
+
when em == "" and prev != nothing
|
|
610
|
+
set em to when contains(prev, "email") then text(prev["email"]) otherwise ""
|
|
611
|
+
when not contains(em, "@")
|
|
612
|
+
raise "falta el email para Let's Encrypt (avisos de vencimiento del certificado): --email vos@dominio.com"
|
|
613
|
+
write_file(DOMAIN_FILE, json_encode({"domain": d, "email": em, "since": now()}))
|
|
614
|
+
-- el QR de emparejamiento apunta al dominio
|
|
615
|
+
auth.set_public_url("https://" + d)
|
|
616
|
+
give {"domain": d, "email": em}
|
|
617
|
+
|
|
618
|
+
export task clear_domain()
|
|
619
|
+
require file(".lampson")
|
|
620
|
+
require file(".lampson/*")
|
|
621
|
+
require env("LAMPSON_*")
|
|
622
|
+
let prev be domain_cfg()
|
|
623
|
+
write_file(DOMAIN_FILE, "{}")
|
|
624
|
+
when prev != nothing and auth.public_url() == "https://" + prev["domain"]
|
|
625
|
+
auth.set_public_url("")
|
|
626
|
+
give prev != nothing
|
|
627
|
+
|
|
628
|
+
export task edge_alive()
|
|
629
|
+
require exec
|
|
630
|
+
require time
|
|
631
|
+
require env("OS")
|
|
632
|
+
give length(edge_pids()) > 0
|
|
633
|
+
|
|
634
|
+
export task start_edge()
|
|
635
|
+
require exec
|
|
636
|
+
require time
|
|
637
|
+
require env("LAMPSON_*")
|
|
638
|
+
require env("OS")
|
|
639
|
+
require file(".lampson")
|
|
640
|
+
require file(".lampson/*")
|
|
641
|
+
let cfg be domain_cfg()
|
|
642
|
+
when cfg == nothing
|
|
643
|
+
give false
|
|
644
|
+
write_file(".lampson/edge.last", text(now()))
|
|
645
|
+
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
646
|
+
let cmd be exe + " serve " + EDGE_FILE + " --port " + text(edge_port()) + " --domain " + cfg["domain"] + " --tls-auto " + cfg["email"] + " > .lampson/edge.log 2>&1"
|
|
647
|
+
spawn_detached(".", cmd, {"LAMPSON_HOME": home()})
|
|
648
|
+
give true
|
|
649
|
+
|
|
650
|
+
-- el edge escucha en :443, un puerto que en un VPS puede ser de otro (nginx, caddy): se reconoce por su línea de
|
|
651
|
+
-- comando, no solo por el puerto
|
|
652
|
+
task edge_pids()
|
|
653
|
+
let out be []
|
|
654
|
+
each l in proc.port_owners()
|
|
655
|
+
when l["port"] == edge_port() and contains(proc.command_of(l["pid"]), EDGE_FILE)
|
|
656
|
+
set out to append(out, l["pid"])
|
|
657
|
+
give out
|
|
658
|
+
|
|
659
|
+
export task stop_edge()
|
|
660
|
+
require exec
|
|
661
|
+
require time
|
|
662
|
+
require env("OS")
|
|
663
|
+
let pids be edge_pids()
|
|
664
|
+
each pid in pids
|
|
665
|
+
proc.kill_pid(pid)
|
|
666
|
+
give length(pids) > 0
|
|
667
|
+
|
|
668
|
+
-- lo llama el tick: dominio configurado + edge caído → levantarlo (con espera tras un intento fallido)
|
|
669
|
+
export task keep_edge()
|
|
670
|
+
require exec
|
|
671
|
+
require time
|
|
672
|
+
require env("LAMPSON_*")
|
|
673
|
+
require env("OS")
|
|
674
|
+
require file(".lampson")
|
|
675
|
+
require file(".lampson/*")
|
|
676
|
+
when domain_cfg() == nothing or edge_alive()
|
|
677
|
+
give false
|
|
678
|
+
let last be 0
|
|
679
|
+
try
|
|
680
|
+
set last to number(read_file(".lampson/edge.last"))
|
|
681
|
+
recover err
|
|
682
|
+
set last to 0
|
|
683
|
+
when now() - last < EDGE_RETRY_S
|
|
684
|
+
give false
|
|
685
|
+
give start_edge()
|
|
686
|
+
|
|
687
|
+
-- últimas líneas del log del edge (para diagnosticar desde la CLI)
|
|
688
|
+
export task edge_log(n)
|
|
689
|
+
require file.read(".lampson")
|
|
690
|
+
require file.read(".lampson/*")
|
|
691
|
+
try
|
|
692
|
+
let lines be where(split(replace(read_file(".lampson/edge.log"), "\r", ""), "\n"), (l) => trim(l) != "")
|
|
693
|
+
give slice(lines, max(0, length(lines) - n), length(lines))
|
|
694
|
+
recover err
|
|
695
|
+
give []
|
|
531
696
|
|
|
532
697
|
-- ---------- política y supervisor (tick del hub) ----------
|
|
533
698
|
export task idle_hours()
|
|
@@ -587,6 +752,7 @@ export task tick()
|
|
|
587
752
|
require file.read("chat.syn")
|
|
588
753
|
when migrate_ports() and hub_alive()
|
|
589
754
|
restart_hub_later()
|
|
755
|
+
keep_edge()
|
|
590
756
|
let started be []
|
|
591
757
|
let stopped be []
|
|
592
758
|
let list be all()
|
|
@@ -669,14 +835,14 @@ export task browse(path)
|
|
|
669
835
|
require env("LAMPSON_*")
|
|
670
836
|
require env("OS")
|
|
671
837
|
let p be when path == nothing or trim(text(path)) == "" then env("HOME", env("USERPROFILE", "/")) otherwise text(path)
|
|
672
|
-
let r be sh("cd \"" +
|
|
673
|
-
let lines be where(split(
|
|
838
|
+
let r be sh("cd \"" + replace(p, "\"", "") + "\" 2>/dev/null && (pwd -W 2>/dev/null || pwd -P) && ls -1 -d -- */ 2>/dev/null", 10)
|
|
839
|
+
let lines be where(split(replace(text(r["stdout"]), "\r", ""), "\n"), (x) => trim(x) != "")
|
|
674
840
|
when length(lines) == 0
|
|
675
841
|
give {"path": p, "dirs": [], "error": "no existe o no se puede leer"}
|
|
676
842
|
let here be lines[0]
|
|
677
843
|
when is_win()
|
|
678
|
-
set here to
|
|
679
|
-
let dirs be apply(slice(lines, 1, length(lines)), (d) =>
|
|
680
|
-
let parts be where(split(
|
|
844
|
+
set here to replace(here, "/", "\\")
|
|
845
|
+
let dirs be apply(slice(lines, 1, length(lines)), (d) => replace(d, "/", ""))
|
|
846
|
+
let parts be where(split(replace(here, "\\", "/"), "/"), (x) => x != "")
|
|
681
847
|
let parent be when length(parts) > 1 then join(slice(parts, 0, length(parts) - 1), when is_win() then "\\" otherwise "/") otherwise (when is_win() then parts[0] + "\\" otherwise "/")
|
|
682
848
|
give {"path": here, "parent": parent, "dirs": where(dirs, (d) => not starts_with(d, "."))}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lampson",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, plugins (your own tools, any language), LSP, MCP, sub-agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/kitecosmic/lampson#readme",
|
|
11
11
|
"bugs": "https://github.com/kitecosmic/lampson/issues",
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"agent",
|
|
14
|
+
"coding-agent",
|
|
15
|
+
"synsema",
|
|
16
|
+
"llm",
|
|
17
|
+
"cli",
|
|
18
|
+
"lsp",
|
|
19
|
+
"mcp"
|
|
20
|
+
],
|
|
13
21
|
"bin": {
|
|
14
22
|
"lampson": "bin/lampson.js"
|
|
15
23
|
},
|
|
@@ -22,6 +30,7 @@
|
|
|
22
30
|
"chat.syn",
|
|
23
31
|
"web.syn",
|
|
24
32
|
"hub.tpl.syn",
|
|
33
|
+
"edge.syn",
|
|
25
34
|
"cli.syn",
|
|
26
35
|
"lampson.ps1",
|
|
27
36
|
"lampson.sh",
|
|
@@ -34,7 +43,7 @@
|
|
|
34
43
|
"node": ">=18"
|
|
35
44
|
},
|
|
36
45
|
"dependencies": {
|
|
37
|
-
"synsema": "^0.6.
|
|
46
|
+
"synsema": "^0.6.42"
|
|
38
47
|
},
|
|
39
48
|
"scripts": {
|
|
40
49
|
"test": "pwsh -NoProfile -File tests/run.ps1"
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* code: el editor de archivos del visor y el tema de highlight.js (archivos abiertos y bloques ``` del chat) */
|
|
2
|
+
|
|
3
|
+
/* ---- editor: textarea transparente sobre el <pre> resaltado; el gutter aparte sigue el scroll (js/editor.js) ---- */
|
|
4
|
+
#viewer.edit { height:100%; display:flex; flex-direction:column; }
|
|
5
|
+
#viewer.edit .vh { flex:none; }
|
|
6
|
+
#editor { flex:1; min-height:0; position:relative; overflow:hidden; background:var(--paper); }
|
|
7
|
+
#editor[hidden] { display:none; }
|
|
8
|
+
/* la misma métrica que #viewer pre (layout.css): 12.5px/1.62 mono, padding 14px 18px, números en 4ch + 16px */
|
|
9
|
+
#egut { position:absolute; left:0; top:0; width:calc(18px + 4ch + 16px); padding:14px 0 14px 18px; font:400 12.5px/1.62 var(--mono); color:var(--ink-3); text-align:right; user-select:none; pointer-events:none; will-change:transform; }
|
|
10
|
+
#egut span { display:block; padding-right:16px; }
|
|
11
|
+
#viewer #ehl, #viewer #eta { position:absolute; inset:0; margin:0; padding:14px 18px 14px calc(18px + 4ch + 16px); font:400 12.5px/1.62 var(--mono); tab-size:4; white-space:pre; overflow-wrap:normal; word-break:normal; letter-spacing:normal; }
|
|
12
|
+
#viewer #ehl { overflow:hidden; pointer-events:none; color:var(--ink); }
|
|
13
|
+
#viewer #ehl code { display:block; font:inherit; white-space:pre; min-height:100%; min-width:max-content; }
|
|
14
|
+
/* min/max-height:none pisan el textarea genérico del composer (layout.css: max-height 220px) */
|
|
15
|
+
#viewer #eta { display:block; width:100%; height:100%; min-height:0; max-height:none; border:0; outline:0; resize:none; background:transparent; color:transparent; -webkit-text-fill-color:transparent; caret-color:var(--ink); overflow:auto; }
|
|
16
|
+
#viewer #eta::selection { background:var(--sel); color:transparent; }
|
|
17
|
+
#viewer #eta[readonly] { caret-color:transparent; cursor:default; }
|
|
18
|
+
/* cabecera del visor en modo editor: lenguaje · líneas · estado; el botón guardar solo si hay algo que guardar */
|
|
19
|
+
.vh .dirty { color:var(--amber); font-weight:600; }
|
|
20
|
+
.vh .note { color:var(--str); }
|
|
21
|
+
.vh .warn { color:var(--amber); }
|
|
22
|
+
.vh .err { color:var(--rubric); }
|
|
23
|
+
.vh a { color:var(--accent); text-decoration:none; border-bottom:1px dashed var(--rule-2); }
|
|
24
|
+
.vh a:hover { color:var(--ink); border-bottom-color:var(--ink-3); }
|
|
25
|
+
.vh #vsave { padding:4px 9px; font-size:9.5px; letter-spacing:.08em; }
|
|
26
|
+
.vh #vsave:disabled { opacity:.4; }
|
|
27
|
+
.vh .x.ask { color:var(--ink); }
|
|
28
|
+
.vh .x.ask .yes { color:var(--rubric); cursor:pointer; font-weight:600; }
|
|
29
|
+
.vh .x.ask .no { cursor:pointer; }
|
|
30
|
+
|
|
31
|
+
/* ---- highlight.js: la paleta se raciona como en el resto de la UI — azul lo estructural (keywords, tags), verde
|
|
32
|
+
strings, ámbar números/literales/builtins/atributos, rubric rojo lo que abre permisos en Synsema (require,
|
|
33
|
+
allow, deny…), gris los comentarios. Funciona en dark y light porque todo son tokens. ---- */
|
|
34
|
+
.hljs { color:var(--ink); background:transparent; }
|
|
35
|
+
.hljs-comment, .hljs-quote, .hljs-meta, .hljs-meta .hljs-keyword, .hljs-meta .hljs-string { color:var(--ink-3); font-style:italic; }
|
|
36
|
+
.hljs-keyword, .hljs-selector-tag, .hljs-tag, .hljs-name, .hljs-doctag, .hljs-section, .hljs-template-tag { color:var(--accent); }
|
|
37
|
+
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-char.escape_, .hljs-symbol, .hljs-bullet, .hljs-link { color:var(--str); }
|
|
38
|
+
.hljs-number, .hljs-literal, .hljs-built_in, .hljs-type, .hljs-attr, .hljs-attribute, .hljs-selector-class, .hljs-selector-id, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-variable, .hljs-template-variable, .hljs-variable.language_, .hljs-property { color:var(--amber); }
|
|
39
|
+
.hljs-title, .hljs-title.function_, .hljs-title.class_, .hljs-title.class_.inherited__, .hljs-title.function_.invoke__ { color:var(--ink); font-weight:600; }
|
|
40
|
+
.hljs-deletion { color:var(--rubric); }
|
|
41
|
+
.hljs-keyword.security_ { color:var(--rubric); }
|
|
42
|
+
.hljs-subst { color:var(--ink); font-weight:400; }
|
|
43
|
+
.hljs-punctuation, .hljs-operator { color:var(--ink-2); }
|
|
44
|
+
.hljs-emphasis { font-style:italic; }
|
|
45
|
+
.hljs-strong { font-weight:600; }
|
|
46
|
+
/* xml/html: el nombre del tag azul, sus atributos ámbar, los valores verdes */
|
|
47
|
+
.hljs-tag .hljs-attr { color:var(--amber); }
|
|
48
|
+
.hljs-tag .hljs-string { color:var(--str); }
|
package/public/css/hub.css
CHANGED
|
@@ -14,3 +14,12 @@ body.hub #stage { display:flex; align-items:center; padding:20px 40px; }
|
|
|
14
14
|
.card.ws .nm { font:600 16px/1.3 var(--serif); color:var(--ink); margin:0 0 4px; }
|
|
15
15
|
.card.ws .pth { font:400 11px/1.4 var(--mono); color:var(--ink-3); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin:0 0 6px; }
|
|
16
16
|
.card.ws .meta { font:400 11px/1.4 var(--mono); color:var(--ink-3); margin:0; }
|
|
17
|
+
/* dispositivos: QR de emparejamiento */
|
|
18
|
+
.pairqr { display:flex; gap:16px; align-items:flex-start; margin-top:16px; flex-wrap:wrap; }
|
|
19
|
+
.pairqr .qr { width:220px; height:220px; background:#fff; border-radius:var(--r); padding:6px; }
|
|
20
|
+
.pairqr .qr svg { width:100%; height:100%; display:block; }
|
|
21
|
+
.pairqr .qrmeta { display:flex; flex-direction:column; gap:8px; min-width:180px; flex:1; }
|
|
22
|
+
@media (max-width: 640px) {
|
|
23
|
+
body.hub #stage { padding:16px; align-items:flex-start; }
|
|
24
|
+
.wsgrid { grid-template-columns:1fr; }
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* login.css — /login y /pair: una tarjeta centrada, pensada primero para el celular */
|
|
2
|
+
body.auth { display:flex; align-items:center; justify-content:center; min-height:100vh; min-height:100dvh; padding:24px 16px calc(24px + env(safe-area-inset-bottom)); }
|
|
3
|
+
.authbox { width:100%; max-width:400px; }
|
|
4
|
+
.authbox .logo { display:block; margin:0 0 18px; border-radius:10px; }
|
|
5
|
+
.authbox .eyebrow { font:400 11px/1 var(--mono); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-3); margin:0 0 8px; }
|
|
6
|
+
.authbox h1 { font:600 26px/1.2 var(--serif); margin:0 0 10px; }
|
|
7
|
+
.authbox .lead { color:var(--ink-2); margin:0 0 22px; }
|
|
8
|
+
.authbox .hint { font:400 12px/1.5 var(--mono); color:var(--ink-3); margin:18px 0 0; }
|
|
9
|
+
.authbox .field { display:block; font:400 11px/1 var(--mono); color:var(--ink-3); margin:0 0 16px; }
|
|
10
|
+
.authbox .field input { display:block; width:100%; margin-top:6px; padding:12px; font:400 16px var(--mono); color:var(--ink); background:var(--paper-2); border:1px solid var(--rule-2); border-radius:var(--r); }
|
|
11
|
+
.authbox .field input:focus { outline:none; border-color:var(--accent); }
|
|
12
|
+
.authbox button.primary { width:100%; padding:14px; font:600 15px var(--mono); color:var(--paper); background:var(--accent); border:0; border-radius:var(--r); cursor:pointer; }
|
|
13
|
+
.authbox button.primary:disabled { opacity:.55; cursor:default; }
|
|
14
|
+
.authbox .msg { font:400 13px/1.5 var(--mono); margin:16px 0 0; min-height:1.5em; color:var(--ink-2); }
|
|
15
|
+
.authbox .msg.err { color:var(--rubric); }
|
|
16
|
+
.authbox .msg.ok { color:var(--ok); }
|
package/public/css/panel.css
CHANGED
|
@@ -154,3 +154,33 @@
|
|
|
154
154
|
.panel .wsbrowser .bitems { max-height:30vh; overflow-y:auto; padding:4px; }
|
|
155
155
|
.panel .wsbrowser .bfoot { padding:8px 10px; border-top:1px solid var(--rule); display:flex; justify-content:flex-end; }
|
|
156
156
|
.panel .wsbrowser .bfoot button { font:400 12px/1 var(--mono); padding:6px 12px; }
|
|
157
|
+
/* computer use (⚙ → Computer use): estado del driver externo e instrucciones de instalación */
|
|
158
|
+
.panel .cu-status { margin:4px 0 14px; padding:10px 12px; border:1px solid var(--rule); border-radius:var(--r); }
|
|
159
|
+
.panel .cu-line { font:500 12.5px/1.5 var(--mono); color:var(--ink); }
|
|
160
|
+
.panel .cu-line.ok { color:#3fb950; }
|
|
161
|
+
.panel .cu-line.warn { color:#d29922; }
|
|
162
|
+
.panel .cu-line.no { color:var(--ink-3); }
|
|
163
|
+
.panel .cu-status ul { margin:6px 0 0 16px; padding:0; }
|
|
164
|
+
.panel .cu-status p { margin:6px 0; }
|
|
165
|
+
.panel .cu-cmd { margin:8px 0; padding:8px 10px; border:1px dashed var(--rule); border-radius:var(--r); font:12px/1.5 var(--mono); color:var(--ink); white-space:pre-wrap; user-select:all; }
|
|
166
|
+
.panel .dform label .chk { display:flex; align-items:center; gap:8px; font:400 13px/1.4 var(--serif); text-transform:none; letter-spacing:0; color:var(--ink); }
|
|
167
|
+
.panel .dform label .chk input { width:16px; height:16px; margin:0; accent-color:var(--accent); }
|
|
168
|
+
/* interruptor visible (computer use): pill con perilla; el texto al lado dice el estado */
|
|
169
|
+
.panel label.toggle { position:relative; display:inline-flex; align-items:center; gap:10px; margin:0 0 0 auto; cursor:pointer; user-select:none; font:500 12.5px/1 var(--mono); text-transform:none; letter-spacing:0; color:var(--ink-3); }
|
|
170
|
+
.panel .dform .trow label.toggle { margin:0; font:500 13px/1.3 var(--serif); color:var(--ink); }
|
|
171
|
+
.panel label.toggle input { position:absolute; opacity:0; width:0; height:0; margin:0; }
|
|
172
|
+
.panel label.toggle .knob { position:relative; flex:none; width:40px; height:22px; border-radius:11px; background:var(--rule); border:1px solid var(--rule); box-sizing:border-box; transition:background .15s, border-color .15s; }
|
|
173
|
+
.panel label.toggle .knob::after { content:""; position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; background:var(--ink); opacity:.75; transition:left .15s, background .15s, opacity .15s; }
|
|
174
|
+
.panel label.toggle input:checked + .knob { background:var(--accent); border-color:var(--accent); }
|
|
175
|
+
.panel label.toggle input:checked + .knob::after { left:20px; background:var(--paper); opacity:1; }
|
|
176
|
+
.panel label.toggle input:disabled + .knob { opacity:.45; cursor:not-allowed; }
|
|
177
|
+
.panel label.toggle.on { color:var(--accent); }
|
|
178
|
+
.panel .dform .trow label.toggle.on { color:var(--ink); }
|
|
179
|
+
.panel .dform .trow { display:flex; flex-direction:column; gap:6px; margin:4px 0 16px; }
|
|
180
|
+
/* ⚙ Apagar o reiniciar: botones que se confirman en el lugar (inlineConfirm) */
|
|
181
|
+
.panel .dform .trow .hbtn.pwr { align-self:flex-start; }
|
|
182
|
+
.panel .dform .trow .hbtn.pwr.danger { color:var(--rubric); border-color:var(--rubric); }
|
|
183
|
+
.panel .dform .trow .hbtn.pwr:disabled { opacity:.45; cursor:default; }
|
|
184
|
+
.panel .dform .trow .hbtn.pwr.ask { text-transform:none; letter-spacing:0; }
|
|
185
|
+
.panel .dform .trow .hbtn.pwr.ask .yes { color:var(--rubric); cursor:pointer; text-decoration:underline; }
|
|
186
|
+
.panel .dform .trow .hbtn.pwr.ask .no { cursor:pointer; }
|
package/public/css/sidebar.css
CHANGED
|
@@ -82,3 +82,24 @@
|
|
|
82
82
|
/* puertos ocupados por lampson: visibles pero sin link ni ✕ */
|
|
83
83
|
.p .port.own { color:var(--ink-3); font-weight:400; }
|
|
84
84
|
.p.own .cm { color:var(--ink-3); }
|
|
85
|
+
|
|
86
|
+
/* explorador de archivos: selección, portapapeles, arrastre, renombrar en línea y menú contextual */
|
|
87
|
+
#tree:focus { outline:none; }
|
|
88
|
+
.row.sel { background:var(--paper-2); }
|
|
89
|
+
.row.cut { opacity:.55; }
|
|
90
|
+
.row.dragging { opacity:.4; }
|
|
91
|
+
.row.drop, #tree.drop { background:var(--accent-bg); border-left-color:var(--accent); }
|
|
92
|
+
.row.flash { background:var(--accent-bg); transition:background .7s; }
|
|
93
|
+
.row input.rn { flex:1; min-width:0; font:inherit; color:var(--ink); background:var(--paper-2); border:1px solid var(--accent); border-radius:var(--r); padding:0 4px; outline:none; }
|
|
94
|
+
.row.new { color:var(--ink); }
|
|
95
|
+
.ctx { position:fixed; z-index:70; min-width:220px; background:var(--paper); border:1px solid var(--rule-2); border-radius:var(--r); box-shadow:0 10px 30px rgba(0,0,0,.35); padding:4px 0; font:400 12.5px/1.5 var(--mono); user-select:none; }
|
|
96
|
+
.ctx .it { display:flex; align-items:center; gap:14px; padding:4px 12px; cursor:pointer; color:var(--ink-2); white-space:nowrap; }
|
|
97
|
+
.ctx .it:hover { background:var(--paper-2); color:var(--ink); }
|
|
98
|
+
.ctx .it .k { margin-left:auto; color:var(--ink-3); font-size:10.5px; }
|
|
99
|
+
.ctx .it.dis { opacity:.4; pointer-events:none; }
|
|
100
|
+
.ctx .it.danger:hover { color:var(--rubric); }
|
|
101
|
+
.ctx .it.ask { color:var(--ink-2); gap:6px; }
|
|
102
|
+
.ctx .it.ask .yes { color:var(--rubric); font-weight:600; cursor:pointer; }
|
|
103
|
+
.ctx .it.ask .no { cursor:pointer; }
|
|
104
|
+
.ctx .it.ask .yes:hover, .ctx .it.ask .no:hover { text-decoration:underline; text-underline-offset:.18em; }
|
|
105
|
+
.ctx .sep { height:1px; background:var(--rule); margin:4px 0; }
|
package/public/css/tokens.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* tokens: fuentes, paleta (dark/light), base y botones — lo que todo lo demás asume */
|
|
2
|
-
/* Misma dirección que
|
|
2
|
+
/* Misma dirección que synsema.dev: tinta sobre papel, una superfamilia (IBM Plex) en dos
|
|
3
3
|
voces — la SERIF para lo que se lee (mensajes, hero), la MONO para estructura y maquinaria
|
|
4
4
|
(header, sidebar, pasos, código, controles). El color se raciona: azul = link / dónde estás,
|
|
5
5
|
rubric rojo = peligro y permisos (la única cosa que vale la pena ver desde lejos). */
|
package/public/hub.html
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
6
6
|
<title>Lampson · workspaces</title>
|
|
7
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
8
|
+
<meta name="theme-color" content="#131412">
|
|
9
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
10
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
11
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
12
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
|
13
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
8
14
|
<link rel="stylesheet" href="/css/tokens.css">
|
|
9
15
|
<link rel="stylesheet" href="/css/layout.css">
|
|
10
16
|
<link rel="stylesheet" href="/css/chat.css">
|
|
@@ -17,6 +23,7 @@
|
|
|
17
23
|
<div class="hrow">
|
|
18
24
|
<span class="brand">lampson<span class="proj">workspaces</span></span>
|
|
19
25
|
<span class="spacer"></span>
|
|
26
|
+
<span class="pill" id="devicesBtn" role="button" tabindex="0">dispositivos</span>
|
|
20
27
|
<button class="theme-toggle" id="theme" type="button" title="cambiar tema"></button>
|
|
21
28
|
</div>
|
|
22
29
|
</header>
|
|
@@ -34,5 +41,8 @@
|
|
|
34
41
|
<script src="/js/panel.js"></script>
|
|
35
42
|
<script src="/js/hub.js"></script>
|
|
36
43
|
<script src="/js/workspaces.js"></script>
|
|
44
|
+
<script src="/vendor/qrcode.js"></script>
|
|
45
|
+
<script src="/js/devices.js"></script>
|
|
46
|
+
<script src="/js/pwa.js"></script>
|
|
37
47
|
</body>
|
|
38
48
|
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<circle cx="32" cy="27" r="11" fill="#fff"/>
|
|
3
|
+
<g stroke="#fff" stroke-width="3.5" stroke-linecap="round">
|
|
4
|
+
<line x1="32" y1="6" x2="32" y2="10"/>
|
|
5
|
+
<line x1="14" y1="13" x2="17" y2="16"/>
|
|
6
|
+
<line x1="50" y1="13" x2="47" y2="16"/>
|
|
7
|
+
<line x1="9" y1="27" x2="13" y2="27"/>
|
|
8
|
+
<line x1="55" y1="27" x2="51" y2="27"/>
|
|
9
|
+
</g>
|
|
10
|
+
<rect x="26" y="40" width="12" height="7" rx="1.5" fill="#fff"/>
|
|
11
|
+
<rect x="27.5" y="49" width="9" height="3" rx="1.5" fill="#fff"/>
|
|
12
|
+
</svg>
|