lampson 0.1.4 → 0.2.0
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/README.md +38 -9
- package/bin/lampson.js +1 -1
- package/chat.syn +11 -1
- package/cli.syn +68 -0
- package/hub.tpl.syn +127 -0
- package/lampson.ps1 +78 -66
- package/lampson.sh +68 -36
- package/lib/approvals.syn +5 -2
- package/lib/lsp.syn +10 -1
- package/lib/mcp.syn +10 -1
- package/lib/settings.syn +13 -5
- package/lib/skills.syn +2 -0
- package/lib/workspaces.syn +555 -0
- package/package.json +3 -1
- package/public/css/hub.css +12 -0
- package/public/hub.html +36 -0
- package/public/index.html +2 -0
- package/public/js/agents.js +3 -3
- package/public/js/app.js +1 -1
- package/public/js/approvals.js +3 -3
- package/public/js/chat.js +2 -2
- package/public/js/config.js +7 -5
- package/public/js/core.js +3 -0
- package/public/js/events.js +1 -1
- package/public/js/hub.js +40 -0
- package/public/js/lamps.js +4 -4
- package/public/js/lsp.js +3 -3
- package/public/js/mcp.js +3 -3
- package/public/js/memory.js +2 -2
- package/public/js/procs.js +6 -6
- package/public/js/schedules.js +7 -7
- package/public/js/sessions.js +6 -6
- package/public/js/terminal.js +1 -1
- package/public/js/todo.js +1 -1
- package/public/js/tree.js +3 -3
- package/public/js/update.js +2 -2
- package/public/js/workspaces.js +83 -0
- package/skills/lampson/SKILL.md +5 -0
- package/skills/synsema/SKILL.md +4 -1
- package/web.syn +70 -51
package/lib/approvals.syn
CHANGED
|
@@ -82,11 +82,14 @@ export task open(source, message, why, timeout_s)
|
|
|
82
82
|
notify(meta, links)
|
|
83
83
|
give id
|
|
84
84
|
|
|
85
|
+
-- bajo el hub cada workspace vive en /w/<slug> (LAMPSON_WS); el link público lleva ese prefijo
|
|
85
86
|
task links_for(id, tok)
|
|
87
|
+
let slug be env("LAMPSON_WS", "")
|
|
88
|
+
let prefix be when slug == "" then "" otherwise "/w/" + slug
|
|
86
89
|
let base be public_url()
|
|
87
90
|
when base == ""
|
|
88
|
-
give {"yes": "", "no": "", "path": "/approve/" + id + "/" + tok}
|
|
89
|
-
give {"yes": base + "/approve/" + id + "/" + tok + "?d=yes", "no": base + "/approve/" + id + "/" + tok + "?d=no", "path": "/approve/" + id + "/" + tok}
|
|
91
|
+
give {"yes": "", "no": "", "path": prefix + "/approve/" + id + "/" + tok}
|
|
92
|
+
give {"yes": base + prefix + "/approve/" + id + "/" + tok + "?d=yes", "no": base + prefix + "/approve/" + id + "/" + tok + "?d=no", "path": prefix + "/approve/" + id + "/" + tok}
|
|
90
93
|
|
|
91
94
|
-- webhook firmado (fire-and-forget)
|
|
92
95
|
task notify(meta, links)
|
package/lib/lsp.syn
CHANGED
|
@@ -22,7 +22,16 @@
|
|
|
22
22
|
|
|
23
23
|
use "./tools/common.syn" as c
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
-- global = compartido por todos los workspaces: .lampson/global/ (junction al .lampson de la instalación) cuando
|
|
26
|
+
-- el proceso corre bajo el hub; .lampson/ en una instalación directa o en los tests
|
|
27
|
+
task global_config()
|
|
28
|
+
try
|
|
29
|
+
when file_exists(".lampson/global")
|
|
30
|
+
give ".lampson/global/lsp.json"
|
|
31
|
+
recover err
|
|
32
|
+
give ".lampson/lsp.json"
|
|
33
|
+
give ".lampson/lsp.json"
|
|
34
|
+
export let GLOBAL_CONFIG be global_config()
|
|
26
35
|
export let PROJECT_CONFIG be "workspace/.lampson/lsp.json"
|
|
27
36
|
let CALL_TIMEOUT be 60
|
|
28
37
|
let START_WAIT be 25
|
package/lib/mcp.syn
CHANGED
|
@@ -23,7 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
use "./tools/common.syn" as c
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
-- global = compartido por todos los workspaces: .lampson/global/ (junction al .lampson de la instalación) cuando
|
|
27
|
+
-- el proceso corre bajo el hub; .lampson/ en una instalación directa o en los tests
|
|
28
|
+
task global_config()
|
|
29
|
+
try
|
|
30
|
+
when file_exists(".lampson/global")
|
|
31
|
+
give ".lampson/global/mcp.json"
|
|
32
|
+
recover err
|
|
33
|
+
give ".lampson/mcp.json"
|
|
34
|
+
give ".lampson/mcp.json"
|
|
35
|
+
export let GLOBAL_CONFIG be global_config()
|
|
27
36
|
export let PROJECT_CONFIG be "workspace/.lampson/mcp.json"
|
|
28
37
|
let CALL_TIMEOUT be 120
|
|
29
38
|
|
package/lib/settings.syn
CHANGED
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
--
|
|
8
8
|
-- Forma: {"provider": "deepseek", "model": "deepseek-chat", "keys": {"deepseek": "sk-…"}}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
-- bajo el hub cada workspace corre con su propio .lampson/ y una junction .lampson/global → <home>/.lampson:
|
|
11
|
+
-- la config (keys, ⚙) es una sola para todos. Sin junction (instalación directa, tests) es .lampson/config.json
|
|
12
|
+
task cfg_path()
|
|
13
|
+
try
|
|
14
|
+
when file_exists(".lampson/global/config.json") or file_exists(".lampson/global")
|
|
15
|
+
give ".lampson/global/config.json"
|
|
16
|
+
recover err
|
|
17
|
+
give ".lampson/config.json"
|
|
18
|
+
give ".lampson/config.json"
|
|
11
19
|
|
|
12
20
|
export task load()
|
|
13
21
|
require file.read(".lampson")
|
|
@@ -18,7 +26,7 @@ export task load()
|
|
|
18
26
|
when env("LAMPSON_NO_CONFIG", "") == "1"
|
|
19
27
|
give {"provider": "", "model": "", "keys": {}}
|
|
20
28
|
try
|
|
21
|
-
let doc be json_decode(read_file(
|
|
29
|
+
let doc be json_decode(read_file(cfg_path()))
|
|
22
30
|
-- claves siempre presentes: quien lee no tiene que chequear (y `and` no cortocircuita)
|
|
23
31
|
when not contains(doc, "keys")
|
|
24
32
|
set doc["keys"] to {}
|
|
@@ -33,7 +41,7 @@ export task load()
|
|
|
33
41
|
export task save(doc)
|
|
34
42
|
require file(".lampson")
|
|
35
43
|
require file(".lampson/*")
|
|
36
|
-
write_file(
|
|
44
|
+
write_file(cfg_path(), json_encode(doc))
|
|
37
45
|
give true
|
|
38
46
|
|
|
39
47
|
-- key guardada para un proveedor, como TEXTO ("" si no hay). Sellarla con as_secret antes de usarla.
|
|
@@ -71,13 +79,13 @@ export task set_default(name, model)
|
|
|
71
79
|
|
|
72
80
|
-- valores de configuración generales (rueda de la UI): tz, public_url, webhook_url, webhook_secret…
|
|
73
81
|
-- Vacío = borrar la clave. Nunca se devuelven secretos al navegador: `values()` marca webhook_secret como has_*.
|
|
74
|
-
export let KEYS be ["tz", "public_url", "webhook_url", "webhook_secret"]
|
|
82
|
+
export let KEYS be ["tz", "public_url", "webhook_url", "webhook_secret", "idle_hours"]
|
|
75
83
|
|
|
76
84
|
-- el archivo REAL, ignorando LAMPSON_NO_CONFIG: escribir con el doc en blanco de los tests pisaría provider/keys
|
|
77
85
|
-- (pasó el 2026-08-29 probando la rueda con un web de test)
|
|
78
86
|
task load_raw()
|
|
79
87
|
try
|
|
80
|
-
let doc be json_decode(read_file(
|
|
88
|
+
let doc be json_decode(read_file(cfg_path()))
|
|
81
89
|
when not contains(doc, "keys")
|
|
82
90
|
set doc["keys"] to {}
|
|
83
91
|
when not contains(doc, "provider")
|
package/lib/skills.syn
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
let ROOTS be [
|
|
29
29
|
{"dir": ".lampson/skills-global", "source": "global"},
|
|
30
30
|
{"dir": ".lampson/skills-claude", "source": "global"},
|
|
31
|
+
{"dir": ".lampson/global/skills-global", "source": "global"},
|
|
32
|
+
{"dir": ".lampson/global/skills-claude", "source": "global"},
|
|
31
33
|
{"dir": "skills", "source": "harness"},
|
|
32
34
|
{"dir": "workspace/.claude/skills", "source": "project"},
|
|
33
35
|
{"dir": "workspace/.agents/skills", "source": "project"},
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
-- lib/workspaces.syn — varios workspaces vivos a la vez: registro, un directorio por workspace, procesos y supervisor
|
|
2
|
+
--
|
|
3
|
+
-- Por qué así (ver ../SPEC-WORKSPACES.md): en Synsema las capabilities de archivo Y los `use "./lib/…"` se resuelven
|
|
4
|
+
-- relativos al CWD del proceso, y `proxy to` es estático (se evalúa al arrancar y anexa el path entero). Entonces:
|
|
5
|
+
-- * cada workspace tiene su propio directorio .lampson/ws/<slug>/ que ES el cwd de su proceso:
|
|
6
|
+
-- workspace → junction al proyecto lib public skills lamps memory → junctions a la instalación
|
|
7
|
+
-- web.syn chat.syn → copias (refrescadas al arrancar) .lampson/ → estado propio (sesiones, tareas…)
|
|
8
|
+
-- .lampson/global → junction a <home>/.lampson (config.json con keys, mcp/lsp globales, skills-*)
|
|
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 808N --bind 127.0.0.1` DESACOPLADO del hub (start /b,
|
|
11
|
+
-- process_group:false): sobrevive a reinicios del hub y a cerrar terminales. Solo loopback.
|
|
12
|
+
-- * el hub (hub.syn, :8080) se GENERA desde hub.tpl.syn con una ruta proxy por workspace y corre con
|
|
13
|
+
-- `synsema serve hub.syn` desacoplado: al registrar/quitar un workspace se reescribe y un helper lo reinicia.
|
|
14
|
+
-- * el hub es supervisor: cada 15 s arranca lo que debe estar vivo (política) y apaga lo inactivo.
|
|
15
|
+
--
|
|
16
|
+
-- Este módulo corre SIEMPRE con cwd = instalación (hub.syn y cli.syn); los procesos de workspace no lo usan.
|
|
17
|
+
|
|
18
|
+
use "./tools/bash.syn" as t_bash
|
|
19
|
+
use "./tools/proc.syn" as proc
|
|
20
|
+
use "./settings.syn" as settings
|
|
21
|
+
|
|
22
|
+
export let ROOT be ".lampson/ws"
|
|
23
|
+
export let REGISTRY be ".lampson/workspaces.json"
|
|
24
|
+
export let HUB_PORT be 8080
|
|
25
|
+
export let HUB_FILE be "hub.syn"
|
|
26
|
+
export let HUB_TEMPLATE be "hub.tpl.syn"
|
|
27
|
+
let FIRST_PORT be 8081
|
|
28
|
+
let LINKS be ["lib", "public", "skills", "lamps", "memory"]
|
|
29
|
+
let COPIES be ["web.syn", "chat.syn"]
|
|
30
|
+
|
|
31
|
+
task is_win()
|
|
32
|
+
give env("OS", "") == "Windows_NT"
|
|
33
|
+
|
|
34
|
+
task sh(cmd, t)
|
|
35
|
+
let sc be t_bash.shell_config()
|
|
36
|
+
give run(sc["shell"], [sc["flag"], cmd], t)
|
|
37
|
+
|
|
38
|
+
-- ruta absoluta de la instalación (cwd), en forma Windows si corresponde
|
|
39
|
+
export task home()
|
|
40
|
+
require exec
|
|
41
|
+
require env("LAMPSON_*")
|
|
42
|
+
require env("OS")
|
|
43
|
+
let h be env("LAMPSON_HOME", "")
|
|
44
|
+
when h != ""
|
|
45
|
+
give h
|
|
46
|
+
let r be sh("pwd -W 2>/dev/null || pwd -P", 10)
|
|
47
|
+
let p be trim(text(r["stdout"]))
|
|
48
|
+
when is_win()
|
|
49
|
+
give replace_text(p, "/", "\\")
|
|
50
|
+
give p
|
|
51
|
+
|
|
52
|
+
export task norm(p)
|
|
53
|
+
let s be lower(replace_text(trim(text(p)), "\\", "/"))
|
|
54
|
+
when matches(s, "/[a-z]/.*")
|
|
55
|
+
set s to slice(s, 1, 2) + ":" + slice(s, 2, length(s))
|
|
56
|
+
while length(s) > 1 and slice(s, length(s) - 1, length(s)) == "/"
|
|
57
|
+
set s to slice(s, 0, length(s) - 1)
|
|
58
|
+
give s
|
|
59
|
+
|
|
60
|
+
-- el mismo slug que memo.slug(): nombre limpio + 8 hex del hash de la ruta normalizada → las sesiones y la memoria
|
|
61
|
+
-- guardadas hasta hoy siguen asociadas al mismo workspace
|
|
62
|
+
export task slug_of(path)
|
|
63
|
+
let n be norm(path)
|
|
64
|
+
let parts be where(split(n, "/"), (x) => x != "")
|
|
65
|
+
let base be when length(parts) > 0 then parts[length(parts) - 1] otherwise "workspace"
|
|
66
|
+
let runs be find_all(base, "[a-z0-9]+")
|
|
67
|
+
let clean be when length(runs) == 0 then "project" otherwise join(runs, "-")
|
|
68
|
+
give clean + "-" + slice(decode(sha256(n), "hex"), 0, 8)
|
|
69
|
+
|
|
70
|
+
-- ---------- registro ----------
|
|
71
|
+
task load_reg()
|
|
72
|
+
try
|
|
73
|
+
let doc be json_decode(read_file(REGISTRY))
|
|
74
|
+
when not contains(doc, "workspaces")
|
|
75
|
+
set doc["workspaces"] to {}
|
|
76
|
+
when not contains(doc, "next_port")
|
|
77
|
+
set doc["next_port"] to FIRST_PORT
|
|
78
|
+
give doc
|
|
79
|
+
recover err
|
|
80
|
+
give {"workspaces": {}, "next_port": FIRST_PORT}
|
|
81
|
+
|
|
82
|
+
task save_reg(doc)
|
|
83
|
+
write_file(REGISTRY, json_encode(doc))
|
|
84
|
+
|
|
85
|
+
export task all()
|
|
86
|
+
require file(".lampson")
|
|
87
|
+
require file(".lampson/*")
|
|
88
|
+
let doc be load_reg()
|
|
89
|
+
let out be []
|
|
90
|
+
each k in sort_by(keys(doc["workspaces"]), (x) => 0 - doc["workspaces"][x]["last_used"])
|
|
91
|
+
set out to append(out, doc["workspaces"][k])
|
|
92
|
+
give out
|
|
93
|
+
|
|
94
|
+
export task get(slug)
|
|
95
|
+
require file(".lampson")
|
|
96
|
+
require file(".lampson/*")
|
|
97
|
+
let doc be load_reg()
|
|
98
|
+
when contains(doc["workspaces"], slug)
|
|
99
|
+
give doc["workspaces"][slug]
|
|
100
|
+
give nothing
|
|
101
|
+
|
|
102
|
+
export task by_path(path)
|
|
103
|
+
require file(".lampson")
|
|
104
|
+
require file(".lampson/*")
|
|
105
|
+
let n be norm(path)
|
|
106
|
+
each w in all()
|
|
107
|
+
when norm(w["path"]) == n
|
|
108
|
+
give w
|
|
109
|
+
give nothing
|
|
110
|
+
|
|
111
|
+
export task put(w)
|
|
112
|
+
require file(".lampson")
|
|
113
|
+
require file(".lampson/*")
|
|
114
|
+
require time
|
|
115
|
+
let doc be load_reg()
|
|
116
|
+
set doc["workspaces"][w["slug"]] to w
|
|
117
|
+
save_reg(doc)
|
|
118
|
+
give w
|
|
119
|
+
|
|
120
|
+
export task touch(slug)
|
|
121
|
+
require file(".lampson")
|
|
122
|
+
require file(".lampson/*")
|
|
123
|
+
require time
|
|
124
|
+
let w be get(slug)
|
|
125
|
+
when w == nothing
|
|
126
|
+
give false
|
|
127
|
+
set w["last_used"] to now()
|
|
128
|
+
put(w)
|
|
129
|
+
give true
|
|
130
|
+
|
|
131
|
+
-- ---------- directorio del workspace ----------
|
|
132
|
+
export task dir_of(slug)
|
|
133
|
+
give ROOT + "/" + slug
|
|
134
|
+
|
|
135
|
+
task link(where_, target)
|
|
136
|
+
when is_win()
|
|
137
|
+
let r be run("cmd", ["/c", "mklink", "/J", replace_text(where_, "/", "\\"), replace_text(target, "/", "\\")], 15)
|
|
138
|
+
give r["exit_code"] == 0
|
|
139
|
+
let r be run("ln", ["-s", target, where_], 10)
|
|
140
|
+
give r["exit_code"] == 0
|
|
141
|
+
|
|
142
|
+
task unlink(where_)
|
|
143
|
+
when is_win()
|
|
144
|
+
run("cmd", ["/c", "rmdir", replace_text(where_, "/", "\\")], 10)
|
|
145
|
+
otherwise
|
|
146
|
+
run("rm", ["-f", where_], 10)
|
|
147
|
+
give true
|
|
148
|
+
|
|
149
|
+
task is_link_dir(p)
|
|
150
|
+
give file_exists(p)
|
|
151
|
+
|
|
152
|
+
-- crea/repara .lampson/ws/<slug>: junctions al proyecto y a la instalación, copias de los entries
|
|
153
|
+
export task prepare(w)
|
|
154
|
+
require exec
|
|
155
|
+
require time
|
|
156
|
+
require env("LAMPSON_*")
|
|
157
|
+
require env("OS")
|
|
158
|
+
require file(".lampson")
|
|
159
|
+
require file(".lampson/*")
|
|
160
|
+
require file.read("web.syn")
|
|
161
|
+
require file.read("chat.syn")
|
|
162
|
+
let d be dir_of(w["slug"])
|
|
163
|
+
let h be home()
|
|
164
|
+
let sep be when is_win() then "\\" otherwise "/"
|
|
165
|
+
let fresh be not file_exists(d + "/.lampson/.keep")
|
|
166
|
+
write_file(d + "/.lampson/.keep", "")
|
|
167
|
+
when fresh
|
|
168
|
+
migrate(w, d)
|
|
169
|
+
-- el proyecto (si la junction apunta a otro lado — carpeta movida — se rehace)
|
|
170
|
+
let ws_link be d + "/workspace"
|
|
171
|
+
when file_exists(ws_link)
|
|
172
|
+
let target be current_target(ws_link)
|
|
173
|
+
when target != "" and norm(target) != norm(w["path"])
|
|
174
|
+
unlink(ws_link)
|
|
175
|
+
when not file_exists(ws_link)
|
|
176
|
+
when not link(ws_link, w["path"])
|
|
177
|
+
raise("could not link " + ws_link + " → " + w["path"])
|
|
178
|
+
each n in LINKS
|
|
179
|
+
when not file_exists(d + "/" + n)
|
|
180
|
+
link(d + "/" + n, h + sep + n)
|
|
181
|
+
when not file_exists(d + "/.lampson/global")
|
|
182
|
+
link(d + "/.lampson/global", h + sep + ".lampson")
|
|
183
|
+
each f in COPIES
|
|
184
|
+
write_file(d + "/" + f, read_file(f))
|
|
185
|
+
give d
|
|
186
|
+
|
|
187
|
+
-- estado guardado ANTES de los workspaces (en <home>/.lampson, separado por slug en cada registro): sesiones y trazas
|
|
188
|
+
-- del proyecto, sus tareas programadas y el encendido de lámparas se mueven a <ws>/.lampson la primera vez
|
|
189
|
+
task migrate(w, d)
|
|
190
|
+
let slug be w["slug"]
|
|
191
|
+
let moved be 0
|
|
192
|
+
try
|
|
193
|
+
each e in list_dir(".lampson/sessions")
|
|
194
|
+
when not e["is_dir"]
|
|
195
|
+
let doc be nothing
|
|
196
|
+
try
|
|
197
|
+
set doc to json_decode(read_file(".lampson/sessions/" + e["name"]))
|
|
198
|
+
recover err
|
|
199
|
+
set doc to nothing
|
|
200
|
+
when doc != nothing and contains(doc, "meta") and contains(doc["meta"], "project") and doc["meta"]["project"] == slug
|
|
201
|
+
let id be replace_text(e["name"], ".json", "")
|
|
202
|
+
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)
|
|
203
|
+
set moved to moved + 1
|
|
204
|
+
recover err
|
|
205
|
+
set moved to moved
|
|
206
|
+
try
|
|
207
|
+
let sdoc be json_decode(read_file(".lampson/schedules.json"))
|
|
208
|
+
let mine be where(sdoc["tasks"], (t) => contains(t, "project") and t["project"] == slug)
|
|
209
|
+
when length(mine) > 0
|
|
210
|
+
write_file(d + "/.lampson/schedules.json", json_encode({"tasks": mine, "seq": when contains(sdoc, "seq") then sdoc["seq"] otherwise length(mine)}))
|
|
211
|
+
set sdoc["tasks"] to where(sdoc["tasks"], (t) => not (contains(t, "project") and t["project"] == slug))
|
|
212
|
+
write_file(".lampson/schedules.json", json_encode(sdoc))
|
|
213
|
+
each t in mine
|
|
214
|
+
sh("mkdir -p '" + d + "/.lampson/schedules' && mv -f '.lampson/schedules/" + t["id"] + ".log' '" + d + "/.lampson/schedules/' 2>/dev/null; true", 10)
|
|
215
|
+
recover err
|
|
216
|
+
set moved to moved
|
|
217
|
+
try
|
|
218
|
+
when file_exists(".lampson/lamps.json")
|
|
219
|
+
write_file(d + "/.lampson/lamps.json", read_file(".lampson/lamps.json"))
|
|
220
|
+
recover err
|
|
221
|
+
set moved to moved
|
|
222
|
+
give moved
|
|
223
|
+
|
|
224
|
+
-- destino real de una junction/symlink (Git Bash: pwd -W dentro)
|
|
225
|
+
task current_target(p)
|
|
226
|
+
try
|
|
227
|
+
let r be sh("cd '" + p + "' && (pwd -W 2>/dev/null || pwd -P)", 10)
|
|
228
|
+
give trim(text(r["stdout"]))
|
|
229
|
+
recover err
|
|
230
|
+
give ""
|
|
231
|
+
|
|
232
|
+
-- registra (si hace falta) y prepara el workspace de una carpeta → la entrada del registro
|
|
233
|
+
export task ensure(path)
|
|
234
|
+
require exec
|
|
235
|
+
require time
|
|
236
|
+
require env("LAMPSON_*")
|
|
237
|
+
require env("OS")
|
|
238
|
+
require file(".lampson")
|
|
239
|
+
require file(".lampson/*")
|
|
240
|
+
require file.read("web.syn")
|
|
241
|
+
require file.read("chat.syn")
|
|
242
|
+
require file.read("hub.tpl.syn")
|
|
243
|
+
require file("hub.syn")
|
|
244
|
+
let existing be by_path(path)
|
|
245
|
+
let w be existing
|
|
246
|
+
when w == nothing
|
|
247
|
+
let doc be load_reg()
|
|
248
|
+
let slug be slug_of(path)
|
|
249
|
+
when contains(doc["workspaces"], slug)
|
|
250
|
+
-- mismo slug, otra ruta (mayúsculas/minúsculas, junction): reasociar
|
|
251
|
+
set w to doc["workspaces"][slug]
|
|
252
|
+
set w["path"] to text(path)
|
|
253
|
+
otherwise
|
|
254
|
+
let port be doc["next_port"]
|
|
255
|
+
set doc["next_port"] to port + 1
|
|
256
|
+
save_reg(doc)
|
|
257
|
+
let parts be where(split(replace_text(text(path), "\\", "/"), "/"), (x) => x != "")
|
|
258
|
+
set w to {"slug": slug, "name": parts[length(parts) - 1], "path": text(path), "port": port, "policy": "auto", "created": now(), "last_used": now()}
|
|
259
|
+
put(w)
|
|
260
|
+
when regen_hub() and hub_alive()
|
|
261
|
+
restart_hub_later()
|
|
262
|
+
prepare(w)
|
|
263
|
+
give w
|
|
264
|
+
|
|
265
|
+
export task remove(slug)
|
|
266
|
+
require exec
|
|
267
|
+
require time
|
|
268
|
+
require env("LAMPSON_*")
|
|
269
|
+
require env("OS")
|
|
270
|
+
require file(".lampson")
|
|
271
|
+
require file(".lampson/*")
|
|
272
|
+
require file.read("hub.tpl.syn")
|
|
273
|
+
require file("hub.syn")
|
|
274
|
+
require net("127.0.0.1")
|
|
275
|
+
let w be get(slug)
|
|
276
|
+
when w == nothing
|
|
277
|
+
raise("no workspace '" + text(slug) + "'")
|
|
278
|
+
stop_ws(w)
|
|
279
|
+
let doc be load_reg()
|
|
280
|
+
let keep be {}
|
|
281
|
+
each k in keys(doc["workspaces"])
|
|
282
|
+
when k != slug
|
|
283
|
+
set keep[k] to doc["workspaces"][k]
|
|
284
|
+
set doc["workspaces"] to keep
|
|
285
|
+
save_reg(doc)
|
|
286
|
+
when regen_hub() and hub_alive()
|
|
287
|
+
restart_hub_later()
|
|
288
|
+
give "workspace '" + slug + "' quitado del registro (la carpeta " + dir_of(slug) + " y el proyecto quedan en disco)"
|
|
289
|
+
|
|
290
|
+
-- ---------- hub generado ----------
|
|
291
|
+
task hub_routes(w)
|
|
292
|
+
let base be "http://127.0.0.1:" + text(w["port"])
|
|
293
|
+
let lines be []
|
|
294
|
+
each m in ["GET", "POST"]
|
|
295
|
+
set lines to append(lines, " route \"" + m + " /w/" + w["slug"] + "/api/*path\"\n proxy to \"" + base + "\"")
|
|
296
|
+
set lines to append(lines, " route \"GET /w/" + w["slug"] + "/approve/*path\"\n proxy to \"" + base + "\"")
|
|
297
|
+
give join(lines, "\n")
|
|
298
|
+
|
|
299
|
+
-- reescribe hub.syn desde la plantilla si cambió (bajo --watch, el hub se reinicia solo)
|
|
300
|
+
export task regen_hub()
|
|
301
|
+
require file(".lampson")
|
|
302
|
+
require file(".lampson/*")
|
|
303
|
+
require file.read("hub.tpl.syn")
|
|
304
|
+
require file("hub.syn")
|
|
305
|
+
let tpl be read_file(HUB_TEMPLATE)
|
|
306
|
+
let blocks be []
|
|
307
|
+
each w in all()
|
|
308
|
+
set blocks to append(blocks, hub_routes(w))
|
|
309
|
+
let body be replace_text(tpl, " -- @workspaces", when length(blocks) == 0 then " -- (sin workspaces registrados todavía)" otherwise join(blocks, "\n"))
|
|
310
|
+
let cur be ""
|
|
311
|
+
try
|
|
312
|
+
set cur to read_file(HUB_FILE)
|
|
313
|
+
recover err
|
|
314
|
+
set cur to ""
|
|
315
|
+
when cur != body
|
|
316
|
+
write_file(HUB_FILE, body)
|
|
317
|
+
give true
|
|
318
|
+
give false
|
|
319
|
+
|
|
320
|
+
-- ---------- procesos ----------
|
|
321
|
+
-- lanzar DESACOPLADO (sobrevive al request y al proceso que lo lanzó); log en <ws>/.lampson/web.log
|
|
322
|
+
-- Windows: `start /b` hereda los handles de stdout → quien capture la salida del lanzador (PowerShell, un pipe)
|
|
323
|
+
-- se queda esperando al nieto. Start-Process con redirección crea handles nuevos: el nieto queda suelto de verdad.
|
|
324
|
+
task spawn_detached(cwd, cmdline, envs)
|
|
325
|
+
when is_win()
|
|
326
|
+
-- WorkingDirectory absoluto: el relativo se resolvería contra el cwd del PowerShell (que ya es `cwd`)
|
|
327
|
+
let h be home()
|
|
328
|
+
let abs be when cwd == "." then h otherwise h + "\\" + replace_text(cwd, "/", "\\")
|
|
329
|
+
let ps be "Start-Process -FilePath cmd -ArgumentList '/c', '" + replace_text(cmdline, "'", "''") + "' -WorkingDirectory '" + replace_text(abs, "'", "''") + "' -WindowStyle Hidden"
|
|
330
|
+
let p be proc_spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], {"cwd": cwd, "env": envs, "process_group": false})
|
|
331
|
+
proc_wait(p, 15)
|
|
332
|
+
proc_close(p)
|
|
333
|
+
otherwise
|
|
334
|
+
let p be proc_spawn("sh", ["-c", "nohup " + cmdline + " >/dev/null 2>&1 &"], {"cwd": cwd, "env": envs, "process_group": false})
|
|
335
|
+
proc_wait(p, 5)
|
|
336
|
+
proc_close(p)
|
|
337
|
+
give true
|
|
338
|
+
|
|
339
|
+
export task start_ws(w)
|
|
340
|
+
require exec
|
|
341
|
+
require time
|
|
342
|
+
require env("LAMPSON_*")
|
|
343
|
+
require env("OS")
|
|
344
|
+
require file(".lampson")
|
|
345
|
+
require file(".lampson/*")
|
|
346
|
+
require file.read("web.syn")
|
|
347
|
+
require file.read("chat.syn")
|
|
348
|
+
require file.read(".env")
|
|
349
|
+
let d be prepare(w)
|
|
350
|
+
let h be home()
|
|
351
|
+
let sep be when is_win() then "\\" otherwise "/"
|
|
352
|
+
let envf be h + sep + ".env"
|
|
353
|
+
let envs be {"LAMPSON_WS": w["slug"], "LAMPSON_WORKSPACE": w["path"], "LAMPSON_PORT": text(w["port"]), "LAMPSON_HOME": h}
|
|
354
|
+
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
355
|
+
-- el .env de la instalación (cwd = home; la capability es relativa, la ruta que se pasa al hijo es absoluta)
|
|
356
|
+
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"
|
|
357
|
+
spawn_detached(d, cmd, envs)
|
|
358
|
+
give true
|
|
359
|
+
|
|
360
|
+
-- ¿responde? → {alive, last_used, schedules_on, version} | nothing
|
|
361
|
+
export task health(w)
|
|
362
|
+
require net("127.0.0.1")
|
|
363
|
+
require time
|
|
364
|
+
try
|
|
365
|
+
let r be http_get("http://127.0.0.1:" + text(w["port"]) + "/w/" + w["slug"] + "/api/health", {"timeout": 2})
|
|
366
|
+
when r["ok"]
|
|
367
|
+
give json_decode(body of r)
|
|
368
|
+
give nothing
|
|
369
|
+
recover err
|
|
370
|
+
give nothing
|
|
371
|
+
|
|
372
|
+
-- esperar hasta que responda (arranque)
|
|
373
|
+
export task wait_ready(w, secs)
|
|
374
|
+
require net("127.0.0.1")
|
|
375
|
+
require time
|
|
376
|
+
let deadline be now() + secs
|
|
377
|
+
while now() < deadline
|
|
378
|
+
when health(w) != nothing
|
|
379
|
+
give true
|
|
380
|
+
sleep(0.4)
|
|
381
|
+
give false
|
|
382
|
+
|
|
383
|
+
export task stop_ws(w)
|
|
384
|
+
require exec
|
|
385
|
+
require time
|
|
386
|
+
require env("LAMPSON_*")
|
|
387
|
+
require env("OS")
|
|
388
|
+
require net("127.0.0.1")
|
|
389
|
+
let victims be where(proc.listeners(), (l) => l["port"] == w["port"])
|
|
390
|
+
each l in victims
|
|
391
|
+
proc.kill_pid(l["pid"])
|
|
392
|
+
give length(victims) > 0
|
|
393
|
+
|
|
394
|
+
-- ---------- hub ----------
|
|
395
|
+
export task hub_alive()
|
|
396
|
+
require net("127.0.0.1")
|
|
397
|
+
require time
|
|
398
|
+
try
|
|
399
|
+
let r be http_get("http://127.0.0.1:" + text(HUB_PORT) + "/api/hub", {"timeout": 2})
|
|
400
|
+
give r["ok"]
|
|
401
|
+
recover err
|
|
402
|
+
give false
|
|
403
|
+
|
|
404
|
+
export task start_hub()
|
|
405
|
+
require exec
|
|
406
|
+
require time
|
|
407
|
+
require env("LAMPSON_*")
|
|
408
|
+
require env("OS")
|
|
409
|
+
require file(".lampson")
|
|
410
|
+
require file(".lampson/*")
|
|
411
|
+
require file.read("hub.tpl.syn")
|
|
412
|
+
require file("hub.syn")
|
|
413
|
+
regen_hub()
|
|
414
|
+
-- desacoplado y con cwd = instalación (synsema daemon arranca el serve con otro cwd y los `use "./lib/…"`,
|
|
415
|
+
-- que son relativos al cwd, no se encuentran). Sin --watch: en esta carpeta el watcher recorre las junctions de
|
|
416
|
+
-- los workspaces (proyectos enteros) y el server nunca llega a escuchar; el reinicio lo hace restart_hub_later.
|
|
417
|
+
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
418
|
+
spawn_detached(".", exe + " serve " + HUB_FILE + " > .lampson/hub.log 2>&1", {"LAMPSON_HOME": home()})
|
|
419
|
+
give true
|
|
420
|
+
|
|
421
|
+
-- hub.syn cambió (workspace nuevo/quitado): un helper DESACOPLADO (cli.syn hub-restart) para el hub y lo vuelve a
|
|
422
|
+
-- arrancar — puede llamarse desde el propio hub (que va a morir en el medio) o desde cli.syn
|
|
423
|
+
export task restart_hub_later()
|
|
424
|
+
require exec
|
|
425
|
+
require time
|
|
426
|
+
require env("LAMPSON_*")
|
|
427
|
+
require env("OS")
|
|
428
|
+
let exe be env("LAMPSON_SYNSEMA", "synsema")
|
|
429
|
+
spawn_detached(".", exe + " run cli.syn > .lampson/hub-restart.log 2>&1", {"LAMPSON_HOME": home(), "LAMPSON_CMD": "hub-restart"})
|
|
430
|
+
give true
|
|
431
|
+
|
|
432
|
+
export task stop_hub()
|
|
433
|
+
require exec
|
|
434
|
+
require time
|
|
435
|
+
require env("LAMPSON_*")
|
|
436
|
+
require env("OS")
|
|
437
|
+
let victims be where(proc.listeners(), (l) => l["port"] == HUB_PORT)
|
|
438
|
+
each l in victims
|
|
439
|
+
proc.kill_pid(l["pid"])
|
|
440
|
+
give length(victims) > 0
|
|
441
|
+
|
|
442
|
+
-- ---------- política y supervisor (tick del hub) ----------
|
|
443
|
+
export task idle_hours()
|
|
444
|
+
require file.read(".lampson")
|
|
445
|
+
require file.read(".lampson/*")
|
|
446
|
+
require env("LAMPSON_*")
|
|
447
|
+
let doc be settings.load()
|
|
448
|
+
when contains(doc, "idle_hours")
|
|
449
|
+
let v be number(text(doc["idle_hours"]))
|
|
450
|
+
give when v < 0 then 0 otherwise v
|
|
451
|
+
give 4
|
|
452
|
+
|
|
453
|
+
-- ¿debe estar vivo? always | auto (usado hace < idle_hours, o con tareas encendidas) | off
|
|
454
|
+
export task should_live(w, hc)
|
|
455
|
+
require file.read(".lampson")
|
|
456
|
+
require file.read(".lampson/*")
|
|
457
|
+
require env("LAMPSON_*")
|
|
458
|
+
require time
|
|
459
|
+
let pol be when contains(w, "policy") then w["policy"] otherwise "auto"
|
|
460
|
+
when pol == "always"
|
|
461
|
+
give true
|
|
462
|
+
when pol == "off"
|
|
463
|
+
give false
|
|
464
|
+
when hc != nothing and contains(hc, "schedules_on") and hc["schedules_on"] > 0
|
|
465
|
+
give true
|
|
466
|
+
when schedules_on_disk(w) > 0
|
|
467
|
+
give true
|
|
468
|
+
let ih be idle_hours()
|
|
469
|
+
when ih == 0
|
|
470
|
+
give true
|
|
471
|
+
let last be when hc != nothing and contains(hc, "last_used") and hc["last_used"] != nothing then hc["last_used"] otherwise w["last_used"]
|
|
472
|
+
give now() - last < ih * 3600
|
|
473
|
+
|
|
474
|
+
-- tareas encendidas según el json del workspace (sirve aunque el proceso esté apagado)
|
|
475
|
+
task schedules_on_disk(w)
|
|
476
|
+
try
|
|
477
|
+
let doc be json_decode(read_file(dir_of(w["slug"]) + "/.lampson/schedules.json"))
|
|
478
|
+
give length(where(doc["tasks"], (t) => t["enabled"] == true))
|
|
479
|
+
recover err
|
|
480
|
+
give 0
|
|
481
|
+
|
|
482
|
+
-- un tick: arranca lo que debe vivir y no responde; apaga lo que no debe y responde
|
|
483
|
+
export task tick()
|
|
484
|
+
require exec
|
|
485
|
+
require time
|
|
486
|
+
require env("LAMPSON_*")
|
|
487
|
+
require env("OS")
|
|
488
|
+
require net("127.0.0.1")
|
|
489
|
+
require file(".lampson")
|
|
490
|
+
require file(".lampson/*")
|
|
491
|
+
require file.read("web.syn")
|
|
492
|
+
require file.read("chat.syn")
|
|
493
|
+
let started be []
|
|
494
|
+
let stopped be []
|
|
495
|
+
each w in all()
|
|
496
|
+
let hc be health(w)
|
|
497
|
+
let want be should_live(w, hc)
|
|
498
|
+
when want and hc == nothing
|
|
499
|
+
start_ws(w)
|
|
500
|
+
set started to append(started, w["slug"])
|
|
501
|
+
otherwise when not want and hc != nothing
|
|
502
|
+
stop_ws(w)
|
|
503
|
+
set stopped to append(stopped, w["slug"])
|
|
504
|
+
give {"started": started, "stopped": stopped}
|
|
505
|
+
|
|
506
|
+
-- resumen para la UI del hub
|
|
507
|
+
export task summary()
|
|
508
|
+
require exec
|
|
509
|
+
require time
|
|
510
|
+
require env("LAMPSON_*")
|
|
511
|
+
require env("OS")
|
|
512
|
+
require net("127.0.0.1")
|
|
513
|
+
require file(".lampson")
|
|
514
|
+
require file(".lampson/*")
|
|
515
|
+
let out be []
|
|
516
|
+
each w in all()
|
|
517
|
+
let hc be health(w)
|
|
518
|
+
set out to append(out, {"slug": w["slug"], "name": w["name"], "path": w["path"], "port": w["port"], "policy": when contains(w, "policy") then w["policy"] otherwise "auto", "created": w["created"], "last_used": when hc != nothing and contains(hc, "last_used") and hc["last_used"] != nothing then hc["last_used"] otherwise w["last_used"], "alive": hc != nothing, "schedules_on": when hc != nothing and contains(hc, "schedules_on") then hc["schedules_on"] otherwise schedules_on_disk(w), "url": "/w/" + w["slug"]})
|
|
519
|
+
give out
|
|
520
|
+
|
|
521
|
+
-- ---------- explorador de carpetas ----------
|
|
522
|
+
-- diálogo nativo del SO (PC con escritorio). Devuelve la ruta elegida, "" si canceló, nothing si no hay diálogo.
|
|
523
|
+
export task pick_folder()
|
|
524
|
+
require exec
|
|
525
|
+
require env("LAMPSON_*")
|
|
526
|
+
require env("OS")
|
|
527
|
+
try
|
|
528
|
+
when is_win()
|
|
529
|
+
let ps be "Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Elegí la carpeta del proyecto (workspace de Lampson)'; $d.ShowNewFolderButton = $true; $f = New-Object System.Windows.Forms.Form; $f.TopMost = $true; if ($d.ShowDialog($f) -eq 'OK') { Write-Output $d.SelectedPath }"
|
|
530
|
+
let r be run("powershell", ["-NoProfile", "-STA", "-Command", ps], 300)
|
|
531
|
+
give trim(text(r["stdout"]))
|
|
532
|
+
let r be run("sh", ["-c", "if command -v osascript >/dev/null; then osascript -e 'POSIX path of (choose folder with prompt \"Carpeta del proyecto\")'; elif command -v zenity >/dev/null; then zenity --file-selection --directory --title='Carpeta del proyecto'; else exit 3; fi"], 300)
|
|
533
|
+
when r["exit_code"] == 3
|
|
534
|
+
give nothing
|
|
535
|
+
give trim(text(r["stdout"]))
|
|
536
|
+
recover err
|
|
537
|
+
give nothing
|
|
538
|
+
|
|
539
|
+
-- listado de subcarpetas por shell (VPS sin escritorio): sin capabilities de archivo sobre el disco
|
|
540
|
+
export task browse(path)
|
|
541
|
+
require exec
|
|
542
|
+
require env("LAMPSON_*")
|
|
543
|
+
require env("OS")
|
|
544
|
+
let p be when path == nothing or trim(text(path)) == "" then env("HOME", env("USERPROFILE", "/")) otherwise text(path)
|
|
545
|
+
let r be sh("cd \"" + replace_text(p, "\"", "") + "\" 2>/dev/null && (pwd -W 2>/dev/null || pwd -P) && ls -1 -d -- */ 2>/dev/null", 10)
|
|
546
|
+
let lines be where(split(replace_text(text(r["stdout"]), "\r", ""), "\n"), (x) => trim(x) != "")
|
|
547
|
+
when length(lines) == 0
|
|
548
|
+
give {"path": p, "dirs": [], "error": "no existe o no se puede leer"}
|
|
549
|
+
let here be lines[0]
|
|
550
|
+
when is_win()
|
|
551
|
+
set here to replace_text(here, "/", "\\")
|
|
552
|
+
let dirs be apply(slice(lines, 1, length(lines)), (d) => replace_text(d, "/", ""))
|
|
553
|
+
let parts be where(split(replace_text(here, "\\", "/"), "/"), (x) => x != "")
|
|
554
|
+
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 "/")
|
|
555
|
+
give {"path": here, "parent": parent, "dirs": where(dirs, (d) => not starts_with(d, "."))}
|