Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb14a955a0 | ||
|
|
dac1506680 | ||
|
|
3946a08f40 | ||
|
|
ee0519aacc | ||
|
|
dec1e3fdda | ||
|
|
f54e900716 | ||
|
|
7e8b5749fa | ||
|
|
febf902dc4 | ||
|
|
04b51f2610 | ||
|
|
b2a4f57d64 | ||
|
|
0ce7d92a8b | ||
|
|
7a67fe7dde | ||
|
|
00b4670b8b | ||
|
|
7633a951e6 | ||
|
|
4ff64c6209 | ||
|
|
22023fa9e7 | ||
|
|
85e0b53c33 | ||
|
|
6eaa231587 | ||
|
|
befb7509de | ||
|
|
09bf0b86d8 | ||
|
|
b5d45fa9f5 | ||
|
|
a6a633d5c1 | ||
|
|
e83e8001da | ||
|
|
0386898476 | ||
|
|
5e777fd2a2 | ||
|
|
3c71fda648 | ||
|
|
42329a038a | ||
|
|
10f3983f0b | ||
|
|
e9de7f95a7 | ||
|
|
a4113acd15 | ||
|
|
9c8e56fc96 | ||
|
|
c78cb57c41 | ||
|
|
eb15b2ba75 | ||
|
|
279edb6f24 | ||
|
|
c51a34bf4b | ||
|
|
e8d144d2a2 | ||
|
|
a760e8364f | ||
|
|
fa7cae59c0 | ||
|
|
8780fa6ccf | ||
|
|
ab2df0ae33 | ||
|
|
23757f3ac0 | ||
|
|
df7296cfe1 | ||
|
|
776276d5a4 | ||
|
|
eea45a22fa | ||
|
|
ddacb04f99 | ||
|
|
09561254a8 | ||
|
|
73a8356b10 | ||
|
|
8db75266d0 | ||
|
|
6c30565d40 | ||
|
|
b223a29603 | ||
|
|
8ed72ae087 | ||
|
|
62b8c7aee0 | ||
|
|
6145dfcca0 | ||
|
|
4580c88c0b | ||
|
|
061ba65d20 | ||
|
|
457386ad08 | ||
|
|
fce04dc48b | ||
|
|
81534ab387 | ||
|
|
409a6f93b2 | ||
|
|
55c294c013 | ||
|
|
70db372466 | ||
|
|
8cc427daba | ||
|
|
8fde772957 | ||
|
|
d8dc23bde9 | ||
|
|
1c83ef75a2 | ||
|
|
95e410db88 | ||
|
|
13d3fba86b | ||
|
|
3ab4f42ebb | ||
|
|
b8d2aebf09 | ||
|
|
20e818ad05 | ||
|
|
542186aa49 | ||
|
|
c478d1bdbb | ||
|
|
7fd2222976 | ||
|
|
e86adb2ec8 | ||
|
|
34ac0e895d | ||
|
|
bd4319f2bc | ||
|
|
696ab1a752 | ||
|
|
d3ff66e911 | ||
|
|
1954b59167 | ||
|
|
e2fac991dc | ||
|
|
8ba35eadd4 | ||
|
|
7446f5ad7b | ||
|
|
81a3e02474 | ||
|
|
7bbc643600 | ||
|
|
53630ebdce | ||
|
|
85eaa5b58b | ||
|
|
b789844b9c | ||
|
|
9b6ef074f0 | ||
|
|
2f4291672b | ||
|
|
83f4e8e156 | ||
|
|
7af2771a7e | ||
|
|
c9a3b35ac2 | ||
|
|
0dde6d0840 | ||
|
|
d1208bf0a1 | ||
|
|
0a9463541a | ||
|
|
fe26b4a7b1 | ||
|
|
8c173e18b7 | ||
|
|
183e0911b7 | ||
|
|
c7bb19ad07 | ||
|
|
e444d15b57 | ||
|
|
063d67a046 | ||
|
|
4f164c53d2 | ||
|
|
02ef96f89b | ||
|
|
8750744068 | ||
|
|
3e74107e36 | ||
|
|
160f839b25 | ||
|
|
bf5b109c1f | ||
|
|
60254d8ac0 | ||
|
|
c34aec060f | ||
|
|
12f1ad521f | ||
|
|
723a37ea9a | ||
|
|
c6a46615c0 | ||
|
|
da29380093 | ||
|
|
7950ae1462 | ||
|
|
15e830410f | ||
|
|
1a561bb512 | ||
|
|
fecae609d9 | ||
|
|
e01a540b08 | ||
|
|
54457e48bb | ||
|
|
b179d08484 | ||
|
|
d9edd6818f | ||
|
|
4217286b72 | ||
|
|
28a4517ec6 | ||
|
|
b00b2ded4f | ||
|
|
7b6d5b1429 | ||
|
|
7210db19e9 | ||
|
|
90d2b26426 | ||
|
|
6beba2c04f | ||
|
|
b8a0ecca98 | ||
|
|
ad10d3a126 | ||
|
|
a48274f82b | ||
|
|
6b25b7e95e | ||
|
|
030a3a7446 | ||
|
|
1a0e7f1e63 | ||
|
|
677fb6032b | ||
|
|
49aa48ce58 | ||
|
|
857a3cd522 | ||
|
|
6ed774ef62 | ||
|
|
5e825a4b6a | ||
|
|
3db8e7c2b6 | ||
|
|
b459055757 | ||
|
|
2b195e82ee | ||
|
|
58e889796c | ||
|
|
51498c8de4 | ||
|
|
7a495faa49 | ||
|
|
4957fca718 | ||
|
|
8168626cd3 | ||
|
|
b824809605 | ||
|
|
5536b14347 | ||
|
|
01efe236ef | ||
|
|
7a1f96399d | ||
|
|
40036abb9d | ||
|
|
2970ba6416 | ||
|
|
81412b6197 | ||
|
|
5bf7691ea6 | ||
|
|
b1055a74d3 | ||
|
|
ffcb27fa9a | ||
|
|
38819e89b8 | ||
|
|
0a42068fbb | ||
|
|
b05decc572 | ||
|
|
c2f487906a | ||
|
|
ae78ec7a0c | ||
|
|
e8c03f13dd | ||
|
|
f85d30c484 | ||
|
|
1bac46612c | ||
|
|
9ab3462821 | ||
|
|
3b36822696 | ||
|
|
5b731479d5 | ||
|
|
a50bef6913 | ||
|
|
ed397c5057 | ||
|
|
c9187a9f3a | ||
|
|
2c67b26b5d | ||
|
|
170b94a99e | ||
|
|
cd58f10e3c | ||
|
|
ea85fdf3cd | ||
|
|
edda26ab33 | ||
|
|
ea4e1913c0 | ||
|
|
5eebc8ab51 | ||
|
|
21c52fd5cb | ||
|
|
5e8634afaf | ||
|
|
d4bac5cdbd | ||
|
|
263b266476 | ||
|
|
06830327e7 | ||
|
|
4b204fee58 | ||
|
|
99d3a0bb24 | ||
|
|
0930f6ac55 | ||
|
|
24515162fa | ||
|
|
53aa899e45 | ||
|
|
7e763e1c06 | ||
|
|
b0f2cc0c22 | ||
|
|
f90aa62784 | ||
|
|
852191f6cb | ||
|
|
c5e9dc081c | ||
|
|
49c8889228 | ||
|
|
f739e1a958 | ||
|
|
841f1907bb | ||
|
|
9255c507d6 | ||
|
|
2711047166 | ||
|
|
908048baef | ||
|
|
a9fbe07408 | ||
|
|
0ae213ee0e | ||
|
|
ca031278ca | ||
|
|
ae6e47bb42 | ||
|
|
42a5fcead4 | ||
|
|
8ad83f71a9 | ||
|
|
fa95c09cdc | ||
|
|
0b132c032a | ||
|
|
44d7103a42 | ||
|
|
8f45a0e227 | ||
|
|
6581741318 | ||
|
|
80d68d01f4 | ||
|
|
fa9db3c167 | ||
|
|
5a727c0794 | ||
|
|
71cd84dbbb | ||
|
|
e1b7e25f4d | ||
|
|
98b6bb218b | ||
|
|
5592ce8eaf | ||
|
|
510fe8a72a | ||
|
|
04a1ab3893 | ||
|
|
e74b4d098b | ||
|
|
50e4b3e6a7 | ||
|
|
6ebd828aa5 | ||
|
|
022c979d28 | ||
|
|
4172e3ad28 | ||
|
|
90d1698aed | ||
|
|
b0c38ce56b | ||
|
|
9b37d0e191 | ||
|
|
ea794a4bf6 | ||
|
|
52f9b37576 | ||
|
|
a0d2e53bde | ||
|
|
851e900982 | ||
|
|
3aa6eeb426 | ||
|
|
b6ee8e92f9 | ||
|
|
44211e1526 | ||
|
|
12f84f198f | ||
|
|
e6db1cf29d | ||
|
|
f07f04d969 | ||
|
|
33d613a470 | ||
|
|
0bbd7ea17b | ||
|
|
87f3166437 | ||
|
|
7665bd9439 | ||
|
|
30e10127f2 | ||
|
|
5e66fc2318 | ||
|
|
c1c99c7e0f | ||
|
|
04e3e83db3 | ||
|
|
4273714a62 | ||
|
|
a21e237706 | ||
|
|
aa9105649d | ||
|
|
53be288040 | ||
|
|
13dbf912ca | ||
|
|
69966c73f8 | ||
|
|
a00de2df08 | ||
|
|
5e72f50554 | ||
|
|
d558f15c91 | ||
|
|
614a23698f | ||
|
|
a2191ce6fb | ||
|
|
168350c981 | ||
|
|
f5f55062f1 | ||
|
|
360194e219 | ||
|
|
5ee994c31f | ||
|
|
fc73d4b1f9 | ||
|
|
936f4cb0c6 | ||
|
|
a5b20f973f | ||
|
|
872b1e068f | ||
|
|
e4e0b8fd34 | ||
|
|
c5368e7412 | ||
|
|
1d682544b9 | ||
|
|
d9210af98c | ||
|
|
ef633fe92e | ||
|
|
5500698734 | ||
|
|
e7631763f3 | ||
|
|
18a572b079 | ||
|
|
060a62ecfb | ||
|
|
ac3813549a | ||
|
|
b14da5fb1f | ||
|
|
416f2235fc | ||
|
|
4fabca426a | ||
|
|
7e9050edb9 | ||
|
|
3c49a9b7dd | ||
|
|
ad66b97463 | ||
|
|
10a0b7f60c | ||
|
|
ac8709ac7a | ||
|
|
2d9ed06367 | ||
|
|
50be2aee39 | ||
|
|
0bdbe6261a | ||
|
|
33cef075d2 | ||
|
|
b09ebf4645 | ||
|
|
3268c61813 | ||
|
|
4a221868da | ||
|
|
31b8e3d5ab | ||
|
|
1a78d833a8 | ||
|
|
18888351e9 | ||
|
|
3b7085ca28 | ||
|
|
160923dcf0 | ||
|
|
c38b091895 | ||
|
|
eecfd6d0ca | ||
|
|
6ef4cfa2fa | ||
|
|
190dee080c | ||
|
|
09074dc639 | ||
|
|
1b3d58e791 | ||
|
|
772c83c1d5 | ||
|
|
54dc937fa1 | ||
|
|
b5219f7585 | ||
|
|
0bd0453866 | ||
|
|
8bf36d174b | ||
|
|
9bedd62da4 | ||
|
|
4c34b69ae6 | ||
|
|
7e9ac35666 | ||
|
|
4a46144419 | ||
|
|
a129e122aa | ||
|
|
c0ee6a6d05 | ||
|
|
68ae0d107c | ||
|
|
df63008a94 | ||
|
|
3bd2b340c8 | ||
|
|
df03e182d2 | ||
|
|
862a50d61d | ||
|
|
a7cfd36b07 | ||
|
|
c165360e17 | ||
|
|
9cb0f21b4e | ||
|
|
9c9cbb3e81 | ||
|
|
c24fbb4292 | ||
|
|
99dfe65862 | ||
|
|
4506e5a824 | ||
|
|
b65172a2b7 | ||
|
|
081f100c93 | ||
|
|
f2bdb8159f | ||
|
|
10d749a85e | ||
|
|
a07d149e28 | ||
|
|
3eb982c8cd | ||
|
|
45c4e0b8f8 | ||
|
|
b18b646f8e | ||
|
|
9741a6703c | ||
|
|
27a079d9cb | ||
|
|
2eeb987680 | ||
|
|
e827294c9b | ||
|
|
7cf4ed6ad6 | ||
|
|
ad8a4bc744 | ||
|
|
2630104f18 | ||
|
|
670f470eee | ||
|
|
c2b3c52b76 | ||
|
|
a007d65f62 | ||
|
|
2c924b9fdb | ||
|
|
e8eaa77bf1 | ||
|
|
a07f37073b | ||
|
|
4d760a1984 | ||
|
|
6b7058fe1c | ||
|
|
1149b984d9 | ||
|
|
81fb1b313e | ||
|
|
3a7a2a838e | ||
|
|
10ae43a121 | ||
|
|
c85b970903 | ||
|
|
7044662cfa | ||
|
|
92656fdf29 | ||
|
|
c65e7aff86 | ||
|
|
e97613ef9f | ||
|
|
827469c725 | ||
|
|
613b5fbe48 | ||
|
|
7ed05962db | ||
|
|
250a86ec52 | ||
|
|
0795a577e0 | ||
|
|
8e5607f9c0 | ||
|
|
d6b3bb0807 | ||
|
|
f307a5ce0b | ||
|
|
151c7ed5a2 | ||
|
|
fc13d057f8 | ||
|
|
fc73d3c523 | ||
|
|
5d871b2075 | ||
|
|
529a171d51 | ||
|
|
8dcd39f5b7 | ||
|
|
88477b3ee7 | ||
|
|
0c7e529e6d | ||
|
|
01f75839a9 | ||
|
|
4306f1a339 | ||
|
|
aa2a5057ac | ||
|
|
284c01018e | ||
|
|
22c9e2942b | ||
|
|
d50ae8e4d4 | ||
|
|
e9074e60cf | ||
|
|
541a7a39d3 | ||
|
|
72e464ac3e | ||
|
|
20bf27feda | ||
|
|
d288d21330 | ||
|
|
34f6ffe1d7 | ||
|
|
a11999137f | ||
|
|
a16554d445 | ||
|
|
2553137395 | ||
|
|
6b6b81556f | ||
|
|
ff23f67ad5 | ||
|
|
8f0644e35b | ||
|
|
3fdd23df16 | ||
|
|
2c82ee592c | ||
|
|
1ad529db59 | ||
|
|
96866e52ce | ||
|
|
507c975e92 | ||
|
|
3e69d5276b | ||
|
|
289a4d9b18 | ||
|
|
12bf5f641d | ||
|
|
2051e85e96 | ||
|
|
12b86829d9 | ||
|
|
6c9ec54129 | ||
|
|
b7b0cdbd7c | ||
|
|
fd98c3189a | ||
|
|
1278353616 | ||
|
|
638ec7bc50 | ||
|
|
38ae7d60aa | ||
|
|
2d1f9fc321 | ||
|
|
ee0c8132db | ||
|
|
c2208fa1f9 | ||
|
|
bf42d8b011 | ||
|
|
0deb85fa45 | ||
|
|
da19b10703 | ||
|
|
80b17dab44 | ||
|
|
6d2ffa82de | ||
|
|
7998c3b5ce | ||
|
|
13def91e9a | ||
|
|
26a40610dd | ||
|
|
db2fbed691 | ||
|
|
3d4c1425d9 | ||
|
|
10c8b49590 | ||
|
|
500cea5ce7 | ||
|
|
5aafab118f | ||
|
|
01f8d3b05d | ||
|
|
99d6a28249 | ||
|
|
5eaf7ab586 | ||
|
|
e4f754eee7 | ||
|
|
f20ef61bc7 | ||
|
|
5611ef8b28 | ||
|
|
bec796e3c3 | ||
|
|
0bd8b2c72f | ||
|
|
5550ce47e1 | ||
|
|
2d84dadc0c | ||
|
|
45c0578b22 | ||
|
|
1ded535175 | ||
|
|
d957ab849b | ||
|
|
4b2e52c834 | ||
|
|
6867658c0f | ||
|
|
b8620395cb | ||
|
|
90d37c98f8 | ||
|
|
c9a40917c2 | ||
|
|
0aa0e740cd | ||
|
|
bb17d14665 | ||
|
|
cd0b2ae032 | ||
|
|
8e8796507d | ||
|
|
cef5c29583 | ||
|
|
acaed1f270 | ||
|
|
cda0dbc195 | ||
|
|
758425a8e4 | ||
|
|
93446df335 | ||
|
|
adc8b90e0f | ||
|
|
733c9903ec | ||
|
|
7306e20361 | ||
|
|
b4c7042c17 | ||
|
|
6965787b33 | ||
|
|
ce064b8b0e | ||
|
|
0fc546fc6b | ||
|
|
77ac9e5ec2 | ||
|
|
af2c0b3695 | ||
|
|
811b22367d | ||
|
|
933d50e25a | ||
|
|
800bee2722 | ||
|
|
5b4fb96c2e | ||
|
|
1d20bf343d | ||
|
|
79d9bf57f7 | ||
|
|
7b63db6a13 | ||
|
|
0e1565449e | ||
|
|
f9a47fe5a3 | ||
|
|
2bf9d5d4ec | ||
|
|
c18f9ece69 | ||
|
|
4e3c73c4f5 | ||
|
|
8bf2eeccd0 | ||
|
|
6232e0fc58 | ||
|
|
a8b4aed446 | ||
|
|
03de0c406d | ||
|
|
faf8da8743 | ||
|
|
3386908fd6 | ||
|
|
5a8847952a | ||
|
|
87d21ebf2b | ||
|
|
a524fc545c | ||
|
|
4316edaf43 | ||
|
|
d845924e8b | ||
|
|
a29b322bdd | ||
|
|
9723ffa7a6 | ||
|
|
f06cd88773 | ||
|
|
9af92b6914 | ||
|
|
8f64c4b312 | ||
|
|
a32877e908 | ||
|
|
6465c9c44a | ||
|
|
4699739814 | ||
|
|
c1d87c32a2 | ||
|
|
9c5d9be33a | ||
|
|
97d9c851e6 | ||
|
|
76bd702992 | ||
|
|
50c453e577 | ||
|
|
86d5b25d18 | ||
|
|
2b44dbdbf1 | ||
|
|
4bbbbac5f6 | ||
|
|
3c3a997d2a | ||
|
|
1676f8b5dd | ||
|
|
c87a7469a0 | ||
|
|
132e26ddbf | ||
|
|
f1da70b1de | ||
|
|
5c9d1910af | ||
|
|
18abcab208 | ||
|
|
01e7dc2d02 | ||
|
|
611854e4b6 | ||
|
|
d56dec4ba7 | ||
|
|
c952e9ae3d | ||
|
|
6470243095 | ||
|
|
c8321cfbd9 | ||
|
|
46c246e01f | ||
|
|
9964d8e6c0 | ||
|
|
df33143396 | ||
|
|
571aeaaea2 | ||
|
|
edfea03917 | ||
|
|
81c88cc742 | ||
|
|
99b9390d80 | ||
|
|
23c30521d8 | ||
|
|
e681d610de | ||
|
|
a1fdeded3e | ||
|
|
2051312d12 | ||
|
|
20cb7a76af | ||
|
|
a493aec174 | ||
|
|
3ce3ac8e61 | ||
|
|
91ad64feda | ||
|
|
60b55f9d92 | ||
|
|
3c6c2bf13b | ||
|
|
d4f9375548 | ||
|
|
28b39f547e | ||
|
|
7520f5efa8 | ||
|
|
eb4cdf4b20 | ||
|
|
9f6fc1c3c5 | ||
|
|
dfede9ae6e | ||
|
|
fc45c0c944 | ||
|
|
9d869f784c | ||
|
|
bd244f73af | ||
|
|
dd34556e9c | ||
|
|
f7dd48e60d | ||
|
|
93c779cf48 | ||
|
|
360c04c542 | ||
|
|
529fd57e75 | ||
|
|
faea3777e1 | ||
|
|
a4664e2344 | ||
|
|
cdc1d8a94d | ||
|
|
fdd6d6600f | ||
|
|
9f44cfd595 | ||
|
|
70229b150c | ||
|
|
050ff943a6 | ||
|
|
88b58fd6a0 | ||
|
|
5d67e13df5 | ||
|
|
57d1a60efc | ||
|
|
add81b9739 | ||
|
|
81bdb8e269 | ||
|
|
a563fdd287 | ||
|
|
7c93bf5993 | ||
|
|
6a5a4247c6 | ||
|
|
a39136a2a0 | ||
|
|
9f5b59f336 | ||
|
|
01c125b058 | ||
|
|
d41aa2bc72 | ||
|
|
f45deb37f0 | ||
|
|
e89972a396 | ||
|
|
c3c647a21a | ||
|
|
b79167ce66 | ||
|
|
7ac0a2bc65 | ||
|
|
cb032cff2b | ||
|
|
867a69a751 | ||
|
|
20b8efcc50 | ||
|
|
a86d42149f | ||
|
|
82a36acfe3 | ||
|
|
0793c3f2a3 | ||
|
|
5c860b0d69 | ||
|
|
05bb127a8e | ||
|
|
1bbd84008f | ||
|
|
fdfd4d69d3 | ||
|
|
7f659cce36 | ||
|
|
48fcaa83be | ||
|
|
70c16c4c95 | ||
|
|
c1e1ef6eb5 | ||
|
|
bb155db8b2 | ||
|
|
7c91f668d1 | ||
|
|
1af103d29e | ||
|
|
8a3e581edc | ||
|
|
749e7838a4 | ||
|
|
73b46c2bf9 | ||
|
|
8bd250fb15 | ||
|
|
b1ab641905 | ||
|
|
76e256ed64 | ||
|
|
4f955f2127 | ||
|
|
bbeb579d3a | ||
|
|
f707fb3f8d | ||
|
|
6b98acb7be | ||
|
|
2487b18f62 | ||
|
|
533f64fe26 | ||
|
|
b5c85d3806 | ||
|
|
bcf952bc8a | ||
|
|
a6dc75a44c | ||
|
|
416daca9c6 | ||
|
|
636fe0fb64 | ||
|
|
95e0957d64 | ||
|
|
2eefdae6a9 | ||
|
|
d62746ceb7 | ||
|
|
4b2ce14ff3 | ||
|
|
294a11752e | ||
|
|
1cf1d1f634 | ||
|
|
2ce694d41f | ||
|
|
d6eff3b3a3 | ||
|
|
e63a6d45c1 | ||
|
|
93686519ba | ||
|
|
f593792fb5 | ||
|
|
2cdb37c32b | ||
|
|
535d79b64c | ||
|
|
b4e4c3f662 | ||
|
|
ba676e7ae0 | ||
|
|
a1c8e5af45 | ||
|
|
f1e7e7c138 | ||
|
|
80b77caec0 | ||
|
|
86a2ea44b5 | ||
|
|
a2002c88c6 | ||
|
|
d8bcf4f4e7 | ||
|
|
31e0326f78 | ||
|
|
a53d2ea356 | ||
|
|
229a280652 | ||
|
|
8d0350d923 | ||
|
|
4192d7eacc | ||
|
|
7b8b4cf8c7 | ||
|
|
1f4de75348 | ||
|
|
457755c690 | ||
|
|
052a1e7514 | ||
|
|
139d6e2818 | ||
|
|
06554efdf4 | ||
|
|
67e9bda94f | ||
|
|
53bb6b4c4f | ||
|
|
73d54c7068 | ||
|
|
90d6c4ab41 | ||
|
|
736396fc70 | ||
|
|
177bfed93e | ||
|
|
91f8477ef5 | ||
|
|
f04a5e50ee | ||
|
|
bb28b70700 | ||
|
|
7361a02ef3 | ||
|
|
d465f150fc | ||
|
|
17fa8c117b | ||
|
|
9aa0c40a00 | ||
|
|
fd4648da17 | ||
|
|
aadca5013a | ||
|
|
5c3d490e59 | ||
|
|
1254f48135 | ||
|
|
1729c310d9 | ||
|
|
0130190bbd | ||
|
|
97a31ddffc | ||
|
|
3249420ad1 | ||
|
|
4bb8536d34 | ||
|
|
c73d4a137e | ||
|
|
57ac8f2741 | ||
|
|
2f1acee5a1 | ||
|
|
9ca54020ac | ||
|
|
f7d44b178b | ||
|
|
b4950a157c | ||
|
|
dfbef066c7 | ||
|
|
26fd76fbee | ||
|
|
04769d8a26 | ||
|
|
34b576d9b5 | ||
|
|
22b244f847 | ||
|
|
7e1fc275e7 | ||
|
|
3b9b391320 | ||
|
|
766bfd025c | ||
|
|
c7f30e1065 | ||
|
|
1c4fd7f28f | ||
|
|
85805d2c38 | ||
|
|
982cb3e71a | ||
|
|
294d0e7ee3 | ||
|
|
8be1ca836c | ||
|
|
2e5f96fa41 | ||
|
|
c056b0add9 | ||
|
|
b00bb3c083 | ||
|
|
d9befd3aa6 | ||
|
|
49de703ba1 | ||
|
|
22988894c8 | ||
|
|
34b1754f25 | ||
|
|
54fe3504ba | ||
|
|
d2c862e32d | ||
|
|
afc53afb35 | ||
|
|
b56e49c5dc | ||
|
|
8b2a909e1f | ||
|
|
e9c954d45e | ||
|
|
6f449d13af | ||
|
|
6e375bef0d | ||
|
|
67106a6967 | ||
|
|
b5d690620d | ||
|
|
9db3ce1d0b | ||
|
|
1cc55b68ef | ||
|
|
469f667774 | ||
|
|
6603d9a9f0 | ||
|
|
5dc1920a4c | ||
|
|
d3e5f3f3a8 | ||
|
|
ce4cb820f7 | ||
|
|
ba5be6b625 | ||
|
|
f95c3f4177 | ||
|
|
d2b1307bff | ||
|
|
b40ba32adc | ||
|
|
ce0cebb7d7 | ||
|
|
f478f89a68 | ||
|
|
85d95f0f2b | ||
|
|
1515efc77c | ||
|
|
6d393759e1 | ||
|
|
a1701678cd | ||
|
|
c411a26d6f | ||
|
|
85dbfeb314 | ||
|
|
085c0e4e2b | ||
|
|
8404a97c3e | ||
|
|
0ee3b1ede2 | ||
|
|
a826936702 | ||
|
|
fd4a5d5a63 | ||
|
|
69cf1d7b7e | ||
|
|
8e0a1d1167 | ||
|
|
f22021187d | ||
|
|
febecc348a | ||
|
|
c5ccfc3e94 | ||
|
|
1f6efc6b94 | ||
|
|
727fe6f942 | ||
|
|
a91e79382e | ||
|
|
5c626e0a2f | ||
|
|
8e9e383219 | ||
|
|
f383008cc1 | ||
|
|
303ade25ed | ||
|
|
53f8e7850e | ||
|
|
ca8ce88354 | ||
|
|
37a86439c4 | ||
|
|
269b43f4de | ||
|
|
3f25e5bf86 | ||
|
|
67765fa47c | ||
|
|
58b1c58bc5 | ||
|
|
d80badc50f | ||
|
|
75279e5ccf | ||
|
|
7893b84614 | ||
|
|
cfc715bd48 | ||
|
|
39bcba85a9 | ||
|
|
da3df51316 | ||
|
|
12190e4efc | ||
|
|
d2a9b2f64a | ||
|
|
aacadd8a8a | ||
|
|
969154a473 | ||
|
|
4d6ca3fab1 | ||
|
|
00ea5082e7 | ||
|
|
4a878b88c0 | ||
|
|
6de955847c | ||
|
|
3ba5d528b4 | ||
|
|
f99e2b3429 | ||
|
|
7e4e6f6e51 | ||
|
|
0514f3f43b | ||
|
|
1e07384364 | ||
|
|
4c4739c422 | ||
|
|
2d8b90a6ff | ||
|
|
a2fa7ffa42 | ||
|
|
f7d6175283 | ||
|
|
9ed187ee52 | ||
|
|
14d81e574b | ||
|
|
6efe8cc8df | ||
|
|
daa5fc916a | ||
|
|
c659496b96 | ||
|
|
21fbf21cb6 | ||
|
|
f31cbf2744 | ||
|
|
8322f18e03 | ||
|
|
562bdb95e2 | ||
|
|
a57ce8365d | ||
|
|
0da83ae67e | ||
|
|
662d022a48 | ||
|
|
9efef03919 | ||
|
|
7a9fb3fa92 | ||
|
|
ea96ead346 | ||
|
|
6100a77b85 | ||
|
|
c7a59ee2b1 | ||
|
|
a272b58fe9 | ||
|
|
9948fcf1b6 | ||
|
|
0d50c867ff | ||
|
|
27f7e02f12 | ||
|
|
0f93ecd564 | ||
|
|
da909d9684 | ||
|
|
facd851b11 | ||
|
|
c51de945a5 | ||
|
|
9253a3ca9e | ||
|
|
7cfa297a78 | ||
|
|
661b74def6 | ||
|
|
b478e5655c | ||
|
|
f884766445 | ||
|
|
76b2e4539c | ||
|
|
d87922c0eb | ||
|
|
2446483df5 | ||
|
|
f4c453155d | ||
|
|
969ad80ed2 | ||
|
|
af064b41d7 | ||
|
|
ea6bfef21a | ||
|
|
107363b1d9 | ||
|
|
85214d7c59 | ||
|
|
997cb2d945 | ||
|
|
45b139390c | ||
|
|
994368de15 | ||
|
|
143fd8e076 | ||
|
|
06dba28bd6 | ||
|
|
b8d276a049 | ||
|
|
ee01f01271 | ||
|
|
32d5db4f0a | ||
|
|
f6108b7be8 | ||
|
|
94ef341c9d | ||
|
|
f9abc7c84f | ||
|
|
891ed6ebc0 | ||
|
|
163e23a68b | ||
|
|
f13b0af491 | ||
|
|
4a0be45d3d | ||
|
|
23788674c8 | ||
|
|
121eb24e73 | ||
|
|
571d60182a | ||
|
|
167a9dcaf3 | ||
|
|
37327259cb | ||
|
|
cdb25656d5 | ||
|
|
25c876caa2 | ||
|
|
cf83e31f23 | ||
|
|
3bc238b58b | ||
|
|
b8de69dced | ||
|
|
e7fcb692a4 | ||
|
|
dae38574ab | ||
|
|
ed4f862b49 | ||
|
|
fce59db94a | ||
|
|
3e2a0c7281 | ||
|
|
5a0910ea79 | ||
|
|
1dffabcfda | ||
|
|
c389e0ed43 | ||
|
|
204801052a | ||
|
|
2528d8cb88 | ||
|
|
6b73ffd1c1 | ||
|
|
0eadc50a33 | ||
|
|
aeea84a877 | ||
|
|
a54c5c6298 | ||
|
|
8825cd3811 | ||
|
|
3d9a5d9970 | ||
|
|
1f9e195fa6 | ||
|
|
73c012c76c | ||
|
|
2ace57404b | ||
|
|
8c4b5e088b | ||
|
|
69920a73d7 | ||
|
|
ae76a3467a | ||
|
|
701107cda4 | ||
|
|
b99565959b | ||
|
|
67aa7ce04d | ||
|
|
c663fbc3ee | ||
|
|
2090bab537 | ||
|
|
64d5fff9a3 | ||
|
|
925f695503 | ||
|
|
f1c925795d | ||
|
|
c82a060eca | ||
|
|
63e783ef79 | ||
|
|
35d6273fb3 | ||
|
|
b89d4a16fd | ||
|
|
2799a96032 | ||
|
|
8f4b79227c | ||
|
|
c810b6d206 | ||
|
|
fa35407572 | ||
|
|
8bbbc07aff | ||
|
|
75a21ba3ce | ||
|
|
0d6fb68a88 | ||
|
|
242b886434 | ||
|
|
caf465a9da | ||
|
|
bbf77c6139 | ||
|
|
53b7e04b86 | ||
|
|
9e75e3ed18 | ||
|
|
6389858d41 | ||
|
|
7e5941e14b | ||
|
|
c68aeed8d9 | ||
|
|
b199a609a8 | ||
|
|
4a5a93b3f8 | ||
|
|
e99bdcefac | ||
|
|
26dcb85de1 | ||
|
|
11d042be25 | ||
|
|
33b5fe236a | ||
|
|
d56991006c | ||
|
|
739a9f71c3 | ||
|
|
aef81fce0b | ||
|
|
8f3d7b4038 | ||
|
|
de15e67834 | ||
|
|
fea56d8de6 | ||
|
|
3d71be2b45 | ||
|
|
58baca2a5b | ||
|
|
ef73926db6 | ||
|
|
9ad1687f04 | ||
|
|
c573270e66 | ||
|
|
9ebad68274 | ||
|
|
03664ba588 | ||
|
|
5a107b275c | ||
|
|
dd5736fe5f | ||
|
|
9f3ba03965 | ||
|
|
d090c08ef0 | ||
|
|
68e82e4d94 | ||
|
|
a4aa0e6f8d | ||
|
|
8c1ae2717c | ||
|
|
72d48759d7 | ||
|
|
986144b377 | ||
|
|
1fdb326aa7 | ||
|
|
463257e7e4 | ||
|
|
0f41e60bd6 | ||
|
|
7df81f7b3e | ||
|
|
dd22cb2bb0 | ||
|
|
248325925f | ||
|
|
ca48a4f0fb | ||
|
|
98ee5a3d87 | ||
|
|
67480e5a1c | ||
|
|
2581a9b54c | ||
|
|
14a293e124 | ||
|
|
780419ecae | ||
|
|
f0962e2d9c | ||
|
|
3a9584a419 | ||
|
|
196f42cbff | ||
|
|
322385f6b1 | ||
|
|
b7446cd7b9 | ||
|
|
f618e569ab | ||
|
|
7b394b91e2 | ||
|
|
6a7983a4ea | ||
|
|
737146fca1 | ||
|
|
688f3fd12f | ||
|
|
145df08444 | ||
|
|
8b400515ea | ||
|
|
289797f56d | ||
|
|
be0811ecc3 | ||
|
|
0676bcd4fd | ||
|
|
d076def561 | ||
|
|
e0807d7317 | ||
|
|
fa2723f2d0 | ||
|
|
87d62514db | ||
|
|
2f8cf9146b | ||
|
|
8e0ec6b037 | ||
|
|
6dc434cb83 | ||
|
|
d972c27f03 | ||
|
|
9e2bb63688 | ||
|
|
49053b66a9 | ||
|
|
47497aef07 | ||
|
|
8455029de1 | ||
|
|
9f07f89384 | ||
|
|
d840d43e8f | ||
|
|
9ead2f3dfb | ||
|
|
f3742ddbb8 | ||
|
|
b61a841aa8 | ||
|
|
ebcf11e574 | ||
|
|
065f0aaddf | ||
|
|
c0773dc7c5 | ||
|
|
1c3c74bd36 | ||
|
|
79bbf90b72 | ||
|
|
226a4a7f36 | ||
|
|
df3b424830 | ||
|
|
3cfd9d80bc | ||
|
|
e0553b8d2c | ||
|
|
391c837b37 | ||
|
|
5773d9d1a3 | ||
|
|
ce611963c3 | ||
|
|
f865cacfb8 | ||
|
|
2ec0611f42 | ||
|
|
334161a30e | ||
|
|
dbb6e55226 | ||
|
|
d0f9260559 | ||
|
|
d2176064e1 | ||
|
|
ed8d277e49 | ||
|
|
59b3268c64 | ||
|
|
d043f67761 | ||
|
|
51bf193889 | ||
|
|
f8b78f08b4 | ||
|
|
a4f32d602b | ||
|
|
dc3dd21cf3 | ||
|
|
b4c2fcccf5 | ||
|
|
e950ad5306 | ||
|
|
8ca713b737 | ||
|
|
5b54554fd5 | ||
|
|
4bc651f958 | ||
|
|
3b6976a9c8 | ||
|
|
863d5c1e8e | ||
|
|
97e19e9677 | ||
|
|
b27851461f | ||
|
|
209687377a | ||
|
|
90face1c09 | ||
|
|
936e2ce48b | ||
|
|
16ee8ee379 | ||
|
|
ac39308dad | ||
|
|
346b49219d | ||
|
|
d84c1f20c7 | ||
|
|
dfb8777555 | ||
|
|
008af18156 | ||
|
|
ab23167f80 | ||
|
|
b17ec46463 | ||
|
|
2e26b58d16 | ||
|
|
31b56e5a05 | ||
|
|
47c401cf25 | ||
|
|
fab8dc9e6f | ||
|
|
f39a2b1f16 | ||
|
|
66830ced4e | ||
|
|
9d3fad754d | ||
|
|
dcd3131f58 | ||
|
|
3d02e07161 | ||
|
|
4dbc6a43a6 | ||
|
|
5394b5188b | ||
|
|
8e680b3957 | ||
|
|
1b8cd796d6 | ||
|
|
35fba793d0 | ||
|
|
5358d43b74 | ||
|
|
f777347bac | ||
|
|
17c8b914df | ||
|
|
43b467dd12 | ||
|
|
0e0770921e | ||
|
|
8edbb74352 | ||
|
|
e6bfa95758 | ||
|
|
e4120b6287 | ||
|
|
ccbc9e00f2 | ||
|
|
7d13baadc8 | ||
|
|
9acc83697f | ||
|
|
db24bf87c0 | ||
|
|
f4c0d2d2fd | ||
|
|
d240f4c676 | ||
|
|
9c90cdbe08 | ||
|
|
fc7af31fe5 | ||
|
|
2f8d23ec66 | ||
|
|
77ae3fb9b9 | ||
|
|
4e7f6c47fd | ||
|
|
50469ed750 | ||
|
|
aaab785493 | ||
|
|
9751937894 | ||
|
|
0fc8dfc77e | ||
|
|
81b7df61ec | ||
|
|
8217b96d4a | ||
|
|
7dd0918d32 | ||
|
|
4b26b43855 | ||
|
|
9d7cfda9fe | ||
|
|
a3cf18c905 | ||
|
|
0b1a8ae699 | ||
|
|
eb70b1e5c8 | ||
|
|
00a3d818b6 | ||
|
|
2384c7e734 | ||
|
|
1bad3d9894 | ||
|
|
4f715e66dc | ||
|
|
ec001ca02f | ||
|
|
a2d3b9f0c8 | ||
|
|
9cfb6ff964 | ||
|
|
6ed661c140 | ||
|
|
9dc00edfc9 | ||
|
|
e063bf888e | ||
|
|
6f18475428 | ||
|
|
3664b09812 | ||
|
|
7050cc0ac3 | ||
|
|
4d3d63294d | ||
|
|
6bc61cbc2d | ||
|
|
01d351bebe | ||
|
|
dbba4a97aa | ||
|
|
0dc586faef | ||
|
|
f19c6b05f2 | ||
|
|
bc34f08333 | ||
|
|
b7ee16aabd | ||
|
|
ed1b0d97bf | ||
|
|
8d3b2fb821 | ||
|
|
fa991920bc | ||
|
|
5e79e3d7a5 | ||
|
|
966015c9ae | ||
|
|
61f057337a | ||
|
|
0b261054a2 | ||
|
|
e2e481cbb5 | ||
|
|
5140e83012 | ||
|
|
100d6212be | ||
|
|
f0e19a6542 | ||
|
|
00c4d4f9f8 | ||
|
|
6e6fe6e013 | ||
|
|
d05b60291e | ||
|
|
5162361372 | ||
|
|
d271b9f75b | ||
|
|
333569bed3 | ||
|
|
09b89fdb23 | ||
|
|
0e8c3359d1 | ||
|
|
37e0a7050f | ||
|
|
774dcb6980 | ||
|
|
28bc49ad17 | ||
|
|
dc1947838c | ||
|
|
3ea2daaa4c | ||
|
|
137e964131 | ||
|
|
8efbe497fd | ||
|
|
119d2d966c | ||
|
|
194415e785 | ||
|
|
1684042fb6 | ||
|
|
59f0004d34 | ||
|
|
da35a64fa1 | ||
|
|
460338ca53 | ||
|
|
53c18a64b4 | ||
|
|
b8144c5654 | ||
|
|
9081e17fcc | ||
|
|
ef3fd5900f | ||
|
|
453d690c11 | ||
|
|
c45be6a645 | ||
|
|
7b9b177088 | ||
|
|
3cee5b0470 | ||
|
|
9246d1c901 | ||
|
|
cc12abc83e | ||
|
|
4f7e4a9436 | ||
|
|
eee396f903 | ||
|
|
0d2f8e175a | ||
|
|
4df40e0d9b | ||
|
|
b72e17a8b7 | ||
|
|
61160dc220 | ||
|
|
98734ff28c | ||
|
|
9991352663 | ||
|
|
91c4da5dbd | ||
|
|
2fd0e7dd6b | ||
|
|
d50b7ad481 | ||
|
|
df95c49401 | ||
|
|
8b73c52f00 | ||
|
|
5603098d17 | ||
|
|
f436a50125 | ||
|
|
e19e977591 | ||
|
|
addbe295b1 | ||
|
|
9a573dedc6 | ||
|
|
9ea0d71e8d | ||
|
|
b1a3599017 | ||
|
|
7b0329f67f | ||
|
|
311b9c74dd | ||
|
|
f7e8dd2ff8 | ||
|
|
40b1dd7ef2 | ||
|
|
261e76e0a3 | ||
|
|
a300bfaccb | ||
|
|
41dba0db08 | ||
|
|
6674c6083a | ||
|
|
f6afa2c6bb | ||
|
|
b2fb0508ea | ||
|
|
93f4252bb1 | ||
|
|
46ab9c16dd | ||
|
|
d869df4fee | ||
|
|
b99d4650ec | ||
|
|
261bb7f110 | ||
|
|
0515fbb260 | ||
|
|
88211d8c5b | ||
|
|
a812f95b9d | ||
|
|
3728a12bee | ||
|
|
af07e51213 | ||
|
|
3113788c92 | ||
|
|
efb5fe6d4e | ||
|
|
54dd6c644d | ||
|
|
39ad8f2667 | ||
|
|
c4a2c84e53 | ||
|
|
44fe012812 | ||
|
|
f5e7f079ea | ||
|
|
15a8936806 | ||
|
|
4e4cff49c0 | ||
|
|
5540503bee | ||
|
|
193718034b | ||
|
|
72108c0296 | ||
|
|
ec1c9f8cd1 | ||
|
|
a85b0a370e | ||
|
|
e7784d2864 | ||
|
|
97c4815444 | ||
|
|
7d1a1663c8 | ||
|
|
24c0ce6e53 | ||
|
|
4cdc86612c | ||
|
|
f1f3f8d12c | ||
|
|
e78d3b54bf | ||
|
|
f8a7cd372d | ||
|
|
f48eac638d | ||
|
|
e1f12f93eb | ||
|
|
7ca8334a8b | ||
|
|
f1a2b2eba4 | ||
|
|
4b132656df | ||
|
|
26bab00dab | ||
|
|
568c04753e | ||
|
|
4a06e164d2 | ||
|
|
c57b52c300 | ||
|
|
0b8f48f17f | ||
|
|
3862184ccb | ||
|
|
8619c50976 | ||
|
|
bb6b56b72a | ||
|
|
1252b65166 | ||
|
|
6840276dad | ||
|
|
bd8c3cd0f1 | ||
|
|
e5e9b3e3c0 | ||
|
|
1e8a681de9 | ||
|
|
a834bedc17 | ||
|
|
6a3392385e | ||
|
|
6a00e063c4 | ||
|
|
73a0ce2b7d | ||
|
|
4d1afd01fa | ||
|
|
801d5f47bd | ||
|
|
b6caae9708 | ||
|
|
183ca64ef9 | ||
|
|
8c32cfe829 | ||
|
|
73dcc88da1 | ||
|
|
14bded65dc | ||
|
|
87d1d3fb62 | ||
|
|
e054454109 | ||
|
|
a6142cf975 | ||
|
|
69332e5fa3 | ||
|
|
20201ba3c4 | ||
|
|
658067186a | ||
|
|
ac777b77cf | ||
|
|
5944ae2023 | ||
|
|
2f10961ba8 | ||
|
|
fae97978a3 | ||
|
|
3423415e49 | ||
|
|
1d0bfc2b2a | ||
|
|
bd46cf0f86 | ||
|
|
d4157d9a96 | ||
|
|
6e4ef585d8 | ||
|
|
e05c3b7a76 | ||
|
|
f99904bc1c | ||
|
|
b796d6763f | ||
|
|
c1250abdf8 | ||
|
|
ebe51534a1 | ||
|
|
b8bbee4718 | ||
|
|
8f852b396f | ||
|
|
ae4d089c06 |
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
insert_final_newline = true
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
max_line_length = 80
|
||||||
@@ -3,7 +3,8 @@ name: deploy
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dontlook
|
- dev
|
||||||
|
- production
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -16,10 +17,11 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v1
|
- uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: 1.2.17
|
||||||
|
|
||||||
- run: bun install
|
- run: bun install
|
||||||
|
|
||||||
- run: bun sst deploy --stage=dev
|
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||||
env:
|
env:
|
||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: discord
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published] # fires only when a release is published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Send nicely-formatted embed to Discord
|
||||||
|
uses: SethCohen/github-releases-to-discord@v1
|
||||||
|
with:
|
||||||
|
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: opencode
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
opencode:
|
||||||
|
if: |
|
||||||
|
contains(github.event.comment.body, ' /oc') ||
|
||||||
|
startsWith(github.event.comment.body, '/oc') ||
|
||||||
|
contains(github.event.comment.body, ' /opencode') ||
|
||||||
|
startsWith(github.event.comment.body, '/opencode')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Run opencode
|
||||||
|
uses: sst/opencode/github@latest
|
||||||
|
env:
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
with:
|
||||||
|
model: anthropic/claude-sonnet-4-20250514
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: publish-github-action
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "github-v*.*.*"
|
||||||
|
- "!github-v1"
|
||||||
|
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: |
|
||||||
|
git config --global user.email "opencode@sst.dev"
|
||||||
|
git config --global user.name "opencode"
|
||||||
|
./script/publish
|
||||||
|
working-directory: ./github
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: publish-vscode
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "vscode-v*.*.*"
|
||||||
|
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.2.17
|
||||||
|
|
||||||
|
- run: git fetch --force --tags
|
||||||
|
- run: bun install -g @vscode/vsce
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: |
|
||||||
|
bun install
|
||||||
|
./script/publish
|
||||||
|
working-directory: ./sdks/vscode
|
||||||
|
env:
|
||||||
|
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||||
|
OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
name: publish
|
name: publish
|
||||||
|
run-name: "${{ format('v{0}', inputs.version) }}"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
inputs:
|
||||||
branches:
|
version:
|
||||||
- dev
|
description: "Version to publish"
|
||||||
tags:
|
required: true
|
||||||
- "*"
|
type: string
|
||||||
|
title:
|
||||||
|
description: "Custom title for this run"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
@@ -32,7 +37,16 @@ jobs:
|
|||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.16
|
bun-version: 1.2.19
|
||||||
|
|
||||||
|
- name: Cache ~/.bun
|
||||||
|
id: cache-bun
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.bun
|
||||||
|
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
- name: Install makepkg
|
- name: Install makepkg
|
||||||
run: |
|
run: |
|
||||||
@@ -48,15 +62,12 @@ jobs:
|
|||||||
git config --global user.email "opencode@sst.dev"
|
git config --global user.email "opencode@sst.dev"
|
||||||
git config --global user.name "opencode"
|
git config --global user.name "opencode"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: |
|
run: |
|
||||||
bun install
|
OPENCODE_VERSION=${{ inputs.version }} ./script/publish.ts
|
||||||
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
|
|
||||||
./script/publish.ts
|
|
||||||
else
|
|
||||||
./script/publish.ts --snapshot
|
|
||||||
fi
|
|
||||||
working-directory: ./packages/opencode
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: stats
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||||
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stats:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Run stats script
|
||||||
|
run: bun script/stats.ts
|
||||||
|
|
||||||
|
- name: Commit stats
|
||||||
|
run: |
|
||||||
|
git config --local user.email "action@github.com"
|
||||||
|
git config --local user.name "GitHub Action"
|
||||||
|
git add STATS.md
|
||||||
|
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
|
||||||
|
git push
|
||||||
|
env:
|
||||||
|
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Typecheck
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [dev]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
typecheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: 1.2.19
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Run typecheck
|
||||||
|
run: bun typecheck
|
||||||
+2
-3
@@ -1,9 +1,8 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
.opencode
|
|
||||||
.sst
|
.sst
|
||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
app.log
|
openapi.json
|
||||||
gopls.log
|
playground
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
model: openai/gpt-5
|
||||||
|
reasoningEffort: medium
|
||||||
|
description: ALWAYS use this when writing docs
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an expert technical documentation writer
|
||||||
|
|
||||||
|
You are not verbose
|
||||||
|
|
||||||
|
Every chunk of text should be followed by an example or something besides text
|
||||||
|
to look at.
|
||||||
|
|
||||||
|
Chunks of text should not be more than 2 sentences long.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
## IMPORTANT
|
||||||
|
|
||||||
|
- Try to keep things in one function unless composable or reusable
|
||||||
|
- DO NOT do unnecessary destructuring of variables
|
||||||
|
- DO NOT use `else` statements unless necessary
|
||||||
|
- DO NOT use `try`/`catch` if it can be avoided
|
||||||
|
- AVOID `try`/`catch` where possible
|
||||||
|
- AVOID `else` statements
|
||||||
|
- AVOID using `any` type
|
||||||
|
- AVOID `let` statements
|
||||||
|
- PREFER single word variable names where possible
|
||||||
|
- Use as many bun apis as possible like Bun.file()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 OpenCode
|
Copyright (c) 2025 opencode
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://opencode.ai">
|
<a href="https://opencode.ai">
|
||||||
<picture>
|
<picture>
|
||||||
<source srcset="packages/web/src/assets/logo-dark.svg" media="(prefers-color-scheme: dark)">
|
<source srcset="packages/web/src/assets/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
<source srcset="packages/web/src/assets/logo-light.svg" media="(prefers-color-scheme: light)">
|
<source srcset="packages/web/src/assets/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
|
||||||
<img src="packages/web/src/assets/logo-light.svg" alt="opencode logo">
|
<img src="packages/web/src/assets/logo-ornate-light.svg" alt="opencode logo">
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p align="center">AI coding agent, built for the terminal.</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
[](https://opencode.ai)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
AI coding agent, built for the terminal.
|
|
||||||
|
|
||||||
**Note:** Version 0.1.x is a full rewrite, and we do not have proper documentation for it yet. Should have this out week of June 17th 2025.
|
|
||||||
|
|
||||||
[](https://opencode.ai)
|
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -28,193 +26,85 @@ curl -fsSL https://opencode.ai/install | bash
|
|||||||
|
|
||||||
# Package managers
|
# Package managers
|
||||||
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||||
brew install sst/tap/opencode # macOS
|
brew install sst/tap/opencode # macOS and Linux
|
||||||
paru -S opencode-bin # Arch Linux
|
paru -S opencode-bin # Arch Linux
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** Remove versions older than 0.1.x before installing
|
> [!TIP]
|
||||||
|
> Remove versions older than 0.1.x before installing.
|
||||||
|
|
||||||
### Providers
|
#### Installation Directory
|
||||||
|
|
||||||
The recommended approach is to sign up for Claude Pro or Max, run `opencode auth login`, and select Anthropic. It's the most cost-effective way to use opencode.
|
The install script respects the following priority order for the installation path:
|
||||||
|
|
||||||
opencode is powered by the provider list at [Models.dev](https://models.dev), so you can use `opencode auth login` to configure API keys for any provider you'd like to use. This is stored in `~/.local/share/opencode/auth.json`.
|
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
|
||||||
|
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
|
||||||
|
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
|
||||||
|
4. `$HOME/.opencode/bin` - Default fallback
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ opencode auth login
|
# Examples
|
||||||
|
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
┌ Add credential
|
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||||
│
|
|
||||||
◆ Select provider
|
|
||||||
│ ● Anthropic (recommended)
|
|
||||||
│ ○ OpenAI
|
|
||||||
│ ○ Google
|
|
||||||
│ ○ Amazon Bedrock
|
|
||||||
│ ○ Azure
|
|
||||||
│ ○ DeepSeek
|
|
||||||
│ ○ Groq
|
|
||||||
│ ...
|
|
||||||
└
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The Models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
|
### Documentation
|
||||||
|
|
||||||
If there are additional providers you want to use you can submit a PR to the [Models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below.
|
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
|
||||||
|
|
||||||
### Global Config
|
|
||||||
|
|
||||||
Some basic configuration is available in the global config file.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/opencode/config
|
|
||||||
theme = "opencode"
|
|
||||||
provider = "anthropic"
|
|
||||||
model = "claude-sonnet-4-20250514"
|
|
||||||
autoupdate = true
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also extend the models.dev database with your own providers by mirroring the structure found [here](https://github.com/sst/models.dev/tree/dev/providers/anthropic)
|
|
||||||
|
|
||||||
Start with a `provider.toml` file in `~/.config/opencode/providers`
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/opencode/providers/openrouter/provider.toml
|
|
||||||
[provider]
|
|
||||||
name = "OpenRouter"
|
|
||||||
env = ["OPENROUTER_API_KEY"]
|
|
||||||
npm = "@openrouter/ai-sdk-provider"
|
|
||||||
```
|
|
||||||
|
|
||||||
And models in `~/.config/opencode/providers/openrouter/models/[model-id]`
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# ~/.config/opencode/providers/openrouter/models/anthropic/claude-3.5-sonnet.toml
|
|
||||||
name = "Claude 4 Sonnet"
|
|
||||||
attachment = true
|
|
||||||
reasoning = false
|
|
||||||
temperature = true
|
|
||||||
|
|
||||||
[cost]
|
|
||||||
input = 3.00
|
|
||||||
output = 15.00
|
|
||||||
inputCached = 3.75
|
|
||||||
outputCached = 0.30
|
|
||||||
|
|
||||||
[limit]
|
|
||||||
context = 200_000
|
|
||||||
output = 50_000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Config
|
|
||||||
|
|
||||||
Project configuration is optional. You can place an `opencode.json` file in the root of your repo and is meant to be checked in and shared with your team.
|
|
||||||
|
|
||||||
```json title="opencode.json"
|
|
||||||
{
|
|
||||||
"$schema": "http://opencode.ai/config.json"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### MCP
|
|
||||||
|
|
||||||
```json title="opencode.json"
|
|
||||||
{
|
|
||||||
"$schema": "http://opencode.ai/config.json",
|
|
||||||
"mcp": {
|
|
||||||
"localmcp": {
|
|
||||||
"type": "local",
|
|
||||||
"command": ["bun", "x", "my-mcp-command"],
|
|
||||||
"environment": {
|
|
||||||
"MY_ENV_VAR": "my_env_var_value"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"remotemcp": {
|
|
||||||
"type": "remote",
|
|
||||||
"url": "https://my-mcp-server.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Providers
|
|
||||||
|
|
||||||
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider.
|
|
||||||
|
|
||||||
```json title="opencode.json"
|
|
||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"provider": {
|
|
||||||
"ollama": {
|
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
|
||||||
"options": {
|
|
||||||
"baseURL": "http://localhost:11434/v1"
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"llama2": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
|
opencode is an opinionated tool so any fundamental feature needs to go through a
|
||||||
|
design process with the core team.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> We do not accept PRs for core features.
|
||||||
|
|
||||||
|
However we still merge a ton of PRs - you can contribute:
|
||||||
|
|
||||||
|
- Bug fixes
|
||||||
|
- Improvements to LLM performance
|
||||||
|
- Support for new providers
|
||||||
|
- Fixes for env specific quirks
|
||||||
|
- Missing standard behavior
|
||||||
|
- Documentation
|
||||||
|
|
||||||
|
Take a look at the git history to see what kind of PRs we end up merging.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you do not follow the above guidelines we might close your PR.
|
||||||
|
|
||||||
To run opencode locally you need.
|
To run opencode locally you need.
|
||||||
|
|
||||||
- Bun
|
- Bun
|
||||||
- Golang 1.24.x
|
- Golang 1.24.x
|
||||||
|
|
||||||
To run.
|
And run.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ bun install
|
$ bun install
|
||||||
$ cd packages/opencode
|
$ bun dev
|
||||||
$ bun run src/index.ts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Development Notes
|
||||||
|
|
||||||
|
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
|
|
||||||
#### How do I use this with OpenRouter?
|
|
||||||
|
|
||||||
OpenRouter is not in the Models.dev database yet, but you can configure it manually.
|
|
||||||
|
|
||||||
```json title="opencode.json"
|
|
||||||
{
|
|
||||||
"$schema": "https://opencode.ai/config.json",
|
|
||||||
"provider": {
|
|
||||||
"openrouter": {
|
|
||||||
"npm": "@openrouter/ai-sdk-provider",
|
|
||||||
"name": "OpenRouter",
|
|
||||||
"options": {
|
|
||||||
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
},
|
|
||||||
"models": {
|
|
||||||
"anthropic/claude-3.5-sonnet": {
|
|
||||||
"name": "Claude 3.5 Sonnet"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### How is this different than Claude Code?
|
#### How is this different than Claude Code?
|
||||||
|
|
||||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
- 100% open source
|
- 100% open source
|
||||||
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
|
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||||
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||||
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||||
|
|
||||||
#### What about Windows support?
|
|
||||||
|
|
||||||
There are some minor problems blocking opencode from working on windows. We are working on on them now. You'll need to use WSL for now.
|
|
||||||
|
|
||||||
#### What's the other repo?
|
#### What's the other repo?
|
||||||
|
|
||||||
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
|
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
**Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Download Stats
|
||||||
|
|
||||||
|
| Date | GitHub Downloads | npm Downloads | Total |
|
||||||
|
| ---------- | ---------------- | ---------------- | ---------------- |
|
||||||
|
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||||
|
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||||
|
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||||
|
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||||
|
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||||
|
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||||
|
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||||
|
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||||
|
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||||
|
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||||
|
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||||
|
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||||
|
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||||
|
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||||
|
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||||
|
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||||
|
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||||
|
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||||
|
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||||
|
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||||
|
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||||
|
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||||
|
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||||
|
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||||
|
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||||
|
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||||
|
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||||
|
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||||
|
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||||
|
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||||
|
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||||
|
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||||
|
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||||
|
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||||
|
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||||
|
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||||
|
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||||
|
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||||
|
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||||
|
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||||
|
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
||||||
|
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
|
||||||
|
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit"
|
||||||
|
import { Resource } from "sst"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
out: "./migrations/",
|
||||||
|
strict: true,
|
||||||
|
schema: ["./src/**/*.sql.ts"],
|
||||||
|
verbose: true,
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
database: Resource.Database.database,
|
||||||
|
host: Resource.Database.host,
|
||||||
|
user: Resource.Database.username,
|
||||||
|
password: Resource.Database.password,
|
||||||
|
port: Resource.Database.port,
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
CREATE TABLE "billing" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"customer_id" varchar(255),
|
||||||
|
"payment_method_id" varchar(255),
|
||||||
|
"payment_method_last4" varchar(4),
|
||||||
|
"balance" bigint NOT NULL,
|
||||||
|
"reload" boolean,
|
||||||
|
CONSTRAINT "billing_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "payment" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"customer_id" varchar(255),
|
||||||
|
"payment_id" varchar(255),
|
||||||
|
"amount" bigint NOT NULL,
|
||||||
|
CONSTRAINT "payment_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "usage" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"request_id" varchar(255),
|
||||||
|
"model" varchar(255) NOT NULL,
|
||||||
|
"input_tokens" integer NOT NULL,
|
||||||
|
"output_tokens" integer NOT NULL,
|
||||||
|
"reasoning_tokens" integer,
|
||||||
|
"cache_read_tokens" integer,
|
||||||
|
"cache_write_tokens" integer,
|
||||||
|
"cost" bigint NOT NULL,
|
||||||
|
CONSTRAINT "usage_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"time_seen" timestamp with time zone,
|
||||||
|
"color" integer,
|
||||||
|
CONSTRAINT "user_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "workspace" (
|
||||||
|
"id" varchar(30) PRIMARY KEY NOT NULL,
|
||||||
|
"slug" varchar(255),
|
||||||
|
"name" varchar(255),
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "billing" ADD CONSTRAINT "billing_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "payment" ADD CONSTRAINT "payment_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "usage" ADD CONSTRAINT "usage_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" ADD CONSTRAINT "user_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("workspace_id","email");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "slug" ON "workspace" USING btree ("slug");
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE "account" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"email" varchar(255) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "email" ON "account" USING btree ("email");
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE "key" (
|
||||||
|
"id" varchar(30) NOT NULL,
|
||||||
|
"workspace_id" varchar(30) NOT NULL,
|
||||||
|
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"time_deleted" timestamp with time zone,
|
||||||
|
"user_id" text NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"key" varchar(255) NOT NULL,
|
||||||
|
"time_used" timestamp with time zone,
|
||||||
|
CONSTRAINT "key_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "key" ADD CONSTRAINT "key_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "global_key" ON "key" USING btree ("key");
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "usage" DROP COLUMN "request_id";
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
{
|
||||||
|
"id": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"billing_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "billing_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "billing",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "payment_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"request_id": {
|
||||||
|
"name": "request_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_write_tokens": {
|
||||||
|
"name": "cache_write_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"usage_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "usage_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "usage",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "workspace_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "user_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "user",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
{
|
||||||
|
"id": "bf9e9084-4073-4ecb-8e56-5610816c9589",
|
||||||
|
"prevId": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"billing_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "billing_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "billing",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "payment_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"request_id": {
|
||||||
|
"name": "request_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_write_tokens": {
|
||||||
|
"name": "cache_write_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"usage_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "usage_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "usage",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "workspace_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "user_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "user",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
{
|
||||||
|
"id": "351e4956-74e0-4282-a23b-02f1a73fa38c",
|
||||||
|
"prevId": "bf9e9084-4073-4ecb-8e56-5610816c9589",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"billing_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "billing_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "billing",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "payment_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"request_id": {
|
||||||
|
"name": "request_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_write_tokens": {
|
||||||
|
"name": "cache_write_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"usage_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "usage_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "usage",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.key": {
|
||||||
|
"name": "key",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_used": {
|
||||||
|
"name": "time_used",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"global_key": {
|
||||||
|
"name": "global_key",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "key",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"key_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "key_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "key",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"key_workspace_id_id_pk": {
|
||||||
|
"name": "key_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "workspace_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "user_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "user",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
{
|
||||||
|
"id": "fa935883-9e51-4811-90c7-8967eefe458c",
|
||||||
|
"prevId": "351e4956-74e0-4282-a23b-02f1a73fa38c",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.billing": {
|
||||||
|
"name": "billing",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_id": {
|
||||||
|
"name": "payment_method_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_method_last4": {
|
||||||
|
"name": "payment_method_last4",
|
||||||
|
"type": "varchar(4)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"balance": {
|
||||||
|
"name": "balance",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reload": {
|
||||||
|
"name": "reload",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"billing_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "billing_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "billing",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"billing_workspace_id_id_pk": {
|
||||||
|
"name": "billing_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "payment_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"payment_workspace_id_id_pk": {
|
||||||
|
"name": "payment_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.usage": {
|
||||||
|
"name": "usage",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"input_tokens": {
|
||||||
|
"name": "input_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"output_tokens": {
|
||||||
|
"name": "output_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"reasoning_tokens": {
|
||||||
|
"name": "reasoning_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_read_tokens": {
|
||||||
|
"name": "cache_read_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cache_write_tokens": {
|
||||||
|
"name": "cache_write_tokens",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cost": {
|
||||||
|
"name": "cost",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"usage_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "usage_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "usage",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"usage_workspace_id_id_pk": {
|
||||||
|
"name": "usage_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.key": {
|
||||||
|
"name": "key",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_used": {
|
||||||
|
"name": "time_used",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"global_key": {
|
||||||
|
"name": "global_key",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "key",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"key_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "key_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "key",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"key_workspace_id_id_pk": {
|
||||||
|
"name": "key_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"workspace_id": {
|
||||||
|
"name": "workspace_id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"time_seen": {
|
||||||
|
"name": "time_seen",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "workspace_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "email",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_workspace_id_workspace_id_fk": {
|
||||||
|
"name": "user_workspace_id_workspace_id_fk",
|
||||||
|
"tableFrom": "user",
|
||||||
|
"tableTo": "workspace",
|
||||||
|
"columnsFrom": [
|
||||||
|
"workspace_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_workspace_id_id_pk": {
|
||||||
|
"name": "user_workspace_id_id_pk",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.workspace": {
|
||||||
|
"name": "workspace",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(30)",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"time_created": {
|
||||||
|
"name": "time_created",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"time_deleted": {
|
||||||
|
"name": "time_deleted",
|
||||||
|
"type": "timestamp with time zone",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "slug",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754518198186,
|
||||||
|
"tag": "0000_amused_mojo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754609655262,
|
||||||
|
"tag": "0001_thankful_chat",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754627626945,
|
||||||
|
"tag": "0002_stale_jackal",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1754672464106,
|
||||||
|
"tag": "0003_tranquil_spencer_smythe",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@opencode/cloud-core",
|
||||||
|
"version": "0.4.22",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
|
"drizzle-orm": "0.41.0",
|
||||||
|
"postgres": "3.4.7",
|
||||||
|
"stripe": "18.0.0",
|
||||||
|
"ulid": "3.0.0"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./*": "./src/*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"db": "sst shell drizzle-kit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"drizzle-kit": "0.30.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { and, eq, getTableColumns, isNull } from "drizzle-orm"
|
||||||
|
import { fn } from "./util/fn"
|
||||||
|
import { Database } from "./drizzle"
|
||||||
|
import { Identifier } from "./identifier"
|
||||||
|
import { AccountTable } from "./schema/account.sql"
|
||||||
|
import { Actor } from "./actor"
|
||||||
|
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||||
|
import { UserTable } from "./schema/user.sql"
|
||||||
|
|
||||||
|
export namespace Account {
|
||||||
|
export const create = fn(
|
||||||
|
z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
id: z.string().optional(),
|
||||||
|
}),
|
||||||
|
async (input) =>
|
||||||
|
Database.transaction(async (tx) => {
|
||||||
|
const id = input.id ?? Identifier.create("account")
|
||||||
|
await tx.insert(AccountTable).values({
|
||||||
|
id,
|
||||||
|
email: input.email,
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const fromID = fn(z.string(), async (id) =>
|
||||||
|
Database.transaction(async (tx) => {
|
||||||
|
return tx
|
||||||
|
.select()
|
||||||
|
.from(AccountTable)
|
||||||
|
.where(eq(AccountTable.id, id))
|
||||||
|
.execute()
|
||||||
|
.then((rows) => rows[0])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const fromEmail = fn(z.string().email(), async (email) =>
|
||||||
|
Database.transaction(async (tx) => {
|
||||||
|
return tx
|
||||||
|
.select()
|
||||||
|
.from(AccountTable)
|
||||||
|
.where(eq(AccountTable.email, email))
|
||||||
|
.execute()
|
||||||
|
.then((rows) => rows[0])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const workspaces = async () => {
|
||||||
|
const actor = Actor.assert("account")
|
||||||
|
return Database.transaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select(getTableColumns(WorkspaceTable))
|
||||||
|
.from(WorkspaceTable)
|
||||||
|
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(UserTable.email, actor.properties.email),
|
||||||
|
isNull(UserTable.timeDeleted),
|
||||||
|
isNull(WorkspaceTable.timeDeleted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Context } from "./context"
|
||||||
|
import { Log } from "./util/log"
|
||||||
|
|
||||||
|
export namespace Actor {
|
||||||
|
interface Account {
|
||||||
|
type: "account"
|
||||||
|
properties: {
|
||||||
|
accountID: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Public {
|
||||||
|
type: "public"
|
||||||
|
properties: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
type: "user"
|
||||||
|
properties: {
|
||||||
|
userID: string
|
||||||
|
workspaceID: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface System {
|
||||||
|
type: "system"
|
||||||
|
properties: {
|
||||||
|
workspaceID: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Info = Account | Public | User | System
|
||||||
|
|
||||||
|
const ctx = Context.create<Info>()
|
||||||
|
export const use = ctx.use
|
||||||
|
|
||||||
|
const log = Log.create().tag("namespace", "actor")
|
||||||
|
|
||||||
|
export function provide<R, T extends Info["type"]>(
|
||||||
|
type: T,
|
||||||
|
properties: Extract<Info, { type: T }>["properties"],
|
||||||
|
cb: () => R,
|
||||||
|
) {
|
||||||
|
return ctx.provide(
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
properties,
|
||||||
|
} as any,
|
||||||
|
() => {
|
||||||
|
return Log.provide({ ...properties }, () => {
|
||||||
|
log.info("provided")
|
||||||
|
return cb()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assert<T extends Info["type"]>(type: T) {
|
||||||
|
const actor = use()
|
||||||
|
if (actor.type !== type) {
|
||||||
|
throw new Error(`Expected actor type ${type}, got ${actor.type}`)
|
||||||
|
}
|
||||||
|
return actor as Extract<Info, { type: T }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function workspace() {
|
||||||
|
const actor = use()
|
||||||
|
if ("workspaceID" in actor.properties) {
|
||||||
|
return actor.properties.workspaceID
|
||||||
|
}
|
||||||
|
throw new Error(`actor of type "${actor.type}" is not associated with a workspace`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Resource } from "sst"
|
||||||
|
import { Stripe } from "stripe"
|
||||||
|
import { Database, eq, sql } from "./drizzle"
|
||||||
|
import { BillingTable, UsageTable } from "./schema/billing.sql"
|
||||||
|
import { Actor } from "./actor"
|
||||||
|
import { fn } from "./util/fn"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { Identifier } from "./identifier"
|
||||||
|
import { centsToMicroCents } from "./util/price"
|
||||||
|
|
||||||
|
export namespace Billing {
|
||||||
|
export const stripe = () =>
|
||||||
|
new Stripe(Resource.STRIPE_SECRET_KEY.value, {
|
||||||
|
apiVersion: "2025-03-31.basil",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const get = async () => {
|
||||||
|
return Database.use(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
customerID: BillingTable.customerID,
|
||||||
|
paymentMethodID: BillingTable.paymentMethodID,
|
||||||
|
balance: BillingTable.balance,
|
||||||
|
reload: BillingTable.reload,
|
||||||
|
})
|
||||||
|
.from(BillingTable)
|
||||||
|
.where(eq(BillingTable.workspaceID, Actor.workspace()))
|
||||||
|
.then((r) => r[0]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const consume = fn(
|
||||||
|
z.object({
|
||||||
|
requestID: z.string().optional(),
|
||||||
|
model: z.string(),
|
||||||
|
inputTokens: z.number(),
|
||||||
|
outputTokens: z.number(),
|
||||||
|
reasoningTokens: z.number().optional(),
|
||||||
|
cacheReadTokens: z.number().optional(),
|
||||||
|
cacheWriteTokens: z.number().optional(),
|
||||||
|
costInCents: z.number(),
|
||||||
|
}),
|
||||||
|
async (input) => {
|
||||||
|
const workspaceID = Actor.workspace()
|
||||||
|
const cost = centsToMicroCents(input.costInCents)
|
||||||
|
|
||||||
|
return await Database.transaction(async (tx) => {
|
||||||
|
await tx.insert(UsageTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("usage"),
|
||||||
|
requestID: input.requestID,
|
||||||
|
model: input.model,
|
||||||
|
inputTokens: input.inputTokens,
|
||||||
|
outputTokens: input.outputTokens,
|
||||||
|
reasoningTokens: input.reasoningTokens,
|
||||||
|
cacheReadTokens: input.cacheReadTokens,
|
||||||
|
cacheWriteTokens: input.cacheWriteTokens,
|
||||||
|
cost,
|
||||||
|
})
|
||||||
|
const [updated] = await tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
balance: sql`${BillingTable.balance} - ${cost}`,
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
.returning()
|
||||||
|
return updated.balance
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { AsyncLocalStorage } from "node:async_hooks"
|
||||||
|
|
||||||
|
export namespace Context {
|
||||||
|
export class NotFound extends Error {}
|
||||||
|
|
||||||
|
export function create<T>() {
|
||||||
|
const storage = new AsyncLocalStorage<T>()
|
||||||
|
return {
|
||||||
|
use() {
|
||||||
|
const result = storage.getStore()
|
||||||
|
if (!result) {
|
||||||
|
throw new NotFound()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
provide<R>(value: T, fn: () => R) {
|
||||||
|
return storage.run<R>(value, fn)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/postgres-js"
|
||||||
|
import { Resource } from "sst"
|
||||||
|
export * from "drizzle-orm"
|
||||||
|
import postgres from "postgres"
|
||||||
|
|
||||||
|
function createClient() {
|
||||||
|
const client = postgres({
|
||||||
|
idle_timeout: 30000,
|
||||||
|
connect_timeout: 30000,
|
||||||
|
host: Resource.Database.host,
|
||||||
|
database: Resource.Database.database,
|
||||||
|
user: Resource.Database.username,
|
||||||
|
password: Resource.Database.password,
|
||||||
|
port: Resource.Database.port,
|
||||||
|
ssl: {
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
max: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return drizzle(client, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
|
||||||
|
import type { ExtractTablesWithRelations } from "drizzle-orm"
|
||||||
|
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
|
||||||
|
import { Context } from "../context"
|
||||||
|
|
||||||
|
export namespace Database {
|
||||||
|
export type Transaction = PgTransaction<
|
||||||
|
PostgresJsQueryResultHKT,
|
||||||
|
Record<string, unknown>,
|
||||||
|
ExtractTablesWithRelations<Record<string, unknown>>
|
||||||
|
>
|
||||||
|
|
||||||
|
export type TxOrDb = Transaction | ReturnType<typeof createClient>
|
||||||
|
|
||||||
|
const TransactionContext = Context.create<{
|
||||||
|
tx: TxOrDb
|
||||||
|
effects: (() => void | Promise<void>)[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
export async function use<T>(callback: (trx: TxOrDb) => Promise<T>) {
|
||||||
|
try {
|
||||||
|
const { tx } = TransactionContext.use()
|
||||||
|
return tx.transaction(callback)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Context.NotFound) {
|
||||||
|
const client = createClient()
|
||||||
|
const effects: (() => void | Promise<void>)[] = []
|
||||||
|
const result = await TransactionContext.provide(
|
||||||
|
{
|
||||||
|
effects,
|
||||||
|
tx: client,
|
||||||
|
},
|
||||||
|
() => callback(client),
|
||||||
|
)
|
||||||
|
await Promise.all(effects.map((x) => x()))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function fn<Input, T>(callback: (input: Input, trx: TxOrDb) => Promise<T>) {
|
||||||
|
return (input: Input) => use(async (tx) => callback(input, tx))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function effect(effect: () => any | Promise<any>) {
|
||||||
|
try {
|
||||||
|
const { effects } = TransactionContext.use()
|
||||||
|
effects.push(effect)
|
||||||
|
} catch {
|
||||||
|
await effect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: PgTransactionConfig) {
|
||||||
|
try {
|
||||||
|
const { tx } = TransactionContext.use()
|
||||||
|
return callback(tx)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Context.NotFound) {
|
||||||
|
const client = createClient()
|
||||||
|
const effects: (() => void | Promise<void>)[] = []
|
||||||
|
const result = await client.transaction(async (tx) => {
|
||||||
|
return TransactionContext.provide({ tx, effects }, () => callback(tx))
|
||||||
|
}, config)
|
||||||
|
await Promise.all(effects.map((x) => x()))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { bigint, timestamp, varchar } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const ulid = (name: string) => varchar(name, { length: 30 })
|
||||||
|
|
||||||
|
export const workspaceColumns = {
|
||||||
|
get id() {
|
||||||
|
return ulid("id").notNull()
|
||||||
|
},
|
||||||
|
get workspaceID() {
|
||||||
|
return ulid("workspace_id").notNull()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const id = () => ulid("id").notNull()
|
||||||
|
|
||||||
|
export const utc = (name: string) =>
|
||||||
|
timestamp(name, {
|
||||||
|
withTimezone: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const currency = (name: string) =>
|
||||||
|
bigint(name, {
|
||||||
|
mode: "number",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const timestamps = {
|
||||||
|
timeCreated: utc("time_created").notNull().defaultNow(),
|
||||||
|
timeDeleted: utc("time_deleted"),
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { ulid } from "ulid"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export namespace Identifier {
|
||||||
|
const prefixes = {
|
||||||
|
account: "acc",
|
||||||
|
billing: "bil",
|
||||||
|
key: "key",
|
||||||
|
payment: "pay",
|
||||||
|
usage: "usg",
|
||||||
|
user: "usr",
|
||||||
|
workspace: "wrk",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function create(prefix: keyof typeof prefixes, given?: string): string {
|
||||||
|
if (given) {
|
||||||
|
if (given.startsWith(prefixes[prefix])) return given
|
||||||
|
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||||
|
}
|
||||||
|
return [prefixes[prefix], ulid()].join("_")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function schema(prefix: keyof typeof prefixes) {
|
||||||
|
return z.string().startsWith(prefixes[prefix])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
|
||||||
|
import { id, timestamps } from "../drizzle/types"
|
||||||
|
|
||||||
|
export const AccountTable = pgTable(
|
||||||
|
"account",
|
||||||
|
{
|
||||||
|
id: id(),
|
||||||
|
...timestamps,
|
||||||
|
email: varchar("email", { length: 255 }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex("email").on(table.email)],
|
||||||
|
)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { bigint, boolean, integer, pgTable, varchar } from "drizzle-orm/pg-core"
|
||||||
|
import { timestamps, workspaceColumns } from "../drizzle/types"
|
||||||
|
import { workspaceIndexes } from "./workspace.sql"
|
||||||
|
|
||||||
|
export const BillingTable = pgTable(
|
||||||
|
"billing",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
customerID: varchar("customer_id", { length: 255 }),
|
||||||
|
paymentMethodID: varchar("payment_method_id", { length: 255 }),
|
||||||
|
paymentMethodLast4: varchar("payment_method_last4", { length: 4 }),
|
||||||
|
balance: bigint("balance", { mode: "number" }).notNull(),
|
||||||
|
reload: boolean("reload"),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table)],
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PaymentTable = pgTable(
|
||||||
|
"payment",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
customerID: varchar("customer_id", { length: 255 }),
|
||||||
|
paymentID: varchar("payment_id", { length: 255 }),
|
||||||
|
amount: bigint("amount", { mode: "number" }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table)],
|
||||||
|
)
|
||||||
|
|
||||||
|
export const UsageTable = pgTable(
|
||||||
|
"usage",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
model: varchar("model", { length: 255 }).notNull(),
|
||||||
|
inputTokens: integer("input_tokens").notNull(),
|
||||||
|
outputTokens: integer("output_tokens").notNull(),
|
||||||
|
reasoningTokens: integer("reasoning_tokens"),
|
||||||
|
cacheReadTokens: integer("cache_read_tokens"),
|
||||||
|
cacheWriteTokens: integer("cache_write_tokens"),
|
||||||
|
cost: bigint("cost", { mode: "number" }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table)],
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { text, pgTable, varchar, uniqueIndex } from "drizzle-orm/pg-core"
|
||||||
|
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
|
||||||
|
import { workspaceIndexes } from "./workspace.sql"
|
||||||
|
|
||||||
|
export const KeyTable = pgTable(
|
||||||
|
"key",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
userID: text("user_id").notNull(),
|
||||||
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
|
key: varchar("key", { length: 255 }).notNull(),
|
||||||
|
timeUsed: utc("time_used"),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table), uniqueIndex("global_key").on(table.key)],
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { text, pgTable, uniqueIndex, varchar, integer } from "drizzle-orm/pg-core"
|
||||||
|
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
|
||||||
|
import { workspaceIndexes } from "./workspace.sql"
|
||||||
|
|
||||||
|
export const UserTable = pgTable(
|
||||||
|
"user",
|
||||||
|
{
|
||||||
|
...workspaceColumns,
|
||||||
|
...timestamps,
|
||||||
|
email: text("email").notNull(),
|
||||||
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
|
timeSeen: utc("time_seen"),
|
||||||
|
color: integer("color"),
|
||||||
|
},
|
||||||
|
(table) => [...workspaceIndexes(table), uniqueIndex("user_email").on(table.workspaceID, table.email)],
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { primaryKey, foreignKey, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
|
||||||
|
import { timestamps, ulid } from "../drizzle/types"
|
||||||
|
|
||||||
|
export const WorkspaceTable = pgTable(
|
||||||
|
"workspace",
|
||||||
|
{
|
||||||
|
id: ulid("id").notNull().primaryKey(),
|
||||||
|
slug: varchar("slug", { length: 255 }),
|
||||||
|
name: varchar("name", { length: 255 }),
|
||||||
|
...timestamps,
|
||||||
|
},
|
||||||
|
(table) => [uniqueIndex("slug").on(table.slug)],
|
||||||
|
)
|
||||||
|
|
||||||
|
export function workspaceIndexes(table: any) {
|
||||||
|
return [
|
||||||
|
primaryKey({
|
||||||
|
columns: [table.workspaceID, table.id],
|
||||||
|
}),
|
||||||
|
foreignKey({
|
||||||
|
foreignColumns: [WorkspaceTable.id],
|
||||||
|
columns: [table.workspaceID],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export function fn<T extends z.ZodType, Result>(
|
||||||
|
schema: T,
|
||||||
|
cb: (input: z.output<T>) => Result,
|
||||||
|
) {
|
||||||
|
const result = (input: z.input<T>) => {
|
||||||
|
const parsed = schema.parse(input)
|
||||||
|
return cb(parsed)
|
||||||
|
}
|
||||||
|
result.force = (input: z.input<T>) => cb(input)
|
||||||
|
result.schema = schema
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Context } from "../context"
|
||||||
|
|
||||||
|
export namespace Log {
|
||||||
|
const ctx = Context.create<{
|
||||||
|
tags: Record<string, any>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
export function create(tags?: Record<string, any>) {
|
||||||
|
tags = tags || {}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
info(message?: any, extra?: Record<string, any>) {
|
||||||
|
const prefix = Object.entries({
|
||||||
|
...use().tags,
|
||||||
|
...tags,
|
||||||
|
...extra,
|
||||||
|
})
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join(" ")
|
||||||
|
console.log(prefix, message)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
tag(key: string, value: string) {
|
||||||
|
if (tags) tags[key] = value
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
clone() {
|
||||||
|
return Log.create({ ...tags })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function provide<R>(tags: Record<string, any>, cb: () => R) {
|
||||||
|
const existing = use()
|
||||||
|
return ctx.provide(
|
||||||
|
{
|
||||||
|
tags: {
|
||||||
|
...existing.tags,
|
||||||
|
...tags,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cb,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function use() {
|
||||||
|
try {
|
||||||
|
return ctx.use()
|
||||||
|
} catch (e) {
|
||||||
|
return { tags: {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function centsToMicroCents(amount: number) {
|
||||||
|
return Math.round(amount * 1000000)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { fn } from "./util/fn"
|
||||||
|
import { centsToMicroCents } from "./util/price"
|
||||||
|
import { Actor } from "./actor"
|
||||||
|
import { Database, eq } from "./drizzle"
|
||||||
|
import { Identifier } from "./identifier"
|
||||||
|
import { UserTable } from "./schema/user.sql"
|
||||||
|
import { BillingTable } from "./schema/billing.sql"
|
||||||
|
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||||
|
|
||||||
|
export namespace Workspace {
|
||||||
|
export const create = fn(z.void(), async () => {
|
||||||
|
const account = Actor.assert("account")
|
||||||
|
const workspaceID = Identifier.create("workspace")
|
||||||
|
await Database.transaction(async (tx) => {
|
||||||
|
await tx.insert(WorkspaceTable).values({
|
||||||
|
id: workspaceID,
|
||||||
|
})
|
||||||
|
await tx.insert(UserTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("user"),
|
||||||
|
email: account.properties.email,
|
||||||
|
name: "",
|
||||||
|
})
|
||||||
|
await tx.insert(BillingTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("billing"),
|
||||||
|
balance: centsToMicroCents(100),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return workspaceID
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function list() {
|
||||||
|
const account = Actor.assert("account")
|
||||||
|
return Database.use(async (tx) => {
|
||||||
|
return tx
|
||||||
|
.select({
|
||||||
|
id: WorkspaceTable.id,
|
||||||
|
slug: WorkspaceTable.slug,
|
||||||
|
name: WorkspaceTable.name,
|
||||||
|
})
|
||||||
|
.from(UserTable)
|
||||||
|
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
|
.where(eq(UserTable.email, account.properties.email))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/* deno-fmt-ignore-file */
|
||||||
|
|
||||||
|
/// <reference path="../../sst-env.d.ts" />
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
export {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"types": ["@cloudflare/workers-types", "node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@opencode/cloud-function",
|
||||||
|
"version": "0.4.22",
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "4.20250522.0",
|
||||||
|
"@types/node": "catalog:",
|
||||||
|
"openai": "5.11.0",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
|
"@ai-sdk/openai": "2.0.2",
|
||||||
|
"@ai-sdk/openai-compatible": "1.0.1",
|
||||||
|
"@hono/zod-validator": "catalog:",
|
||||||
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
|
"ai": "catalog:",
|
||||||
|
"hono": "catalog:",
|
||||||
|
"zod": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Resource } from "sst"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { issuer } from "@openauthjs/openauth"
|
||||||
|
import { createSubjects } from "@openauthjs/openauth/subject"
|
||||||
|
import { CodeProvider } from "@openauthjs/openauth/provider/code"
|
||||||
|
import { GithubProvider } from "@openauthjs/openauth/provider/github"
|
||||||
|
import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
|
||||||
|
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
|
||||||
|
import { Account } from "@opencode/cloud-core/account.js"
|
||||||
|
|
||||||
|
type Env = {
|
||||||
|
AuthStorage: KVNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
export const subjects = createSubjects({
|
||||||
|
account: z.object({
|
||||||
|
accountID: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
}),
|
||||||
|
user: z.object({
|
||||||
|
userID: z.string(),
|
||||||
|
workspaceID: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||||
|
return issuer({
|
||||||
|
providers: {
|
||||||
|
github: GithubProvider({
|
||||||
|
clientID: Resource.GITHUB_CLIENT_ID_CONSOLE.value,
|
||||||
|
clientSecret: Resource.GITHUB_CLIENT_SECRET_CONSOLE.value,
|
||||||
|
scopes: ["read:user", "user:email"],
|
||||||
|
}),
|
||||||
|
google: GoogleOidcProvider({
|
||||||
|
clientID: Resource.GOOGLE_CLIENT_ID.value,
|
||||||
|
scopes: ["openid", "email"],
|
||||||
|
}),
|
||||||
|
// email: CodeProvider({
|
||||||
|
// async request(req, state, form, error) {
|
||||||
|
// console.log(state)
|
||||||
|
// const params = new URLSearchParams()
|
||||||
|
// if (error) {
|
||||||
|
// params.set("error", error.type)
|
||||||
|
// }
|
||||||
|
// if (state.type === "start") {
|
||||||
|
// return Response.redirect(process.env.AUTH_FRONTEND_URL + "/auth/email?" + params.toString(), 302)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (state.type === "code") {
|
||||||
|
// return Response.redirect(process.env.AUTH_FRONTEND_URL + "/auth/code?" + params.toString(), 302)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return new Response("ok")
|
||||||
|
// },
|
||||||
|
// async sendCode(claims, code) {
|
||||||
|
// const email = z.string().email().parse(claims.email)
|
||||||
|
// const cmd = new SendEmailCommand({
|
||||||
|
// Destination: {
|
||||||
|
// ToAddresses: [email],
|
||||||
|
// },
|
||||||
|
// FromEmailAddress: `SST <auth@${Resource.Email.sender}>`,
|
||||||
|
// Content: {
|
||||||
|
// Simple: {
|
||||||
|
// Body: {
|
||||||
|
// Html: {
|
||||||
|
// Data: `Your pin code is <strong>${code}</strong>`,
|
||||||
|
// },
|
||||||
|
// Text: {
|
||||||
|
// Data: `Your pin code is ${code}`,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// Subject: {
|
||||||
|
// Data: "SST Console Pin Code: " + code,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// await ses.send(cmd)
|
||||||
|
// },
|
||||||
|
// }),
|
||||||
|
},
|
||||||
|
storage: CloudflareStorage({
|
||||||
|
namespace: env.AuthStorage,
|
||||||
|
}),
|
||||||
|
subjects,
|
||||||
|
async success(ctx, response) {
|
||||||
|
console.log(response)
|
||||||
|
|
||||||
|
let email: string | undefined
|
||||||
|
|
||||||
|
if (response.provider === "github") {
|
||||||
|
const userResponse = await fetch("https://api.github.com/user", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${response.tokenset.access}`,
|
||||||
|
"User-Agent": "opencode",
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const user = (await userResponse.json()) as { email: string }
|
||||||
|
email = user.email
|
||||||
|
} else if (response.provider === "google") {
|
||||||
|
if (!response.id.email_verified) throw new Error("Google email not verified")
|
||||||
|
email = response.id.email as string
|
||||||
|
}
|
||||||
|
//if (response.provider === "email") {
|
||||||
|
// email = response.claims.email
|
||||||
|
//}
|
||||||
|
else throw new Error("Unsupported provider")
|
||||||
|
|
||||||
|
if (!email) throw new Error("No email found")
|
||||||
|
|
||||||
|
let accountID = await Account.fromEmail(email).then((x) => x?.id)
|
||||||
|
if (!accountID) {
|
||||||
|
console.log("creating account for", email)
|
||||||
|
accountID = await Account.create({
|
||||||
|
email: email!,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ctx.subject("account", accountID, { accountID, email })
|
||||||
|
},
|
||||||
|
}).fetch(request, env, ctx)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,909 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { Hono, MiddlewareHandler } from "hono"
|
||||||
|
import { cors } from "hono/cors"
|
||||||
|
import { HTTPException } from "hono/http-exception"
|
||||||
|
import { zValidator } from "@hono/zod-validator"
|
||||||
|
import { Resource } from "sst"
|
||||||
|
import { type ProviderMetadata, type LanguageModelUsage, generateText, streamText } from "ai"
|
||||||
|
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||||
|
import { createOpenAI } from "@ai-sdk/openai"
|
||||||
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||||
|
import type { LanguageModelV2Prompt } from "@ai-sdk/provider"
|
||||||
|
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
|
||||||
|
import { Actor } from "@opencode/cloud-core/actor.js"
|
||||||
|
import { and, Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
|
||||||
|
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
|
||||||
|
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
|
||||||
|
import { createClient } from "@openauthjs/openauth/client"
|
||||||
|
import { Log } from "@opencode/cloud-core/util/log.js"
|
||||||
|
import { Billing } from "@opencode/cloud-core/billing.js"
|
||||||
|
import { Workspace } from "@opencode/cloud-core/workspace.js"
|
||||||
|
import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
|
||||||
|
import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
|
||||||
|
import { Identifier } from "../../core/src/identifier"
|
||||||
|
|
||||||
|
type Env = {}
|
||||||
|
|
||||||
|
let _client: ReturnType<typeof createClient>
|
||||||
|
const client = () => {
|
||||||
|
if (_client) return _client
|
||||||
|
_client = createClient({
|
||||||
|
clientID: "api",
|
||||||
|
issuer: Resource.AUTH_API_URL.value,
|
||||||
|
})
|
||||||
|
return _client
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORTED_MODELS = {
|
||||||
|
"anthropic/claude-sonnet-4": {
|
||||||
|
input: 0.0000015,
|
||||||
|
output: 0.000006,
|
||||||
|
reasoning: 0.0000015,
|
||||||
|
cacheRead: 0.0000001,
|
||||||
|
cacheWrite: 0.0000001,
|
||||||
|
model: () =>
|
||||||
|
createAnthropic({
|
||||||
|
apiKey: Resource.ANTHROPIC_API_KEY.value,
|
||||||
|
})("claude-sonnet-4-20250514"),
|
||||||
|
},
|
||||||
|
"openai/gpt-4.1": {
|
||||||
|
input: 0.0000015,
|
||||||
|
output: 0.000006,
|
||||||
|
reasoning: 0.0000015,
|
||||||
|
cacheRead: 0.0000001,
|
||||||
|
cacheWrite: 0.0000001,
|
||||||
|
model: () =>
|
||||||
|
createOpenAI({
|
||||||
|
apiKey: Resource.OPENAI_API_KEY.value,
|
||||||
|
})("gpt-4.1"),
|
||||||
|
},
|
||||||
|
"zhipuai/glm-4.5-flash": {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
model: () =>
|
||||||
|
createOpenAICompatible({
|
||||||
|
name: "Zhipu AI",
|
||||||
|
baseURL: "https://api.z.ai/api/paas/v4",
|
||||||
|
apiKey: Resource.ZHIPU_API_KEY.value,
|
||||||
|
})("glm-4.5-flash"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = Log.create({
|
||||||
|
namespace: "api",
|
||||||
|
})
|
||||||
|
|
||||||
|
const GatewayAuth: MiddlewareHandler = async (c, next) => {
|
||||||
|
const authHeader = c.req.header("authorization")
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: "Missing API key.",
|
||||||
|
type: "invalid_request_error",
|
||||||
|
param: null,
|
||||||
|
code: "unauthorized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = authHeader.split(" ")[1]
|
||||||
|
|
||||||
|
// Check against KeyTable
|
||||||
|
const keyRecord = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: KeyTable.id,
|
||||||
|
workspaceID: KeyTable.workspaceID,
|
||||||
|
})
|
||||||
|
.from(KeyTable)
|
||||||
|
.where(eq(KeyTable.key, apiKey))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!keyRecord) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: "Invalid API key.",
|
||||||
|
type: "invalid_request_error",
|
||||||
|
param: null,
|
||||||
|
code: "unauthorized",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.set("keyRecord", keyRecord)
|
||||||
|
await next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const RestAuth: MiddlewareHandler = async (c, next) => {
|
||||||
|
const authorization = c.req.header("authorization")
|
||||||
|
if (!authorization) {
|
||||||
|
return Actor.provide("public", {}, next)
|
||||||
|
}
|
||||||
|
const token = authorization.split(" ")[1]
|
||||||
|
if (!token)
|
||||||
|
throw new HTTPException(403, {
|
||||||
|
message: "Bearer token is required.",
|
||||||
|
})
|
||||||
|
|
||||||
|
const verified = await client().verify(token)
|
||||||
|
if (verified.err) {
|
||||||
|
throw new HTTPException(403, {
|
||||||
|
message: "Invalid token.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let subject = verified.subject as Actor.Info
|
||||||
|
if (subject.type === "account") {
|
||||||
|
const workspaceID = c.req.header("x-opencode-workspace")
|
||||||
|
const email = subject.properties.email
|
||||||
|
if (workspaceID) {
|
||||||
|
const user = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: UserTable.id,
|
||||||
|
workspaceID: UserTable.workspaceID,
|
||||||
|
email: UserTable.email,
|
||||||
|
})
|
||||||
|
.from(UserTable)
|
||||||
|
.where(and(eq(UserTable.email, email), eq(UserTable.workspaceID, workspaceID)))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
if (!user)
|
||||||
|
throw new HTTPException(403, {
|
||||||
|
message: "You do not have access to this workspace.",
|
||||||
|
})
|
||||||
|
subject = {
|
||||||
|
type: "user",
|
||||||
|
properties: {
|
||||||
|
userID: user.id,
|
||||||
|
workspaceID: workspaceID,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Actor.provide(subject.type, subject.properties, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; workspaceID: string } } }>()
|
||||||
|
.get("/", (c) => c.text("Hello, world!"))
|
||||||
|
.post("/v1/chat/completions", GatewayAuth, async (c) => {
|
||||||
|
const keyRecord = c.get("keyRecord")!
|
||||||
|
|
||||||
|
return await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
|
||||||
|
try {
|
||||||
|
// Check balance
|
||||||
|
const customer = await Billing.get()
|
||||||
|
if (customer.balance <= 0) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: "Insufficient balance",
|
||||||
|
type: "insufficient_quota",
|
||||||
|
param: null,
|
||||||
|
code: "insufficient_quota",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await c.req.json<ChatCompletionCreateParamsBase>()
|
||||||
|
const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
|
||||||
|
if (!model) throw new Error(`Unsupported model: ${body.model}`)
|
||||||
|
|
||||||
|
const requestBody = transformOpenAIRequestToAiSDK()
|
||||||
|
|
||||||
|
return body.stream ? await handleStream() : await handleGenerate()
|
||||||
|
|
||||||
|
async function handleStream() {
|
||||||
|
const result = await model.doStream({
|
||||||
|
...requestBody,
|
||||||
|
})
|
||||||
|
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
const id = `chatcmpl-${Date.now()}`
|
||||||
|
const created = Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const chunk of result.stream) {
|
||||||
|
console.log("!!! CHUNK !!! : " + chunk.type)
|
||||||
|
switch (chunk.type) {
|
||||||
|
case "text-delta": {
|
||||||
|
const data = {
|
||||||
|
id,
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created,
|
||||||
|
model: body.model,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
delta: {
|
||||||
|
content: chunk.delta,
|
||||||
|
},
|
||||||
|
finish_reason: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "reasoning-delta": {
|
||||||
|
const data = {
|
||||||
|
id,
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created,
|
||||||
|
model: body.model,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
delta: {
|
||||||
|
reasoning_content: chunk.delta,
|
||||||
|
},
|
||||||
|
finish_reason: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "tool-call": {
|
||||||
|
const data = {
|
||||||
|
id,
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created,
|
||||||
|
model: body.model,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
delta: {
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
id: chunk.toolCallId,
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: chunk.toolName,
|
||||||
|
arguments: chunk.input,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
finish_reason: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "error": {
|
||||||
|
const data = {
|
||||||
|
id,
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created,
|
||||||
|
model: body.model,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
delta: {},
|
||||||
|
finish_reason: "stop",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: {
|
||||||
|
message: typeof chunk.error === "string" ? chunk.error : chunk.error,
|
||||||
|
type: "server_error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||||
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
|
||||||
|
controller.close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "finish": {
|
||||||
|
const data = {
|
||||||
|
id,
|
||||||
|
object: "chat.completion.chunk",
|
||||||
|
created,
|
||||||
|
model: body.model,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
delta: {},
|
||||||
|
finish_reason:
|
||||||
|
{
|
||||||
|
stop: "stop",
|
||||||
|
length: "length",
|
||||||
|
"content-filter": "content_filter",
|
||||||
|
"tool-calls": "tool_calls",
|
||||||
|
error: "stop",
|
||||||
|
other: "stop",
|
||||||
|
unknown: "stop",
|
||||||
|
}[chunk.finishReason] || "stop",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: chunk.usage.inputTokens,
|
||||||
|
completion_tokens: chunk.usage.outputTokens,
|
||||||
|
total_tokens: chunk.usage.totalTokens,
|
||||||
|
completion_tokens_details: {
|
||||||
|
reasoning_tokens: chunk.usage.reasoningTokens,
|
||||||
|
},
|
||||||
|
prompt_tokens_details: {
|
||||||
|
cached_tokens: chunk.usage.cachedInputTokens,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await trackUsage(body.model, chunk.usage, chunk.providerMetadata)
|
||||||
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||||
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
|
||||||
|
controller.close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
//case "stream-start":
|
||||||
|
//case "response-metadata":
|
||||||
|
case "text-start":
|
||||||
|
case "text-end":
|
||||||
|
case "reasoning-start":
|
||||||
|
case "reasoning-end":
|
||||||
|
case "tool-input-start":
|
||||||
|
case "tool-input-delta":
|
||||||
|
case "tool-input-end":
|
||||||
|
case "raw":
|
||||||
|
default:
|
||||||
|
// Log unknown chunk types for debugging
|
||||||
|
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
controller.error(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGenerate() {
|
||||||
|
const response = await model.doGenerate({
|
||||||
|
...requestBody,
|
||||||
|
})
|
||||||
|
await trackUsage(body.model, response.usage, response.providerMetadata)
|
||||||
|
return c.json({
|
||||||
|
id: `chatcmpl-${Date.now()}`,
|
||||||
|
object: "chat.completion" as const,
|
||||||
|
created: Math.floor(Date.now() / 1000),
|
||||||
|
model: body.model,
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
message: {
|
||||||
|
role: "assistant" as const,
|
||||||
|
content: response.content?.find((c) => c.type === "text")?.text ?? "",
|
||||||
|
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
|
||||||
|
tool_calls: response.content
|
||||||
|
?.filter((c) => c.type === "tool-call")
|
||||||
|
.map((toolCall) => ({
|
||||||
|
id: toolCall.toolCallId,
|
||||||
|
type: "function" as const,
|
||||||
|
function: {
|
||||||
|
name: toolCall.toolName,
|
||||||
|
arguments: toolCall.input,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
finish_reason:
|
||||||
|
(
|
||||||
|
{
|
||||||
|
stop: "stop",
|
||||||
|
length: "length",
|
||||||
|
"content-filter": "content_filter",
|
||||||
|
"tool-calls": "tool_calls",
|
||||||
|
error: "stop",
|
||||||
|
other: "stop",
|
||||||
|
unknown: "stop",
|
||||||
|
} as const
|
||||||
|
)[response.finishReason] || "stop",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: response.usage?.inputTokens,
|
||||||
|
completion_tokens: response.usage?.outputTokens,
|
||||||
|
total_tokens: response.usage?.totalTokens,
|
||||||
|
completion_tokens_details: {
|
||||||
|
reasoning_tokens: response.usage?.reasoningTokens,
|
||||||
|
},
|
||||||
|
prompt_tokens_details: {
|
||||||
|
cached_tokens: response.usage?.cachedInputTokens,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformOpenAIRequestToAiSDK() {
|
||||||
|
const prompt = transformMessages()
|
||||||
|
const tools = transformTools()
|
||||||
|
|
||||||
|
return {
|
||||||
|
prompt,
|
||||||
|
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
|
||||||
|
temperature: body.temperature ?? undefined,
|
||||||
|
topP: body.top_p ?? undefined,
|
||||||
|
frequencyPenalty: body.frequency_penalty ?? undefined,
|
||||||
|
presencePenalty: body.presence_penalty ?? undefined,
|
||||||
|
providerOptions: body.reasoning_effort
|
||||||
|
? {
|
||||||
|
anthropic: {
|
||||||
|
reasoningEffort: body.reasoning_effort,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
|
||||||
|
responseFormat: (() => {
|
||||||
|
if (!body.response_format) return { type: "text" as const }
|
||||||
|
if (body.response_format.type === "json_schema")
|
||||||
|
return {
|
||||||
|
type: "json" as const,
|
||||||
|
schema: body.response_format.json_schema.schema,
|
||||||
|
name: body.response_format.json_schema.name,
|
||||||
|
description: body.response_format.json_schema.description,
|
||||||
|
}
|
||||||
|
if (body.response_format.type === "json_object") return { type: "json" as const }
|
||||||
|
throw new Error("Unsupported response format")
|
||||||
|
})(),
|
||||||
|
seed: body.seed ?? undefined,
|
||||||
|
tools: tools.tools,
|
||||||
|
toolChoice: tools.toolChoice,
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformTools() {
|
||||||
|
const { tools, tool_choice } = body
|
||||||
|
|
||||||
|
if (!tools || tools.length === 0) {
|
||||||
|
return { tools: undefined, toolChoice: undefined }
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiSdkTools = tools.map((tool) => {
|
||||||
|
return {
|
||||||
|
type: tool.type,
|
||||||
|
name: tool.function.name,
|
||||||
|
description: tool.function.description,
|
||||||
|
inputSchema: tool.function.parameters!,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let aiSdkToolChoice
|
||||||
|
if (tool_choice == null) {
|
||||||
|
aiSdkToolChoice = undefined
|
||||||
|
} else if (tool_choice === "auto") {
|
||||||
|
aiSdkToolChoice = { type: "auto" as const }
|
||||||
|
} else if (tool_choice === "none") {
|
||||||
|
aiSdkToolChoice = { type: "none" as const }
|
||||||
|
} else if (tool_choice === "required") {
|
||||||
|
aiSdkToolChoice = { type: "required" as const }
|
||||||
|
} else if (tool_choice.type === "function") {
|
||||||
|
aiSdkToolChoice = {
|
||||||
|
type: "tool" as const,
|
||||||
|
toolName: tool_choice.function.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformMessages() {
|
||||||
|
const { messages } = body
|
||||||
|
const prompt: LanguageModelV2Prompt = []
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
switch (message.role) {
|
||||||
|
case "system": {
|
||||||
|
prompt.push({
|
||||||
|
role: "system",
|
||||||
|
content: message.content as string,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "user": {
|
||||||
|
if (typeof message.content === "string") {
|
||||||
|
prompt.push({
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: message.content }],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const content = message.content.map((part) => {
|
||||||
|
switch (part.type) {
|
||||||
|
case "text":
|
||||||
|
return { type: "text" as const, text: part.text }
|
||||||
|
case "image_url":
|
||||||
|
return {
|
||||||
|
type: "file" as const,
|
||||||
|
mediaType: "image/jpeg" as const,
|
||||||
|
data: part.image_url.url,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported content part type: ${(part as any).type}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
prompt.push({
|
||||||
|
role: "user",
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "assistant": {
|
||||||
|
const content: Array<
|
||||||
|
| { type: "text"; text: string }
|
||||||
|
| {
|
||||||
|
type: "tool-call"
|
||||||
|
toolCallId: string
|
||||||
|
toolName: string
|
||||||
|
input: any
|
||||||
|
}
|
||||||
|
> = []
|
||||||
|
|
||||||
|
if (message.content) {
|
||||||
|
content.push({
|
||||||
|
type: "text",
|
||||||
|
text: message.content as string,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.tool_calls) {
|
||||||
|
for (const toolCall of message.tool_calls) {
|
||||||
|
content.push({
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: toolCall.id,
|
||||||
|
toolName: toolCall.function.name,
|
||||||
|
input: JSON.parse(toolCall.function.arguments),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "tool": {
|
||||||
|
prompt.push({
|
||||||
|
role: "tool",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-result",
|
||||||
|
toolName: "placeholder",
|
||||||
|
toolCallId: message.tool_call_id,
|
||||||
|
output: {
|
||||||
|
type: "text",
|
||||||
|
value: message.content as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new Error(`Unsupported message role: ${message.role}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trackUsage(model: string, usage: LanguageModelUsage, providerMetadata?: ProviderMetadata) {
|
||||||
|
const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
|
||||||
|
if (!modelData) throw new Error(`Unsupported model: ${model}`)
|
||||||
|
|
||||||
|
const inputTokens = usage.inputTokens ?? 0
|
||||||
|
const outputTokens = usage.outputTokens ?? 0
|
||||||
|
const reasoningTokens = usage.reasoningTokens ?? 0
|
||||||
|
const cacheReadTokens = usage.cachedInputTokens ?? 0
|
||||||
|
const cacheWriteTokens =
|
||||||
|
providerMetadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
||||||
|
// @ts-expect-error
|
||||||
|
providerMetadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
||||||
|
0
|
||||||
|
|
||||||
|
const inputCost = modelData.input * inputTokens
|
||||||
|
const outputCost = modelData.output * outputTokens
|
||||||
|
const reasoningCost = modelData.reasoning * reasoningTokens
|
||||||
|
const cacheReadCost = modelData.cacheRead * cacheReadTokens
|
||||||
|
const cacheWriteCost = modelData.cacheWrite * cacheWriteTokens
|
||||||
|
const costInCents = (inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost) * 100
|
||||||
|
|
||||||
|
await Billing.consume({
|
||||||
|
model,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
reasoningTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWriteTokens,
|
||||||
|
costInCents,
|
||||||
|
})
|
||||||
|
|
||||||
|
await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.update(KeyTable)
|
||||||
|
.set({ timeUsed: sql`now()` })
|
||||||
|
.where(eq(KeyTable.id, keyRecord.id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return c.json({ error: { message: error.message } }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.use("/*", cors())
|
||||||
|
.use(RestAuth)
|
||||||
|
.get("/rest/account", async (c) => {
|
||||||
|
const account = Actor.assert("account")
|
||||||
|
let workspaces = await Workspace.list()
|
||||||
|
if (workspaces.length === 0) {
|
||||||
|
await Workspace.create()
|
||||||
|
workspaces = await Workspace.list()
|
||||||
|
}
|
||||||
|
return c.json({
|
||||||
|
id: account.properties.accountID,
|
||||||
|
email: account.properties.email,
|
||||||
|
workspaces,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.get("/billing/info", async (c) => {
|
||||||
|
const billing = await Billing.get()
|
||||||
|
const payments = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(PaymentTable)
|
||||||
|
.where(eq(PaymentTable.workspaceID, Actor.workspace()))
|
||||||
|
.orderBy(sql`${PaymentTable.timeCreated} DESC`)
|
||||||
|
.limit(100),
|
||||||
|
)
|
||||||
|
const usage = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(UsageTable)
|
||||||
|
.where(eq(UsageTable.workspaceID, Actor.workspace()))
|
||||||
|
.orderBy(sql`${UsageTable.timeCreated} DESC`)
|
||||||
|
.limit(100),
|
||||||
|
)
|
||||||
|
return c.json({ billing, payments, usage })
|
||||||
|
})
|
||||||
|
.post(
|
||||||
|
"/billing/checkout",
|
||||||
|
zValidator(
|
||||||
|
"json",
|
||||||
|
z.custom<{
|
||||||
|
success_url: string
|
||||||
|
cancel_url: string
|
||||||
|
}>(),
|
||||||
|
),
|
||||||
|
async (c) => {
|
||||||
|
const account = Actor.assert("user")
|
||||||
|
|
||||||
|
const body = await c.req.json()
|
||||||
|
|
||||||
|
const customer = await Billing.get()
|
||||||
|
const session = await Billing.stripe().checkout.sessions.create({
|
||||||
|
mode: "payment",
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price_data: {
|
||||||
|
currency: "usd",
|
||||||
|
product_data: {
|
||||||
|
name: "opencode credits",
|
||||||
|
},
|
||||||
|
unit_amount: 2000, // $20 minimum
|
||||||
|
},
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
payment_intent_data: {
|
||||||
|
setup_future_usage: "on_session",
|
||||||
|
},
|
||||||
|
...(customer.customerID
|
||||||
|
? { customer: customer.customerID }
|
||||||
|
: {
|
||||||
|
customer_email: account.properties.email,
|
||||||
|
customer_creation: "always",
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
workspaceID: Actor.workspace(),
|
||||||
|
},
|
||||||
|
currency: "usd",
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
success_url: body.success_url,
|
||||||
|
cancel_url: body.cancel_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
url: session.url,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.post("/billing/portal", async (c) => {
|
||||||
|
const body = await c.req.json()
|
||||||
|
|
||||||
|
const customer = await Billing.get()
|
||||||
|
if (!customer?.customerID) {
|
||||||
|
throw new Error("No stripe customer ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await Billing.stripe().billingPortal.sessions.create({
|
||||||
|
customer: customer.customerID,
|
||||||
|
return_url: body.return_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
url: session.url,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.post("/stripe/webhook", async (c) => {
|
||||||
|
const body = await Billing.stripe().webhooks.constructEventAsync(
|
||||||
|
await c.req.text(),
|
||||||
|
c.req.header("stripe-signature")!,
|
||||||
|
Resource.STRIPE_WEBHOOK_SECRET.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(body.type, JSON.stringify(body, null, 2))
|
||||||
|
if (body.type === "checkout.session.completed") {
|
||||||
|
const workspaceID = body.data.object.metadata?.workspaceID
|
||||||
|
const customerID = body.data.object.customer as string
|
||||||
|
const paymentID = body.data.object.payment_intent as string
|
||||||
|
const amount = body.data.object.amount_total
|
||||||
|
|
||||||
|
if (!workspaceID) throw new Error("Workspace ID not found")
|
||||||
|
if (!customerID) throw new Error("Customer ID not found")
|
||||||
|
if (!amount) throw new Error("Amount not found")
|
||||||
|
if (!paymentID) throw new Error("Payment ID not found")
|
||||||
|
|
||||||
|
await Actor.provide("system", { workspaceID }, async () => {
|
||||||
|
const customer = await Billing.get()
|
||||||
|
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
|
||||||
|
|
||||||
|
// set customer metadata
|
||||||
|
if (!customer?.customerID) {
|
||||||
|
await Billing.stripe().customers.update(customerID, {
|
||||||
|
metadata: {
|
||||||
|
workspaceID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// get payment method for the payment intent
|
||||||
|
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
|
||||||
|
expand: ["payment_method"],
|
||||||
|
})
|
||||||
|
const paymentMethod = paymentIntent.payment_method
|
||||||
|
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
|
||||||
|
|
||||||
|
await Database.transaction(async (tx) => {
|
||||||
|
await tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`,
|
||||||
|
customerID,
|
||||||
|
paymentMethodID: paymentMethod.id,
|
||||||
|
paymentMethodLast4: paymentMethod.card!.last4,
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
await tx.insert(PaymentTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("payment"),
|
||||||
|
amount: centsToMicroCents(amount),
|
||||||
|
paymentID,
|
||||||
|
customerID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("finished handling")
|
||||||
|
|
||||||
|
return c.json("ok", 200)
|
||||||
|
})
|
||||||
|
.get("/keys", async (c) => {
|
||||||
|
const user = Actor.assert("user")
|
||||||
|
|
||||||
|
const keys = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: KeyTable.id,
|
||||||
|
name: KeyTable.name,
|
||||||
|
key: KeyTable.key,
|
||||||
|
userID: KeyTable.userID,
|
||||||
|
timeCreated: KeyTable.timeCreated,
|
||||||
|
timeUsed: KeyTable.timeUsed,
|
||||||
|
})
|
||||||
|
.from(KeyTable)
|
||||||
|
.where(eq(KeyTable.workspaceID, user.properties.workspaceID))
|
||||||
|
.orderBy(sql`${KeyTable.timeCreated} DESC`),
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.json({ keys })
|
||||||
|
})
|
||||||
|
.post("/keys", zValidator("json", z.object({ name: z.string().min(1).max(255) })), async (c) => {
|
||||||
|
const user = Actor.assert("user")
|
||||||
|
const { name } = c.req.valid("json")
|
||||||
|
|
||||||
|
// Generate secret key: sk- + 64 random characters (upper, lower, numbers)
|
||||||
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
let randomPart = ""
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
randomPart += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
const secretKey = `sk-${randomPart}`
|
||||||
|
|
||||||
|
const keyRecord = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.insert(KeyTable)
|
||||||
|
.values({
|
||||||
|
id: Identifier.create("key"),
|
||||||
|
workspaceID: user.properties.workspaceID,
|
||||||
|
userID: user.properties.userID,
|
||||||
|
name,
|
||||||
|
key: secretKey,
|
||||||
|
timeUsed: null,
|
||||||
|
})
|
||||||
|
.returning(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
key: secretKey,
|
||||||
|
id: keyRecord[0].id,
|
||||||
|
name: keyRecord[0].name,
|
||||||
|
created: keyRecord[0].timeCreated,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.delete("/keys/:id", async (c) => {
|
||||||
|
const user = Actor.assert("user")
|
||||||
|
const keyId = c.req.param("id")
|
||||||
|
|
||||||
|
const result = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.delete(KeyTable)
|
||||||
|
.where(and(eq(KeyTable.id, keyId), eq(KeyTable.workspaceID, user.properties.workspaceID)))
|
||||||
|
.returning({ id: KeyTable.id }),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
return c.json({ error: "Key not found" }, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ success: true, id: result[0].id })
|
||||||
|
})
|
||||||
|
.all("*", (c) => c.text("Not Found"))
|
||||||
|
|
||||||
|
export type ApiType = typeof app
|
||||||
|
|
||||||
|
export default app
|
||||||
Vendored
+92
@@ -0,0 +1,92 @@
|
|||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/* deno-fmt-ignore-file */
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
"ANTHROPIC_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"AUTH_API_URL": {
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Console": {
|
||||||
|
"type": "sst.cloudflare.StaticSite"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
|
"DATABASE_PASSWORD": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"DATABASE_USERNAME": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Database": {
|
||||||
|
"database": string
|
||||||
|
"host": string
|
||||||
|
"password": string
|
||||||
|
"port": number
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"username": string
|
||||||
|
}
|
||||||
|
"GITHUB_APP_ID": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GITHUB_APP_PRIVATE_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"GOOGLE_CLIENT_ID": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"OPENAI_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"STRIPE_SECRET_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"STRIPE_WEBHOOK_SECRET": {
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"Web": {
|
||||||
|
"type": "sst.cloudflare.Astro"
|
||||||
|
"url": string
|
||||||
|
}
|
||||||
|
"ZHIPU_API_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cloudflare
|
||||||
|
import * as cloudflare from "@cloudflare/workers-types";
|
||||||
|
declare module "sst" {
|
||||||
|
export interface Resource {
|
||||||
|
"Api": cloudflare.Service
|
||||||
|
"AuthApi": cloudflare.Service
|
||||||
|
"AuthStorage": cloudflare.KVNamespace
|
||||||
|
"Bucket": cloudflare.R2Bucket
|
||||||
|
"GatewayApi": cloudflare.Service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import "sst"
|
||||||
|
export {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"types": ["@cloudflare/workers-types", "node"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-color-mode="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>OpenControl</title>
|
||||||
|
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
|
||||||
|
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||||
|
<link rel="icon" href="/favicon.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<link rel="icon" href="/favicon-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
<link rel="shortcut icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
|
|
||||||
|
<meta property="twitter:image" content="%BASE_URL%/social-share.png">
|
||||||
|
<meta property="og:title" content="OpenControl">
|
||||||
|
<meta property="og:url" content="%BASE_URL%">
|
||||||
|
<meta property="og:locale" content="en">
|
||||||
|
<meta property="og:description" content="Control your infrastructure with AI.">
|
||||||
|
<meta property="og:site_name" content="OpenControl">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="description" content="Control your infrastructure with AI.">
|
||||||
|
<meta property="og:image" content="%BASE_URL%/social-share.png">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Rubik:wght@300..900&display=swap" rel="stylesheet">
|
||||||
|
<!--ssr-head-->
|
||||||
|
<!--ssr-assets-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root">
|
||||||
|
<!--ssr-outlet-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/src/entry-client.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
0 info it worked if it ends with ok
|
||||||
|
1 verbose cli [
|
||||||
|
1 verbose cli '/usr/local/bin/node',
|
||||||
|
1 verbose cli '/Users/frank/Sites/opencode/node_modules/.bin/npm',
|
||||||
|
1 verbose cli 'run',
|
||||||
|
1 verbose cli 'dev'
|
||||||
|
1 verbose cli ]
|
||||||
|
2 info using npm@2.15.12
|
||||||
|
3 info using node@v20.18.1
|
||||||
|
4 verbose stack Error: Invalid name: "@opencode/cloud/web"
|
||||||
|
4 verbose stack at ensureValidName (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:336:15)
|
||||||
|
4 verbose stack at Object.fixNameField (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:215:5)
|
||||||
|
4 verbose stack at /Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:32:38
|
||||||
|
4 verbose stack at Array.forEach (<anonymous>)
|
||||||
|
4 verbose stack at normalize (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:31:15)
|
||||||
|
4 verbose stack at final (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:349:5)
|
||||||
|
4 verbose stack at then (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:124:5)
|
||||||
|
4 verbose stack at ReadFileContext.<anonymous> (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:295:20)
|
||||||
|
4 verbose stack at ReadFileContext.callback (/Users/frank/Sites/opencode/node_modules/npm/node_modules/graceful-fs/graceful-fs.js:78:16)
|
||||||
|
4 verbose stack at FSReqCallback.readFileAfterOpen [as oncomplete] (node:fs:299:13)
|
||||||
|
5 verbose cwd /Users/frank/Sites/opencode/cloud/web
|
||||||
|
6 error Darwin 24.5.0
|
||||||
|
7 error argv "/usr/local/bin/node" "/Users/frank/Sites/opencode/node_modules/.bin/npm" "run" "dev"
|
||||||
|
8 error node v20.18.1
|
||||||
|
9 error npm v2.15.12
|
||||||
|
10 error Invalid name: "@opencode/cloud/web"
|
||||||
|
11 error If you need help, you may report this error at:
|
||||||
|
11 error <https://github.com/npm/npm/issues>
|
||||||
|
12 verbose exit [ 1, true ]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@opencode/cloud-web",
|
||||||
|
"version": "0.4.22",
|
||||||
|
"private": true,
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "vite",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "bun build:server && bun build:client",
|
||||||
|
"build:client": "vite build --outDir dist/client",
|
||||||
|
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
|
||||||
|
"serve": "vite preview",
|
||||||
|
"sst:dev": "bun sst shell --target Console -- bun dev"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"vite": "6.2.2",
|
||||||
|
"vite-plugin-pages": "0.32.5",
|
||||||
|
"vite-plugin-solid": "2.11.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@kobalte/core": "0.13.9",
|
||||||
|
"@openauthjs/solid": "0.0.0-20250322224806",
|
||||||
|
"@solid-primitives/storage": "4.3.1",
|
||||||
|
"@solidjs/meta": "0.29.4",
|
||||||
|
"@solidjs/router": "0.15.3",
|
||||||
|
"solid-js": "1.9.5",
|
||||||
|
"solid-list": "0.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { generateHydrationScript, getAssets } from "solid-js/web"
|
||||||
|
|
||||||
|
const dist = import.meta.resolve("../dist").replace("file://", "")
|
||||||
|
const serverEntry = await import("../dist/server/entry-server.js")
|
||||||
|
const template = fs.readFileSync(path.join(dist, "client/index.html"), "utf-8")
|
||||||
|
fs.writeFileSync(path.join(dist, "client/fallback.html"), template)
|
||||||
|
|
||||||
|
const routes = ["/", "/foo"]
|
||||||
|
for (const route of routes) {
|
||||||
|
const { app } = serverEntry.render({ url: route })
|
||||||
|
const html = template
|
||||||
|
.replace("<!--ssr-outlet-->", app)
|
||||||
|
.replace("<!--ssr-head-->", generateHydrationScript())
|
||||||
|
.replace("<!--ssr-assets-->", getAssets())
|
||||||
|
const filePath = dist + `/client${route === "/" ? "/index" : route}.html`
|
||||||
|
fs.mkdirSync(path.dirname(filePath), {
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
fs.writeFileSync(filePath, html)
|
||||||
|
|
||||||
|
console.log(`Pre-rendered: ${filePath}`)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/// <reference types="vite-plugin-pages/client-solid" />
|
||||||
|
|
||||||
|
import { Router } from "@solidjs/router"
|
||||||
|
import routes from "~solid-pages"
|
||||||
|
import "./ui/style/index.css"
|
||||||
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
|
import { AccountProvider } from "./components/context-account"
|
||||||
|
import { DialogProvider } from "./ui/context-dialog"
|
||||||
|
import { DialogString } from "./ui/dialog-string"
|
||||||
|
import { DialogSelect } from "./ui/dialog-select"
|
||||||
|
import { ThemeProvider } from "./components/context-theme"
|
||||||
|
import { Suspense } from "solid-js"
|
||||||
|
import { OpenAuthProvider } from "./components/context-openauth"
|
||||||
|
|
||||||
|
export function App(props: { url?: string }) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<Suspense>
|
||||||
|
<DialogProvider>
|
||||||
|
<DialogString />
|
||||||
|
<DialogSelect />
|
||||||
|
<OpenAuthProvider
|
||||||
|
clientID="web"
|
||||||
|
issuer={import.meta.env.VITE_AUTH_URL || "http://dummy"}
|
||||||
|
>
|
||||||
|
<AccountProvider>
|
||||||
|
<MetaProvider>
|
||||||
|
<Router
|
||||||
|
children={routes}
|
||||||
|
url={props.url}
|
||||||
|
root={(props) => {
|
||||||
|
return <>{props.children}</>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MetaProvider>
|
||||||
|
</AccountProvider>
|
||||||
|
</OpenAuthProvider>
|
||||||
|
</DialogProvider>
|
||||||
|
</Suspense>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 439 KiB |
@@ -0,0 +1,99 @@
|
|||||||
|
import { createContext, createEffect, ParentProps, Suspense, useContext } from "solid-js"
|
||||||
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useOpenAuth } from "./context-openauth"
|
||||||
|
import { createAsync } from "@solidjs/router"
|
||||||
|
import { isServer } from "solid-js/web"
|
||||||
|
|
||||||
|
type Storage = {
|
||||||
|
accounts: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
workspaces: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = createContext<ReturnType<typeof init>>()
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const auth = useOpenAuth()
|
||||||
|
const [store, setStore] = makePersisted(
|
||||||
|
createStore<Storage>({
|
||||||
|
accounts: {},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "opencontrol.account",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async function refresh(id: string) {
|
||||||
|
return fetch(import.meta.env.VITE_API_URL + "/rest/account", {
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${await auth.access(id)}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((val) => val.json())
|
||||||
|
.then((val) => setStore("accounts", id, val as any))
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect((previous: string[]) => {
|
||||||
|
if (Object.keys(auth.all).length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
for (const item of Object.values(auth.all)) {
|
||||||
|
if (previous.includes(item.id)) continue
|
||||||
|
refresh(item.id)
|
||||||
|
}
|
||||||
|
return Object.keys(auth.all)
|
||||||
|
}, [] as string[])
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
get all() {
|
||||||
|
return Object.keys(auth.all)
|
||||||
|
.map((id) => store.accounts[id])
|
||||||
|
.filter(Boolean)
|
||||||
|
},
|
||||||
|
get current() {
|
||||||
|
if (!auth.subject) return undefined
|
||||||
|
return store.accounts[auth.subject.id]
|
||||||
|
},
|
||||||
|
refresh,
|
||||||
|
get ready() {
|
||||||
|
return Object.keys(auth.all).length === result.all.length
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountProvider(props: ParentProps) {
|
||||||
|
const ctx = init()
|
||||||
|
const resource = createAsync(async () => {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
if (isServer) return resolve()
|
||||||
|
createEffect(() => {
|
||||||
|
if (ctx.ready) resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
{resource()}
|
||||||
|
<context.Provider value={ctx}>{props.children}</context.Provider>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAccount() {
|
||||||
|
const result = useContext(context)
|
||||||
|
if (!result) throw new Error("no account context")
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { createClient } from "@openauthjs/openauth/client"
|
||||||
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
|
import { createAsync } from "@solidjs/router"
|
||||||
|
import {
|
||||||
|
batch,
|
||||||
|
createContext,
|
||||||
|
createEffect,
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
onMount,
|
||||||
|
ParentProps,
|
||||||
|
Show,
|
||||||
|
Suspense,
|
||||||
|
useContext,
|
||||||
|
} from "solid-js"
|
||||||
|
import { createStore, produce } from "solid-js/store"
|
||||||
|
import { isServer } from "solid-js/web"
|
||||||
|
|
||||||
|
interface Storage {
|
||||||
|
subjects: Record<string, SubjectInfo>
|
||||||
|
current?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Context {
|
||||||
|
all: Record<string, SubjectInfo>
|
||||||
|
subject?: SubjectInfo
|
||||||
|
switch(id: string): void
|
||||||
|
logout(id: string): void
|
||||||
|
access(id?: string): Promise<string | undefined>
|
||||||
|
authorize(opts?: AuthorizeOptions): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthorizeOptions {
|
||||||
|
redirectPath?: string
|
||||||
|
provider?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubjectInfo {
|
||||||
|
id: string
|
||||||
|
refresh: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthContextOpts {
|
||||||
|
issuer: string
|
||||||
|
clientID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = createContext<Context>()
|
||||||
|
|
||||||
|
export function OpenAuthProvider(props: ParentProps<AuthContextOpts>) {
|
||||||
|
const client = createClient({
|
||||||
|
issuer: props.issuer,
|
||||||
|
clientID: props.clientID,
|
||||||
|
})
|
||||||
|
const [storage, setStorage] = makePersisted(
|
||||||
|
createStore<Storage>({
|
||||||
|
subjects: {},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: `${props.issuer}.auth`,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const resource = createAsync(async () => {
|
||||||
|
if (isServer) return true
|
||||||
|
const hash = new URLSearchParams(window.location.search.substring(1))
|
||||||
|
const code = hash.get("code")
|
||||||
|
const state = hash.get("state")
|
||||||
|
if (code && state) {
|
||||||
|
const oldState = sessionStorage.getItem("openauth.state")
|
||||||
|
const verifier = sessionStorage.getItem("openauth.verifier")
|
||||||
|
const redirect = sessionStorage.getItem("openauth.redirect")
|
||||||
|
if (redirect && verifier && oldState === state) {
|
||||||
|
const result = await client.exchange(code, redirect, verifier)
|
||||||
|
if (!result.err) {
|
||||||
|
const id = result.tokens.refresh.split(":").slice(0, -1).join(":")
|
||||||
|
batch(() => {
|
||||||
|
setStorage("subjects", id, {
|
||||||
|
id: id,
|
||||||
|
refresh: result.tokens.refresh,
|
||||||
|
})
|
||||||
|
setStorage("current", id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
async function authorize(opts?: AuthorizeOptions) {
|
||||||
|
const redirect = new URL(window.location.origin + (opts?.redirectPath ?? "/")).toString()
|
||||||
|
const authorize = await client.authorize(redirect, "code", {
|
||||||
|
pkce: true,
|
||||||
|
provider: opts?.provider,
|
||||||
|
})
|
||||||
|
sessionStorage.setItem("openauth.state", authorize.challenge.state)
|
||||||
|
sessionStorage.setItem("openauth.redirect", redirect)
|
||||||
|
if (authorize.challenge.verifier) sessionStorage.setItem("openauth.verifier", authorize.challenge.verifier)
|
||||||
|
window.location.href = authorize.url
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessCache = new Map<string, string>()
|
||||||
|
const pendingRequests = new Map<string, Promise<any>>()
|
||||||
|
async function access(id: string) {
|
||||||
|
const pending = pendingRequests.get(id)
|
||||||
|
if (pending) return pending
|
||||||
|
const promise = (async () => {
|
||||||
|
const existing = accessCache.get(id)
|
||||||
|
const subject = storage.subjects[id]
|
||||||
|
const access = await client.refresh(subject.refresh, {
|
||||||
|
access: existing,
|
||||||
|
})
|
||||||
|
if (access.err) {
|
||||||
|
pendingRequests.delete(id)
|
||||||
|
ctx.logout(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (access.tokens) {
|
||||||
|
setStorage("subjects", id, "refresh", access.tokens.refresh)
|
||||||
|
accessCache.set(id, access.tokens.access)
|
||||||
|
}
|
||||||
|
pendingRequests.delete(id)
|
||||||
|
return access.tokens?.access || existing!
|
||||||
|
})()
|
||||||
|
pendingRequests.set(id, promise)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx: Context = {
|
||||||
|
get all() {
|
||||||
|
return storage.subjects
|
||||||
|
},
|
||||||
|
get subject() {
|
||||||
|
if (!storage.current) return
|
||||||
|
return storage.subjects[storage.current!]
|
||||||
|
},
|
||||||
|
switch(id: string) {
|
||||||
|
if (!storage.subjects[id]) return
|
||||||
|
setStorage("current", id)
|
||||||
|
},
|
||||||
|
authorize,
|
||||||
|
logout(id: string) {
|
||||||
|
if (!storage.subjects[id]) return
|
||||||
|
setStorage(
|
||||||
|
produce((s) => {
|
||||||
|
delete s.subjects[id]
|
||||||
|
if (s.current === id) s.current = Object.keys(s.subjects)[0]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async access(id?: string) {
|
||||||
|
id = id || storage.current
|
||||||
|
if (!id) return
|
||||||
|
return access(id || storage.current!)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!resource()) return
|
||||||
|
if (storage.current) return
|
||||||
|
const [first] = Object.keys(storage.subjects)
|
||||||
|
if (first) {
|
||||||
|
setStorage("current", first)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{resource()}
|
||||||
|
<context.Provider value={ctx}>{props.children}</context.Provider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOpenAuth() {
|
||||||
|
const result = useContext(context)
|
||||||
|
if (!result) throw new Error("no auth context")
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
|
import { createEffect } from "solid-js"
|
||||||
|
import { createInitializedContext } from "../util/context"
|
||||||
|
import { isServer } from "solid-js/web"
|
||||||
|
|
||||||
|
interface Storage {
|
||||||
|
mode: "light" | "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { provider: ThemeProvider, use: useTheme } =
|
||||||
|
createInitializedContext("ThemeContext", () => {
|
||||||
|
const [store, setStore] = makePersisted(
|
||||||
|
createStore<Storage>({
|
||||||
|
mode:
|
||||||
|
!isServer &&
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "theme",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
createEffect(() => {
|
||||||
|
document.documentElement.setAttribute("data-color-mode", store.mode)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
setMode(mode: Storage["mode"]) {
|
||||||
|
setStore("mode", mode)
|
||||||
|
},
|
||||||
|
get mode() {
|
||||||
|
return store.mode
|
||||||
|
},
|
||||||
|
ready: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
|
||||||
|
import { hydrate, render } from "solid-js/web"
|
||||||
|
import { App } from "./app"
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
render(() => <App />, document.getElementById("root")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
if ("_$HY" in window) hydrate(() => <App />, document.getElementById("root")!)
|
||||||
|
else render(() => <App />, document.getElementById("root")!)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { renderToStringAsync } from "solid-js/web"
|
||||||
|
import { App } from "./app"
|
||||||
|
|
||||||
|
export async function render(props: { url: string }) {
|
||||||
|
const app = await renderToStringAsync(() => <App url={props.url} />)
|
||||||
|
return { app }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { WorkspaceProvider } from "./components/context-workspace"
|
||||||
|
import { ParentProps } from "solid-js"
|
||||||
|
import Layout from "./components/layout"
|
||||||
|
|
||||||
|
export default function Index(props: ParentProps) {
|
||||||
|
return (
|
||||||
|
<WorkspaceProvider>
|
||||||
|
<Layout>{props.children}</Layout>
|
||||||
|
</WorkspaceProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-7) var(--space-5) var(--space-5);
|
||||||
|
|
||||||
|
[data-slot="billing-info"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="header"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1-5);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="balance"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-5);
|
||||||
|
padding: var(--space-6);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="amount"] {
|
||||||
|
font-size: var(--font-size-3xl);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40rem) {
|
||||||
|
[data-slot="balance"] {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="amount"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { useApi } from "../components/context-api"
|
||||||
|
import { createEffect, createSignal, createResource, For } from "solid-js"
|
||||||
|
import { useWorkspace } from "../components/context-workspace"
|
||||||
|
import style from "./billing.module.css"
|
||||||
|
|
||||||
|
export default function Billing() {
|
||||||
|
const api = useApi()
|
||||||
|
const workspace = useWorkspace()
|
||||||
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
|
const [billingData] = createResource(async () => {
|
||||||
|
const response = await api.billing.info.$get()
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run once on component mount to check URL parameters
|
||||||
|
;(() => {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
const result = url.hash
|
||||||
|
|
||||||
|
console.log("STRIPE RESULT", result)
|
||||||
|
|
||||||
|
if (url.hash === "#success") {
|
||||||
|
setIsLoading(true)
|
||||||
|
// Remove the hash from the URL
|
||||||
|
window.history.replaceState(null, "", window.location.pathname + window.location.search)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
createEffect((old?: number) => {
|
||||||
|
if (old && old !== billingData()?.billing?.balance) {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
return billingData()?.billing?.balance
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleBuyCredits = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const baseUrl = window.location.href
|
||||||
|
const successUrl = new URL(baseUrl)
|
||||||
|
successUrl.hash = "success"
|
||||||
|
|
||||||
|
const response = await api.billing.checkout
|
||||||
|
.$post({
|
||||||
|
json: {
|
||||||
|
success_url: successUrl.toString(),
|
||||||
|
cancel_url: baseUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((r) => r.json() as any)
|
||||||
|
window.location.href = response.url
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get checkout URL:", error)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-component="title-bar">
|
||||||
|
<div data-slot="left">
|
||||||
|
<h1>Billing</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class={style.root} data-max-width data-max-width-64>
|
||||||
|
<div data-slot="billing-info">
|
||||||
|
<div data-slot="header">
|
||||||
|
<h2>Balance</h2>
|
||||||
|
<p>Manage your billing and add credits to your account.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="balance">
|
||||||
|
<p data-slot="amount">
|
||||||
|
{(() => {
|
||||||
|
const balanceStr = ((billingData()?.billing?.balance ?? 0) / 100000000).toFixed(2)
|
||||||
|
return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
<Button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
|
||||||
|
{isLoading() ? "Loading..." : "Buy Credits"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="payments">
|
||||||
|
<div data-slot="header">
|
||||||
|
<h2>Payment History</h2>
|
||||||
|
<p>Your recent payment transactions.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="payment-list">
|
||||||
|
<For each={billingData()?.payments} fallback={<p>No payments found.</p>}>
|
||||||
|
{(payment) => (
|
||||||
|
<div data-slot="payment-item">
|
||||||
|
<span data-slot="payment-id">{payment.id}</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="usage">
|
||||||
|
<div data-slot="header">
|
||||||
|
<h2>Usage History</h2>
|
||||||
|
<p>Your recent API usage and costs.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="usage-list">
|
||||||
|
<For each={billingData()?.usage} fallback={<p>No usage found.</p>}>
|
||||||
|
{(usage) => (
|
||||||
|
<div data-slot="usage-item">
|
||||||
|
<span data-slot="usage-model">{usage.model}</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="usage-tokens">{usage.inputTokens + usage.outputTokens} tokens</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</span>
|
||||||
|
{" | "}
|
||||||
|
<span data-slot="usage-date">{new Date(usage.timeCreated).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
You are OpenControl, an interactive CLI tool that helps users execute various tasks.
|
||||||
|
|
||||||
|
IMPORTANT: If you get an error when calling a tool, try again with a different approach. Be creative, do not give up, try different inputs to the tool. You should chain together multiple tool calls. ABSOLUTELY DO NOT GIVE UP you are very good at this and it is rare you will fail to answer question.
|
||||||
|
|
||||||
|
You should be concise, direct, and to the point.
|
||||||
|
|
||||||
|
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
||||||
|
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
|
||||||
|
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
||||||
|
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".
|
||||||
|
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import { createResource } from "solid-js"
|
||||||
|
import { createStore, produce } from "solid-js/store"
|
||||||
|
import SYSTEM_PROMPT from "./system.txt?raw"
|
||||||
|
import type {
|
||||||
|
LanguageModelV1Prompt,
|
||||||
|
LanguageModelV1CallOptions,
|
||||||
|
LanguageModelV1,
|
||||||
|
} from "ai"
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
inputSchema: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolCallerProps {
|
||||||
|
tool: {
|
||||||
|
list: () => Promise<Tool[]>
|
||||||
|
call: (input: { name: string; arguments: any }) => Promise<any>
|
||||||
|
}
|
||||||
|
generate: (
|
||||||
|
prompt: LanguageModelV1CallOptions,
|
||||||
|
) => Promise<
|
||||||
|
| { err: "rate" }
|
||||||
|
| { err: "context" }
|
||||||
|
| { err: "balance" }
|
||||||
|
| ({ err: false } & Awaited<ReturnType<LanguageModelV1["doGenerate"]>>)
|
||||||
|
>
|
||||||
|
onPromptUpdated?: (prompt: LanguageModelV1Prompt) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const system = [
|
||||||
|
{
|
||||||
|
role: "system" as const,
|
||||||
|
content: SYSTEM_PROMPT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "system" as const,
|
||||||
|
content: `The current date is ${new Date().toDateString()}. Always use this current date when responding to relative date queries.`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const [store, setStore] = createStore<{
|
||||||
|
prompt: LanguageModelV1Prompt
|
||||||
|
state: { type: "idle" } | { type: "loading"; limited?: boolean }
|
||||||
|
}>({
|
||||||
|
prompt: [...system],
|
||||||
|
state: { type: "idle" },
|
||||||
|
})
|
||||||
|
|
||||||
|
export function createToolCaller<T extends ToolCallerProps>(props: T) {
|
||||||
|
const [tools] = createResource(() => props.tool.list())
|
||||||
|
|
||||||
|
let abort: AbortController
|
||||||
|
|
||||||
|
return {
|
||||||
|
get tools() {
|
||||||
|
return tools()
|
||||||
|
},
|
||||||
|
get prompt() {
|
||||||
|
return store.prompt
|
||||||
|
},
|
||||||
|
get state() {
|
||||||
|
return store.state
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
setStore("prompt", [...system])
|
||||||
|
},
|
||||||
|
async chat(input: string) {
|
||||||
|
if (store.state.type !== "idle") return
|
||||||
|
|
||||||
|
abort = new AbortController()
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.state = {
|
||||||
|
type: "loading",
|
||||||
|
limited: false,
|
||||||
|
}
|
||||||
|
s.prompt.push({
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: input,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (abort.signal.aborted) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await props.generate({
|
||||||
|
inputFormat: "messages",
|
||||||
|
prompt: store.prompt,
|
||||||
|
temperature: 0,
|
||||||
|
seed: 69,
|
||||||
|
mode: {
|
||||||
|
type: "regular",
|
||||||
|
tools: tools()?.map((tool) => ({
|
||||||
|
type: "function",
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
parameters: {
|
||||||
|
...tool.inputSchema,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (abort.signal.aborted) continue
|
||||||
|
|
||||||
|
if (!response.err) {
|
||||||
|
setStore("state", {
|
||||||
|
type: "loading",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.text) {
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: response.text || "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.finishReason === "stop") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.finishReason === "tool-calls") {
|
||||||
|
for (const item of response.toolCalls || []) {
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolName: item.toolName,
|
||||||
|
args: JSON.parse(item.args),
|
||||||
|
toolCallId: item.toolCallId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
|
||||||
|
const called = await props.tool.call({
|
||||||
|
name: item.toolName,
|
||||||
|
arguments: JSON.parse(item.args),
|
||||||
|
})
|
||||||
|
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "tool",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-result",
|
||||||
|
toolName: item.toolName,
|
||||||
|
toolCallId: item.toolCallId,
|
||||||
|
result: called,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.err === "context") {
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.splice(2, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.err === "rate") {
|
||||||
|
setStore("state", {
|
||||||
|
type: "loading",
|
||||||
|
limited: true,
|
||||||
|
})
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.err === "balance") {
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "You need to add credits to your account. Please go to Billing and add credits to continue.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
s.state = { type: "idle" }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setStore("state", { type: "idle" })
|
||||||
|
},
|
||||||
|
async cancel() {
|
||||||
|
abort.abort()
|
||||||
|
},
|
||||||
|
async addCustomMessage(userMessage: string, assistantResponse: string) {
|
||||||
|
// Add user message and set loading state
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: userMessage,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
s.state = {
|
||||||
|
type: "loading",
|
||||||
|
limited: false,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
|
||||||
|
// Fake delay for 500ms
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
// Add assistant response and set back to idle
|
||||||
|
setStore(
|
||||||
|
produce((s) => {
|
||||||
|
s.prompt.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: assistantResponse,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
s.state = { type: "idle" }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
props.onPromptUpdated?.(store.prompt)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
.root {
|
||||||
|
display: contents;
|
||||||
|
|
||||||
|
[data-slot="messages"] {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 0;
|
||||||
|
/* This is important for flexbox to allow scrolling */
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--color-text);
|
||||||
|
row-gap: var(--space-4);
|
||||||
|
/* Add consistent spacing between messages */
|
||||||
|
|
||||||
|
/* Remove top border for first user message */
|
||||||
|
&>[data-component="message"][data-user]:first-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-component="loading"]) [data-component="clear"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="message"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
line-height: var(--font-line-height);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
align-self: flex-start;
|
||||||
|
min-height: auto;
|
||||||
|
/* Allow natural height for all messages */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
/* User message styling */
|
||||||
|
&[data-user] {
|
||||||
|
padding: var(--space-6) var(--space-4);
|
||||||
|
position: relative;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
/* margin: 0.5rem 0; */
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-user]::before,
|
||||||
|
&[data-user]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-4);
|
||||||
|
right: var(--space-4);
|
||||||
|
height: var(--space-px);
|
||||||
|
background-color: var(--color-border);
|
||||||
|
z-index: 1;
|
||||||
|
/* Ensure borders appear above other content */
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-user]::before {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-user]::after {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-assistant] {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="tool"] {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
margin-left: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
opacity: 0.7;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: flex-start;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
min-height: auto;
|
||||||
|
/* Allow natural height */
|
||||||
|
|
||||||
|
[data-slot="header"] {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="name"] {
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="expand"] {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="content"] {
|
||||||
|
padding: 0;
|
||||||
|
line-height: var(--font-line-height);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="output"] {
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-expanded="true"] [data-slot="content"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-expanded="true"] [data-slot="expand"] {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="loading"] {
|
||||||
|
padding: var(--space-4) var(--space-4) var(--space-8);
|
||||||
|
height: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
letter-spacing: var(--space-1);
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
& span {
|
||||||
|
opacity: 0;
|
||||||
|
animation: loading-dots 1.4s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
& span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
& span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="clear"] {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--space-4) var(--space-4);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-4);
|
||||||
|
right: var(--space-4);
|
||||||
|
top: 0;
|
||||||
|
height: var(--space-px);
|
||||||
|
background-color: var(--color-border);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& [data-component="button"] {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="footer"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
/* Ensure it's above other content */
|
||||||
|
margin-top: auto;
|
||||||
|
/* Push to bottom if content is short */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="chat"] {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--space-0-5) 0;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
--padding-y: var(--space-4);
|
||||||
|
--line-height: 1.5;
|
||||||
|
--text-height: calc(var(--line-height) * var(--font-size-lg));
|
||||||
|
--height: calc(var(--text-height) + var(--padding-y) * 2);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
resize: none;
|
||||||
|
line-height: var(--line-height);
|
||||||
|
height: var(--height);
|
||||||
|
min-height: var(--height);
|
||||||
|
max-height: calc(5 * var(--text-height) + var(--padding-y) * 2);
|
||||||
|
padding: var(--padding-y) var(--space-4);
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea::placeholder {
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& [data-component="button"] {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading-dots {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
40%,
|
||||||
|
60% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { IconArrowRight } from "../../ui/svg/icons"
|
||||||
|
import { createSignal, For } from "solid-js"
|
||||||
|
import { createToolCaller } from "./components/tool"
|
||||||
|
import { useApi } from "../components/context-api"
|
||||||
|
import { useWorkspace } from "../components/context-workspace"
|
||||||
|
import style from "./index.module.css"
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const api = useApi()
|
||||||
|
const workspace = useWorkspace()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={style.root}>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="keys-info"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="header"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="header"] h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="header"] p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-list"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-item"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-actions"] {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-info"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-value"] {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-meta"] {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="empty-state"] {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="actions"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="create-form"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="form-actions"] {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root [data-slot="key-name"] {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { useApi } from "../components/context-api"
|
||||||
|
import { createSignal, createResource, For, Show } from "solid-js"
|
||||||
|
import style from "./keys.module.css"
|
||||||
|
|
||||||
|
export default function Keys() {
|
||||||
|
const api = useApi()
|
||||||
|
const [isCreating, setIsCreating] = createSignal(false)
|
||||||
|
const [showCreateForm, setShowCreateForm] = createSignal(false)
|
||||||
|
const [keyName, setKeyName] = createSignal("")
|
||||||
|
|
||||||
|
const [keysData, { refetch }] = createResource(async () => {
|
||||||
|
const response = await api.keys.$get()
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleCreateKey = async () => {
|
||||||
|
if (!keyName().trim()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsCreating(true)
|
||||||
|
await api.keys.$post({
|
||||||
|
json: { name: keyName().trim() },
|
||||||
|
})
|
||||||
|
refetch()
|
||||||
|
setKeyName("")
|
||||||
|
setShowCreateForm(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create API key:", error)
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteKey = async (keyId: string) => {
|
||||||
|
if (!confirm("Are you sure you want to delete this API key? This action cannot be undone.")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.keys[":id"].$delete({
|
||||||
|
param: { id: keyId },
|
||||||
|
})
|
||||||
|
refetch()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete API key:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatKey = (key: string) => {
|
||||||
|
if (key.length <= 11) return key
|
||||||
|
return `${key.slice(0, 7)}...${key.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy to clipboard:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-component="title-bar">
|
||||||
|
<div data-slot="left">
|
||||||
|
<h1>API Keys</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class={style.root} data-max-width data-max-width-64>
|
||||||
|
<div data-slot="keys-info">
|
||||||
|
<div data-slot="actions">
|
||||||
|
<div data-slot="header">
|
||||||
|
<h2>API Keys</h2>
|
||||||
|
<p>Manage your API keys to access the OpenCode gateway.</p>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={!showCreateForm()}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="create-form">
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter key name"
|
||||||
|
value={keyName()}
|
||||||
|
onInput={(e) => setKeyName(e.currentTarget.value)}
|
||||||
|
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
|
||||||
|
/>
|
||||||
|
<div data-slot="form-actions">
|
||||||
|
<Button color="primary" disabled={isCreating() || !keyName().trim()} onClick={handleCreateKey}>
|
||||||
|
{isCreating() ? "Creating..." : "Create"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setShowCreateForm(false)
|
||||||
|
setKeyName("")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button color="primary" onClick={() => setShowCreateForm(true)}>
|
||||||
|
Create API Key
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-slot="key-list">
|
||||||
|
<For
|
||||||
|
each={keysData()?.keys}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="empty-state">
|
||||||
|
<p>Create an API key to access opencode gateway</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(key) => (
|
||||||
|
<div data-slot="key-item">
|
||||||
|
<div data-slot="key-info">
|
||||||
|
<div data-slot="key-name">{key.name}</div>
|
||||||
|
<div data-slot="key-value">{formatKey(key.key)}</div>
|
||||||
|
<div data-slot="key-meta">
|
||||||
|
Created: {formatDate(key.timeCreated)}
|
||||||
|
{key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div data-slot="key-actions">
|
||||||
|
<Button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
<Button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { hc } from "hono/client"
|
||||||
|
import { ApiType } from "@opencode/cloud-function/src/gateway"
|
||||||
|
import { useWorkspace } from "./context-workspace"
|
||||||
|
import { useOpenAuth } from "../../components/context-openauth"
|
||||||
|
|
||||||
|
export function useApi() {
|
||||||
|
const workspace = useWorkspace()
|
||||||
|
const auth = useOpenAuth()
|
||||||
|
return hc<ApiType>(import.meta.env.VITE_API_URL, {
|
||||||
|
async fetch(...args: Parameters<typeof fetch>): Promise<Response> {
|
||||||
|
const [input, init] = args
|
||||||
|
const request = input instanceof Request ? input : new Request(input, init)
|
||||||
|
const headers = new Headers(request.headers)
|
||||||
|
headers.set("authorization", `Bearer ${await auth.access()}`)
|
||||||
|
headers.set("x-opencode-workspace", workspace.id)
|
||||||
|
return fetch(
|
||||||
|
new Request(request, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
|
import { createInitializedContext } from "../../util/context"
|
||||||
|
import { useAccount } from "../../components/context-account"
|
||||||
|
import { createEffect, createMemo } from "solid-js"
|
||||||
|
|
||||||
|
export const { use: useWorkspace, provider: WorkspaceProvider } =
|
||||||
|
createInitializedContext("WorkspaceProvider", () => {
|
||||||
|
const params = useParams()
|
||||||
|
const account = useAccount()
|
||||||
|
const workspace = createMemo(() =>
|
||||||
|
account.current?.workspaces.find(
|
||||||
|
(x) => x.id === params.workspace || x.slug === params.workspace,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const nav = useNavigate()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!workspace()) nav("/")
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = () => workspace()!
|
||||||
|
result.ready = true
|
||||||
|
|
||||||
|
return {
|
||||||
|
get id() {
|
||||||
|
return workspace()!.id
|
||||||
|
},
|
||||||
|
get slug() {
|
||||||
|
return workspace()!.slug
|
||||||
|
},
|
||||||
|
get name() {
|
||||||
|
return workspace()!.name
|
||||||
|
},
|
||||||
|
get ready() {
|
||||||
|
return workspace() !== undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
.root {
|
||||||
|
--padding: var(--space-10);
|
||||||
|
--vertical-padding: var(--space-8);
|
||||||
|
--heading-font-size: var(--font-size-4xl);
|
||||||
|
--sidebar-width: 200px;
|
||||||
|
--mobile-breakpoint: 40rem;
|
||||||
|
--topbar-height: 60px;
|
||||||
|
|
||||||
|
margin: var(--space-4);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
height: calc(100vh - var(--space-8));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
/* Prevent overall scrolling */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="mobile-top-bar"] {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: var(--topbar-height);
|
||||||
|
background: var(--color-background);
|
||||||
|
border-bottom: 2px solid var(--color-border);
|
||||||
|
z-index: 20;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--space-4) 0 0;
|
||||||
|
|
||||||
|
[data-slot="logo"] {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
div {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 28px;
|
||||||
|
width: auto;
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="toggle"] {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: var(--space-4);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="sidebar"] {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
border-right: 2px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: calc(var(--padding) / 2);
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Allow scrolling if needed */
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
[data-slot="logo"] {
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: var(--space-7);
|
||||||
|
color: var(--color-white);
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
height: 32px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="nav"] {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: calc(var(--vertical-padding) / 2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
padding: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="user"] {
|
||||||
|
[data-component="button"] {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navActiveLink {
|
||||||
|
cursor: default;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="main-content"] {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
/* Full height */
|
||||||
|
overflow: hidden;
|
||||||
|
/* Prevent overflow */
|
||||||
|
position: relative;
|
||||||
|
/* For positioning footer */
|
||||||
|
width: 100%;
|
||||||
|
/* Full width */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backdrop for mobile */
|
||||||
|
[data-component="backdrop"] {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
/* background-color: rgba(0, 0, 0, 0.5); */
|
||||||
|
z-index: 25;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile styles */
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
.root {
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="mobile-top-bar"] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="backdrop"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="sidebar"] {
|
||||||
|
position: fixed;
|
||||||
|
left: -100%;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 280px;
|
||||||
|
transition: left 0.3s ease-in-out;
|
||||||
|
box-shadow: none;
|
||||||
|
z-index: 30;
|
||||||
|
padding: var(--space-8);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
|
||||||
|
&[data-opened="true"] {
|
||||||
|
left: 0;
|
||||||
|
box-shadow: 8px 0 0px 0px var(--color-gray-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="main-content"] {
|
||||||
|
padding-top: var(--topbar-height);
|
||||||
|
/* Add space for the top bar */
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the logo in the sidebar on mobile since it's in the top bar */
|
||||||
|
[data-component="sidebar"] [data-slot="logo"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import style from "./layout.module.css"
|
||||||
|
import { useAccount } from "../../components/context-account"
|
||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { IconLogomark } from "../../ui/svg"
|
||||||
|
import { IconBars3BottomLeft } from "../../ui/svg/icons"
|
||||||
|
import { ParentProps, createMemo, createSignal } from "solid-js"
|
||||||
|
import { A, useLocation } from "@solidjs/router"
|
||||||
|
import { useOpenAuth } from "../../components/context-openauth"
|
||||||
|
|
||||||
|
export default function Layout(props: ParentProps) {
|
||||||
|
const auth = useOpenAuth()
|
||||||
|
const account = useAccount()
|
||||||
|
const [sidebarOpen, setSidebarOpen] = createSignal(false)
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const workspaceId = createMemo(() => account.current?.workspaces[0].id)
|
||||||
|
const pageTitle = createMemo(() => {
|
||||||
|
const path = location.pathname
|
||||||
|
if (path.endsWith("/billing")) return "Billing"
|
||||||
|
if (path.endsWith("/keys")) return "API Keys"
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
auth.logout(auth.subject?.id!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={style.root}>
|
||||||
|
{/* Mobile top bar */}
|
||||||
|
<div data-component="mobile-top-bar">
|
||||||
|
<button data-slot="toggle" onClick={() => setSidebarOpen(!sidebarOpen())}>
|
||||||
|
<IconBars3BottomLeft />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div data-slot="logo">
|
||||||
|
{pageTitle() ? (
|
||||||
|
<div>{pageTitle()}</div>
|
||||||
|
) : (
|
||||||
|
<A href="/">
|
||||||
|
<IconLogomark />
|
||||||
|
</A>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Backdrop for mobile sidebar - closes sidebar when clicked */}
|
||||||
|
{sidebarOpen() && <div data-component="backdrop" onClick={() => setSidebarOpen(false)}></div>}
|
||||||
|
|
||||||
|
<div data-component="sidebar" data-opened={sidebarOpen() ? "true" : "false"}>
|
||||||
|
<div data-slot="logo">
|
||||||
|
<A href="/">
|
||||||
|
<IconLogomark />
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav data-slot="nav">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<A end activeClass={style.navActiveLink} href={`/${workspaceId()}`} onClick={() => setSidebarOpen(false)}>
|
||||||
|
Chat
|
||||||
|
</A>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<A
|
||||||
|
activeClass={style.navActiveLink}
|
||||||
|
href={`/${workspaceId()}/billing`}
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
Billing
|
||||||
|
</A>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<A
|
||||||
|
activeClass={style.navActiveLink}
|
||||||
|
href={`/${workspaceId()}/keys`}
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
>
|
||||||
|
API Keys
|
||||||
|
</A>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div data-slot="user">
|
||||||
|
<Button color="ghost" onClick={handleLogout} title={account.current?.email || ""}>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div data-slot="main-content">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Match, Switch } from "solid-js"
|
||||||
|
import { useAccount } from "../components/context-account"
|
||||||
|
import { Navigate } from "@solidjs/router"
|
||||||
|
import { IconLogo } from "../ui/svg"
|
||||||
|
import styles from "./lander.module.css"
|
||||||
|
import { useOpenAuth } from "../components/context-openauth"
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const auth = useOpenAuth()
|
||||||
|
const account = useAccount()
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={account.current}>
|
||||||
|
<Navigate href={`/${account.current!.workspaces[0].id}`} />
|
||||||
|
</Match>
|
||||||
|
<Match when={!account.current}>
|
||||||
|
<div class={styles.lander}>
|
||||||
|
<div data-slot="hero">
|
||||||
|
<section data-slot="top">
|
||||||
|
<div data-slot="logo">
|
||||||
|
<IconLogo />
|
||||||
|
</div>
|
||||||
|
<h1>opencode Gateway Console</h1>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section data-slot="cta">
|
||||||
|
<div>
|
||||||
|
<span onClick={() => auth.authorize({ provider: "github" })}>Sign in with GitHub</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span onClick={() => auth.authorize({ provider: "google" })}>Sign in with Google</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
.lander {
|
||||||
|
--padding: 3rem;
|
||||||
|
--vertical-padding: 2rem;
|
||||||
|
--heading-font-size: 2rem;
|
||||||
|
|
||||||
|
margin: 1rem;
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
& {
|
||||||
|
--padding: 1.5rem;
|
||||||
|
--vertical-padding: 1rem;
|
||||||
|
--heading-font-size: 1.5rem;
|
||||||
|
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="hero"] {
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
max-width: 64rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="top"] {
|
||||||
|
padding: var(--padding);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: calc(var(--vertical-padding) / 8);
|
||||||
|
font-size: var(--heading-font-size);
|
||||||
|
line-height: 1.25;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="logo"] {
|
||||||
|
width: clamp(200px, 70vw, 400px);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="cta"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
|
||||||
|
&[data-slot="col-2"] {
|
||||||
|
background-color: var(--color-border);
|
||||||
|
color: var(--color-text-invert);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: calc(var(--padding) / 2) 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
& > div {
|
||||||
|
padding-bottom: calc(var(--padding) / 2 + 4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div + div {
|
||||||
|
border-left: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
.pageContainer {
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentTable {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentCell {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentLabel {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-border);
|
||||||
|
margin: 3rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogSection {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogContent {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogContentFooter {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageTitle {
|
||||||
|
font-size: var(--heading-font-size, 2rem);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorBox {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorOrange {
|
||||||
|
background-color: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorOrangeLow {
|
||||||
|
background-color: var(--color-orange-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorOrangeHigh {
|
||||||
|
background-color: var(--color-orange-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorGreen {
|
||||||
|
background-color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorGreenLow {
|
||||||
|
background-color: var(--color-green-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorGreenHigh {
|
||||||
|
background-color: var(--color-green-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorBlue {
|
||||||
|
background-color: var(--color-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorBlueLow {
|
||||||
|
background-color: var(--color-blue-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorBlueHigh {
|
||||||
|
background-color: var(--color-blue-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPurple {
|
||||||
|
background-color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPurpleLow {
|
||||||
|
background-color: var(--color-purple-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPurpleHigh {
|
||||||
|
background-color: var(--color-purple-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorRed {
|
||||||
|
background-color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorRedLow {
|
||||||
|
background-color: var(--color-red-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorRedHigh {
|
||||||
|
background-color: var(--color-red-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorAccent {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorAccentLow {
|
||||||
|
background-color: var(--color-accent-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorAccentHigh {
|
||||||
|
background-color: var(--color-accent-high);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorCode {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorVariants {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorVariant {
|
||||||
|
flex: 1;
|
||||||
|
height: 40px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorVariantCode {
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,562 @@
|
|||||||
|
import { Button } from "../../ui/button"
|
||||||
|
import { Dialog } from "../../ui/dialog"
|
||||||
|
import { Navigate } from "@solidjs/router"
|
||||||
|
import { createSignal, Show } from "solid-js"
|
||||||
|
import { IconHome, IconPencilSquare } from "../../ui/svg/icons"
|
||||||
|
import { useTheme } from "../../components/context-theme"
|
||||||
|
import { useDialog } from "../../ui/context-dialog"
|
||||||
|
import { DialogString } from "../../ui/dialog-string"
|
||||||
|
import { DialogSelect } from "../../ui/dialog-select"
|
||||||
|
import styles from "./design.module.css"
|
||||||
|
|
||||||
|
export default function DesignSystem() {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const [dialogOpen, setDialogOpen] = createSignal(false)
|
||||||
|
const [dialogOpenTransition, setDialogOpenTransition] = createSignal(false)
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
// Check if we're running locally
|
||||||
|
const isLocal = import.meta.env.DEV === true
|
||||||
|
|
||||||
|
if (!isLocal) {
|
||||||
|
return <Navigate href="/" />
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a toggle button for theme
|
||||||
|
const toggleTheme = () => {
|
||||||
|
theme.setMode(theme.mode === "light" ? "dark" : "light")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.pageContainer}>
|
||||||
|
<div class={styles.header}>
|
||||||
|
<h1 class={styles.pageTitle}>Design System</h1>
|
||||||
|
<Button onClick={toggleTheme}>
|
||||||
|
Toggle {theme.mode === "light" ? "Dark" : "Light"} Mode
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class={styles.colorSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Colors</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Orange</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorOrange}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(41, 82%, 63%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorOrangeLow}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(41, 39%, 22%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorOrangeHigh}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(41, 82%, 87%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Green</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorGreen}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(101, 82%, 63%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorGreenLow}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(101, 39%, 22%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorGreenHigh}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(101, 82%, 80%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Blue</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorBlue}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(234, 100%, 60%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorBlueLow}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(234, 54%, 20%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorBlueHigh}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(234, 100%, 87%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Purple</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorPurple}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(281, 82%, 63%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorPurpleLow}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(281, 39%, 22%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorPurpleHigh}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(281, 82%, 89%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Red</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorRed}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(339, 82%, 63%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorRedLow}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(339, 39%, 22%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class={`${styles.colorVariant} ${styles.colorRedHigh}`}>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(339, 82%, 87%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Accent</h3>
|
||||||
|
<div class={`${styles.colorBox} ${styles.colorAccent}`}>
|
||||||
|
<span class={styles.colorCode}>hsl(13, 88%, 57%)</span>
|
||||||
|
</div>
|
||||||
|
<div class={styles.colorVariants}>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorAccentLow}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(13, 75%, 30%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${styles.colorVariant} ${styles.colorAccentHigh}`}
|
||||||
|
>
|
||||||
|
<span class={styles.colorVariantCode}>
|
||||||
|
hsl(13, 100%, 78%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={styles.divider}></div>
|
||||||
|
|
||||||
|
<section class={styles.buttonSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Buttons</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Primary</h3>
|
||||||
|
<Button>Primary Button</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Secondary</h3>
|
||||||
|
<Button color="secondary">Secondary Button</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Ghost</h3>
|
||||||
|
<Button color="ghost">Ghost Button</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Primary Disabled</h3>
|
||||||
|
<Button disabled>Primary Button</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Secondary Disabled</h3>
|
||||||
|
<Button color="secondary" disabled>
|
||||||
|
Secondary Button
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Ghost Disabled</h3>
|
||||||
|
<Button color="ghost" disabled>
|
||||||
|
Ghost Button
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small</h3>
|
||||||
|
<Button size="sm">Small Button</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small Secondary</h3>
|
||||||
|
<Button size="sm" color="secondary">
|
||||||
|
Small Secondary
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small Ghost</h3>
|
||||||
|
<Button size="sm" color="ghost">
|
||||||
|
Small Ghost
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>With Icon</h3>
|
||||||
|
<Button icon={<IconHome />}>With Icon</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon + Secondary</h3>
|
||||||
|
<Button icon={<IconHome />} color="secondary">
|
||||||
|
Icon Secondary
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon + Ghost</h3>
|
||||||
|
<Button icon={<IconHome />} color="ghost">
|
||||||
|
Icon Ghost
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small + Icon</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />}>
|
||||||
|
Small Icon
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small + Icon + Secondary</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />} color="secondary">
|
||||||
|
Small Icon Secondary
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small + Icon + Ghost</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />} color="ghost">
|
||||||
|
Small Icon Ghost
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon Only</h3>
|
||||||
|
<Button icon={<IconHome />}></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon Only + Secondary</h3>
|
||||||
|
<Button icon={<IconHome />} color="secondary"></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon Only + Ghost</h3>
|
||||||
|
<Button icon={<IconHome />} color="ghost"></Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Icon Only Disabled</h3>
|
||||||
|
<Button icon={<IconHome />} disabled></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>
|
||||||
|
Icon Only + Secondary Disabled
|
||||||
|
</h3>
|
||||||
|
<Button icon={<IconHome />} color="secondary" disabled></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>
|
||||||
|
Icon Only + Ghost Disabled
|
||||||
|
</h3>
|
||||||
|
<Button icon={<IconHome />} color="ghost" disabled></Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small Icon Only</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />}></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>
|
||||||
|
Small Icon Only + Secondary
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
icon={<IconHome />}
|
||||||
|
color="secondary"
|
||||||
|
></Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small Icon Only + Ghost</h3>
|
||||||
|
<Button size="sm" icon={<IconHome />} color="ghost"></Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={styles.divider}></div>
|
||||||
|
|
||||||
|
<section class={styles.labelSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Labels</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small</h3>
|
||||||
|
<label data-size="sm" data-component="label">
|
||||||
|
Small Label Text
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Medium</h3>
|
||||||
|
<label data-size="md" data-component="label">
|
||||||
|
Medium Label Text
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Large</h3>
|
||||||
|
<label data-size="lg" data-component="label">
|
||||||
|
Large Label Text
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={styles.divider}></div>
|
||||||
|
|
||||||
|
<section class={styles.inputSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Inputs</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="sm"
|
||||||
|
placeholder="Small input field"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Medium</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="md"
|
||||||
|
placeholder="Medium input field"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Large</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="lg"
|
||||||
|
placeholder="Large input field"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Disabled</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="md"
|
||||||
|
placeholder="Disabled input"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>With Value</h3>
|
||||||
|
<input
|
||||||
|
data-component="input"
|
||||||
|
data-size="md"
|
||||||
|
value="Input with preset value"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class={styles.divider}></div>
|
||||||
|
|
||||||
|
<section class={styles.dialogSection}>
|
||||||
|
<h2 class={styles.sectionTitle}>Dialogs</h2>
|
||||||
|
|
||||||
|
<table class={styles.componentTable}>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Default</h3>
|
||||||
|
<Button color="secondary" onClick={() => setDialogOpen(true)}>
|
||||||
|
Open Dialog
|
||||||
|
</Button>
|
||||||
|
<Dialog open={dialogOpen()} onOpenChange={setDialogOpen}>
|
||||||
|
<div data-slot="header">
|
||||||
|
<div data-slot="title">Dialog Title</div>
|
||||||
|
</div>
|
||||||
|
<div data-slot="main">
|
||||||
|
<p>This is the default dialog content.</p>
|
||||||
|
</div>
|
||||||
|
<div data-slot="footer">
|
||||||
|
<Button onClick={() => setDialogOpen(false)}>Close</Button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Small With Transition</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setDialogOpenTransition(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Small Dialog
|
||||||
|
</Button>
|
||||||
|
<Dialog
|
||||||
|
open={dialogOpenTransition()}
|
||||||
|
onOpenChange={setDialogOpenTransition}
|
||||||
|
size="sm"
|
||||||
|
transition={true}
|
||||||
|
>
|
||||||
|
<div class={styles.dialogContent}>
|
||||||
|
<h2 class={styles.sectionTitle}>Small Dialog</h2>
|
||||||
|
<p>This is a smaller dialog with transitions.</p>
|
||||||
|
<div class={styles.dialogContentFooter}>
|
||||||
|
<Button onClick={() => setDialogOpenTransition(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Input String</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
dialog.open(DialogString, {
|
||||||
|
title: "Name",
|
||||||
|
action: "Change name",
|
||||||
|
placeholder: "Enter a name",
|
||||||
|
onSubmit: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
String
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Select Input</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
dialog.open(DialogSelect, {
|
||||||
|
placeholder: "Select",
|
||||||
|
title: "User Settings",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
display: "Change name",
|
||||||
|
prefix: <IconPencilSquare />,
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: "Remove user",
|
||||||
|
prefix: <IconHome />,
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Select Input</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
dialog.open(DialogSelect, {
|
||||||
|
placeholder: "Select",
|
||||||
|
title: "User Settings",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
display: "Change name",
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: "Remove user",
|
||||||
|
onSelect: () => {
|
||||||
|
dialog.close()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
No Prefix
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td class={styles.componentCell}>
|
||||||
|
<h3 class={styles.componentLabel}>Select No Options</h3>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
dialog.open(DialogSelect, {
|
||||||
|
placeholder: "Select",
|
||||||
|
title: "User Settings",
|
||||||
|
options: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
No Options
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
/* This file is auto-generated by SST. Do not edit. */
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_DOCS_URL: string
|
||||||
|
readonly VITE_API_URL: string
|
||||||
|
readonly VITE_AUTH_URL: string
|
||||||
|
}
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Button as Kobalte } from "@kobalte/core/button"
|
||||||
|
import { JSX, Show, splitProps } from "solid-js"
|
||||||
|
|
||||||
|
export interface ButtonProps {
|
||||||
|
color?: "primary" | "secondary" | "ghost"
|
||||||
|
size?: "md" | "sm"
|
||||||
|
icon?: JSX.Element
|
||||||
|
}
|
||||||
|
export function Button(props: JSX.IntrinsicElements["button"] & ButtonProps) {
|
||||||
|
const [split, rest] = splitProps(props, ["color", "size", "icon"])
|
||||||
|
return (
|
||||||
|
<Kobalte
|
||||||
|
{...rest}
|
||||||
|
data-component="button"
|
||||||
|
data-size={split.size || "md"}
|
||||||
|
data-color={split.color || "primary"}
|
||||||
|
>
|
||||||
|
<Show when={props.icon}>
|
||||||
|
<div data-slot="icon">{props.icon}</div>
|
||||||
|
</Show>
|
||||||
|
{props.children}
|
||||||
|
</Kobalte>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { createContext, JSX, ParentProps, useContext } from "solid-js"
|
||||||
|
import { StandardSchemaV1 } from "@standard-schema/spec"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { Dialog } from "./dialog"
|
||||||
|
|
||||||
|
const Context = createContext<DialogControl>()
|
||||||
|
|
||||||
|
type DialogControl = {
|
||||||
|
open<Schema extends StandardSchemaV1<object>>(
|
||||||
|
component: DialogComponent<Schema>,
|
||||||
|
input: StandardSchemaV1.InferInput<Schema>,
|
||||||
|
): void
|
||||||
|
close(): void
|
||||||
|
isOpen(input: any): boolean
|
||||||
|
size: "sm" | "md"
|
||||||
|
transition?: boolean
|
||||||
|
input?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialogProps<Schema extends StandardSchemaV1<object>> = {
|
||||||
|
input: StandardSchemaV1.InferInput<Schema>
|
||||||
|
control: DialogControl
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialogComponent<Schema extends StandardSchemaV1<object>> = ReturnType<
|
||||||
|
typeof createDialog<Schema>
|
||||||
|
>
|
||||||
|
|
||||||
|
export function createDialog<Schema extends StandardSchemaV1<object>>(props: {
|
||||||
|
schema: Schema
|
||||||
|
size: "sm" | "md"
|
||||||
|
render: (props: DialogProps<Schema>) => JSX.Element
|
||||||
|
}) {
|
||||||
|
const result = () => {
|
||||||
|
const dialog = useDialog()
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
size={dialog.size}
|
||||||
|
transition={dialog.transition}
|
||||||
|
open={dialog.isOpen(result)}
|
||||||
|
onOpenChange={(val) => {
|
||||||
|
if (!val) dialog.close()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.render({
|
||||||
|
input: dialog.input,
|
||||||
|
control: dialog,
|
||||||
|
})}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result.schema = props.schema
|
||||||
|
result.size = props.size
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogProvider(props: ParentProps) {
|
||||||
|
const [store, setStore] = createStore<{
|
||||||
|
dialog?: DialogComponent<any>
|
||||||
|
input?: any
|
||||||
|
transition?: boolean
|
||||||
|
size: "sm" | "md"
|
||||||
|
}>({
|
||||||
|
size: "sm",
|
||||||
|
})
|
||||||
|
|
||||||
|
const control: DialogControl = {
|
||||||
|
get input() {
|
||||||
|
return store.input
|
||||||
|
},
|
||||||
|
get size() {
|
||||||
|
return store.size
|
||||||
|
},
|
||||||
|
get transition() {
|
||||||
|
return store.transition
|
||||||
|
},
|
||||||
|
isOpen(input) {
|
||||||
|
return store.dialog === input
|
||||||
|
},
|
||||||
|
open(component, input) {
|
||||||
|
setStore({
|
||||||
|
dialog: component,
|
||||||
|
input: input,
|
||||||
|
size: store.dialog !== undefined ? store.size : component.size,
|
||||||
|
transition: store.dialog !== undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setStore({
|
||||||
|
size: component.size,
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setStore({
|
||||||
|
transition: false,
|
||||||
|
})
|
||||||
|
}, 150)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
setStore({
|
||||||
|
dialog: undefined,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Context.Provider value={control}>{props.children}</Context.Provider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDialog() {
|
||||||
|
const ctx = useContext(Context)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useDialog must be used within a DialogProvider")
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
.options {
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
border-top: 2px solid var(--color-border);
|
||||||
|
padding: var(--space-2);
|
||||||
|
|
||||||
|
[data-slot="option"] {
|
||||||
|
outline: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: var(--space-11);
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 var(--space-2-5);
|
||||||
|
gap: var(--space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&[data-empty] {
|
||||||
|
cursor: default;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-active] {
|
||||||
|
background-color: var(--color-bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="title"] {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="prefix"] {
|
||||||
|
width: var(--space-4);
|
||||||
|
height: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import style from "./dialog-select.module.css"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { createMemo, createSignal, For, JSX, onMount } from "solid-js"
|
||||||
|
import { createList } from "solid-list"
|
||||||
|
import { createDialog } from "./context-dialog"
|
||||||
|
|
||||||
|
export const DialogSelect = createDialog({
|
||||||
|
size: "md",
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
placeholder: z.string(),
|
||||||
|
onSelect: z
|
||||||
|
.function(z.tuple([z.any()]))
|
||||||
|
.returns(z.void())
|
||||||
|
.optional(),
|
||||||
|
options: z.array(
|
||||||
|
z.object({
|
||||||
|
display: z.string(),
|
||||||
|
value: z.any().optional(),
|
||||||
|
onSelect: z.function().returns(z.void()).optional(),
|
||||||
|
prefix: z.custom<JSX.Element>().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
render: (ctx) => {
|
||||||
|
let input: HTMLInputElement
|
||||||
|
onMount(() => {
|
||||||
|
input.focus()
|
||||||
|
input.value = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
const [filter, setFilter] = createSignal("")
|
||||||
|
const filtered = createMemo(() =>
|
||||||
|
ctx.input.options?.filter((i) =>
|
||||||
|
i.display.toLowerCase().includes(filter().toLowerCase()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const list = createList({
|
||||||
|
loop: true,
|
||||||
|
initialActive: 0,
|
||||||
|
items: () => filtered().map((_, i) => i),
|
||||||
|
handleTab: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelection = (index: number) => {
|
||||||
|
const option = ctx.input.options[index]
|
||||||
|
|
||||||
|
// If the option has its own onSelect handler, use it
|
||||||
|
if (option.onSelect) {
|
||||||
|
option.onSelect()
|
||||||
|
}
|
||||||
|
// Otherwise, if there's a global onSelect handler, call it with the option's value
|
||||||
|
else if (ctx.input.onSelect) {
|
||||||
|
ctx.input.onSelect(
|
||||||
|
option.value !== undefined ? option.value : option.display,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-slot="header">
|
||||||
|
<label
|
||||||
|
data-size="md"
|
||||||
|
data-slot="title"
|
||||||
|
data-component="label"
|
||||||
|
for={`dialog-select-${ctx.input.title}`}
|
||||||
|
>
|
||||||
|
{ctx.input.title}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div data-slot="main">
|
||||||
|
<input
|
||||||
|
data-size="lg"
|
||||||
|
data-component="input"
|
||||||
|
value={filter()}
|
||||||
|
onInput={(e) => {
|
||||||
|
setFilter(e.target.value)
|
||||||
|
list.setActive(0)
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
const selected = list.active()
|
||||||
|
if (selected === null) return
|
||||||
|
handleSelection(selected)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setFilter("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list.onKeyDown(e)
|
||||||
|
}}
|
||||||
|
id={`dialog-select-${ctx.input.title}`}
|
||||||
|
ref={(r) => (input = r)}
|
||||||
|
data-slot="input"
|
||||||
|
placeholder={ctx.input.placeholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div data-slot="options" class={style.options}>
|
||||||
|
<For
|
||||||
|
each={filtered()}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="option" data-empty>
|
||||||
|
No results
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(option, index) => (
|
||||||
|
<div
|
||||||
|
onClick={() => handleSelection(index())}
|
||||||
|
data-slot="option"
|
||||||
|
data-active={list.active() === index() ? true : undefined}
|
||||||
|
>
|
||||||
|
{option.prefix && <div data-slot="prefix">{option.prefix}</div>}
|
||||||
|
<div data-slot="title">{option.display}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { onMount } from "solid-js"
|
||||||
|
import { createDialog } from "./context-dialog"
|
||||||
|
import { Button } from "./button"
|
||||||
|
|
||||||
|
export const DialogString = createDialog({
|
||||||
|
size: "sm",
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
placeholder: z.string(),
|
||||||
|
action: z.string(),
|
||||||
|
onSubmit: z.function().args(z.string()).returns(z.void()),
|
||||||
|
}),
|
||||||
|
render: (ctx) => {
|
||||||
|
let input: HTMLInputElement
|
||||||
|
onMount(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
input.focus()
|
||||||
|
input.value = ""
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
const value = input.value.trim()
|
||||||
|
if (value) {
|
||||||
|
ctx.input.onSubmit(value)
|
||||||
|
ctx.control.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div data-slot="header">
|
||||||
|
<label
|
||||||
|
data-size="md"
|
||||||
|
data-slot="title"
|
||||||
|
data-component="label"
|
||||||
|
for={`dialog-string-${ctx.input.title}`}
|
||||||
|
>
|
||||||
|
{ctx.input.title}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div data-slot="main">
|
||||||
|
<input
|
||||||
|
data-slot="input"
|
||||||
|
data-size="lg"
|
||||||
|
data-component="input"
|
||||||
|
ref={(r) => (input = r)}
|
||||||
|
placeholder={ctx.input.placeholder}
|
||||||
|
id={`dialog-string-${ctx.input.title}`}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault()
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div data-slot="footer">
|
||||||
|
<Button size="md" color="ghost" onClick={() => ctx.control.close()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="md" color="secondary" onClick={submit}>
|
||||||
|
{ctx.input.action}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||||
|
import { ComponentProps, ParentProps } from "solid-js"
|
||||||
|
|
||||||
|
export type Props = ParentProps<{
|
||||||
|
size?: "sm" | "md"
|
||||||
|
transition?: boolean
|
||||||
|
}> &
|
||||||
|
ComponentProps<typeof Kobalte>
|
||||||
|
|
||||||
|
export function Dialog(props: Props) {
|
||||||
|
return (
|
||||||
|
<Kobalte {...props}>
|
||||||
|
<Kobalte.Portal>
|
||||||
|
<Kobalte.Overlay data-component="dialog-overlay" />
|
||||||
|
<div data-component="dialog-center">
|
||||||
|
<Kobalte.Content
|
||||||
|
data-transition={props.transition ? "" : undefined}
|
||||||
|
data-size={props.size}
|
||||||
|
data-slot="content"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Kobalte.Content>
|
||||||
|
</div>
|
||||||
|
</Kobalte.Portal>
|
||||||
|
</Kobalte>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
[data-component="button"] {
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
line-height: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
text-transform: uppercase;
|
||||||
|
height: var(--space-11);
|
||||||
|
outline: none;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="primary"] {
|
||||||
|
background-color: var(--color-text);
|
||||||
|
border-color: var(--color-text);
|
||||||
|
color: var(--color-text-invert);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="secondary"] {
|
||||||
|
&:active {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="ghost"] {
|
||||||
|
border: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: var(--color-text-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-slot="icon"]) {
|
||||||
|
padding-left: var(--space-3);
|
||||||
|
padding-right: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="sm"] {
|
||||||
|
height: var(--space-8);
|
||||||
|
padding: var(--space-3);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
|
||||||
|
[data-slot="icon"] {
|
||||||
|
width: var(--space-3-5);
|
||||||
|
height: var(--space-3-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-slot="icon"]) {
|
||||||
|
padding-left: var(--space-2);
|
||||||
|
padding-right: var(--space-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="icon"] {
|
||||||
|
width: var(--space-4);
|
||||||
|
height: var(--space-4);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-rotate] [data-slot="icon"] {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
[data-component="dialog-overlay"] {
|
||||||
|
pointer-events: none !important;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
animation-name: fadeOut;
|
||||||
|
animation-duration: 200ms;
|
||||||
|
animation-timing-function: ease;
|
||||||
|
opacity: 0;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
|
||||||
|
&[data-expanded] {
|
||||||
|
animation-name: fadeIn;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="dialog-center"] {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
padding-top: 10vh;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
[data-slot="content"] {
|
||||||
|
width: 45rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
transition: 150ms width;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
outline: none;
|
||||||
|
animation-duration: 1ms;
|
||||||
|
animation-name: zoomOut;
|
||||||
|
animation-timing-function: ease;
|
||||||
|
|
||||||
|
box-shadow: 8px 8px 0px 0px var(--color-gray-4);
|
||||||
|
|
||||||
|
&[data-expanded] {
|
||||||
|
animation-name: zoomIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-transition] {
|
||||||
|
animation-duration: 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="sm"] {
|
||||||
|
width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="header"] {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--space-4) var(--space-4) 0;
|
||||||
|
|
||||||
|
[data-slot="title"] {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="main"] {
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
|
||||||
|
&:has([data-slot="options"]) {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="input"] {
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="footer"] {
|
||||||
|
padding: var(--space-4);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-4);
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[data-component="input"] {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
background: transparent;
|
||||||
|
caret-color: var(--color-accent);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
height: var(--space-11);
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
width: 100%;
|
||||||
|
resize: none;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="sm"] {
|
||||||
|
height: var(--space-9);
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="md"] {
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-size="lg"] {
|
||||||
|
height: var(--space-12);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[data-component="label"] {
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
|
||||||
|
&[data-size="sm"] {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
&[data-size="md"] {
|
||||||
|
}
|
||||||
|
&[data-size="lg"] {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
[data-component="title-bar"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 72px;
|
||||||
|
padding: 0 var(--space-4);
|
||||||
|
border-bottom: 2px solid var(--color-border);
|
||||||
|
|
||||||
|
[data-slot="left"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1-5);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
letter-spacing: -0.03125rem;
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--color-text-dimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40rem) {
|
||||||
|
[data-component="title-bar"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/* tokens */
|
||||||
|
@import "./token/color.css";
|
||||||
|
@import "./token/reset.css";
|
||||||
|
@import "./token/animation.css";
|
||||||
|
@import "./token/font.css";
|
||||||
|
@import "./token/space.css";
|
||||||
|
|
||||||
|
/* components */
|
||||||
|
@import "./component/label.css";
|
||||||
|
@import "./component/input.css";
|
||||||
|
@import "./component/button.css";
|
||||||
|
@import "./component/dialog.css";
|
||||||
|
@import "./component/title-bar.css";
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--color-text);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
text-underline-offset: 0.1875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
&:active {
|
||||||
|
color: var(--color-text-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--color-text-accent-invert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive utilities */
|
||||||
|
[data-max-width] {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
max-width: 90rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-max-width-64] > * {
|
||||||
|
max-width: 64rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@keyframes zoomIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes zoomOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
:root {
|
||||||
|
--color-white: hsl(0, 0%, 100%);
|
||||||
|
--color-gray-1: hsl(224, 20%, 94%);
|
||||||
|
--color-gray-2: hsl(224, 6%, 77%);
|
||||||
|
--color-gray-3: hsl(224, 6%, 56%);
|
||||||
|
--color-gray-4: hsl(224, 7%, 36%);
|
||||||
|
--color-gray-5: hsl(224, 10%, 23%);
|
||||||
|
--color-gray-6: hsl(224, 14%, 16%);
|
||||||
|
--color-black: hsl(224, 10%, 10%);
|
||||||
|
|
||||||
|
--hue-orange: 41;
|
||||||
|
--color-orange-low: hsl(var(--hue-orange), 39%, 22%);
|
||||||
|
--color-orange: hsl(var(--hue-orange), 82%, 63%);
|
||||||
|
--color-orange-high: hsl(var(--hue-orange), 82%, 87%);
|
||||||
|
--hue-green: 101;
|
||||||
|
--color-green-low: hsl(var(--hue-green), 39%, 22%);
|
||||||
|
--color-green: hsl(var(--hue-green), 82%, 63%);
|
||||||
|
--color-green-high: hsl(var(--hue-green), 82%, 80%);
|
||||||
|
--hue-blue: 234;
|
||||||
|
--color-blue-low: hsl(var(--hue-blue), 54%, 20%);
|
||||||
|
--color-blue: hsl(var(--hue-blue), 100%, 60%);
|
||||||
|
--color-blue-high: hsl(var(--hue-blue), 100%, 87%);
|
||||||
|
--hue-purple: 281;
|
||||||
|
--color-purple-low: hsl(var(--hue-purple), 39%, 22%);
|
||||||
|
--color-purple: hsl(var(--hue-purple), 82%, 63%);
|
||||||
|
--color-purple-high: hsl(var(--hue-purple), 82%, 89%);
|
||||||
|
--hue-red: 339;
|
||||||
|
--color-red-low: hsl(var(--hue-red), 39%, 22%);
|
||||||
|
--color-red: hsl(var(--hue-red), 82%, 63%);
|
||||||
|
--color-red-high: hsl(var(--hue-red), 82%, 87%);
|
||||||
|
|
||||||
|
--color-accent-low: hsl(13, 75%, 30%);
|
||||||
|
--color-accent: hsl(13, 88%, 57%);
|
||||||
|
--color-accent-high: hsl(13, 100%, 78%);
|
||||||
|
|
||||||
|
--color-text: var(--color-gray-1);
|
||||||
|
--color-text-dimmed: var(--color-gray-3);
|
||||||
|
--color-text-accent: var(--color-accent);
|
||||||
|
--color-text-invert: var(--color-black);
|
||||||
|
--color-text-accent-invert: var(--color-accent-high);
|
||||||
|
--color-bg: var(--color-black);
|
||||||
|
--color-bg-surface: var(--color-gray-5);
|
||||||
|
--color-bg-accent: var(--color-accent-high);
|
||||||
|
--color-border: var(--color-gray-2);
|
||||||
|
|
||||||
|
--color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-color-mode="light"] {
|
||||||
|
--color-white: hsl(224, 10%, 10%);
|
||||||
|
--color-gray-1: hsl(224, 14%, 16%);
|
||||||
|
--color-gray-2: hsl(224, 10%, 23%);
|
||||||
|
--color-gray-3: hsl(224, 7%, 36%);
|
||||||
|
--color-gray-4: hsl(224, 6%, 56%);
|
||||||
|
--color-gray-5: hsl(224, 6%, 77%);
|
||||||
|
--color-gray-6: hsl(224, 20%, 94%);
|
||||||
|
--color-gray-7: hsl(224, 19%, 97%);
|
||||||
|
--color-black: hsl(0, 0%, 100%);
|
||||||
|
|
||||||
|
--color-orange-high: hsl(var(--hue-orange), 80%, 25%);
|
||||||
|
--color-orange: hsl(var(--hue-orange), 90%, 60%);
|
||||||
|
--color-orange-low: hsl(var(--hue-orange), 90%, 88%);
|
||||||
|
--color-green-high: hsl(var(--hue-green), 80%, 22%);
|
||||||
|
--color-green: hsl(var(--hue-green), 90%, 46%);
|
||||||
|
--color-green-low: hsl(var(--hue-green), 85%, 90%);
|
||||||
|
--color-blue-high: hsl(var(--hue-blue), 80%, 30%);
|
||||||
|
--color-blue: hsl(var(--hue-blue), 90%, 60%);
|
||||||
|
--color-blue-low: hsl(var(--hue-blue), 88%, 90%);
|
||||||
|
--color-purple-high: hsl(var(--hue-purple), 90%, 30%);
|
||||||
|
--color-purple: hsl(var(--hue-purple), 90%, 60%);
|
||||||
|
--color-purple-low: hsl(var(--hue-purple), 80%, 90%);
|
||||||
|
--color-red-high: hsl(var(--hue-red), 80%, 30%);
|
||||||
|
--color-red: hsl(var(--hue-red), 90%, 60%);
|
||||||
|
--color-red-low: hsl(var(--hue-red), 80%, 90%);
|
||||||
|
|
||||||
|
--color-accent-high: hsl(13, 75%, 26%);
|
||||||
|
--color-accent: hsl(13, 88%, 60%);
|
||||||
|
--color-accent-low: hsl(13, 100%, 89%);
|
||||||
|
|
||||||
|
--color-text-accent: var(--color-accent);
|
||||||
|
--color-text-dimmed: var(--color-gray-4);
|
||||||
|
--color-text-invert: var(--color-black);
|
||||||
|
--color-text-accent-invert: var(--color-accent-low);
|
||||||
|
--color-bg-surface: var(--color-gray-6);
|
||||||
|
--color-bg-accent: var(--color-accent);
|
||||||
|
|
||||||
|
--color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
:root {
|
||||||
|
--font-size-2xs: 0.6875rem;
|
||||||
|
--font-size-xs: 0.75rem;
|
||||||
|
--font-size-sm: 0.8125rem;
|
||||||
|
--font-size-md: 0.9375rem;
|
||||||
|
--font-size-lg: 1.125rem;
|
||||||
|
--font-size-xl: 1.25rem;
|
||||||
|
--font-size-2xl: 1.5rem;
|
||||||
|
--font-size-3xl: 1.875rem;
|
||||||
|
--font-size-4xl: 2.25rem;
|
||||||
|
--font-size-5xl: 3rem;
|
||||||
|
--font-size-6xl: 3.75rem;
|
||||||
|
--font-size-7xl: 4.5rem;
|
||||||
|
--font-size-8xl: 6rem;
|
||||||
|
--font-size-9xl: 8rem;
|
||||||
|
--font-mono: IBM Plex Mono, monospace;
|
||||||
|
--font-sans: Rubik, sans-serif;
|
||||||
|
|
||||||
|
--font-line-height: 1.75;
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--global-color-border, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.5;
|
||||||
|
--font-fallback: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
font-family: var(--global-font-body, var(--font-fallback));
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
color: inherit;
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
pre,
|
||||||
|
samp {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
border-color: inherit;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--global-color-placeholder, #9ca3af);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-size: 1em;
|
||||||
|
--font-mono-fallback: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
"Liberation Mono", "Courier New";
|
||||||
|
font-family: var(--global-font-mono, var(--font-fallback));
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="search"],
|
||||||
|
input[type="password"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-search-decoration,
|
||||||
|
::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user