lampson 0.1.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/.env.example +29 -0
- package/LICENSE +21 -0
- package/README.md +382 -0
- package/bin/lampson.js +81 -0
- package/chat.syn +799 -0
- package/lamps/example-hello/lamp.json +16 -0
- package/lamps/example-hello/lamp.syn +19 -0
- package/lampson.cmd +4 -0
- package/lampson.ps1 +88 -0
- package/lampson.sh +42 -0
- package/lib/agents.syn +471 -0
- package/lib/git.syn +58 -0
- package/lib/lamps.syn +386 -0
- package/lib/loop.syn +455 -0
- package/lib/lsp.syn +503 -0
- package/lib/mcp.syn +403 -0
- package/lib/permission.syn +154 -0
- package/lib/prompt.syn +75 -0
- package/lib/provider.syn +522 -0
- package/lib/session.syn +111 -0
- package/lib/settings.syn +70 -0
- package/lib/skills.syn +179 -0
- package/lib/tools/bash.syn +105 -0
- package/lib/tools/common.sh +49 -0
- package/lib/tools/common.syn +91 -0
- package/lib/tools/edit.syn +32 -0
- package/lib/tools/find.syn +35 -0
- package/lib/tools/grep.syn +31 -0
- package/lib/tools/img.ps1 +36 -0
- package/lib/tools/img.sh +22 -0
- package/lib/tools/ls.syn +18 -0
- package/lib/tools/memo.syn +148 -0
- package/lib/tools/proc.sh +42 -0
- package/lib/tools/proc.syn +314 -0
- package/lib/tools/process.syn +46 -0
- package/lib/tools/read.syn +25 -0
- package/lib/tools/skill.syn +14 -0
- package/lib/tools/todo.syn +97 -0
- package/lib/tools/write.syn +22 -0
- package/lib/tools.syn +198 -0
- package/lib/trace.syn +116 -0
- package/lib/tree.syn +59 -0
- package/lib/update.syn +57 -0
- package/package.json +40 -0
- package/public/fonts/plex-mono-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-400-latin.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-mono-600-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400-latin.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-400i-latin.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin-ext.woff2 +0 -0
- package/public/fonts/plex-serif-600-latin.woff2 +0 -0
- package/public/index.html +1268 -0
- package/public/vendor/xterm-addon-fit.js +2 -0
- package/public/vendor/xterm.css +218 -0
- package/public/vendor/xterm.js +2 -0
- package/skills/debugging/SKILL.md +33 -0
- package/skills/lampson/SKILL.md +117 -0
- package/skills/synsema/SKILL.md +75 -0
- package/web.syn +468 -0
package/lib/git.syn
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- lib/git.syn — estado de git del workspace (rama + archivos cambiados) para la UI
|
|
2
|
+
use "./tools/common.syn" as c
|
|
3
|
+
|
|
4
|
+
let G be "git"
|
|
5
|
+
|
|
6
|
+
task run_git(args)
|
|
7
|
+
try
|
|
8
|
+
let r be run(G, args, 15, {"cwd": c.ROOT})
|
|
9
|
+
when r["exit_code"] != 0
|
|
10
|
+
give nothing
|
|
11
|
+
give r["stdout"]
|
|
12
|
+
recover err
|
|
13
|
+
give nothing
|
|
14
|
+
|
|
15
|
+
-- {repo: bool, branch, changes: {path: "M"|"A"|"D"|"??"|"R"|…}, counts: {modified, added, deleted, untracked}}
|
|
16
|
+
export task status()
|
|
17
|
+
require exec("git")
|
|
18
|
+
let out be run_git(["status", "--porcelain=v1", "-b", "--untracked-files=all"])
|
|
19
|
+
when out == nothing
|
|
20
|
+
give {"repo": false, "branch": "", "changes": {}, "counts": {"modified": 0, "added": 0, "deleted": 0, "untracked": 0}}
|
|
21
|
+
let branch be ""
|
|
22
|
+
let changes be {}
|
|
23
|
+
let counts be {"modified": 0, "added": 0, "deleted": 0, "untracked": 0}
|
|
24
|
+
each line in split(replace_text(out, "\r", ""), "\n")
|
|
25
|
+
when starts_with(line, "## ")
|
|
26
|
+
set branch to split(split(slice(line, 3, length(line)), "...")[0], " ")[0]
|
|
27
|
+
otherwise when length(line) > 3
|
|
28
|
+
let xy be slice(line, 0, 2)
|
|
29
|
+
let path be replace_text(trim(slice(line, 3, length(line))), "\"", "")
|
|
30
|
+
when contains(path, " -> ")
|
|
31
|
+
set path to split(path, " -> ")[1]
|
|
32
|
+
let code be trim(xy)
|
|
33
|
+
when xy == "??"
|
|
34
|
+
set code to "??"
|
|
35
|
+
set counts["untracked"] to counts["untracked"] + 1
|
|
36
|
+
otherwise when contains(xy, "D")
|
|
37
|
+
set code to "D"
|
|
38
|
+
set counts["deleted"] to counts["deleted"] + 1
|
|
39
|
+
otherwise when contains(xy, "A")
|
|
40
|
+
set code to "A"
|
|
41
|
+
set counts["added"] to counts["added"] + 1
|
|
42
|
+
otherwise
|
|
43
|
+
set code to "M"
|
|
44
|
+
set counts["modified"] to counts["modified"] + 1
|
|
45
|
+
set changes[path] to code
|
|
46
|
+
give {"repo": true, "branch": branch, "changes": changes, "counts": counts}
|
|
47
|
+
|
|
48
|
+
-- una línea para el banner de terminal
|
|
49
|
+
export task summary()
|
|
50
|
+
require exec("git")
|
|
51
|
+
let s be status()
|
|
52
|
+
when not s["repo"]
|
|
53
|
+
give "sin git"
|
|
54
|
+
let k be s["counts"]
|
|
55
|
+
let n be k["modified"] + k["added"] + k["deleted"] + k["untracked"]
|
|
56
|
+
when n == 0
|
|
57
|
+
give "git " + s["branch"] + " · limpio"
|
|
58
|
+
give "git " + s["branch"] + " · " + text(k["modified"]) + " modificados · " + text(k["untracked"]) + " nuevos · " + text(k["deleted"]) + " borrados"
|
package/lib/lamps.syn
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
-- lib/lamps.syn — "lámparas": plugins de tools que el usuario (o el agente) escribe y el humano ENCIENDE
|
|
2
|
+
--
|
|
3
|
+
-- Qué es una lámpara: una carpeta con un manifiesto `lamp.json` y el código que implementa sus tools.
|
|
4
|
+
-- lamps/<nombre>/ GLOBAL (lampson se instala una vez → vale para todos los proyectos)
|
|
5
|
+
-- workspace/.lampson/lamps/<nombre>/ del PROYECTO (el agente puede crearlas con write: es su workspace)
|
|
6
|
+
-- lamp.json = {"name", "description", "kind": "syn" | "exec",
|
|
7
|
+
-- "entry": "lamp.syn" (kind=syn: un programa Synsema)
|
|
8
|
+
-- "command": "python lamp.py" (kind=exec: cualquier ejecutable — js, py, sh…)
|
|
9
|
+
-- "caps": "file.read=workspace/*" (kind=syn, opcional: techo EXTRA sobre stdout,time,env=LAMP_*)
|
|
10
|
+
-- "timeout": 60,
|
|
11
|
+
-- "tools": [{"name", "description", "parameters": {JSON Schema}, "readonly": false}]}
|
|
12
|
+
--
|
|
13
|
+
-- Cómo corre (patrón "safe runner" de la doc 22-sandbox): cada llamada es UN proceso hijo que termina.
|
|
14
|
+
-- kind=syn → `synsema run --cap-set stdout,time,env=LAMP_*[,caps] <entry>`: el techo lo fija el
|
|
15
|
+
-- manifiesto que el humano aprobó al encenderla; un `require` de más en el código de la
|
|
16
|
+
-- lámpara falla con "above the host ceiling" — no hay forma de escalar desde adentro.
|
|
17
|
+
-- kind=exec → el comando tal cual (sin techo de lenguaje: por eso encender es SIEMPRE humano).
|
|
18
|
+
-- La tool y sus args viajan por env: LAMP_TOOL, LAMP_ARGS (JSON), LAMP_DIR (carpeta de la lámpara,
|
|
19
|
+
-- relativa a la raíz de lampson = cwd del hijo si kind=syn; si kind=exec el cwd es el workspace),
|
|
20
|
+
-- LAMP_WORKSPACE (ruta real del proyecto). La lámpara imprime el resultado (texto o JSON) por stdout.
|
|
21
|
+
--
|
|
22
|
+
-- Encendido: `.lampson/lamps.json` {"enabled": {"<nombre>": true}}. APAGADA por defecto: descubrir una
|
|
23
|
+
-- carpeta nueva no la activa; la enciende el humano (UI: barra superior; terminal: /lamps on <nombre>) o el
|
|
24
|
+
-- agente con la tool `lamp` (action=enable → ask SIEMPRE, como skill install / mcp add). Crear = write;
|
|
25
|
+
-- encender = permiso. Sin `use` dinámico (Synsema no lo tiene, a propósito): nada de esto entra al proceso
|
|
26
|
+
-- de lampson, solo corre como hijo.
|
|
27
|
+
--
|
|
28
|
+
-- Las tools encendidas entran al catálogo como `lamp_<lámpara>_<tool>`; loop.execute las despacha acá
|
|
29
|
+
-- (registry las marca con "lamp", como "mcp"). permission.syn: ask por defecto, yolo permite, strict deniega;
|
|
30
|
+
-- las readonly también en plan/review/explore.
|
|
31
|
+
|
|
32
|
+
use "./tools/common.syn" as c
|
|
33
|
+
|
|
34
|
+
export let GLOBAL_DIR be "lamps"
|
|
35
|
+
export let PROJECT_DIR be "workspace/.lampson/lamps"
|
|
36
|
+
export let STATE_FILE be ".lampson/lamps.json"
|
|
37
|
+
let DEFAULT_TIMEOUT be 60
|
|
38
|
+
let BASE_CAPS be "stdout,time,env=LAMP_*"
|
|
39
|
+
|
|
40
|
+
-- nombre de lámpara: SIN "_" (el separador de lamp_<lámpara>_<tool>); las tools sí pueden llevarlo
|
|
41
|
+
task valid_name(name)
|
|
42
|
+
when name == nothing or name == ""
|
|
43
|
+
give false
|
|
44
|
+
give matches(text(name), "[a-zA-Z0-9-]{1,32}")
|
|
45
|
+
|
|
46
|
+
task valid_tool_name(name)
|
|
47
|
+
when name == nothing or name == ""
|
|
48
|
+
give false
|
|
49
|
+
give matches(text(name), "[a-zA-Z0-9_-]{1,40}")
|
|
50
|
+
|
|
51
|
+
-- lee y valida un manifiesto; devuelve {ok, lamp} o {ok: false, error}
|
|
52
|
+
task read_manifest(dir, name, scope)
|
|
53
|
+
let path be dir + "/" + name + "/lamp.json"
|
|
54
|
+
let doc be nothing
|
|
55
|
+
try
|
|
56
|
+
set doc to json_decode(read_file(path))
|
|
57
|
+
recover err
|
|
58
|
+
give {"ok": false, "name": name, "scope": scope, "error": "no lamp.json: " + text(err)}
|
|
59
|
+
let kind be when contains(doc, "kind") then lower(text(doc["kind"])) otherwise "syn"
|
|
60
|
+
when kind != "syn" and kind != "exec"
|
|
61
|
+
give {"ok": false, "name": name, "scope": scope, "error": "kind must be syn or exec"}
|
|
62
|
+
when kind == "syn" and not contains(doc, "entry")
|
|
63
|
+
give {"ok": false, "name": name, "scope": scope, "error": "kind=syn needs \"entry\" (the .syn file)"}
|
|
64
|
+
when kind == "exec" and not contains(doc, "command")
|
|
65
|
+
give {"ok": false, "name": name, "scope": scope, "error": "kind=exec needs \"command\""}
|
|
66
|
+
when not contains(doc, "tools") or length(doc["tools"]) == 0
|
|
67
|
+
give {"ok": false, "name": name, "scope": scope, "error": "no tools declared"}
|
|
68
|
+
let tools be []
|
|
69
|
+
each t in doc["tools"]
|
|
70
|
+
when not contains(t, "name") or not valid_tool_name(t["name"])
|
|
71
|
+
give {"ok": false, "name": name, "scope": scope, "error": "a tool has no valid name"}
|
|
72
|
+
set tools to append(tools, {"name": text(t["name"]), "description": when contains(t, "description") then text(t["description"]) otherwise "", "parameters": when contains(t, "parameters") then t["parameters"] otherwise {"type": "object", "properties": {}}, "readonly": contains(t, "readonly") and t["readonly"] == true})
|
|
73
|
+
give {"ok": true, "name": name, "scope": scope, "lamp": {"name": name, "scope": scope, "dir": dir + "/" + name, "kind": kind, "description": when contains(doc, "description") then text(doc["description"]) otherwise "", "entry": when contains(doc, "entry") then text(doc["entry"]) otherwise "", "command": when contains(doc, "command") then text(doc["command"]) otherwise "", "caps": when contains(doc, "caps") then text(doc["caps"]) otherwise "", "timeout": when contains(doc, "timeout") then floor(number(doc["timeout"])) otherwise DEFAULT_TIMEOUT, "tools": tools}}
|
|
74
|
+
|
|
75
|
+
task scan(dir, scope)
|
|
76
|
+
let out be []
|
|
77
|
+
try
|
|
78
|
+
each e in list_dir(dir)
|
|
79
|
+
when e["is_dir"] and valid_name(e["name"])
|
|
80
|
+
set out to append(out, read_manifest(dir, e["name"], scope))
|
|
81
|
+
recover err
|
|
82
|
+
give out
|
|
83
|
+
give out
|
|
84
|
+
|
|
85
|
+
task load_state()
|
|
86
|
+
try
|
|
87
|
+
let doc be json_decode(read_file(STATE_FILE))
|
|
88
|
+
give when contains(doc, "enabled") then doc["enabled"] otherwise {}
|
|
89
|
+
recover err
|
|
90
|
+
give {}
|
|
91
|
+
|
|
92
|
+
-- todas las lámparas descubiertas (proyecto pisa a global por nombre), con enabled y error.
|
|
93
|
+
-- LAMPSON_LAMPS_DIR = una carpeta extra (tests).
|
|
94
|
+
export task all()
|
|
95
|
+
require file(".lampson")
|
|
96
|
+
require file(".lampson/*")
|
|
97
|
+
require file.read("lamps")
|
|
98
|
+
require file.read("lamps/*")
|
|
99
|
+
require file("workspace")
|
|
100
|
+
require file("workspace/*")
|
|
101
|
+
require env("LAMPSON_*")
|
|
102
|
+
let by_name be {}
|
|
103
|
+
each r in scan(GLOBAL_DIR, "global")
|
|
104
|
+
set by_name[r["name"]] to r
|
|
105
|
+
each r in scan(PROJECT_DIR, "project")
|
|
106
|
+
set by_name[r["name"]] to r
|
|
107
|
+
when env("LAMPSON_LAMPS_DIR", "") != ""
|
|
108
|
+
each r in scan(env("LAMPSON_LAMPS_DIR", ""), "extra")
|
|
109
|
+
set by_name[r["name"]] to r
|
|
110
|
+
let st be load_state()
|
|
111
|
+
let out be []
|
|
112
|
+
each n in sort_by(keys(by_name), (x) => x)
|
|
113
|
+
let r be by_name[n]
|
|
114
|
+
let on be contains(st, n) and st[n] == true
|
|
115
|
+
when r["ok"]
|
|
116
|
+
let l be r["lamp"]
|
|
117
|
+
set l["enabled"] to on
|
|
118
|
+
set l["error"] to nothing
|
|
119
|
+
set out to append(out, l)
|
|
120
|
+
otherwise
|
|
121
|
+
set out to append(out, {"name": n, "scope": r["scope"], "dir": "", "kind": "?", "description": "", "entry": "", "command": "", "caps": "", "timeout": 0, "tools": [], "enabled": false, "error": r["error"]})
|
|
122
|
+
give out
|
|
123
|
+
|
|
124
|
+
export task enabled()
|
|
125
|
+
require file(".lampson")
|
|
126
|
+
require file(".lampson/*")
|
|
127
|
+
require file.read("lamps")
|
|
128
|
+
require file.read("lamps/*")
|
|
129
|
+
require file("workspace")
|
|
130
|
+
require file("workspace/*")
|
|
131
|
+
require env("LAMPSON_*")
|
|
132
|
+
give where(all(), (l) => l["enabled"] and l["error"] == nothing)
|
|
133
|
+
|
|
134
|
+
-- encender/apagar (el humano, o el agente tras aprobación). Encender una lámpara rota es error.
|
|
135
|
+
export task set_enabled(name, on)
|
|
136
|
+
require file(".lampson")
|
|
137
|
+
require file(".lampson/*")
|
|
138
|
+
require file.read("lamps")
|
|
139
|
+
require file.read("lamps/*")
|
|
140
|
+
require file("workspace")
|
|
141
|
+
require file("workspace/*")
|
|
142
|
+
require env("LAMPSON_*")
|
|
143
|
+
when not valid_name(name)
|
|
144
|
+
raise("invalid lamp name")
|
|
145
|
+
let found be where(all(), (l) => l["name"] == name)
|
|
146
|
+
when length(found) == 0
|
|
147
|
+
raise("no lamp named '" + name + "' (global: " + GLOBAL_DIR + "/<name>/lamp.json · project: " + PROJECT_DIR + "/<name>/lamp.json)")
|
|
148
|
+
when on and found[0]["error"] != nothing
|
|
149
|
+
raise("lamp '" + name + "' is broken: " + text(found[0]["error"]))
|
|
150
|
+
let st be load_state()
|
|
151
|
+
set st[name] to on == true
|
|
152
|
+
write_file(STATE_FILE, json_encode({"enabled": st}))
|
|
153
|
+
let l be found[0]
|
|
154
|
+
when on
|
|
155
|
+
give "lamp '" + name + "' ON (" + l["kind"] + ", " + text(length(l["tools"])) + " tools" + (when l["kind"] == "syn" then ", ceiling: " + ceiling_of(l) otherwise ", NO capability ceiling: exec") + ") — its tools are in the catalog from the next turn as lamp_" + name + "_<tool>"
|
|
156
|
+
give "lamp '" + name + "' OFF"
|
|
157
|
+
|
|
158
|
+
task ceiling_of(l)
|
|
159
|
+
give when l["caps"] == "" then BASE_CAPS otherwise BASE_CAPS + "," + l["caps"]
|
|
160
|
+
|
|
161
|
+
-- ---------- catálogo ----------
|
|
162
|
+
export task tool_name(lamp, tool)
|
|
163
|
+
give "lamp_" + lamp + "_" + tool
|
|
164
|
+
|
|
165
|
+
task split_name(full)
|
|
166
|
+
let segs be split(slice(full, 5, length(full)), "_")
|
|
167
|
+
when length(segs) < 2
|
|
168
|
+
raise("bad lamp tool name '" + full + "'")
|
|
169
|
+
give {"lamp": segs[0], "tool": join(slice(segs, 1, length(segs)), "_")}
|
|
170
|
+
|
|
171
|
+
export task catalog(readonly_only)
|
|
172
|
+
require file(".lampson")
|
|
173
|
+
require file(".lampson/*")
|
|
174
|
+
require file.read("lamps")
|
|
175
|
+
require file.read("lamps/*")
|
|
176
|
+
require file("workspace")
|
|
177
|
+
require file("workspace/*")
|
|
178
|
+
require env("LAMPSON_*")
|
|
179
|
+
let out be []
|
|
180
|
+
each l in enabled()
|
|
181
|
+
each t in l["tools"]
|
|
182
|
+
when not readonly_only or t["readonly"]
|
|
183
|
+
set out to append(out, {"name": tool_name(l["name"], t["name"]), "description": "[lamp " + l["name"] + "] " + t["description"], "parameters": t["parameters"]})
|
|
184
|
+
give out
|
|
185
|
+
|
|
186
|
+
export task names(readonly_only)
|
|
187
|
+
require file(".lampson")
|
|
188
|
+
require file(".lampson/*")
|
|
189
|
+
require file.read("lamps")
|
|
190
|
+
require file.read("lamps/*")
|
|
191
|
+
require file("workspace")
|
|
192
|
+
require file("workspace/*")
|
|
193
|
+
require env("LAMPSON_*")
|
|
194
|
+
give apply(catalog(readonly_only), (s) => s["name"])
|
|
195
|
+
|
|
196
|
+
-- ---------- llamada: un proceso hijo por invocación ----------
|
|
197
|
+
task run_lamp(exe, argv, cwd, envs, t)
|
|
198
|
+
let p be proc_spawn(exe, argv, {"cwd": cwd, "env": envs, "stderr": "merge", "on_full": "drop_oldest"})
|
|
199
|
+
proc_close_stdin(p)
|
|
200
|
+
let deadline be now() + t
|
|
201
|
+
let lines be []
|
|
202
|
+
let code be nothing
|
|
203
|
+
let timed_out be false
|
|
204
|
+
while code == nothing and not timed_out
|
|
205
|
+
let left be deadline - now()
|
|
206
|
+
when left <= 0
|
|
207
|
+
set timed_out to true
|
|
208
|
+
otherwise
|
|
209
|
+
let ev be proc_recv(p, when left < 0.2 then 0.2 otherwise left)
|
|
210
|
+
when ev == nothing
|
|
211
|
+
set timed_out to now() >= deadline
|
|
212
|
+
otherwise when ev["type"] == "exit"
|
|
213
|
+
set code to ev["data"]["exit_code"]
|
|
214
|
+
otherwise
|
|
215
|
+
set lines to append(lines, ev["data"])
|
|
216
|
+
proc_close(p)
|
|
217
|
+
let out be join(lines, "\n")
|
|
218
|
+
when timed_out
|
|
219
|
+
give "ERROR: lamp timed out after " + text(t) + "s (killed). Output so far:\n" + out
|
|
220
|
+
when code != 0
|
|
221
|
+
give "ERROR: lamp exited with code " + text(code) + "\n" + out
|
|
222
|
+
give when trim(out) == "" then "(no output)" otherwise out
|
|
223
|
+
|
|
224
|
+
-- separa "python lamp.py --x" en exe + args (comillas simples/dobles respetadas)
|
|
225
|
+
export task split_command(line)
|
|
226
|
+
let toks be []
|
|
227
|
+
let cur be ""
|
|
228
|
+
let q be ""
|
|
229
|
+
let has be false
|
|
230
|
+
let i be 0
|
|
231
|
+
while i < length(line)
|
|
232
|
+
let ch be slice(line, i, i + 1)
|
|
233
|
+
set i to i + 1
|
|
234
|
+
when q != ""
|
|
235
|
+
when ch == q
|
|
236
|
+
set q to ""
|
|
237
|
+
otherwise
|
|
238
|
+
set cur to cur + ch
|
|
239
|
+
otherwise when ch == "\"" or ch == "'"
|
|
240
|
+
set q to ch
|
|
241
|
+
set has to true
|
|
242
|
+
otherwise when ch == " " or ch == "\t"
|
|
243
|
+
when cur != "" or has
|
|
244
|
+
set toks to append(toks, cur)
|
|
245
|
+
set cur to ""
|
|
246
|
+
set has to false
|
|
247
|
+
otherwise
|
|
248
|
+
set cur to cur + ch
|
|
249
|
+
when cur != "" or has
|
|
250
|
+
set toks to append(toks, cur)
|
|
251
|
+
give toks
|
|
252
|
+
|
|
253
|
+
export task call(full_name, args)
|
|
254
|
+
require exec
|
|
255
|
+
require time
|
|
256
|
+
require file(".lampson")
|
|
257
|
+
require file(".lampson/*")
|
|
258
|
+
require file.read("lamps")
|
|
259
|
+
require file.read("lamps/*")
|
|
260
|
+
require file("workspace")
|
|
261
|
+
require file("workspace/*")
|
|
262
|
+
require env("LAMPSON_*")
|
|
263
|
+
let parts be split_name(full_name)
|
|
264
|
+
let found be where(enabled(), (l) => l["name"] == parts["lamp"])
|
|
265
|
+
when length(found) == 0
|
|
266
|
+
give "ERROR: lamp '" + parts["lamp"] + "' is not enabled (the user turns lamps on)"
|
|
267
|
+
let l be found[0]
|
|
268
|
+
let tl be where(l["tools"], (t) => t["name"] == parts["tool"])
|
|
269
|
+
when length(tl) == 0
|
|
270
|
+
give "ERROR: lamp '" + l["name"] + "' has no tool '" + parts["tool"] + "'"
|
|
271
|
+
-- LAMP_DIR relativa a la raíz de lampson (cwd del hijo para kind=syn); LAMP_WORKSPACE = la ruta real del
|
|
272
|
+
-- proyecto (lampson.ps1 la exporta; sin ella, la junction)
|
|
273
|
+
let envs be {"LAMP_TOOL": parts["tool"], "LAMP_ARGS": json_encode(when args == nothing then {} otherwise args), "LAMP_DIR": l["dir"], "LAMP_WORKSPACE": env("LAMPSON_WORKSPACE", c.ROOT)}
|
|
274
|
+
when l["kind"] == "syn"
|
|
275
|
+
-- techo del manifiesto: el código de la lámpara no puede pedir más de lo que el humano aprobó
|
|
276
|
+
give c.truncate(run_lamp(env("LAMPSON_SYNSEMA", "synsema"), ["run", "--cap-set", ceiling_of(l), l["dir"] + "/" + l["entry"]], ".", envs, l["timeout"]), c.MAX_OUTPUT)
|
|
277
|
+
let argv be split_command(l["command"])
|
|
278
|
+
when length(argv) == 0
|
|
279
|
+
give "ERROR: lamp '" + l["name"] + "' has an empty command"
|
|
280
|
+
-- el comando corre desde la RAÍZ DEL WORKSPACE (para que "python lamp.py" vea el proyecto), así que un
|
|
281
|
+
-- token que nombra un archivo de la carpeta de la lámpara se resuelve solo (2026-08-28: "python lamp.py"
|
|
282
|
+
-- salía con code 2 porque lamp.py no estaba en el cwd)
|
|
283
|
+
let rel_dir be when starts_with(l["dir"], c.ROOT + "/") then slice(l["dir"], length(c.ROOT) + 1, length(l["dir"])) otherwise "../" + l["dir"]
|
|
284
|
+
let resolved be []
|
|
285
|
+
each tok in argv
|
|
286
|
+
when not contains(tok, "/") and not contains(tok, "\\") and file_exists(l["dir"] + "/" + tok)
|
|
287
|
+
set resolved to append(resolved, rel_dir + "/" + tok)
|
|
288
|
+
otherwise
|
|
289
|
+
set resolved to append(resolved, tok)
|
|
290
|
+
give c.truncate(run_lamp(resolved[0], slice(resolved, 1, length(resolved)), c.ROOT, envs, l["timeout"]), c.MAX_OUTPUT)
|
|
291
|
+
|
|
292
|
+
-- crear una lámpara DEL PROYECTO (workspace/.lampson/lamps/<name>/): escribe lamp.json + el código, revalida el
|
|
293
|
+
-- manifiesto y, si es syn, corre `synsema check` sobre el entry. Como cordis_define en dsh: define y valida,
|
|
294
|
+
-- NO ejecuta ni enciende — encender sigue siendo del humano (set_enabled tras ask).
|
|
295
|
+
-- `files` = {"lamp.syn": "…", "helper.py": "…"}: nombres simples (sin / ni ..) dentro de la carpeta.
|
|
296
|
+
export task create(name, manifest, files)
|
|
297
|
+
require exec
|
|
298
|
+
require time
|
|
299
|
+
require file(".lampson")
|
|
300
|
+
require file(".lampson/*")
|
|
301
|
+
require file.read("lamps")
|
|
302
|
+
require file.read("lamps/*")
|
|
303
|
+
require file("workspace")
|
|
304
|
+
require file("workspace/*")
|
|
305
|
+
require env("LAMPSON_*")
|
|
306
|
+
when not valid_name(name)
|
|
307
|
+
raise("invalid lamp name '" + text(name) + "': letters, digits and - only (no _), max 32")
|
|
308
|
+
when manifest == nothing or not contains(manifest, "tools")
|
|
309
|
+
raise("manifest needs at least \"tools\" (and \"entry\" for kind=syn or \"command\" for kind=exec)")
|
|
310
|
+
let dir be PROJECT_DIR + "/" + name
|
|
311
|
+
let m be manifest
|
|
312
|
+
set m["name"] to name
|
|
313
|
+
when not contains(m, "kind")
|
|
314
|
+
set m["kind"] to "syn"
|
|
315
|
+
write_file(dir + "/lamp.json", json_encode(m))
|
|
316
|
+
let written be ["lamp.json"]
|
|
317
|
+
when files != nothing
|
|
318
|
+
each fname in keys(files)
|
|
319
|
+
when not matches(fname, "[A-Za-z0-9_.-]{1,64}") or starts_with(fname, ".")
|
|
320
|
+
raise("bad file name '" + fname + "' (simple names inside the lamp folder only)")
|
|
321
|
+
write_file(dir + "/" + fname, text(files[fname]))
|
|
322
|
+
set written to append(written, fname)
|
|
323
|
+
let found be where(all(), (l) => l["name"] == name)
|
|
324
|
+
when length(found) == 0
|
|
325
|
+
raise("lamp written but not discovered (?)")
|
|
326
|
+
let l be found[0]
|
|
327
|
+
when l["error"] != nothing
|
|
328
|
+
give "lamp '" + name + "' written to " + dir + " but its manifest is INVALID: " + text(l["error"]) + " — fix it with edit and call lamp(action=list) to re-check"
|
|
329
|
+
let note be ""
|
|
330
|
+
when l["kind"] == "syn"
|
|
331
|
+
let chk be run_lamp(env("LAMPSON_SYNSEMA", "synsema"), ["check", l["dir"] + "/" + l["entry"]], ".", {}, 30)
|
|
332
|
+
when starts_with(chk, "ERROR")
|
|
333
|
+
give "lamp '" + name + "' written to " + dir + " but `synsema check " + l["entry"] + "` FAILED:\n" + chk + "\nFix the code with edit; it stays off until it checks and the user turns it on."
|
|
334
|
+
set note to " · synsema check OK · ceiling when on: " + ceiling_of(l)
|
|
335
|
+
when l["kind"] == "exec"
|
|
336
|
+
set note to " · exec (no capability ceiling: the user will see that when approving)"
|
|
337
|
+
give "lamp '" + name + "' created (project, " + l["kind"] + ", " + text(length(l["tools"])) + " tools: " + join(apply(l["tools"], (t) => t["name"]), ", ") + ") in " + dir + " [" + join(written, ", ") + "]" + note + (when l["kind"] == "exec" then ". The command runs from the workspace root; a file of the lamp folder named in the command is resolved automatically" otherwise "") + ". It is OFF until the user turns it on: offer to do it (lamp action=enable asks them), or point them to the lamp switch of their UI. Its tools will be lamp_" + name + "_<tool>."
|
|
338
|
+
|
|
339
|
+
-- eliminar una lámpara DEL PROYECTO: borra su carpeta (no hay delete_file en el runtime → rm -rf por el shell
|
|
340
|
+
-- de bash.syn) y la saca del estado. Las globales (lamps/) se borran a mano: son de la instalación, no del repo.
|
|
341
|
+
export task remove(name)
|
|
342
|
+
require exec
|
|
343
|
+
require time
|
|
344
|
+
require env("LAMPSON_*")
|
|
345
|
+
require env("OS")
|
|
346
|
+
require file(".lampson")
|
|
347
|
+
require file(".lampson/*")
|
|
348
|
+
require file.read("lamps")
|
|
349
|
+
require file.read("lamps/*")
|
|
350
|
+
require file("workspace")
|
|
351
|
+
require file("workspace/*")
|
|
352
|
+
when not valid_name(name)
|
|
353
|
+
raise("invalid lamp name")
|
|
354
|
+
let found be where(all(), (l) => l["name"] == name)
|
|
355
|
+
when length(found) == 0
|
|
356
|
+
raise("no lamp named '" + name + "'")
|
|
357
|
+
let l be found[0]
|
|
358
|
+
when l["scope"] != "project"
|
|
359
|
+
raise("'" + name + "' is a " + l["scope"] + " lamp (" + GLOBAL_DIR + "/" + name + "): delete that folder by hand")
|
|
360
|
+
let dir be PROJECT_DIR + "/" + name
|
|
361
|
+
let is_win be env("OS", "") == "Windows_NT"
|
|
362
|
+
let sh be env("LAMPSON_SHELL", when is_win then "C:\\Program Files\\Git\\bin\\bash.exe" otherwise "bash")
|
|
363
|
+
let r be run(sh, ["-c", "rm -rf '" + dir + "'"], 30, {"cwd": "."})
|
|
364
|
+
when file_exists(dir + "/lamp.json")
|
|
365
|
+
raise("could not delete " + dir + ": " + text(r["stderr"]))
|
|
366
|
+
let st be load_state()
|
|
367
|
+
let clean be {}
|
|
368
|
+
each k in keys(st)
|
|
369
|
+
when k != name
|
|
370
|
+
set clean[k] to st[k]
|
|
371
|
+
write_file(STATE_FILE, json_encode({"enabled": clean}))
|
|
372
|
+
give "lamp '" + name + "' deleted (" + dir + ")"
|
|
373
|
+
|
|
374
|
+
-- resumen para UI/terminal/tool
|
|
375
|
+
export task summary()
|
|
376
|
+
require file(".lampson")
|
|
377
|
+
require file(".lampson/*")
|
|
378
|
+
require file.read("lamps")
|
|
379
|
+
require file.read("lamps/*")
|
|
380
|
+
require file("workspace")
|
|
381
|
+
require file("workspace/*")
|
|
382
|
+
require env("LAMPSON_*")
|
|
383
|
+
let out be []
|
|
384
|
+
each l in all()
|
|
385
|
+
set out to append(out, {"name": l["name"], "scope": l["scope"], "kind": l["kind"], "description": l["description"], "enabled": l["enabled"], "error": l["error"], "caps": when l["kind"] == "syn" and l["error"] == nothing then ceiling_of(l) otherwise "", "command": when l["kind"] == "exec" then l["command"] otherwise "", "tools": apply(l["tools"], (t) => t["name"]), "tool_specs": l["tools"], "dir": l["dir"]})
|
|
386
|
+
give out
|