syntropy 0.38.1 → 0.40.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +2 -0
- data/TODO.md +55 -144
- data/cmd/console.rb +2 -4
- data/cmd/new/template/config/production.rb +2 -1
- data/cmd/new.rb +1 -1
- data/cmd/serve.rb +29 -4
- data/cmd/test.rb +12 -4
- data/examples/agent/Dockerfile +25 -0
- data/examples/agent/Gemfile +3 -0
- data/examples/agent/README.md +40 -0
- data/examples/agent/TODO.md +5 -0
- data/examples/agent/app/_layout/default.rb +15 -0
- data/examples/agent/app/_lib/storage.rb +13 -0
- data/examples/agent/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/agent/app/assets/agent.js +63 -0
- data/examples/agent/app/assets/minigfm.js +207 -0
- data/examples/agent/app/assets/model.js +45 -0
- data/examples/agent/app/assets/style.css +25 -0
- data/examples/agent/app/assets/tools.js +74 -0
- data/examples/agent/app/assets/ui.js +50 -0
- data/examples/agent/app/index.rb +19 -0
- data/examples/agent/app/test.rb +7 -0
- data/examples/agent/config/Caddyfile +5 -0
- data/examples/agent/config/development.rb +5 -0
- data/examples/agent/config/production.rb +5 -0
- data/examples/agent/config/test.rb +5 -0
- data/examples/agent/docker-compose.yml +71 -0
- data/examples/agent/response_example.json +108 -0
- data/examples/agent/test/test_app.rb +14 -0
- data/examples/blog/app/foo.rb +1 -0
- data/examples/blog/app/posts/[id]/edit.rb +3 -3
- data/examples/blog/app/posts/[id]/index.rb +8 -10
- data/examples/blog/app/posts/index.rb +6 -8
- data/examples/blog/app/posts/new.rb +3 -3
- data/examples/blog/config/production.rb +2 -1
- data/lib/syntropy/app.rb +12 -60
- data/lib/syntropy/applets/builtin/auto_refresh/watch.js +1 -1
- data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +7 -0
- data/lib/syntropy/controller_extensions.rb +1 -1
- data/lib/syntropy/dev_mode.rb +8 -3
- data/lib/syntropy/http/client.rb +17 -1
- data/lib/syntropy/http/client_connection.rb +16 -0
- data/lib/syntropy/http/io_extensions.rb +32 -4
- data/lib/syntropy/http/server.rb +55 -24
- data/lib/syntropy/http/server_connection.rb +72 -2
- data/lib/syntropy/logger.rb +8 -0
- data/lib/syntropy/markdown.rb +139 -11
- data/lib/syntropy/module_loader.rb +110 -68
- data/lib/syntropy/request/request_info.rb +8 -0
- data/lib/syntropy/request/response.rb +76 -6
- data/lib/syntropy/request/validation.rb +6 -0
- data/lib/syntropy/storage/connection_pool.rb +34 -0
- data/lib/syntropy/test.rb +14 -5
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +9 -2
- data/syntropy.gemspec +4 -4
- data/test/fixtures/app/_layout/default.rb +1 -1
- data/test/fixtures/app/_layout/kuku.rb +8 -0
- data/test/fixtures/app/_lib/circular/a.rb +2 -0
- data/test/fixtures/app/_lib/circular/b.rb +2 -0
- data/test/fixtures/app/_lib/circular/c.rb +2 -0
- data/test/fixtures/app/mod/concurrent.rb +9 -0
- data/test/test_app.rb +1 -1
- data/test/test_caching.rb +3 -3
- data/test/test_markdown.rb +268 -0
- data/test/test_module_loader.rb +32 -4
- data/test/test_request.rb +7 -0
- data/test/test_response.rb +30 -0
- data/test/test_schema.rb +1 -0
- metadata +40 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96429a90f9e886ffafc25c1e4c88a46157fb79751d21fdbed7af10252ff5d16e
|
|
4
|
+
data.tar.gz: 040ebeaf507ada9e986847b7630739f9451c88d5384c736fd632f55d4928bff2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f75c54e095c15c1742786f712062142619a0e7c23ee22295f3dc93825d476027ab26a962054ad3cf868f8d89045af2c3c29733b648d9042ba2594595783ab234
|
|
7
|
+
data.tar.gz: d60a7f2d439b615db027c9f678ab4fba2aae38deb1c68b9d9e5b29bdbe22d282f7d14e1111458c97e3b7ad004eef1a428714376d40bd1370fa668e08765fe739
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
# 0.40.0 2026-08-24
|
|
2
|
+
|
|
3
|
+
- Update UringMachine
|
|
4
|
+
|
|
5
|
+
# 0.39.0 2026-06-21
|
|
6
|
+
|
|
7
|
+
- Add support for embedded Papercraft snippets in Markdown files
|
|
8
|
+
- Reimplement Markdown rendering
|
|
9
|
+
- Increase cache-control max-age to one week
|
|
10
|
+
- Add support for relative redirects, add `Request#rel`
|
|
11
|
+
- Add support for concurrent loading, add mutex lock to module loader
|
|
12
|
+
- Fix `Syntropy.load_config`
|
|
13
|
+
- Read module source file using UringMachine
|
|
14
|
+
- Raise on circular module dependency
|
|
15
|
+
- Streamline CLI options across commands
|
|
16
|
+
|
|
1
17
|
# 0.38.1 2026-06-13
|
|
2
18
|
|
|
3
19
|
- Fix builtin app, controller extensions
|
data/README.md
CHANGED
|
@@ -52,6 +52,8 @@ Syntropy is based on:
|
|
|
52
52
|
- [Extralite](https://github.com/digital-fabric/extralite) a fast and innovative
|
|
53
53
|
SQLite wrapper for Ruby.
|
|
54
54
|
|
|
55
|
+
Syntropy is 100% brain-coded.
|
|
56
|
+
|
|
55
57
|
## Examples
|
|
56
58
|
|
|
57
59
|
To get a taste of some of Syntropy's capabilities, you can run the included
|
data/TODO.md
CHANGED
|
@@ -1,8 +1,54 @@
|
|
|
1
1
|
## Immediate
|
|
2
2
|
|
|
3
|
-
- [ ]
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
- [ ] Nicer logging in development mode:
|
|
4
|
+
|
|
5
|
+
Nice terminal formatting with colors:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
sharon@nf1:~/tmp/capatest2$ npx serve dist
|
|
9
|
+
|
|
10
|
+
┌───────────────────────────────────────────┐
|
|
11
|
+
│ │
|
|
12
|
+
│ Serving! │
|
|
13
|
+
│ │
|
|
14
|
+
│ - Local: http://localhost:3000 │
|
|
15
|
+
│ - Network: http://192.168.0.106:3000 │
|
|
16
|
+
│ │
|
|
17
|
+
│ Copied local address to clipboard! │
|
|
18
|
+
│ │
|
|
19
|
+
└───────────────────────────────────────────┘
|
|
20
|
+
|
|
21
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /
|
|
22
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 42 ms
|
|
23
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/index-edfizW3i.js
|
|
24
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/index-PVjztr5e.css
|
|
25
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 3 ms
|
|
26
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 12 ms
|
|
27
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-Sh0ICmPV-D227nRX-.js
|
|
28
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 3 ms
|
|
29
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-C4t5ymfq-4gquBJ2r.js
|
|
30
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 6 ms
|
|
31
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-D6Ynv7Xh-CRAF8up3.js
|
|
32
|
+
HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 4 ms
|
|
33
|
+
^C
|
|
34
|
+
INFO Gracefully shutting down. Please wait...
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- [ ] Ability to load modules from builtin applet
|
|
38
|
+
- [ ] Can we mount them on the app's module loader?
|
|
39
|
+
|
|
40
|
+
Why we need that? The use case is making use of a default pub/sub instance
|
|
41
|
+
|
|
42
|
+
- [ ] Pub/sub
|
|
43
|
+
- [ ] Ruby side
|
|
44
|
+
- [ ] JS side
|
|
45
|
+
- [ ] Reimplement `auto_refresh` using a *default* event bus provided by
|
|
46
|
+
Syntropy
|
|
47
|
+
|
|
48
|
+
- [ ] An alternative to the pub/sub design - add streaming responses to the
|
|
49
|
+
JSON/jS API (using SSE).
|
|
50
|
+
|
|
51
|
+
## Collections
|
|
6
52
|
|
|
7
53
|
- [ ] Collection - treat directories and files as collections of data.
|
|
8
54
|
|
|
@@ -34,7 +80,6 @@
|
|
|
34
80
|
|
|
35
81
|
- [ ] Improve serving of static files:
|
|
36
82
|
- [ ] support for compression
|
|
37
|
-
- [ ] support for caching headers
|
|
38
83
|
- [ ] add `Request#render_static_file(route, fn)
|
|
39
84
|
|
|
40
85
|
## Missing for a first public release
|
|
@@ -43,21 +88,18 @@
|
|
|
43
88
|
- [ ] Website
|
|
44
89
|
- [v] Frontend part of JSON API
|
|
45
90
|
- [v] Auto-refresh page when file changes
|
|
46
|
-
- [ ] SQLite database capabilities
|
|
47
|
-
- [ ] Stores
|
|
48
|
-
- [ ] KV store (with TTL)
|
|
49
91
|
- [v] Examples
|
|
50
92
|
- [v] Reactive app - counter or some other simple app showing interaction with
|
|
51
93
|
server
|
|
52
|
-
- [
|
|
94
|
+
- [v] blog
|
|
53
95
|
|
|
54
96
|
## Testing facilities
|
|
55
97
|
|
|
56
98
|
- What do we need to test?
|
|
57
|
-
- Routes
|
|
58
|
-
- Route responses
|
|
59
|
-
- Changes to state / DB
|
|
60
|
-
- Rendered HTML - presence of certain markup / elements / text
|
|
99
|
+
- [v] Routes
|
|
100
|
+
- [v] Route responses
|
|
101
|
+
- [v] Changes to state / DB
|
|
102
|
+
- [ ] Rendered HTML - presence of certain markup / elements / text
|
|
61
103
|
|
|
62
104
|
## Support for applets
|
|
63
105
|
|
|
@@ -93,138 +135,7 @@ class Syntropy::Admin < Syntropy::App
|
|
|
93
135
|
end
|
|
94
136
|
```
|
|
95
137
|
|
|
96
|
-
##
|
|
97
|
-
|
|
98
|
-
We need a way to inject cookies into the response. This probably should be done
|
|
99
|
-
in the TP2 code:
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
@@default_set_cookie_attr = 'HttpOnly'
|
|
103
|
-
def self.default_set_cookie_attr=(v)
|
|
104
|
-
@@default_set_cookie_attr = v
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def set_cookie(key, value, attr = @@default_set_cookie_attr)
|
|
108
|
-
@buffered_headers ||= +''
|
|
109
|
-
@buffered_headers << format(
|
|
110
|
-
"Set-Cookie: %<key>s=%<value>s; %<attr>s\n",
|
|
111
|
-
key:, value:, attr:
|
|
112
|
-
)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def set_headers(headers)
|
|
116
|
-
@buffered_headers ||= +''
|
|
117
|
-
@buffered_headers << format_headers(headers)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
...
|
|
121
|
-
|
|
122
|
-
req.set_cookie('at', 'foobar', 'SameSite=none; Secure; HttpOnly')
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
## Middleware
|
|
126
|
-
|
|
127
|
-
Some standard middleware:
|
|
128
|
-
|
|
129
|
-
- request rewriter
|
|
130
|
-
- logger
|
|
131
|
-
- auth
|
|
132
|
-
- selector + terminator
|
|
133
|
-
|
|
134
|
-
```Ruby
|
|
135
|
-
# For the chainable DSL shown below, we need to create a custom class:
|
|
136
|
-
class Syntropy::Middleware::Selector
|
|
137
|
-
def initialize(select_proc, terminator_proc = nil)
|
|
138
|
-
@select_proc = select_proc
|
|
139
|
-
@terminator_proc = terminator_proc
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def to_proc
|
|
143
|
-
->(req, proc) {
|
|
144
|
-
@select_proc.(req) ? @terminator_proc.(req) : proc(req)
|
|
145
|
-
}
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def terminate(&proc)
|
|
149
|
-
@terminator_proc = proc
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
```Ruby
|
|
155
|
-
# a _site.rb file can be used to wrap a whole app
|
|
156
|
-
# site/_site.rb
|
|
157
|
-
|
|
158
|
-
# this means we route according to the host header, with each
|
|
159
|
-
export Syntropy.route_by_host
|
|
160
|
-
|
|
161
|
-
# we can also rewrite requests:
|
|
162
|
-
rewriter = Syntropy
|
|
163
|
-
.select { it.host =~ /^tolkora\.(org|com)$/ }
|
|
164
|
-
.terminate { it.redirect_permanent('https://tolkora.net') }
|
|
165
|
-
|
|
166
|
-
# This is actuall a pretty interesting DSL design:
|
|
167
|
-
# a chain of operations that compose functions. So, we can select a
|
|
168
|
-
export rewriter.wrap(default_app)
|
|
169
|
-
|
|
170
|
-
# composing
|
|
171
|
-
export rewriter.wrap(Syntropy.some_custom_app.wrap(app))
|
|
172
|
-
|
|
173
|
-
# or maybe
|
|
174
|
-
export rewriter << some_other_middleware << app
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## CLI tool for setting up a site repo:
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
# clone a newly created repo
|
|
181
|
-
~/repo$ git clone https://github.com/foo/bar
|
|
182
|
-
...
|
|
183
|
-
~/repo$ syntropy setup bar
|
|
184
|
-
|
|
185
|
-
(syntropy banner)
|
|
186
|
-
|
|
187
|
-
Setting up Syntropy project in /home/sharon/repo/bar:
|
|
188
|
-
|
|
189
|
-
bar/
|
|
190
|
-
bin/
|
|
191
|
-
start
|
|
192
|
-
stop
|
|
193
|
-
restart
|
|
194
|
-
console
|
|
195
|
-
server
|
|
196
|
-
docker-compose.yml
|
|
197
|
-
Dockerfile
|
|
198
|
-
Gemfile
|
|
199
|
-
proxy/
|
|
200
|
-
README.md
|
|
201
|
-
site/
|
|
202
|
-
_layout/
|
|
203
|
-
default.rb
|
|
204
|
-
_lib/
|
|
205
|
-
about.md
|
|
206
|
-
articles/
|
|
207
|
-
long-form.md
|
|
208
|
-
assets/
|
|
209
|
-
js/
|
|
210
|
-
css/
|
|
211
|
-
style.css
|
|
212
|
-
img/
|
|
213
|
-
syntropy.png
|
|
214
|
-
index.rb
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Some of the files might need templating, but we can maybe do without, or at
|
|
218
|
-
least make it as generic as possible.
|
|
219
|
-
|
|
220
|
-
`syntropy setup` steps:
|
|
221
|
-
|
|
222
|
-
1. Verify existence of target directory
|
|
223
|
-
2. Copy files from Syntropy template to target directory
|
|
224
|
-
3. Do chmod +x for bin/*
|
|
225
|
-
4. Do bundle install in the target directory
|
|
226
|
-
5. Show some information with regard to how to get started working with the
|
|
227
|
-
repo
|
|
138
|
+
## Some more CLI commands
|
|
228
139
|
|
|
229
140
|
`syntropy provision` steps:
|
|
230
141
|
|
data/cmd/console.rb
CHANGED
|
@@ -17,11 +17,11 @@ env = {
|
|
|
17
17
|
parser = OptionParser.new do |o|
|
|
18
18
|
o.banner = 'Usage: syntropy console [options]'
|
|
19
19
|
|
|
20
|
-
o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
|
|
20
|
+
o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
|
|
21
21
|
env[:app_root] = path
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
|
|
24
|
+
o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
|
|
25
25
|
env[:config_root] = path
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -72,8 +72,6 @@ env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
|
|
|
72
72
|
@app = Syntropy::App.load(env)
|
|
73
73
|
@env = env
|
|
74
74
|
@machine = env[:machine]
|
|
75
|
-
@connection_pool = @app.connection_pool if @app.respond_to?(:connection_pool)
|
|
76
|
-
@schema = @app.schema if @app.respond_to?(:schema)
|
|
77
75
|
@module_loader = @app.module_loader
|
|
78
76
|
|
|
79
77
|
require 'uringmachine/fiber_scheduler'
|
data/cmd/new.rb
CHANGED
|
@@ -44,7 +44,7 @@ template_path = File.join(__dir__, 'new/template')
|
|
|
44
44
|
begin
|
|
45
45
|
`mkdir -p "#{path}"`
|
|
46
46
|
system("cp -rv#{opts[:yes] ? '' : 'i'} #{template_path}/* \"#{path}/\"")
|
|
47
|
-
puts "Your app is ready in #{
|
|
47
|
+
puts "Your app is ready in #{full_path}"
|
|
48
48
|
rescue => e
|
|
49
49
|
p e
|
|
50
50
|
p e.backtrace
|
data/cmd/serve.rb
CHANGED
|
@@ -20,17 +20,16 @@ env = {
|
|
|
20
20
|
parser = OptionParser.new do |o|
|
|
21
21
|
o.banner = 'Usage: syntropy serve [options]'
|
|
22
22
|
|
|
23
|
-
o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
|
|
23
|
+
o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
|
|
24
24
|
env[:app_root] = path
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
o.on('-b', '--bind
|
|
28
|
-
'Bind address (default: http://0.0.0.0:1234). You can specify this flag multiple times to bind to multiple addresses.') do
|
|
27
|
+
o.on('-b', '--bind ADDRESS', String, 'Bind address (default: 0.0.0.0:1234)') do
|
|
29
28
|
env[:bind] ||= []
|
|
30
29
|
env[:bind] << it
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
|
|
32
|
+
o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
|
|
34
33
|
env[:config_root] = path
|
|
35
34
|
end
|
|
36
35
|
|
|
@@ -100,4 +99,30 @@ Fiber.set_scheduler(UM::FiberScheduler.new(env[:machine]))
|
|
|
100
99
|
require 'syntropy/dev_mode' if Syntropy.dev_mode
|
|
101
100
|
|
|
102
101
|
app = Syntropy::App.load(env)
|
|
102
|
+
|
|
103
|
+
if env[:mode] == 'development'
|
|
104
|
+
@main_fiber = Fiber.current
|
|
105
|
+
env[:machine].spin do
|
|
106
|
+
@app = app
|
|
107
|
+
@env = env
|
|
108
|
+
@machine = env[:machine]
|
|
109
|
+
@module_loader = @app.module_loader
|
|
110
|
+
|
|
111
|
+
loop do
|
|
112
|
+
puts "Enter command: (c + Enter to open console, q + Enter to quit)"
|
|
113
|
+
command = $stdin.gets.chomp
|
|
114
|
+
case command
|
|
115
|
+
when 'c'
|
|
116
|
+
t = Thread.new {
|
|
117
|
+
require 'irb'
|
|
118
|
+
IRB.start
|
|
119
|
+
}
|
|
120
|
+
t.join
|
|
121
|
+
when 'q'
|
|
122
|
+
@machine.schedule(@main_fiber, UM::Terminate.new)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
103
128
|
Syntropy.run(env) { app.call(it) }
|
data/cmd/test.rb
CHANGED
|
@@ -16,11 +16,11 @@ MINITEST_ARGV = []
|
|
|
16
16
|
parser = OptionParser.new do |o|
|
|
17
17
|
o.banner = 'Usage: syntropy test [options]'
|
|
18
18
|
|
|
19
|
-
o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
|
|
19
|
+
o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
|
|
20
20
|
env[:app_root] = path
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
|
|
23
|
+
o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
|
|
24
24
|
env[:config_root] = path
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -29,6 +29,10 @@ parser = OptionParser.new do |o|
|
|
|
29
29
|
exit
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
o.on('-f', '--file PATH', 'Set test file') do |path|
|
|
33
|
+
env[:test_file] = path
|
|
34
|
+
end
|
|
35
|
+
|
|
32
36
|
o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
|
|
33
37
|
env[:mount_path] = path
|
|
34
38
|
env[:builtin_applet_path] = File.join(path, '.syntropy')
|
|
@@ -82,7 +86,11 @@ Syntropy.load_config(env)
|
|
|
82
86
|
$stdout.sync = true
|
|
83
87
|
$stderr.sync = true
|
|
84
88
|
|
|
85
|
-
|
|
89
|
+
if env[:test_file]
|
|
90
|
+
require(File.expand_path(env[:test_file]))
|
|
91
|
+
else
|
|
92
|
+
Dir.glob("#{File.expand_path(env[:test_root])}/test_*.rb").each { require(it) }
|
|
93
|
+
end
|
|
86
94
|
|
|
87
95
|
def restart_on_file_change(machine, dir, restart_argv)
|
|
88
96
|
machine.file_watch(dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) {
|
|
@@ -92,7 +100,7 @@ def restart_on_file_change(machine, dir, restart_argv)
|
|
|
92
100
|
exec('ruby', __FILE__, *restart_argv)
|
|
93
101
|
end
|
|
94
102
|
|
|
95
|
-
Syntropy::Test.
|
|
103
|
+
Syntropy::Test.global_env = env
|
|
96
104
|
Minitest.run MINITEST_ARGV
|
|
97
105
|
|
|
98
106
|
if env[:watch_mode]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FROM ruby:4.0-slim AS base
|
|
2
|
+
WORKDIR /syntropy
|
|
3
|
+
RUN apt-get update -qq && apt-get install -y \
|
|
4
|
+
curl \
|
|
5
|
+
build-essential \
|
|
6
|
+
libpq-dev \
|
|
7
|
+
libsqlite3-dev \
|
|
8
|
+
libssl-dev \
|
|
9
|
+
libyaml-dev \
|
|
10
|
+
pkg-config && \
|
|
11
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
12
|
+
|
|
13
|
+
ENV BUNDLE_PATH="/usr/local/bundle"
|
|
14
|
+
COPY Gemfile Gemfile.lock ./
|
|
15
|
+
RUN bundle install
|
|
16
|
+
|
|
17
|
+
RUN groupadd --system --gid 1000 syntropy && \
|
|
18
|
+
useradd syntropy --uid 1000 --gid 1000 --create-home --shell /bin/bash
|
|
19
|
+
USER 1000:1000
|
|
20
|
+
|
|
21
|
+
EXPOSE 1234
|
|
22
|
+
|
|
23
|
+
# Start the main process
|
|
24
|
+
# CMD ["bash"]
|
|
25
|
+
CMD ["bundle", "exec", "syntropy", "serve"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# My awesome Syntropy app
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
$ bundle install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Running the web server
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
$ bundle exec syntropy serve
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Starting the app console
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle exec syntropy console
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Running tests
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ bundle exec syntropy tests
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Usage with Docker Compose
|
|
28
|
+
|
|
29
|
+
You can also run the app on Docker Compose:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# run the app server
|
|
33
|
+
$ docker compose up
|
|
34
|
+
|
|
35
|
+
# run the console
|
|
36
|
+
$ docker compose run --remove-orphans console
|
|
37
|
+
|
|
38
|
+
# run tests
|
|
39
|
+
$ docker compose run --remove-orphans test
|
|
40
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export template { |**props|
|
|
2
|
+
html {
|
|
3
|
+
head {
|
|
4
|
+
title "My awesome Syntropy web app"
|
|
5
|
+
meta charset: 'utf-8'
|
|
6
|
+
meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
|
|
7
|
+
link rel: "stylesheet", href: 'https://cdn.simplecss.org/simple.min.css'
|
|
8
|
+
# link rel: "stylesheet", type: "text/css", href: "/assets/style.css"
|
|
9
|
+
}
|
|
10
|
+
body {
|
|
11
|
+
render_children(**props)
|
|
12
|
+
auto_refresh!
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export self
|
|
2
|
+
|
|
3
|
+
def connection_pool
|
|
4
|
+
@connection_pool ||= Storage::ConnectionPool.new(@machine, @env[:config][:storage][:path], 4)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def schema
|
|
8
|
+
Storage::Schema.new(module_loader: @module_loader, schema_root: '_schema')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def migrate!
|
|
12
|
+
schema.apply(connection_pool)
|
|
13
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as UI from './ui.js';
|
|
2
|
+
import * as Model from './model.js';
|
|
3
|
+
import * as Tools from './tools.js';
|
|
4
|
+
|
|
5
|
+
export async function runPromptExchange(callHandler) {
|
|
6
|
+
const section = UI.createNewSection();
|
|
7
|
+
const prompt = await UI.askPrompt(section);
|
|
8
|
+
UI.addSectionEntryMarkdown(section, `*Prompt*: ${prompt}`);
|
|
9
|
+
const ctx = Model.buildExchangeBody(prompt);
|
|
10
|
+
|
|
11
|
+
while (true) {
|
|
12
|
+
UI.addPendingEntry(section);
|
|
13
|
+
const response = await Model.query(ctx);
|
|
14
|
+
UI.removePendingEntry(section);
|
|
15
|
+
if (!(await processResponse(section, ctx, response, callHandler)))
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function processResponse(section, ctx, response, callHandler) {
|
|
21
|
+
const first_choice = response.choices[0];
|
|
22
|
+
if (first_choice.finish_reason == "tool_calls") {
|
|
23
|
+
Model.addMessage(ctx, first_choice.message);
|
|
24
|
+
const tool_calls = first_choice.message.tool_calls;
|
|
25
|
+
const tool_results = await performToolCalls(section, ctx, tool_calls, callHandler);
|
|
26
|
+
console.log("tool results", tool_results);
|
|
27
|
+
Model.mergeToolResults(ctx, tool_results);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
else if (first_choice.finish_reason == "stop") {
|
|
31
|
+
UI.addSectionEntryMarkdown(section, first_choice.message.content);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function performToolCalls(section, ctx, tool_calls, callHandler) {
|
|
38
|
+
const pending = tool_calls.map(async (c) => await runTool(section, ctx, callHandler, c));
|
|
39
|
+
return await Promise.all(pending);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function runTool(section, ctx, callHandler, call) {
|
|
43
|
+
UI.addSectionEntryMarkdown(section, `Running \`${call.function.name}(${call.function.arguments})\``);
|
|
44
|
+
const name = call.function.name;
|
|
45
|
+
const args = JSON.parse(call.function.arguments);
|
|
46
|
+
try {
|
|
47
|
+
const res = await callHandler(name, args);
|
|
48
|
+
return {
|
|
49
|
+
id: call.id,
|
|
50
|
+
result: res
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
return {
|
|
55
|
+
id: call.id,
|
|
56
|
+
error: e.message
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
runPromptExchange(Tools.toolHandler);
|