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/README.md
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
reads, searches, edits and runs commands through tools that are confined to that project by the
|
|
5
5
|
language itself — and every step is visible, in the terminal or in a web UI.
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **Workspaces, always on**: every project folder is a *workspace* with its own process (sessions, scheduled tasks,
|
|
8
|
+
MCP, lamps, dev servers), all behind one hub at http://127.0.0.1:8080 — open several at once and switch without
|
|
9
|
+
killing the others. `lampson` (terminal) and `lampson --web` (browser) open the workspace of the current folder;
|
|
10
|
+
`lampson --install` keeps the hub alive across logins.
|
|
8
11
|
- **Any OpenAI- or Anthropic-compatible API** over raw HTTP: DeepSeek, Kimi, Groq, Grok, OpenRouter,
|
|
9
12
|
Ollama, Anthropic, MiniMax… one `.env` line to switch.
|
|
10
13
|
- **Least-privilege by construction**: each tool declares its capabilities; the runtime enforces
|
|
@@ -124,16 +127,38 @@ does the same with persistent sessions and memory.
|
|
|
124
127
|
> Status: developed and tested on Windows 11; the Docker image (Ubuntu 24.04) is built by CI. The Linux/macOS
|
|
125
128
|
> installer is written but not yet exercised on a real machine — issues welcome.
|
|
126
129
|
|
|
127
|
-
##
|
|
128
|
-
|
|
129
|
-
Lampson can run things with nobody at the keyboard. Scheduled tasks run inside **any** open Lampson — the web UI
|
|
130
|
-
or the terminal REPL (a background thread ticks every 30 s). To run them with nothing open:
|
|
130
|
+
## Workspaces and the hub (how it runs)
|
|
131
131
|
|
|
132
132
|
```
|
|
133
|
-
lampson
|
|
134
|
-
|
|
133
|
+
lampson # in a project folder: registers it as a workspace (first time), makes sure the hub and
|
|
134
|
+
# the workspace's process are up, opens the terminal on it
|
|
135
|
+
lampson --web # same, then opens http://127.0.0.1:8080/w/<slug> in the browser
|
|
136
|
+
http://127.0.0.1:8080 # the workspaces screen: open one, create one (native folder dialog; a server-side
|
|
137
|
+
# browser on a VPS), turn them on/off, life policy
|
|
138
|
+
lampson --hub start|stop|status|logs|restart
|
|
139
|
+
lampson --install # Windows: Scheduled Task at logon · Linux: systemd --user unit (Restart=always)
|
|
135
140
|
```
|
|
136
141
|
|
|
142
|
+
Why processes: in Synsema a tool's file capability is a literal path relative to the process cwd, so a workspace is
|
|
143
|
+
a directory `~/lampson/.lampson/ws/<slug>/` with a junction `workspace` → your project (plus junctions to the
|
|
144
|
+
install's `lib/`, `public/`, `skills/`, `lamps/`, `memory/`), and its own `.lampson/` state. Its process
|
|
145
|
+
(`synsema serve web.syn --port 808N --bind 127.0.0.1`) only ever sees that folder. The hub (`hub.syn`, generated
|
|
146
|
+
from `hub.tpl.syn`, one proxy route per workspace) is the only listener you use: it serves the UI and forwards
|
|
147
|
+
`/w/<slug>/api/…` — SSE and the WebSocket terminal included (Synsema ≥ 0.6.12). Workspace processes are detached
|
|
148
|
+
from the hub, so a hub restart never kills them. The hub is also the supervisor: every 15 s it starts what should
|
|
149
|
+
be alive and stops what is idle (⚙ → hours of inactivity, `0` = never; a workspace with enabled scheduled tasks or
|
|
150
|
+
policy «siempre vivo» stays up).
|
|
151
|
+
|
|
152
|
+
On a VPS put the hub behind your domain (`synsema serve hub.syn --port 443 --domain … --tls-auto …` or your
|
|
153
|
+
edge); workspace ports never leave loopback. `LAMPSON_PUBLIC_URL` = `https://host` and approval links carry
|
|
154
|
+
`/w/<slug>` automatically.
|
|
155
|
+
|
|
156
|
+
## Scheduled tasks and the resident process
|
|
157
|
+
|
|
158
|
+
Lampson can run things with nobody at the keyboard. Scheduled tasks run inside the workspace's process (the hub
|
|
159
|
+
keeps it alive while it has enabled tasks) — or inside the terminal REPL when no process is up (a background thread
|
|
160
|
+
ticks every 30 s). `lampson --install` makes the hub survive reboots.
|
|
161
|
+
|
|
137
162
|
Timezone, public URL and webhook are set from the ⚙ button of the web header (tabs: General · Aprobaciones a
|
|
138
163
|
distancia · Proveedor; stored in `lampson/.lampson/config.json`, `.env` wins) or from `.env`.
|
|
139
164
|
|
|
@@ -230,9 +255,13 @@ run Lampson in a container.
|
|
|
230
255
|
## Architecture
|
|
231
256
|
|
|
232
257
|
```
|
|
233
|
-
lampson.ps1 / .sh launcher:
|
|
258
|
+
lampson.ps1 / .sh launcher: resolves the workspace (cli.syn), ensures hub + process, opens terminal or browser
|
|
259
|
+
cli.syn what launchers ask Synsema: ensure/list/hub-start/hub-stop/hub-restart (last line = JSON)
|
|
260
|
+
hub.tpl.syn template of the hub (:8080): workspaces screen + API, supervisor tick, one proxy route per workspace
|
|
261
|
+
lib/workspaces.syn registry (.lampson/workspaces.json), ws/<slug> dirs + junctions, detached processes, health,
|
|
262
|
+
life policy, folder picker (native dialog / server-side browser)
|
|
234
263
|
chat.syn terminal REPL (colors, approvals via Synsema's native `approve`)
|
|
235
|
-
web.syn
|
|
264
|
+
web.syn one process per workspace: /w/:slug/api/… → SSE chat, sessions, tree, processes, schedules…
|
|
236
265
|
public/ web UI (no build step, no dependencies; classic scripts served by `static`)
|
|
237
266
|
index.html markup only: header, the two side panels, the chat; loads css/ and js/ in order
|
|
238
267
|
css/ tokens (fonts, palette, base) · layout (grid, header, panels, chat, composer) · sidebar · chat · panel
|
package/bin/lampson.js
CHANGED
|
@@ -22,7 +22,7 @@ const home = process.env.LAMPSON_HOME || path.join(os.homedir(), 'lampson');
|
|
|
22
22
|
const marker = path.join(home, '.npm-installed');
|
|
23
23
|
|
|
24
24
|
// what gets copied into the home: the runtime, never state. Keep in sync with "files" in package.json.
|
|
25
|
-
const CODE = ['lib', 'public', 'skills', 'chat.syn', 'web.syn', 'lampson.ps1', 'lampson.sh', 'lampson.cmd', '.env.example', 'README.md', 'LICENSE'];
|
|
25
|
+
const CODE = ['lib', 'public', 'skills', 'chat.syn', 'web.syn', 'hub.tpl.syn', 'cli.syn', 'lampson.ps1', 'lampson.sh', 'lampson.cmd', '.env.example', 'README.md', 'LICENSE'];
|
|
26
26
|
|
|
27
27
|
// copia un archivo; en unix, los scripts de shell y los .syn salen con LF aunque el paquete se haya armado
|
|
28
28
|
// en Windows (un CRLF en lampson.sh rompe bash: "set: pipefail\r: invalid option")
|
package/chat.syn
CHANGED
|
@@ -722,7 +722,17 @@ agent Sched
|
|
|
722
722
|
otherwise when waited >= 30
|
|
723
723
|
set waited to 0
|
|
724
724
|
try
|
|
725
|
-
|
|
725
|
+
-- bajo el hub el proceso del workspace (LAMPSON_PORT) ya hace tick: no duplicar corridas
|
|
726
|
+
let port be env("LAMPSON_PORT", "")
|
|
727
|
+
let other be false
|
|
728
|
+
when port != ""
|
|
729
|
+
try
|
|
730
|
+
let hr be http_get("http://127.0.0.1:" + port + "/w/" + env("LAMPSON_WS", "x") + "/api/health", {"timeout": 2})
|
|
731
|
+
set other to hr["ok"]
|
|
732
|
+
recover e2
|
|
733
|
+
set other to false
|
|
734
|
+
when not other
|
|
735
|
+
lampson_sched_tick()
|
|
726
736
|
recover err
|
|
727
737
|
print("[lampson] scheduler: " + text(err))
|
|
728
738
|
otherwise
|
package/cli.syn
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- cli.syn — lo que los launchers (lampson.ps1 / lampson.sh) le piden a Synsema. Corre con cwd = instalación.
|
|
2
|
+
--
|
|
3
|
+
-- LAMPSON_CMD=ensure LAMPSON_WORKSPACE=<ruta> synsema run cli.syn
|
|
4
|
+
-- registra/prepara el workspace, asegura el hub y su proceso → imprime UNA línea JSON {slug, port, dir, url, alive}
|
|
5
|
+
-- LAMPSON_CMD=hub-status → {alive}
|
|
6
|
+
-- LAMPSON_CMD=hub-start | hub-stop
|
|
7
|
+
-- LAMPSON_CMD=list
|
|
8
|
+
-- La salida útil va en la ÚLTIMA línea (JSON) para que el launcher la parsee.
|
|
9
|
+
|
|
10
|
+
require exec
|
|
11
|
+
require time
|
|
12
|
+
require net("127.0.0.1")
|
|
13
|
+
require env("LAMPSON_*")
|
|
14
|
+
require env("OS")
|
|
15
|
+
require env("HOME")
|
|
16
|
+
require env("USERPROFILE")
|
|
17
|
+
require file(".lampson")
|
|
18
|
+
require file(".lampson/*")
|
|
19
|
+
require file.read("web.syn")
|
|
20
|
+
require file.read(".env")
|
|
21
|
+
require file.read("chat.syn")
|
|
22
|
+
require file.read("hub.tpl.syn")
|
|
23
|
+
require file("hub.syn")
|
|
24
|
+
|
|
25
|
+
use "./lib/workspaces.syn" as ws
|
|
26
|
+
|
|
27
|
+
let cmd be env("LAMPSON_CMD", "ensure")
|
|
28
|
+
|
|
29
|
+
when cmd == "ensure"
|
|
30
|
+
let path be env("LAMPSON_WORKSPACE", "")
|
|
31
|
+
when path == ""
|
|
32
|
+
raise("LAMPSON_WORKSPACE required")
|
|
33
|
+
let w be ws.ensure(path)
|
|
34
|
+
when not ws.hub_alive()
|
|
35
|
+
ws.start_hub()
|
|
36
|
+
let hc be ws.health(w)
|
|
37
|
+
when hc == nothing
|
|
38
|
+
ws.start_ws(w)
|
|
39
|
+
ws.wait_ready(w, 10)
|
|
40
|
+
set hc to ws.health(w)
|
|
41
|
+
ws.touch(w["slug"])
|
|
42
|
+
print(json_encode({"slug": w["slug"], "port": w["port"], "dir": ws.dir_of(w["slug"]), "url": "http://127.0.0.1:" + text(ws.HUB_PORT) + "/w/" + w["slug"], "alive": hc != nothing, "hub": ws.hub_alive()}))
|
|
43
|
+
otherwise when cmd == "hub-status"
|
|
44
|
+
print(json_encode({"alive": ws.hub_alive(), "workspaces": ws.summary()}))
|
|
45
|
+
otherwise when cmd == "hub-start"
|
|
46
|
+
when not ws.hub_alive()
|
|
47
|
+
ws.start_hub()
|
|
48
|
+
print(json_encode({"alive": true}))
|
|
49
|
+
otherwise when cmd == "hub-stop"
|
|
50
|
+
each w in ws.all()
|
|
51
|
+
ws.stop_ws(w)
|
|
52
|
+
ws.stop_hub()
|
|
53
|
+
print(json_encode({"alive": false}))
|
|
54
|
+
otherwise when cmd == "hub-restart"
|
|
55
|
+
-- lanzado desacoplado por workspaces.restart_hub_later: espera a que el hub termine lo que estaba haciendo,
|
|
56
|
+
-- lo para y lo vuelve a arrancar con el hub.syn regenerado (bajo systemd, Restart=always ya lo levanta solo)
|
|
57
|
+
sleep(1.5)
|
|
58
|
+
ws.stop_hub()
|
|
59
|
+
sleep(2)
|
|
60
|
+
when not ws.hub_alive()
|
|
61
|
+
ws.start_hub()
|
|
62
|
+
print(json_encode({"restarted": true}))
|
|
63
|
+
otherwise when cmd == "list"
|
|
64
|
+
each w in ws.summary()
|
|
65
|
+
print((when w["alive"] then "● " otherwise "○ ") + w["slug"] + " " + w["path"] + " :" + text(w["port"]) + " " + w["policy"])
|
|
66
|
+
print(json_encode({"count": length(ws.all())}))
|
|
67
|
+
otherwise
|
|
68
|
+
raise("unknown LAMPSON_CMD " + cmd)
|
package/hub.tpl.syn
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
-- hub.tpl.syn — PLANTILLA del hub. lib/workspaces.syn genera hub.syn (gitignored) reemplazando `-- @workspaces` por una
|
|
2
|
+
-- ruta proxy por workspace; el hub corre con `synsema daemon start hub.syn --watch` y se reinicia solo al cambiar.
|
|
3
|
+
--
|
|
4
|
+
-- El hub es lo único que escucha hacia afuera (:8080; en un VPS, con --domain/--tls-auto):
|
|
5
|
+
-- GET / pantalla de workspaces (public/hub.html)
|
|
6
|
+
-- GET /w/<slug> la UI de ese workspace (public/index.html; los assets /css /js los sirve el hub)
|
|
7
|
+
-- * /w/<slug>/api/… proxy al proceso del workspace (127.0.0.1:808N) — SSE y WebSocket pasan (Synsema ≥ 0.6.12)
|
|
8
|
+
-- GET /w/<slug>/approve/… links de aprobación de ese workspace
|
|
9
|
+
-- /api/workspaces… registro, crear (diálogo nativo o explorador), start/stop/quitar, política
|
|
10
|
+
-- Supervisor: cada 15 s arranca los workspaces que deben vivir y apaga los inactivos (política en ⚙ → idle_hours).
|
|
11
|
+
-- Solo loopback puede usar la API del hub (misma regla que web.syn); LAMPSON_WEB_TOKEN para scripts remotos.
|
|
12
|
+
|
|
13
|
+
intent: "Lampson hub: workspaces registry, supervisor and reverse proxy to one process per workspace"
|
|
14
|
+
|
|
15
|
+
require serve(8080)
|
|
16
|
+
require net("127.0.0.1")
|
|
17
|
+
require time
|
|
18
|
+
require exec
|
|
19
|
+
require env("LAMPSON_*")
|
|
20
|
+
require env("OS")
|
|
21
|
+
require env("HOME")
|
|
22
|
+
require env("USERPROFILE")
|
|
23
|
+
require file(".lampson")
|
|
24
|
+
require file(".lampson/*")
|
|
25
|
+
require file.read("web.syn")
|
|
26
|
+
require file.read(".env")
|
|
27
|
+
require file.read("chat.syn")
|
|
28
|
+
require file.read("hub.tpl.syn")
|
|
29
|
+
require file("hub.syn")
|
|
30
|
+
require file.read("public")
|
|
31
|
+
require file.read("public/*")
|
|
32
|
+
|
|
33
|
+
use "./lib/workspaces.syn" as ws
|
|
34
|
+
use "./lib/settings.syn" as settings
|
|
35
|
+
|
|
36
|
+
task check_client(token, request)
|
|
37
|
+
let ip be text(ip of request)
|
|
38
|
+
when ip == "127.0.0.1" or ip == "::1" or ip == "::ffff:127.0.0.1"
|
|
39
|
+
give {"local": true}
|
|
40
|
+
let want be env("LAMPSON_WEB_TOKEN", "")
|
|
41
|
+
when want != "" and token == want
|
|
42
|
+
give {"local": false}
|
|
43
|
+
give nothing
|
|
44
|
+
|
|
45
|
+
task hub_tick()
|
|
46
|
+
give ws.tick()
|
|
47
|
+
cron_every(15, hub_tick)
|
|
48
|
+
|
|
49
|
+
serve on 8080
|
|
50
|
+
max_streams 200
|
|
51
|
+
auth with check_client
|
|
52
|
+
static "./public"
|
|
53
|
+
|
|
54
|
+
route "GET /"
|
|
55
|
+
give html(read_file("public/hub.html"))
|
|
56
|
+
|
|
57
|
+
-- la UI de un workspace: el mismo index.html; el frontend deduce /w/<slug> de la URL (BASE en js/core.js).
|
|
58
|
+
-- Sin barra final: el runtime normaliza "/w/x/" → "/w/x" (301), así que la ruta canónica es esta
|
|
59
|
+
route "GET /w/:slug"
|
|
60
|
+
give html(read_file("public/index.html"))
|
|
61
|
+
|
|
62
|
+
route "GET /api/hub" requires auth
|
|
63
|
+
give {"ok": true, "port": ws.HUB_PORT, "idle_hours": ws.idle_hours(), "home": ws.home()}
|
|
64
|
+
|
|
65
|
+
route "GET /api/workspaces" requires auth
|
|
66
|
+
give {"workspaces": ws.summary(), "idle_hours": ws.idle_hours()}
|
|
67
|
+
|
|
68
|
+
-- registrar una carpeta como workspace (y arrancarla)
|
|
69
|
+
route "POST /api/workspaces" requires auth
|
|
70
|
+
expect body {path: text}
|
|
71
|
+
let b be json of request
|
|
72
|
+
try
|
|
73
|
+
let w be ws.ensure(text(b["path"]))
|
|
74
|
+
ws.start_ws(w)
|
|
75
|
+
ws.wait_ready(w, 8)
|
|
76
|
+
give {"ok": true, "workspace": w, "url": "/w/" + w["slug"] + "/"}
|
|
77
|
+
recover err
|
|
78
|
+
give fail(400, text(err))
|
|
79
|
+
|
|
80
|
+
route "POST /api/workspaces/start" requires auth
|
|
81
|
+
expect body {slug: text}
|
|
82
|
+
let b be json of request
|
|
83
|
+
let w be ws.get(text(b["slug"]))
|
|
84
|
+
when w == nothing
|
|
85
|
+
give not_found("no such workspace")
|
|
86
|
+
ws.start_ws(w)
|
|
87
|
+
give {"ok": ws.wait_ready(w, 8)}
|
|
88
|
+
|
|
89
|
+
route "POST /api/workspaces/stop" requires auth
|
|
90
|
+
expect body {slug: text}
|
|
91
|
+
let b be json of request
|
|
92
|
+
let w be ws.get(text(b["slug"]))
|
|
93
|
+
when w == nothing
|
|
94
|
+
give not_found("no such workspace")
|
|
95
|
+
give {"ok": ws.stop_ws(w)}
|
|
96
|
+
|
|
97
|
+
route "POST /api/workspaces/remove" requires auth
|
|
98
|
+
expect body {slug: text}
|
|
99
|
+
let b be json of request
|
|
100
|
+
try
|
|
101
|
+
give {"ok": true, "result": ws.remove(text(b["slug"]))}
|
|
102
|
+
recover err
|
|
103
|
+
give fail(400, text(err))
|
|
104
|
+
|
|
105
|
+
-- política por workspace: auto | always | off
|
|
106
|
+
route "POST /api/workspaces/policy" requires auth
|
|
107
|
+
expect body {slug: text, policy: text}
|
|
108
|
+
let b be json of request
|
|
109
|
+
let w be ws.get(text(b["slug"]))
|
|
110
|
+
when w == nothing
|
|
111
|
+
give not_found("no such workspace")
|
|
112
|
+
when not contains(["auto", "always", "off"], text(b["policy"]))
|
|
113
|
+
give fail(400, "policy must be auto | always | off")
|
|
114
|
+
set w["policy"] to text(b["policy"])
|
|
115
|
+
ws.put(w)
|
|
116
|
+
give {"ok": true}
|
|
117
|
+
|
|
118
|
+
-- diálogo nativo del SO (PC); nothing → la UI usa el explorador propio
|
|
119
|
+
route "POST /api/workspaces/pick" requires auth
|
|
120
|
+
let p be ws.pick_folder()
|
|
121
|
+
give {"path": p, "native": p != nothing}
|
|
122
|
+
|
|
123
|
+
route "GET /api/workspaces/browse" requires auth
|
|
124
|
+
give ws.browse(when contains(query, "path") then text(query.path) otherwise "")
|
|
125
|
+
|
|
126
|
+
-- proxies generados (una por workspace registrado):
|
|
127
|
+
-- @workspaces
|
package/lampson.ps1
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
# lampson.ps1 — launcher Windows.
|
|
1
|
+
# lampson.ps1 — launcher Windows. Cada proyecto es un WORKSPACE con su propio proceso; un hub en :8080 los sirve a todos.
|
|
2
2
|
#
|
|
3
|
-
# cd C:\mi\proyecto ; lampson #
|
|
4
|
-
# cd C:\mi\proyecto ; lampson --web #
|
|
5
|
-
# lampson --workspace C:\otro\proyecto # elegir la
|
|
6
|
-
# lampson --
|
|
7
|
-
# lampson --
|
|
8
|
-
# lampson --
|
|
9
|
-
# lampson --update # actualizar Lampson (git pull) y salir
|
|
10
|
-
# lampson --help
|
|
3
|
+
# cd C:\mi\proyecto ; lampson # terminal sobre este workspace (lo registra y lo arranca si hace falta)
|
|
4
|
+
# cd C:\mi\proyecto ; lampson --web # abre http://127.0.0.1:8080/w/<slug>/ en el navegador
|
|
5
|
+
# lampson --workspace C:\otro\proyecto # elegir la carpeta explícitamente
|
|
6
|
+
# lampson --hub start|stop|status|logs # el servicio residente (workspaces + tareas programadas)
|
|
7
|
+
# lampson --install | --uninstall # arrancar el hub al iniciar sesión (Tarea Programada)
|
|
8
|
+
# lampson --agent plan · --yolo|--strict|--ask · --update · --help
|
|
11
9
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
10
|
+
# Cómo funciona (ver SPEC-WORKSPACES.md): .lampson\ws\<slug>\ es el cwd del proceso del workspace, con una junction
|
|
11
|
+
# `workspace` al proyecto y junctions a lib/public/skills/lamps/memory de esta instalación. El registro y los procesos
|
|
12
|
+
# los maneja lib/workspaces.syn vía cli.syn; este script solo resuelve la carpeta, llama a cli.syn y abre lo pedido.
|
|
15
13
|
$ErrorActionPreference = "Stop"
|
|
16
14
|
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
17
15
|
$caller = (Get-Location).Path
|
|
18
|
-
$mount = Join-Path $here "workspace"
|
|
19
16
|
|
|
20
17
|
# --- args: acepta --flag y -Flag, sin distinguir mayúsculas ---
|
|
21
|
-
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""; $
|
|
18
|
+
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""; $Hub = ""; $Install = ""
|
|
22
19
|
$i = 0
|
|
23
20
|
while ($i -lt $args.Count) {
|
|
24
21
|
$a = [string]$args[$i]
|
|
25
22
|
switch -Regex ($a.ToLower()) {
|
|
26
23
|
'^--?(web|w)$' { $Web = $true }
|
|
27
|
-
'^--?(daemon|d)$'
|
|
24
|
+
'^--?(hub|daemon|d)$' { $i++; $Hub = if ($i -lt $args.Count) { ([string]$args[$i]).ToLower() } else { "status" } }
|
|
25
|
+
'^--?install$' { $Install = "install" }
|
|
26
|
+
'^--?uninstall$' { $Install = "uninstall" }
|
|
28
27
|
'^--?(workspace|ws)$' { $i++; $Workspace = [string]$args[$i] }
|
|
29
28
|
'^--?(agent|a)$' { $i++; $Agent = [string]$args[$i] }
|
|
30
29
|
'^--?(yolo|y|dangerously-skip-permissions)$' { $Perm = "yolo" }
|
|
@@ -32,71 +31,84 @@ while ($i -lt $args.Count) {
|
|
|
32
31
|
'^--?(ask)$' { $Perm = "ask" }
|
|
33
32
|
'^--?(permission|p)$' { $i++; $Perm = ([string]$args[$i]).ToLower() }
|
|
34
33
|
'^--?(update|u)$' { Write-Host "actualizando Lampson en $here"; git -C $here pull --ff-only origin main; Write-Host ("lampson " + (git -C $here rev-parse --short HEAD)); exit $LASTEXITCODE }
|
|
35
|
-
'^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First
|
|
34
|
+
'^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First 8 | ForEach-Object { $_.TrimStart('#',' ') }; exit 0 }
|
|
36
35
|
default { if ($Workspace -eq "" -and -not $a.StartsWith("-")) { $Workspace = $a } else { Write-Error "argumento desconocido: $a (probá lampson --help)"; exit 1 } }
|
|
37
36
|
}
|
|
38
37
|
$i++
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
if ($Workspace -ne "") {
|
|
44
|
-
if (-not (Test-Path -LiteralPath $Workspace -PathType Container)) { Write-Error "el workspace no existe o no es un directorio: $Workspace"; exit 1 }
|
|
45
|
-
$target = (Resolve-Path -LiteralPath $Workspace).Path
|
|
46
|
-
$home_ = [Environment]::GetFolderPath("UserProfile")
|
|
47
|
-
if ($target.TrimEnd('\') -eq $home_.TrimEnd('\') -or $target -match '^[A-Za-z]:\\?$') {
|
|
48
|
-
Write-Error "'$target' es tu carpeta personal / la raíz del disco, no un proyecto. Entrá al repo (cd) y volvé a correr lampson, o usá --workspace C:\ruta\al\proyecto"; exit 1
|
|
49
|
-
}
|
|
50
|
-
if ($target -eq $here) { Write-Error "no montes el propio directorio de lampson como workspace desde fuera; usá --workspace"; exit 1 }
|
|
51
|
-
if (Test-Path -LiteralPath $mount) {
|
|
52
|
-
$item = Get-Item -LiteralPath $mount -Force
|
|
53
|
-
if ($item.LinkType -ne "Junction") { Write-Error "./workspace existe y no es una junction; movelo antes de montar otro proyecto"; exit 1 }
|
|
54
|
-
# un daemon/web corriendo sobre OTRO proyecto (latido reciente del scheduler) quedaría apuntando a este: aviso
|
|
55
|
-
$prev = $item.Target; if ($prev -is [array]) { $prev = $prev[0] }
|
|
56
|
-
$hb = Join-Path $here ".lampson\schedules.heartbeat"
|
|
57
|
-
if ($prev -and $prev.TrimEnd('\').ToLower() -ne $target.TrimEnd('\').ToLower() -and (Test-Path -LiteralPath $hb) -and ((Get-Date) - (Get-Item -LiteralPath $hb).LastWriteTime).TotalSeconds -lt 90) {
|
|
58
|
-
Write-Host "AVISO: hay un lampson (web/daemon) corriendo sobre $prev; al montar $target sus tareas programadas quedan en pausa. Paralo o reinicialo desde su carpeta: lampson --daemon restart" -ForegroundColor Yellow
|
|
59
|
-
}
|
|
60
|
-
$item.Delete()
|
|
61
|
-
}
|
|
62
|
-
New-Item -ItemType Junction -Path $mount -Target $target | Out-Null
|
|
63
|
-
} elseif (-not (Test-Path -LiteralPath $mount)) {
|
|
64
|
-
Write-Error "no hay workspace: corré lampson desde el directorio del proyecto, o lampson --workspace C:\ruta"; exit 1
|
|
65
|
-
}
|
|
66
|
-
$target = (Get-Item -LiteralPath $mount -Force).Target
|
|
67
|
-
if ($target -is [array]) { $target = $target[0] }
|
|
68
|
-
|
|
69
|
-
# --- skills externas globales (npx skills add -g → ~/.agents/skills; las de Claude Code → ~/.claude/skills) ---
|
|
70
|
-
# Se montan como junction bajo .lampson/ porque una capability no puede apuntar a una ruta dinámica (HOME).
|
|
40
|
+
# skills externas globales (npx skills add -g → ~/.agents/skills; Claude Code → ~/.claude/skills), montadas bajo .lampson\
|
|
41
|
+
# (una capability no puede apuntar a HOME); los workspaces las ven por su junction .lampson\global
|
|
71
42
|
$skillMounts = @{ "skills-global" = (Join-Path $HOME ".agents\skills"); "skills-claude" = (Join-Path $HOME ".claude\skills") }
|
|
72
43
|
New-Item -ItemType Directory -Force (Join-Path $here ".lampson") | Out-Null
|
|
73
44
|
foreach ($k in $skillMounts.Keys) {
|
|
74
45
|
$link = Join-Path $here ".lampson\$k"; $src = $skillMounts[$k]
|
|
75
|
-
if (Test-Path -LiteralPath $link)
|
|
76
|
-
|
|
46
|
+
if (-not (Test-Path -LiteralPath $link) -and (Test-Path -LiteralPath $src -PathType Container)) { New-Item -ItemType Junction -Path $link -Target $src | Out-Null }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Invoke-Cli([string]$cmd, [string]$ws) {
|
|
50
|
+
# cli.syn imprime la respuesta como la ÚLTIMA línea (JSON)
|
|
51
|
+
$env:LAMPSON_CMD = $cmd; if ($ws -ne "") { $env:LAMPSON_WORKSPACE = $ws }
|
|
52
|
+
$out = & synsema run cli.syn 2>&1
|
|
53
|
+
if ($LASTEXITCODE -ne 0) { $out | ForEach-Object { Write-Host $_ }; throw "cli.syn falló ($cmd)" }
|
|
54
|
+
$lines = @($out | ForEach-Object { [string]$_ })
|
|
55
|
+
$lines | Select-Object -SkipLast 1 | ForEach-Object { Write-Host $_ }
|
|
56
|
+
return ($lines[-1] | ConvertFrom-Json)
|
|
77
57
|
}
|
|
78
58
|
|
|
79
|
-
# --- arrancar desde el directorio de lampson (ahí viven .env, lib/, skills/, .lampson/) ---
|
|
80
|
-
# Push/Pop: si PowerShell ejecuta este .ps1 en la shell del usuario (pasa cuando lampson.ps1 y lampson.cmd
|
|
81
|
-
# comparten nombre en el PATH), el cwd del usuario debe quedar como estaba al salir.
|
|
82
59
|
Push-Location $here
|
|
83
60
|
try {
|
|
84
|
-
$env:
|
|
85
|
-
|
|
86
|
-
if ($
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
61
|
+
$env:LAMPSON_HOME = $here
|
|
62
|
+
# --- hub / servicio ---
|
|
63
|
+
if ($Hub -ne "") {
|
|
64
|
+
switch ($Hub) {
|
|
65
|
+
"start" { $r = Invoke-Cli "hub-start" ""; Write-Host "hub: http://127.0.0.1:8080" }
|
|
66
|
+
"stop" { $r = Invoke-Cli "hub-stop" ""; Write-Host "hub y workspaces parados" }
|
|
67
|
+
"status" { $r = Invoke-Cli "hub-status" ""; Write-Host ("hub " + $(if ($r.alive) { "● vivo · http://127.0.0.1:8080" } else { "○ parado" })); foreach ($w in $r.workspaces) { Write-Host (" " + $(if ($w.alive) { "●" } else { "○" }) + " " + $w.name + " " + $w.path + " " + $w.policy) } }
|
|
68
|
+
"logs" { if (Test-Path ".lampson\hub.log") { Get-Content ".lampson\hub.log" -Tail 40 } else { Write-Host "sin log todavía" } }
|
|
69
|
+
"restart" { $null = Invoke-Cli "hub-stop" ""; Start-Sleep 1; $null = Invoke-Cli "hub-start" ""; Write-Host "hub reiniciado" }
|
|
70
|
+
default { Write-Error "uso: lampson --hub start|stop|status|logs|restart"; exit 1 }
|
|
71
|
+
}
|
|
72
|
+
exit 0
|
|
73
|
+
}
|
|
74
|
+
# --- arranque al iniciar sesión: Tarea Programada que corre el hub (synsema daemon, --watch) ---
|
|
75
|
+
if ($Install -eq "install") {
|
|
76
|
+
$cmd = "cmd /c cd /d `"$here`" && set LAMPSON_HOME=$here && synsema serve hub.syn > .lampson\hub.log 2>&1"
|
|
77
|
+
schtasks /Create /F /TN "Lampson hub" /SC ONLOGON /TR $cmd | Out-Null
|
|
78
|
+
Write-Host "instalado: el hub de Lampson arranca al iniciar sesión (Tarea Programada «Lampson hub»). Arrancándolo ahora…"
|
|
79
|
+
$null = Invoke-Cli "hub-start" ""
|
|
80
|
+
exit 0
|
|
81
|
+
}
|
|
82
|
+
if ($Install -eq "uninstall") { schtasks /Delete /F /TN "Lampson hub" | Out-Null; Write-Host "quitado el arranque al iniciar sesión (el hub sigue corriendo hasta lampson --hub stop)"; exit 0 }
|
|
83
|
+
|
|
84
|
+
# --- resolver el proyecto: explícito > directorio actual (si no es el propio lampson) ---
|
|
85
|
+
if ($Workspace -eq "" -and $caller -ne $here) { $Workspace = $caller }
|
|
86
|
+
if ($Workspace -eq "") { Write-Error "corré lampson desde la carpeta del proyecto, o lampson --workspace C:\ruta (lampson --hub status lista los workspaces)"; exit 1 }
|
|
87
|
+
if (-not (Test-Path -LiteralPath $Workspace -PathType Container)) { Write-Error "el workspace no existe o no es un directorio: $Workspace"; exit 1 }
|
|
88
|
+
$target = (Resolve-Path -LiteralPath $Workspace).Path
|
|
89
|
+
$home_ = [Environment]::GetFolderPath("UserProfile")
|
|
90
|
+
if ($target.TrimEnd('\') -eq $home_.TrimEnd('\') -or $target -match '^[A-Za-z]:\\?$') {
|
|
91
|
+
Write-Error "'$target' es tu carpeta personal / la raíz del disco, no un proyecto. Entrá al repo (cd) y volvé a correr lampson, o usá --workspace C:\ruta\al\proyecto"; exit 1
|
|
92
|
+
}
|
|
93
|
+
if ($target -eq $here) { Write-Error "no montes el propio directorio de lampson como workspace"; exit 1 }
|
|
94
|
+
|
|
95
|
+
# registrar/preparar el workspace y asegurar hub + proceso
|
|
96
|
+
$r = Invoke-Cli "ensure" $target
|
|
97
|
+
$wsDir = Join-Path $here ($r.dir -replace '/', '\')
|
|
98
|
+
if ($Web) {
|
|
99
|
+
Write-Host ("Lampson web · " + $r.url + " (workspace " + $r.slug + $(if ($r.alive) { " ● vivo" } else { " ○ arrancando" }) + ")")
|
|
100
|
+
Start-Process $r.url
|
|
101
|
+
exit 0
|
|
99
102
|
}
|
|
103
|
+
# terminal: chat.syn dentro del directorio del workspace (misma junction `workspace` que el proceso web)
|
|
104
|
+
Push-Location $wsDir
|
|
105
|
+
try {
|
|
106
|
+
$env:LAMPSON_WORKSPACE = $target; $env:LAMPSON_WS = $r.slug; $env:LAMPSON_PORT = [string]$r.port
|
|
107
|
+
if ($Agent -ne "") { $env:LAMPSON_AGENT = $Agent }
|
|
108
|
+
if ($Perm -ne "") { $env:LAMPSON_PERMISSION = $Perm }
|
|
109
|
+
$envFile = Join-Path $here ".env"
|
|
110
|
+
if (Test-Path -LiteralPath $envFile) { synsema run --env-file $envFile chat.syn } else { synsema run chat.syn }
|
|
111
|
+
} finally { Pop-Location }
|
|
100
112
|
} finally {
|
|
101
113
|
Pop-Location
|
|
102
114
|
}
|
package/lampson.sh
CHANGED
|
@@ -1,56 +1,88 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# lampson.sh — launcher unix.
|
|
3
|
-
# cd /mi/proyecto && lampson #
|
|
4
|
-
# cd /mi/proyecto && lampson --web #
|
|
5
|
-
# lampson --
|
|
6
|
-
# lampson --
|
|
2
|
+
# lampson.sh — launcher unix. Cada proyecto es un WORKSPACE con su propio proceso; un hub en :8080 los sirve a todos.
|
|
3
|
+
# cd /mi/proyecto && lampson # terminal sobre este workspace
|
|
4
|
+
# cd /mi/proyecto && lampson --web # abre http://127.0.0.1:8080/w/<slug>/
|
|
5
|
+
# lampson --hub start|stop|status|logs|restart · --install (systemd --user) · --uninstall
|
|
6
|
+
# lampson --workspace /ruta [--agent plan] [--yolo|--strict|--ask] · --update
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
9
|
caller="$(pwd)"
|
|
10
|
-
ws=""; web=0;
|
|
10
|
+
ws=""; web=0; hub=""; install=""
|
|
11
11
|
while [ $# -gt 0 ]; do
|
|
12
12
|
case "$1" in
|
|
13
13
|
--web) web=1 ;;
|
|
14
|
-
--daemon) shift;
|
|
14
|
+
--hub|--daemon) shift; hub="${1:-status}" ;;
|
|
15
|
+
--install) install=install ;;
|
|
16
|
+
--uninstall) install=uninstall ;;
|
|
15
17
|
--workspace) shift; ws="$1" ;;
|
|
16
18
|
--agent) shift; export LAMPSON_AGENT="$1" ;;
|
|
17
19
|
--yolo|--dangerously-skip-permissions) export LAMPSON_PERMISSION=yolo ;;
|
|
18
20
|
--strict) export LAMPSON_PERMISSION=strict ;;
|
|
19
21
|
--ask) export LAMPSON_PERMISSION=ask ;;
|
|
20
22
|
--update) echo "actualizando Lampson en $here"; git -C "$here" pull --ff-only origin main; echo "lampson $(git -C "$here" rev-parse --short HEAD)"; exit $? ;;
|
|
21
|
-
|
|
23
|
+
-*) echo "uso: lampson [--web] [--hub start|stop|status|logs|restart] [--install|--uninstall] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
|
|
24
|
+
*) ws="$1" ;;
|
|
22
25
|
esac
|
|
23
26
|
shift
|
|
24
27
|
done
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
[ -d "$ws" ] || { echo "el workspace no existe o no es un directorio: $ws" >&2; exit 1; }
|
|
28
|
-
ws="$(cd "$ws" && pwd)"
|
|
29
|
-
if [ -e "$here/workspace" ] && [ ! -L "$here/workspace" ]; then echo "./workspace existe y no es un symlink" >&2; exit 1; fi
|
|
30
|
-
# un daemon/web corriendo sobre OTRO proyecto (latido reciente del scheduler) quedaría apuntando a este: aviso
|
|
31
|
-
if [ -L "$here/workspace" ] && [ -f "$here/.lampson/schedules.heartbeat" ]; then
|
|
32
|
-
prev="$(readlink -f "$here/workspace")"
|
|
33
|
-
if [ "$prev" != "$ws" ] && [ $(( $(date +%s) - $(stat -c %Y "$here/.lampson/schedules.heartbeat") )) -lt 90 ]; then
|
|
34
|
-
echo "AVISO: hay un lampson (web/daemon) corriendo sobre $prev; al montar $ws sus tareas programadas quedan en pausa. Paralo o reinicialo desde su carpeta: lampson --daemon restart" >&2
|
|
35
|
-
fi
|
|
36
|
-
fi
|
|
37
|
-
rm -f "$here/workspace"; ln -s "$ws" "$here/workspace"
|
|
38
|
-
elif [ ! -e "$here/workspace" ]; then
|
|
39
|
-
echo "no hay workspace: corré lampson desde el directorio del proyecto, o --workspace /ruta" >&2; exit 1
|
|
40
|
-
fi
|
|
41
|
-
export LAMPSON_WORKSPACE="$(readlink -f "$here/workspace")"
|
|
42
|
-
# skills externas globales (npx skills add -g → ~/.agents/skills; Claude Code → ~/.claude/skills), montadas bajo .lampson/
|
|
28
|
+
export LAMPSON_HOME="$here"
|
|
29
|
+
# skills externas globales, montadas bajo .lampson/ (los workspaces las ven por .lampson/global)
|
|
43
30
|
mkdir -p "$here/.lampson"
|
|
44
31
|
for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/skills"; do
|
|
45
32
|
link="$here/.lampson/${pair%%:*}"; src="${pair#*:}"
|
|
46
|
-
[ -
|
|
47
|
-
[ -d "$src" ] && ln -s "$src" "$link"
|
|
33
|
+
[ -e "$link" ] || { [ -d "$src" ] && ln -s "$src" "$link"; } || true
|
|
48
34
|
done
|
|
49
|
-
|
|
50
|
-
cd "$here"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
35
|
+
cli() { # última línea = JSON
|
|
36
|
+
local out; out="$(cd "$here" && LAMPSON_CMD="$1" LAMPSON_WORKSPACE="${2:-}" synsema run cli.syn)" || { echo "$out" >&2; exit 1; }
|
|
37
|
+
echo "$out" | sed '$d' >&2 || true
|
|
38
|
+
echo "$out" | tail -n 1
|
|
39
|
+
}
|
|
40
|
+
json() { node -e "const j=JSON.parse(process.argv[1]);const v=j[process.argv[2]];console.log(typeof v==='object'?JSON.stringify(v):v)" "$1" "$2" 2>/dev/null || python3 -c "import json,sys;print(json.loads(sys.argv[1])[sys.argv[2]])" "$1" "$2"; }
|
|
41
|
+
if [ -n "$hub" ]; then
|
|
42
|
+
case "$hub" in
|
|
43
|
+
start) cli hub-start >/dev/null; echo "hub: http://127.0.0.1:8080" ;;
|
|
44
|
+
stop) cli hub-stop >/dev/null; echo "hub y workspaces parados" ;;
|
|
45
|
+
status) r="$(cli hub-status)"; echo "hub $( [ "$(json "$r" alive)" = "true" ] && echo '● vivo · http://127.0.0.1:8080' || echo '○ parado')"; (cd "$here" && LAMPSON_CMD=list synsema run cli.syn | sed '$d') ;;
|
|
46
|
+
logs) [ -f "$here/.lampson/hub.log" ] && tail -n 40 "$here/.lampson/hub.log" || echo "sin log todavía" ;;
|
|
47
|
+
restart) cli hub-stop >/dev/null; sleep 1; cli hub-start >/dev/null; echo "hub reiniciado" ;;
|
|
48
|
+
*) echo "uso: lampson --hub start|stop|status|logs|restart" >&2; exit 1 ;;
|
|
49
|
+
esac
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
if [ "$install" = install ]; then
|
|
53
|
+
unit="$HOME/.config/systemd/user/lampson-hub.service"; mkdir -p "$(dirname "$unit")"
|
|
54
|
+
cat > "$unit" <<EOF
|
|
55
|
+
[Unit]
|
|
56
|
+
Description=Lampson hub (workspaces, tareas programadas, proxy)
|
|
57
|
+
After=network.target
|
|
58
|
+
[Service]
|
|
59
|
+
WorkingDirectory=$here
|
|
60
|
+
Environment=LAMPSON_HOME=$here
|
|
61
|
+
ExecStart=$(command -v synsema) serve hub.syn
|
|
62
|
+
Restart=always
|
|
63
|
+
RestartSec=3
|
|
64
|
+
TimeoutStopSec=20
|
|
65
|
+
[Install]
|
|
66
|
+
WantedBy=default.target
|
|
67
|
+
EOF
|
|
68
|
+
(cd "$here" && LAMPSON_CMD=hub-stop synsema run cli.syn >/dev/null 2>&1 || true)
|
|
69
|
+
systemctl --user daemon-reload && systemctl --user enable --now lampson-hub.service && echo "instalado: systemd --user lampson-hub.service (arranca con tu sesión; 'loginctl enable-linger $USER' para que viva sin sesión abierta)"
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
if [ "$install" = uninstall ]; then systemctl --user disable --now lampson-hub.service || true; rm -f "$HOME/.config/systemd/user/lampson-hub.service"; systemctl --user daemon-reload; echo "quitado"; exit 0; fi
|
|
73
|
+
# --- proyecto ---
|
|
74
|
+
[ -z "$ws" ] && [ "$caller" != "$here" ] && ws="$caller"
|
|
75
|
+
[ -n "$ws" ] || { echo "corré lampson desde la carpeta del proyecto, o --workspace /ruta (lampson --hub status lista los workspaces)" >&2; exit 1; }
|
|
76
|
+
[ -d "$ws" ] || { echo "el workspace no existe o no es un directorio: $ws" >&2; exit 1; }
|
|
77
|
+
ws="$(cd "$ws" && pwd -P)"
|
|
78
|
+
[ "$ws" = "$HOME" ] || [ "$ws" = "/" ] && { echo "'$ws' no es un proyecto" >&2; exit 1; }
|
|
79
|
+
r="$(cli ensure "$ws")"
|
|
80
|
+
slug="$(json "$r" slug)"; port="$(json "$r" port)"; url="$(json "$r" url)"; dir="$(json "$r" dir)"
|
|
81
|
+
if [ "$web" = 1 ]; then
|
|
82
|
+
echo "Lampson web · $url (workspace $slug)"
|
|
83
|
+
(xdg-open "$url" >/dev/null 2>&1 || open "$url" >/dev/null 2>&1 || true)
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
cd "$here/$dir"
|
|
87
|
+
export LAMPSON_WORKSPACE="$ws" LAMPSON_WS="$slug" LAMPSON_PORT="$port"
|
|
88
|
+
if [ -f "$here/.env" ]; then exec synsema run --env-file "$here/.env" chat.syn; else exec synsema run chat.syn; fi
|