Skip to main content

max / makenotwork

sando: scaffold deploy controller (daemon + TUI) Home-rolled MNW server CI/CD controller. Axum daemon (sandod) + ratatui TUI (sando) under sando/. Tiers/nodes/gates declared in sando.toml so topology changes are config edits rather than schema migrations. SQLite state on the MakeMachine. Route table stubbed with todo!() bodies; both crates cargo-check clean. MIT licensed.
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-23 01:47 UTC
Commit: b90ce2361cb8c46253c1b4c6a7f36c0833dba881
Parent: 9764587
18 files changed, +1537 insertions, -0 deletions
@@ -0,0 +1,21 @@
1 + MIT License
2 +
3 + Copyright (c) 2026 Max Jacobson
4 +
5 + Permission is hereby granted, free of charge, to any person obtaining a copy
6 + of this software and associated documentation files (the "Software"), to deal
7 + in the Software without restriction, including without limitation the rights
8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 + copies of the Software, and to permit persons to whom the Software is
10 + furnished to do so, subject to the following conditions:
11 +
12 + The above copyright notice and this permission notice shall be included in all
13 + copies or substantial portions of the Software.
14 +
15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 + SOFTWARE.
@@ -0,0 +1,2554 @@
1 + # This file is automatically @generated by Cargo.
2 + # It is not intended for manual editing.
3 + version = 4
4 +
5 + [[package]]
6 + name = "aho-corasick"
7 + version = "1.1.4"
8 + source = "registry+https://github.com/rust-lang/crates.io-index"
9 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
10 + dependencies = [
11 + "memchr",
12 + ]
13 +
14 + [[package]]
15 + name = "allocator-api2"
16 + version = "0.2.21"
17 + source = "registry+https://github.com/rust-lang/crates.io-index"
18 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
19 +
20 + [[package]]
21 + name = "android_system_properties"
22 + version = "0.1.5"
23 + source = "registry+https://github.com/rust-lang/crates.io-index"
24 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
25 + dependencies = [
26 + "libc",
27 + ]
28 +
29 + [[package]]
30 + name = "anyhow"
31 + version = "1.0.102"
32 + source = "registry+https://github.com/rust-lang/crates.io-index"
33 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
34 +
35 + [[package]]
36 + name = "atoi"
37 + version = "2.0.0"
38 + source = "registry+https://github.com/rust-lang/crates.io-index"
39 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
40 + dependencies = [
41 + "num-traits",
42 + ]
43 +
44 + [[package]]
45 + name = "atomic-waker"
46 + version = "1.1.2"
47 + source = "registry+https://github.com/rust-lang/crates.io-index"
48 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
49 +
50 + [[package]]
51 + name = "autocfg"
52 + version = "1.5.1"
53 + source = "registry+https://github.com/rust-lang/crates.io-index"
54 + checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
55 +
56 + [[package]]
57 + name = "axum"
58 + version = "0.8.9"
59 + source = "registry+https://github.com/rust-lang/crates.io-index"
60 + checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
61 + dependencies = [
62 + "axum-core",
63 + "axum-macros",
64 + "base64",
65 + "bytes",
66 + "form_urlencoded",
67 + "futures-util",
68 + "http",
69 + "http-body",
70 + "http-body-util",
71 + "hyper",
72 + "hyper-util",
73 + "itoa",
74 + "matchit",
75 + "memchr",
76 + "mime",
77 + "percent-encoding",
78 + "pin-project-lite",
79 + "serde_core",
80 + "serde_json",
81 + "serde_path_to_error",
82 + "serde_urlencoded",
83 + "sha1",
84 + "sync_wrapper",
85 + "tokio",
86 + "tokio-tungstenite",
87 + "tower",
88 + "tower-layer",
89 + "tower-service",
90 + "tracing",
91 + ]
92 +
93 + [[package]]
94 + name = "axum-core"
95 + version = "0.5.6"
96 + source = "registry+https://github.com/rust-lang/crates.io-index"
97 + checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
98 + dependencies = [
99 + "bytes",
100 + "futures-core",
101 + "http",
102 + "http-body",
103 + "http-body-util",
104 + "mime",
105 + "pin-project-lite",
106 + "sync_wrapper",
107 + "tower-layer",
108 + "tower-service",
109 + "tracing",
110 + ]
111 +
112 + [[package]]
113 + name = "axum-macros"
114 + version = "0.5.1"
115 + source = "registry+https://github.com/rust-lang/crates.io-index"
116 + checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"
117 + dependencies = [
118 + "proc-macro2",
119 + "quote",
120 + "syn",
121 + ]
122 +
123 + [[package]]
124 + name = "base64"
125 + version = "0.22.1"
126 + source = "registry+https://github.com/rust-lang/crates.io-index"
127 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
128 +
129 + [[package]]
130 + name = "base64ct"
131 + version = "1.8.3"
132 + source = "registry+https://github.com/rust-lang/crates.io-index"
133 + checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
134 +
135 + [[package]]
136 + name = "bitflags"
137 + version = "2.11.1"
138 + source = "registry+https://github.com/rust-lang/crates.io-index"
139 + checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
140 + dependencies = [
141 + "serde_core",
142 + ]
143 +
144 + [[package]]
145 + name = "block-buffer"
146 + version = "0.10.4"
147 + source = "registry+https://github.com/rust-lang/crates.io-index"
148 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
149 + dependencies = [
150 + "generic-array",
151 + ]
152 +
153 + [[package]]
154 + name = "bumpalo"
155 + version = "3.20.3"
156 + source = "registry+https://github.com/rust-lang/crates.io-index"
157 + checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
158 +
159 + [[package]]
160 + name = "byteorder"
161 + version = "1.5.0"
162 + source = "registry+https://github.com/rust-lang/crates.io-index"
163 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
164 +
165 + [[package]]
166 + name = "bytes"
167 + version = "1.11.1"
168 + source = "registry+https://github.com/rust-lang/crates.io-index"
169 + checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
170 +
171 + [[package]]
172 + name = "cc"
173 + version = "1.2.62"
174 + source = "registry+https://github.com/rust-lang/crates.io-index"
175 + checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
176 + dependencies = [
177 + "find-msvc-tools",
178 + "shlex",
179 + ]
180 +
181 + [[package]]
182 + name = "cfg-if"
183 + version = "1.0.4"
184 + source = "registry+https://github.com/rust-lang/crates.io-index"
185 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
186 +
187 + [[package]]
188 + name = "chrono"
189 + version = "0.4.44"
190 + source = "registry+https://github.com/rust-lang/crates.io-index"
191 + checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
192 + dependencies = [
193 + "iana-time-zone",
194 + "js-sys",
195 + "num-traits",
196 + "serde",
197 + "wasm-bindgen",
198 + "windows-link",
199 + ]
200 +
201 + [[package]]
202 + name = "concurrent-queue"
203 + version = "2.5.0"
204 + source = "registry+https://github.com/rust-lang/crates.io-index"
205 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
206 + dependencies = [
207 + "crossbeam-utils",
208 + ]
209 +
210 + [[package]]
211 + name = "const-oid"
212 + version = "0.9.6"
213 + source = "registry+https://github.com/rust-lang/crates.io-index"
214 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
215 +
216 + [[package]]
217 + name = "core-foundation-sys"
218 + version = "0.8.7"
219 + source = "registry+https://github.com/rust-lang/crates.io-index"
220 + checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
221 +
222 + [[package]]
223 + name = "cpufeatures"
224 + version = "0.2.17"
225 + source = "registry+https://github.com/rust-lang/crates.io-index"
226 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
227 + dependencies = [
228 + "libc",
229 + ]
230 +
231 + [[package]]
232 + name = "crc"
233 + version = "3.4.0"
234 + source = "registry+https://github.com/rust-lang/crates.io-index"
235 + checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
236 + dependencies = [
237 + "crc-catalog",
238 + ]
239 +
240 + [[package]]
241 + name = "crc-catalog"
242 + version = "2.5.0"
243 + source = "registry+https://github.com/rust-lang/crates.io-index"
244 + checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
245 +
246 + [[package]]
247 + name = "crossbeam-epoch"
248 + version = "0.9.18"
249 + source = "registry+https://github.com/rust-lang/crates.io-index"
250 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
251 + dependencies = [
252 + "crossbeam-utils",
253 + ]
254 +
255 + [[package]]
256 + name = "crossbeam-queue"
257 + version = "0.3.12"
258 + source = "registry+https://github.com/rust-lang/crates.io-index"
259 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
260 + dependencies = [
261 + "crossbeam-utils",
262 + ]
263 +
264 + [[package]]
265 + name = "crossbeam-utils"
266 + version = "0.8.21"
267 + source = "registry+https://github.com/rust-lang/crates.io-index"
268 + checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
269 +
270 + [[package]]
271 + name = "crypto-common"
272 + version = "0.1.7"
273 + source = "registry+https://github.com/rust-lang/crates.io-index"
274 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
275 + dependencies = [
276 + "generic-array",
277 + "typenum",
278 + ]
279 +
280 + [[package]]
281 + name = "data-encoding"
282 + version = "2.11.0"
283 + source = "registry+https://github.com/rust-lang/crates.io-index"
284 + checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
285 +
286 + [[package]]
287 + name = "der"
288 + version = "0.7.10"
289 + source = "registry+https://github.com/rust-lang/crates.io-index"
290 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
291 + dependencies = [
292 + "const-oid",
293 + "pem-rfc7468",
294 + "zeroize",
295 + ]
296 +
297 + [[package]]
298 + name = "digest"
299 + version = "0.10.7"
300 + source = "registry+https://github.com/rust-lang/crates.io-index"
301 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
302 + dependencies = [
303 + "block-buffer",
304 + "const-oid",
305 + "crypto-common",
306 + "subtle",
307 + ]
308 +
309 + [[package]]
310 + name = "displaydoc"
311 + version = "0.2.5"
312 + source = "registry+https://github.com/rust-lang/crates.io-index"
313 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
314 + dependencies = [
315 + "proc-macro2",
316 + "quote",
317 + "syn",
318 + ]
319 +
320 + [[package]]
321 + name = "dotenvy"
322 + version = "0.15.7"
323 + source = "registry+https://github.com/rust-lang/crates.io-index"
324 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
325 +
326 + [[package]]
327 + name = "either"
328 + version = "1.16.0"
329 + source = "registry+https://github.com/rust-lang/crates.io-index"
330 + checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
331 + dependencies = [
332 + "serde",
333 + ]
334 +
335 + [[package]]
336 + name = "equivalent"
337 + version = "1.0.2"
338 + source = "registry+https://github.com/rust-lang/crates.io-index"
339 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
340 +
341 + [[package]]
342 + name = "errno"
343 + version = "0.3.14"
344 + source = "registry+https://github.com/rust-lang/crates.io-index"
345 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
346 + dependencies = [
347 + "libc",
348 + "windows-sys 0.61.2",
349 + ]
350 +
351 + [[package]]
352 + name = "etcetera"
353 + version = "0.8.0"
354 + source = "registry+https://github.com/rust-lang/crates.io-index"
355 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
356 + dependencies = [
357 + "cfg-if",
358 + "home",
359 + "windows-sys 0.48.0",
360 + ]
361 +
362 + [[package]]
363 + name = "event-listener"
364 + version = "5.4.1"
365 + source = "registry+https://github.com/rust-lang/crates.io-index"
366 + checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
367 + dependencies = [
368 + "concurrent-queue",
369 + "parking",
370 + "pin-project-lite",
371 + ]
372 +
373 + [[package]]
374 + name = "evmap"
375 + version = "11.0.0"
376 + source = "registry+https://github.com/rust-lang/crates.io-index"
377 + checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
378 + dependencies = [
379 + "hashbag",
380 + "left-right",
381 + "smallvec",
382 + ]
383 +
384 + [[package]]
385 + name = "find-msvc-tools"
386 + version = "0.1.9"
387 + source = "registry+https://github.com/rust-lang/crates.io-index"
388 + checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
389 +
390 + [[package]]
391 + name = "flume"
392 + version = "0.11.1"
393 + source = "registry+https://github.com/rust-lang/crates.io-index"
394 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
395 + dependencies = [
396 + "futures-core",
397 + "futures-sink",
398 + "spin",
399 + ]
400 +
401 + [[package]]
402 + name = "foldhash"
403 + version = "0.1.5"
404 + source = "registry+https://github.com/rust-lang/crates.io-index"
405 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
406 +
407 + [[package]]
408 + name = "foldhash"
409 + version = "0.2.0"
410 + source = "registry+https://github.com/rust-lang/crates.io-index"
411 + checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
412 +
413 + [[package]]
414 + name = "form_urlencoded"
415 + version = "1.2.2"
416 + source = "registry+https://github.com/rust-lang/crates.io-index"
417 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
418 + dependencies = [
419 + "percent-encoding",
420 + ]
421 +
422 + [[package]]
423 + name = "futures-channel"
424 + version = "0.3.32"
425 + source = "registry+https://github.com/rust-lang/crates.io-index"
426 + checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
427 + dependencies = [
428 + "futures-core",
429 + "futures-sink",
430 + ]
431 +
432 + [[package]]
433 + name = "futures-core"
434 + version = "0.3.32"
435 + source = "registry+https://github.com/rust-lang/crates.io-index"
436 + checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
437 +
438 + [[package]]
439 + name = "futures-executor"
440 + version = "0.3.32"
441 + source = "registry+https://github.com/rust-lang/crates.io-index"
442 + checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
443 + dependencies = [
444 + "futures-core",
445 + "futures-task",
446 + "futures-util",
447 + ]
448 +
449 + [[package]]
450 + name = "futures-intrusive"
451 + version = "0.5.0"
452 + source = "registry+https://github.com/rust-lang/crates.io-index"
453 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
454 + dependencies = [
455 + "futures-core",
456 + "lock_api",
457 + "parking_lot",
458 + ]
459 +
460 + [[package]]
461 + name = "futures-io"
462 + version = "0.3.32"
463 + source = "registry+https://github.com/rust-lang/crates.io-index"
464 + checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
465 +
466 + [[package]]
467 + name = "futures-sink"
468 + version = "0.3.32"
469 + source = "registry+https://github.com/rust-lang/crates.io-index"
470 + checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
471 +
472 + [[package]]
473 + name = "futures-task"
474 + version = "0.3.32"
475 + source = "registry+https://github.com/rust-lang/crates.io-index"
476 + checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
477 +
478 + [[package]]
479 + name = "futures-util"
480 + version = "0.3.32"
481 + source = "registry+https://github.com/rust-lang/crates.io-index"
482 + checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
483 + dependencies = [
484 + "futures-core",
485 + "futures-io",
486 + "futures-sink",
487 + "futures-task",
488 + "memchr",
489 + "pin-project-lite",
490 + "slab",
491 + ]
492 +
493 + [[package]]
494 + name = "generator"
495 + version = "0.8.8"
496 + source = "registry+https://github.com/rust-lang/crates.io-index"
497 + checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
498 + dependencies = [
499 + "cc",
500 + "cfg-if",
Lines truncated
@@ -0,0 +1,24 @@
1 + [package]
2 + name = "sando-daemon"
3 + version = "0.1.0"
4 + edition = "2024"
5 + license = "MIT"
6 +
7 + [[bin]]
8 + name = "sandod"
9 + path = "src/main.rs"
10 +
11 + [dependencies]
12 + axum = { version = "0.8.8", features = ["macros", "ws"] }
13 + tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread", "net", "signal", "fs", "process"] }
14 + serde = { version = "1.0.228", features = ["derive"] }
15 + serde_json = "1"
16 + toml = "0.8"
17 + sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "chrono", "migrate"] }
18 + tracing = "0.1.44"
19 + tracing-subscriber = { version = "0.3.22", features = ["env-filter", "json"] }
20 + metrics = "0.24"
21 + metrics-exporter-prometheus = { version = "0.18.1", default-features = false }
22 + anyhow = "1.0.102"
23 + thiserror = "2.0.18"
24 + chrono = { version = "0.4", features = ["serde"] }
@@ -0,0 +1,70 @@
1 + -- Sando v0 schema.
2 + -- Tiers and nodes are modeled as rows so topology changes are config edits,
3 + -- not schema migrations. The daemon syncs (tiers, nodes) from sando.toml on
4 + -- startup; mutable per-tier state (current_version, burn_in_started_at) lives
5 + -- in tier_state and survives restarts.
6 +
7 + CREATE TABLE tiers (
8 + name TEXT PRIMARY KEY,
9 + ord INTEGER NOT NULL,
10 + provisioned INTEGER NOT NULL DEFAULT 0,
11 + canary TEXT NOT NULL DEFAULT 'sequential'
12 + );
13 +
14 + CREATE TABLE nodes (
15 + name TEXT PRIMARY KEY,
16 + tier TEXT NOT NULL REFERENCES tiers(name),
17 + ssh_target TEXT NOT NULL,
18 + release_root TEXT NOT NULL
19 + );
20 +
21 + CREATE INDEX nodes_by_tier ON nodes(tier);
22 +
23 + CREATE TABLE versions (
24 + version TEXT PRIMARY KEY,
25 + git_sha TEXT NOT NULL,
26 + built_at TEXT NOT NULL,
27 + artifact_path TEXT NOT NULL
28 + );
29 +
30 + CREATE TABLE deploys (
31 + id INTEGER PRIMARY KEY AUTOINCREMENT,
32 + version TEXT NOT NULL REFERENCES versions(version),
33 + tier TEXT NOT NULL REFERENCES tiers(name),
34 + node TEXT REFERENCES nodes(name),
35 + started_at TEXT NOT NULL,
36 + finished_at TEXT,
37 + outcome TEXT NOT NULL DEFAULT 'in_progress',
38 + hotfix INTEGER NOT NULL DEFAULT 0,
39 + reset_burn_in INTEGER NOT NULL DEFAULT 0
40 + );
41 +
42 + CREATE INDEX deploys_by_tier_version ON deploys(tier, version);
43 +
44 + CREATE TABLE gate_runs (
45 + id INTEGER PRIMARY KEY AUTOINCREMENT,
46 + version TEXT NOT NULL REFERENCES versions(version),
47 + tier TEXT NOT NULL REFERENCES tiers(name),
48 + gate_kind TEXT NOT NULL,
49 + started_at TEXT NOT NULL,
50 + finished_at TEXT,
51 + passed INTEGER,
52 + detail TEXT
53 + );
54 +
55 + CREATE INDEX gate_runs_lookup ON gate_runs(tier, version, gate_kind);
56 +
57 + CREATE TABLE tier_state (
58 + tier TEXT PRIMARY KEY REFERENCES tiers(name),
59 + current_version TEXT REFERENCES versions(version),
60 + previous_version TEXT REFERENCES versions(version),
61 + burn_in_started_at TEXT
62 + );
63 +
64 + CREATE TABLE backups (
65 + id INTEGER PRIMARY KEY AUTOINCREMENT,
66 + fetched_at TEXT NOT NULL,
67 + source TEXT NOT NULL,
68 + local_path TEXT NOT NULL,
69 + byte_size INTEGER
70 + );
@@ -0,0 +1,4 @@
1 + # Local dev defaults. Override path with SANDO_CONFIG.
2 + listen = "127.0.0.1:7766"
3 + db_path = "./sando.db"
4 + topology_path = "../sando.toml"
@@ -0,0 +1,19 @@
1 + use anyhow::{Context, Result};
2 + use serde::Deserialize;
3 + use std::path::PathBuf;
4 +
5 + #[derive(Debug, Deserialize)]
6 + pub struct Config {
7 + pub listen: String,
8 + pub db_path: PathBuf,
9 + pub topology_path: PathBuf,
10 + }
11 +
12 + impl Config {
13 + pub fn load() -> Result<Self> {
14 + let path = std::env::var("SANDO_CONFIG").unwrap_or_else(|_| "sando-daemon.toml".into());
15 + let raw = std::fs::read_to_string(&path)
16 + .with_context(|| format!("reading daemon config at {path}"))?;
17 + Ok(toml::from_str(&raw)?)
18 + }
19 + }
@@ -0,0 +1,19 @@
1 + use anyhow::Result;
2 + use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
3 + use sqlx::SqlitePool;
4 + use std::path::Path;
5 + use std::str::FromStr;
6 +
7 + pub async fn connect(path: &Path) -> Result<SqlitePool> {
8 + let url = format!("sqlite://{}?mode=rwc", path.display());
9 + let opts = SqliteConnectOptions::from_str(&url)?
10 + .create_if_missing(true)
11 + .foreign_keys(true);
12 + let pool = SqlitePoolOptions::new().max_connections(4).connect_with(opts).await?;
13 + Ok(pool)
14 + }
15 +
16 + pub async fn migrate(pool: &SqlitePool) -> Result<()> {
17 + sqlx::migrate!("./migrations").run(pool).await?;
18 + Ok(())
19 + }
@@ -0,0 +1,27 @@
1 + use axum::http::StatusCode;
2 + use axum::response::{IntoResponse, Response};
3 +
4 + #[derive(Debug, thiserror::Error)]
5 + pub enum Error {
6 + #[error("not found")]
7 + NotFound,
8 + #[error("gate not satisfied: {0}")]
9 + GateBlocked(String),
10 + #[error(transparent)]
11 + Db(#[from] sqlx::Error),
12 + #[error(transparent)]
13 + Other(#[from] anyhow::Error),
14 + }
15 +
16 + impl IntoResponse for Error {
17 + fn into_response(self) -> Response {
18 + let status = match &self {
19 + Error::NotFound => StatusCode::NOT_FOUND,
20 + Error::GateBlocked(_) => StatusCode::CONFLICT,
21 + _ => StatusCode::INTERNAL_SERVER_ERROR,
22 + };
23 + (status, self.to_string()).into_response()
24 + }
25 + }
26 +
27 + pub type Result<T> = std::result::Result<T, Error>;
@@ -0,0 +1,12 @@
1 + use crate::topology::Gate;
2 +
3 + #[derive(Debug, Clone)]
4 + pub struct GateOutcome {
5 + pub gate: String,
6 + pub passed: bool,
7 + pub detail: Option<String>,
8 + }
9 +
10 + pub async fn run(_gate: &Gate) -> GateOutcome {
11 + todo!("gate execution: cargo test, migration dry-run, boot smoke, burn-in check, manual confirm")
12 + }
@@ -0,0 +1,36 @@
1 + use anyhow::Result;
2 + use std::net::SocketAddr;
3 +
4 + mod config;
5 + mod db;
6 + mod error;
7 + mod gates;
8 + mod metrics;
9 + mod routes;
10 + mod state;
11 + mod topology;
12 +
13 + #[tokio::main]
14 + async fn main() -> Result<()> {
15 + tracing_subscriber::fmt()
16 + .with_env_filter(
17 + tracing_subscriber::EnvFilter::try_from_default_env()
18 + .unwrap_or_else(|_| "sando_daemon=info,tower_http=info".into()),
19 + )
20 + .init();
21 +
22 + let cfg = config::Config::load()?;
23 + let topo = topology::Topology::load(&cfg.topology_path)?;
24 + let pool = db::connect(&cfg.db_path).await?;
25 + db::migrate(&pool).await?;
26 +
27 + let prom = metrics::init();
28 + let app_state = state::AppState { pool, topo, prom };
29 + let app = routes::router(app_state);
30 +
31 + let addr: SocketAddr = cfg.listen.parse()?;
32 + tracing::info!(%addr, "sando daemon listening");
33 + let listener = tokio::net::TcpListener::bind(addr).await?;
34 + axum::serve(listener, app).await?;
35 + Ok(())
36 + }
@@ -0,0 +1,12 @@
1 + use axum::{extract::State, response::IntoResponse};
2 + use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
3 +
4 + pub fn init() -> PrometheusHandle {
5 + PrometheusBuilder::new()
6 + .install_recorder()
7 + .expect("install prometheus recorder")
8 + }
9 +
10 + pub async fn render(State(handle): State<PrometheusHandle>) -> impl IntoResponse {
11 + handle.render()
12 + }
@@ -0,0 +1,75 @@
1 + use crate::error::Result;
2 + use crate::state::AppState;
3 + use axum::extract::{Path, State, WebSocketUpgrade};
4 + use axum::response::IntoResponse;
5 + use axum::routing::{get, post};
6 + use axum::{Json, Router};
7 + use serde::{Deserialize, Serialize};
8 +
9 + pub fn router(state: AppState) -> Router {
10 + let prom = state.prom.clone();
11 + Router::new()
12 + .route("/state", get(get_state))
13 + .route("/promote/{tier}", post(promote))
14 + .route("/rollback/{tier}", post(rollback))
15 + .route("/rebuild", post(rebuild))
16 + .route("/backup/fetch", post(backup_fetch))
17 + .route("/events", get(events_ws))
18 + .with_state(state)
19 + .route("/metrics", get(crate::metrics::render).with_state(prom))
20 + }
21 +
22 + #[derive(Serialize)]
23 + struct StateView {
24 + tiers: Vec<TierView>,
25 + }
26 +
27 + #[derive(Serialize)]
28 + struct TierView {
29 + name: String,
30 + provisioned: bool,
31 + current_version: Option<String>,
32 + gates_passed: Vec<String>,
33 + }
34 +
35 + async fn get_state(State(_s): State<AppState>) -> Result<Json<StateView>> {
36 + todo!("read tier/version/gate state from sqlite")
37 + }
38 +
39 + #[derive(Deserialize)]
40 + struct PromoteBody {
41 + version: String,
42 + #[serde(default)]
43 + hotfix: bool,
44 + #[serde(default)]
45 + reset_burn_in: bool,
46 + }
47 +
48 + async fn promote(
49 + State(_s): State<AppState>,
50 + Path(_tier): Path<String>,
51 + Json(_body): Json<PromoteBody>,
52 + ) -> Result<Json<serde_json::Value>> {
53 + todo!("verify gates, atomic symlink swap on each node, sequential canary per topology")
54 + }
55 +
56 + async fn rollback(
57 + State(_s): State<AppState>,
58 + Path(_tier): Path<String>,
59 + ) -> Result<Json<serde_json::Value>> {
60 + todo!("flip symlink to previous release, restart unit")
61 + }
62 +
63 + async fn rebuild(State(_s): State<AppState>) -> Result<Json<serde_json::Value>> {
64 + todo!("triggered by post-receive hook; build + self-deploy + run MM gates")
65 + }
66 +
67 + async fn backup_fetch(State(_s): State<AppState>) -> Result<Json<serde_json::Value>> {
68 + todo!("pull latest prod backup from configured source to local_path")
69 + }
70 +
71 + async fn events_ws(ws: WebSocketUpgrade, State(_s): State<AppState>) -> impl IntoResponse {
72 + ws.on_upgrade(|_socket| async move {
73 + // tail of deploy/gate events for the TUI
74 + })
75 + }
@@ -0,0 +1,10 @@
1 + use crate::topology::Topology;
2 + use metrics_exporter_prometheus::PrometheusHandle;
3 + use sqlx::SqlitePool;
4 +
5 + #[derive(Clone)]
6 + pub struct AppState {
7 + pub pool: SqlitePool,
8 + pub topo: Topology,
9 + pub prom: PrometheusHandle,
10 + }
@@ -0,0 +1,80 @@
1 + use anyhow::{Context, Result};
2 + use serde::{Deserialize, Serialize};
3 + use std::path::Path;
4 +
5 + #[derive(Debug, Clone, Serialize, Deserialize)]
6 + pub struct Topology {
7 + pub repo: RepoConfig,
8 + pub backup: BackupConfig,
9 + #[serde(rename = "tier")]
10 + pub tiers: Vec<Tier>,
11 + }
12 +
13 + #[derive(Debug, Clone, Serialize, Deserialize)]
14 + pub struct RepoConfig {
15 + pub bare_path: String,
16 + pub branch: String,
17 + }
18 +
19 + #[derive(Debug, Clone, Serialize, Deserialize)]
20 + pub struct BackupConfig {
21 + pub source: String,
22 + pub local_path: String,
23 + }
24 +
25 + #[derive(Debug, Clone, Serialize, Deserialize)]
26 + pub struct Tier {
27 + pub name: String,
28 + #[serde(default)]
29 + pub provisioned: bool,
30 + pub gates: Vec<Gate>,
31 + #[serde(default)]
32 + pub canary: CanaryPolicy,
33 + #[serde(default, rename = "node")]
34 + pub nodes: Vec<Node>,
35 + }
36 +
37 + #[derive(Debug, Clone, Serialize, Deserialize)]
38 + pub struct Node {
39 + pub name: String,
40 + pub ssh_target: String,
41 + pub release_root: String,
42 + }
43 +
44 + #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
45 + #[serde(rename_all = "snake_case")]
46 + pub enum CanaryPolicy {
47 + #[default]
48 + Sequential,
49 + Parallel,
50 + }
51 +
52 + #[derive(Debug, Clone, Serialize, Deserialize)]
53 + #[serde(tag = "kind", rename_all = "snake_case")]
54 + pub enum Gate {
55 + CargoTest,
56 + MigrationDryRun,
57 + BootSmoke,
58 + BurnIn { hours: u32 },
59 + ManualConfirm,
60 + }
61 +
62 + impl Topology {
63 + pub fn load(path: &Path) -> Result<Self> {
64 + let raw = std::fs::read_to_string(path)
65 + .with_context(|| format!("reading topology at {}", path.display()))?;
66 + let topo: Topology = toml::from_str(&raw)?;
67 + topo.validate()?;
68 + Ok(topo)
69 + }
70 +
71 + fn validate(&self) -> Result<()> {
72 + anyhow::ensure!(!self.tiers.is_empty(), "topology must declare at least one tier");
73 + for t in &self.tiers {
74 + if t.provisioned && t.nodes.is_empty() && t.name != "mm" {
75 + anyhow::bail!("tier {} is provisioned but has no nodes", t.name);
76 + }
77 + }
78 + Ok(())
79 + }
80 + }
@@ -0,0 +1,69 @@
1 + # Sando topology config.
2 + #
3 + # Tiers run in declaration order. Each tier lists the gates that must pass to
4 + # unlock promotion *to* the next tier, the nodes it ships to, and the canary
5 + # policy for shipping within the tier.
6 + #
7 + # Day-one wiring: MM (local) -> A (testnot.work) -> B (prod-1). C is declared
8 + # but not provisioned; adding the second prod node later is a config edit
9 + # (set provisioned = true, fill in [[tier.node]]).
10 +
11 + [repo]
12 + bare_path = "/srv/sando/mnw.git"
13 + branch = "main"
14 +
15 + [backup]
16 + # Source of the prod-backup clone used by migration_dry_run on MM.
17 + # For localhost dev this can be a file:// path to a fixture dump.
18 + source = "rsync://astra/var/backups/mnw/latest.sql.gz"
19 + local_path = "/srv/sando/backups/latest.sql.gz"
20 +
21 + # ---- MM: local pre-staging gate ----
22 + [[tier]]
23 + name = "mm"
24 + provisioned = true
25 + canary = "sequential"
26 + gates = [
27 + { kind = "cargo_test" },
28 + { kind = "migration_dry_run" },
29 + { kind = "boot_smoke" },
30 + ]
31 + # MM is the daemon's own host; no remote node row.
32 +
33 + # ---- A: testnot.work staging ----
34 + [[tier]]
35 + name = "a"
36 + provisioned = true
37 + canary = "sequential"
38 + gates = [
39 + { kind = "boot_smoke" },
40 + { kind = "burn_in", hours = 48 },
41 + ]
42 + [[tier.node]]
43 + name = "testnot-1"
44 + ssh_target = "deploy@testnot.work"
45 + release_root = "/opt/mnw"
46 +
47 + # ---- B: prod-1 ----
48 + [[tier]]
49 + name = "b"
50 + provisioned = true
51 + canary = "sequential"
52 + gates = [
53 + { kind = "boot_smoke" },
54 + { kind = "manual_confirm" },
55 + ]
56 + [[tier.node]]
57 + name = "prod-1"
58 + ssh_target = "deploy@prod-1.makenot.work"
59 + release_root = "/opt/mnw"
60 +
61 + # ---- C: prod-2 (declared, not yet provisioned) ----
62 + [[tier]]
63 + name = "c"
64 + provisioned = false
65 + canary = "sequential"
66 + gates = [
67 + { kind = "boot_smoke" },
68 + ]
69 + # [[tier.node]] entries to be added when the second prod node ships.
@@ -0,0 +1,1778 @@
1 + # This file is automatically @generated by Cargo.
2 + # It is not intended for manual editing.
3 + version = 4
4 +
5 + [[package]]
6 + name = "allocator-api2"
7 + version = "0.2.21"
8 + source = "registry+https://github.com/rust-lang/crates.io-index"
9 + checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
10 +
11 + [[package]]
12 + name = "anyhow"
13 + version = "1.0.102"
14 + source = "registry+https://github.com/rust-lang/crates.io-index"
15 + checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
16 +
17 + [[package]]
18 + name = "atomic-waker"
19 + version = "1.1.2"
20 + source = "registry+https://github.com/rust-lang/crates.io-index"
21 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
22 +
23 + [[package]]
24 + name = "base64"
25 + version = "0.22.1"
26 + source = "registry+https://github.com/rust-lang/crates.io-index"
27 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
28 +
29 + [[package]]
30 + name = "bitflags"
31 + version = "2.11.1"
32 + source = "registry+https://github.com/rust-lang/crates.io-index"
33 + checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
34 +
35 + [[package]]
36 + name = "bumpalo"
37 + version = "3.20.3"
38 + source = "registry+https://github.com/rust-lang/crates.io-index"
39 + checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
40 +
41 + [[package]]
42 + name = "bytes"
43 + version = "1.11.1"
44 + source = "registry+https://github.com/rust-lang/crates.io-index"
45 + checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
46 +
47 + [[package]]
48 + name = "cassowary"
49 + version = "0.3.0"
50 + source = "registry+https://github.com/rust-lang/crates.io-index"
51 + checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
52 +
53 + [[package]]
54 + name = "castaway"
55 + version = "0.2.4"
56 + source = "registry+https://github.com/rust-lang/crates.io-index"
57 + checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
58 + dependencies = [
59 + "rustversion",
60 + ]
61 +
62 + [[package]]
63 + name = "cc"
64 + version = "1.2.62"
65 + source = "registry+https://github.com/rust-lang/crates.io-index"
66 + checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
67 + dependencies = [
68 + "find-msvc-tools",
69 + "shlex",
70 + ]
71 +
72 + [[package]]
73 + name = "cfg-if"
74 + version = "1.0.4"
75 + source = "registry+https://github.com/rust-lang/crates.io-index"
76 + checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
77 +
78 + [[package]]
79 + name = "cfg_aliases"
80 + version = "0.2.1"
81 + source = "registry+https://github.com/rust-lang/crates.io-index"
82 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
83 +
84 + [[package]]
85 + name = "compact_str"
86 + version = "0.8.1"
87 + source = "registry+https://github.com/rust-lang/crates.io-index"
88 + checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
89 + dependencies = [
90 + "castaway",
91 + "cfg-if",
92 + "itoa",
93 + "rustversion",
94 + "ryu",
95 + "static_assertions",
96 + ]
97 +
98 + [[package]]
99 + name = "crossterm"
100 + version = "0.28.1"
101 + source = "registry+https://github.com/rust-lang/crates.io-index"
102 + checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
103 + dependencies = [
104 + "bitflags",
105 + "crossterm_winapi",
106 + "mio",
107 + "parking_lot",
108 + "rustix",
109 + "signal-hook",
110 + "signal-hook-mio",
111 + "winapi",
112 + ]
113 +
114 + [[package]]
115 + name = "crossterm_winapi"
116 + version = "0.9.1"
117 + source = "registry+https://github.com/rust-lang/crates.io-index"
118 + checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
119 + dependencies = [
120 + "winapi",
121 + ]
122 +
123 + [[package]]
124 + name = "darling"
125 + version = "0.23.0"
126 + source = "registry+https://github.com/rust-lang/crates.io-index"
127 + checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
128 + dependencies = [
129 + "darling_core",
130 + "darling_macro",
131 + ]
132 +
133 + [[package]]
134 + name = "darling_core"
135 + version = "0.23.0"
136 + source = "registry+https://github.com/rust-lang/crates.io-index"
137 + checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
138 + dependencies = [
139 + "ident_case",
140 + "proc-macro2",
141 + "quote",
142 + "strsim",
143 + "syn",
144 + ]
145 +
146 + [[package]]
147 + name = "darling_macro"
148 + version = "0.23.0"
149 + source = "registry+https://github.com/rust-lang/crates.io-index"
150 + checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
151 + dependencies = [
152 + "darling_core",
153 + "quote",
154 + "syn",
155 + ]
156 +
157 + [[package]]
158 + name = "displaydoc"
159 + version = "0.2.5"
160 + source = "registry+https://github.com/rust-lang/crates.io-index"
161 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
162 + dependencies = [
163 + "proc-macro2",
164 + "quote",
165 + "syn",
166 + ]
167 +
168 + [[package]]
169 + name = "either"
170 + version = "1.16.0"
171 + source = "registry+https://github.com/rust-lang/crates.io-index"
172 + checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
173 +
174 + [[package]]
175 + name = "equivalent"
176 + version = "1.0.2"
177 + source = "registry+https://github.com/rust-lang/crates.io-index"
178 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
179 +
180 + [[package]]
181 + name = "errno"
182 + version = "0.3.14"
183 + source = "registry+https://github.com/rust-lang/crates.io-index"
184 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
185 + dependencies = [
186 + "libc",
187 + "windows-sys 0.61.2",
188 + ]
189 +
190 + [[package]]
191 + name = "find-msvc-tools"
192 + version = "0.1.9"
193 + source = "registry+https://github.com/rust-lang/crates.io-index"
194 + checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
195 +
196 + [[package]]
197 + name = "foldhash"
198 + version = "0.1.5"
199 + source = "registry+https://github.com/rust-lang/crates.io-index"
200 + checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
201 +
202 + [[package]]
203 + name = "form_urlencoded"
204 + version = "1.2.2"
205 + source = "registry+https://github.com/rust-lang/crates.io-index"
206 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
207 + dependencies = [
208 + "percent-encoding",
209 + ]
210 +
211 + [[package]]
212 + name = "futures-channel"
213 + version = "0.3.32"
214 + source = "registry+https://github.com/rust-lang/crates.io-index"
215 + checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
216 + dependencies = [
217 + "futures-core",
218 + ]
219 +
220 + [[package]]
221 + name = "futures-core"
222 + version = "0.3.32"
223 + source = "registry+https://github.com/rust-lang/crates.io-index"
224 + checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
225 +
226 + [[package]]
227 + name = "futures-task"
228 + version = "0.3.32"
229 + source = "registry+https://github.com/rust-lang/crates.io-index"
230 + checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
231 +
232 + [[package]]
233 + name = "futures-util"
234 + version = "0.3.32"
235 + source = "registry+https://github.com/rust-lang/crates.io-index"
236 + checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
237 + dependencies = [
238 + "futures-core",
239 + "futures-task",
240 + "pin-project-lite",
241 + "slab",
242 + ]
243 +
244 + [[package]]
245 + name = "getrandom"
246 + version = "0.2.17"
247 + source = "registry+https://github.com/rust-lang/crates.io-index"
248 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
249 + dependencies = [
250 + "cfg-if",
251 + "js-sys",
252 + "libc",
253 + "wasi",
254 + "wasm-bindgen",
255 + ]
256 +
257 + [[package]]
258 + name = "getrandom"
259 + version = "0.3.4"
260 + source = "registry+https://github.com/rust-lang/crates.io-index"
261 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
262 + dependencies = [
263 + "cfg-if",
264 + "js-sys",
265 + "libc",
266 + "r-efi",
267 + "wasip2",
268 + "wasm-bindgen",
269 + ]
270 +
271 + [[package]]
272 + name = "hashbrown"
273 + version = "0.15.5"
274 + source = "registry+https://github.com/rust-lang/crates.io-index"
275 + checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
276 + dependencies = [
277 + "allocator-api2",
278 + "equivalent",
279 + "foldhash",
280 + ]
281 +
282 + [[package]]
283 + name = "heck"
284 + version = "0.5.0"
285 + source = "registry+https://github.com/rust-lang/crates.io-index"
286 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
287 +
288 + [[package]]
289 + name = "http"
290 + version = "1.4.0"
291 + source = "registry+https://github.com/rust-lang/crates.io-index"
292 + checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
293 + dependencies = [
294 + "bytes",
295 + "itoa",
296 + ]
297 +
298 + [[package]]
299 + name = "http-body"
300 + version = "1.0.1"
301 + source = "registry+https://github.com/rust-lang/crates.io-index"
302 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
303 + dependencies = [
304 + "bytes",
305 + "http",
306 + ]
307 +
308 + [[package]]
309 + name = "http-body-util"
310 + version = "0.1.3"
311 + source = "registry+https://github.com/rust-lang/crates.io-index"
312 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
313 + dependencies = [
314 + "bytes",
315 + "futures-core",
316 + "http",
317 + "http-body",
318 + "pin-project-lite",
319 + ]
320 +
321 + [[package]]
322 + name = "httparse"
323 + version = "1.10.1"
324 + source = "registry+https://github.com/rust-lang/crates.io-index"
325 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
326 +
327 + [[package]]
328 + name = "hyper"
329 + version = "1.9.0"
330 + source = "registry+https://github.com/rust-lang/crates.io-index"
331 + checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
332 + dependencies = [
333 + "atomic-waker",
334 + "bytes",
335 + "futures-channel",
336 + "futures-core",
337 + "http",
338 + "http-body",
339 + "httparse",
340 + "itoa",
341 + "pin-project-lite",
342 + "smallvec",
343 + "tokio",
344 + "want",
345 + ]
346 +
347 + [[package]]
348 + name = "hyper-rustls"
349 + version = "0.27.9"
350 + source = "registry+https://github.com/rust-lang/crates.io-index"
351 + checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
352 + dependencies = [
353 + "http",
354 + "hyper",
355 + "hyper-util",
356 + "rustls",
357 + "tokio",
358 + "tokio-rustls",
359 + "tower-service",
360 + "webpki-roots",
361 + ]
362 +
363 + [[package]]
364 + name = "hyper-util"
365 + version = "0.1.20"
366 + source = "registry+https://github.com/rust-lang/crates.io-index"
367 + checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
368 + dependencies = [
369 + "base64",
370 + "bytes",
371 + "futures-channel",
372 + "futures-util",
373 + "http",
374 + "http-body",
375 + "hyper",
376 + "ipnet",
377 + "libc",
378 + "percent-encoding",
379 + "pin-project-lite",
380 + "socket2",
381 + "tokio",
382 + "tower-service",
383 + "tracing",
384 + ]
385 +
386 + [[package]]
387 + name = "icu_collections"
388 + version = "2.2.0"
389 + source = "registry+https://github.com/rust-lang/crates.io-index"
390 + checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
391 + dependencies = [
392 + "displaydoc",
393 + "potential_utf",
394 + "utf8_iter",
395 + "yoke",
396 + "zerofrom",
397 + "zerovec",
398 + ]
399 +
400 + [[package]]
401 + name = "icu_locale_core"
402 + version = "2.2.0"
403 + source = "registry+https://github.com/rust-lang/crates.io-index"
404 + checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
405 + dependencies = [
406 + "displaydoc",
407 + "litemap",
408 + "tinystr",
409 + "writeable",
410 + "zerovec",
411 + ]
412 +
413 + [[package]]
414 + name = "icu_normalizer"
415 + version = "2.2.0"
416 + source = "registry+https://github.com/rust-lang/crates.io-index"
417 + checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
418 + dependencies = [
419 + "icu_collections",
420 + "icu_normalizer_data",
421 + "icu_properties",
422 + "icu_provider",
423 + "smallvec",
424 + "zerovec",
425 + ]
426 +
427 + [[package]]
428 + name = "icu_normalizer_data"
429 + version = "2.2.0"
430 + source = "registry+https://github.com/rust-lang/crates.io-index"
431 + checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
432 +
433 + [[package]]
434 + name = "icu_properties"
435 + version = "2.2.0"
436 + source = "registry+https://github.com/rust-lang/crates.io-index"
437 + checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
438 + dependencies = [
439 + "icu_collections",
440 + "icu_locale_core",
441 + "icu_properties_data",
442 + "icu_provider",
443 + "zerotrie",
444 + "zerovec",
445 + ]
446 +
447 + [[package]]
448 + name = "icu_properties_data"
449 + version = "2.2.0"
450 + source = "registry+https://github.com/rust-lang/crates.io-index"
451 + checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
452 +
453 + [[package]]
454 + name = "icu_provider"
455 + version = "2.2.0"
456 + source = "registry+https://github.com/rust-lang/crates.io-index"
457 + checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
458 + dependencies = [
459 + "displaydoc",
460 + "icu_locale_core",
461 + "writeable",
462 + "yoke",
463 + "zerofrom",
464 + "zerotrie",
465 + "zerovec",
466 + ]
467 +
468 + [[package]]
469 + name = "ident_case"
470 + version = "1.0.1"
471 + source = "registry+https://github.com/rust-lang/crates.io-index"
472 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
473 +
474 + [[package]]
475 + name = "idna"
476 + version = "1.1.0"
477 + source = "registry+https://github.com/rust-lang/crates.io-index"
478 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
479 + dependencies = [
480 + "idna_adapter",
481 + "smallvec",
482 + "utf8_iter",
483 + ]
484 +
485 + [[package]]
486 + name = "idna_adapter"
487 + version = "1.2.2"
488 + source = "registry+https://github.com/rust-lang/crates.io-index"
489 + checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
490 + dependencies = [
491 + "icu_normalizer",
492 + "icu_properties",
493 + ]
494 +
495 + [[package]]
496 + name = "indoc"
497 + version = "2.0.7"
498 + source = "registry+https://github.com/rust-lang/crates.io-index"
499 + checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
500 + dependencies = [
Lines truncated
@@ -0,0 +1,18 @@
1 + [package]
2 + name = "sando-tui"
3 + version = "0.1.0"
4 + edition = "2024"
5 + license = "MIT"
6 +
7 + [[bin]]
8 + name = "sando"
9 + path = "src/main.rs"
10 +
11 + [dependencies]
12 + ratatui = "0.29"
13 + crossterm = "0.28"
14 + tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread", "net", "signal"] }
15 + reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
16 + serde = { version = "1.0.228", features = ["derive"] }
17 + serde_json = "1"
18 + anyhow = "1.0.102"
@@ -0,0 +1,41 @@
1 + use anyhow::Result;
2 + use crossterm::event::{self, Event, KeyCode};
3 + use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
4 + use ratatui::prelude::*;
5 + use ratatui::widgets::{Block, Borders, Paragraph};
6 + use std::io;
7 + use std::time::Duration;
8 +
9 + fn main() -> Result<()> {
10 + enable_raw_mode()?;
11 + let mut stdout = io::stdout();
12 + crossterm::execute!(stdout, EnterAlternateScreen)?;
13 + let backend = CrosstermBackend::new(stdout);
14 + let mut term = Terminal::new(backend)?;
15 +
16 + let res = run(&mut term);
17 +
18 + disable_raw_mode()?;
19 + crossterm::execute!(term.backend_mut(), LeaveAlternateScreen)?;
20 + term.show_cursor()?;
21 + res
22 + }
23 +
24 + fn run<B: Backend>(term: &mut Terminal<B>) -> Result<()> {
25 + loop {
26 + term.draw(|f| {
27 + let area = f.area();
28 + let block = Block::default().title("sando").borders(Borders::ALL);
29 + let body = Paragraph::new("v0 scaffold. press q to quit.").block(block);
30 + f.render_widget(body, area);
31 + })?;
32 +
33 + if event::poll(Duration::from_millis(200))? {
34 + if let Event::Key(k) = event::read()? {
35 + if matches!(k.code, KeyCode::Char('q') | KeyCode::Esc) {
36 + return Ok(());
37 + }
38 + }
39 + }
40 + }
41 + }