mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Compare commits
583 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dc4d82c26 | ||
|
|
811a4b7158 | ||
|
|
05ea87643a | ||
|
|
bb673b2a75 | ||
|
|
01a20dcf70 | ||
|
|
73be2beea7 | ||
|
|
14adac592d | ||
|
|
f4616cf945 | ||
|
|
67289765c6 | ||
|
|
ab0a9047f4 | ||
|
|
488344a74d | ||
|
|
6bdd9e5972 | ||
|
|
5b6fce1a4d | ||
|
|
05f9740e8e | ||
|
|
6b2a20cf0d | ||
|
|
d0965d19b1 | ||
|
|
381e2a5467 | ||
|
|
03305a47ef | ||
|
|
d74747fbf0 | ||
|
|
f707da0274 | ||
|
|
2fb7d29841 | ||
|
|
8a798a5bca | ||
|
|
689368fe8d | ||
|
|
5cd106973c | ||
|
|
f3d987ee03 | ||
|
|
bbe311c4de | ||
|
|
4f3ebcaa31 | ||
|
|
96b88c1715 | ||
|
|
193b680f06 | ||
|
|
2d9f98f434 | ||
|
|
ffcc814ba5 | ||
|
|
1f5aa50862 | ||
|
|
a2c896d0ae | ||
|
|
205893cc1f | ||
|
|
b00f6292c4 | ||
|
|
8d952fcfbf | ||
|
|
54dcbd8759 | ||
|
|
381646ebfb | ||
|
|
aaa04507e0 | ||
|
|
a92f3b031d | ||
|
|
567c744624 | ||
|
|
11c233b8fe | ||
|
|
aadcf179bc | ||
|
|
52c350093a | ||
|
|
a35c9f4263 | ||
|
|
dc467695f4 | ||
|
|
1ef344b02a | ||
|
|
f93570848f | ||
|
|
cf0548a0d0 | ||
|
|
ea665af455 | ||
|
|
7ec4acf2b1 | ||
|
|
44128caf2c | ||
|
|
e7efd773b3 | ||
|
|
9c71b0eb88 | ||
|
|
d3b426d500 | ||
|
|
d114c3e473 | ||
|
|
eecf73dc77 | ||
|
|
358cdd7b7c | ||
|
|
5cd898fabe | ||
|
|
8351156589 | ||
|
|
ef5d1d803d | ||
|
|
760421613d | ||
|
|
46ecf3addd | ||
|
|
93c4ba50a1 | ||
|
|
d568f9abb1 | ||
|
|
d1337a6c85 | ||
|
|
eb6d4a3c2d | ||
|
|
4820fd5821 | ||
|
|
f588719440 | ||
|
|
6524409313 | ||
|
|
d8f4f8a899 | ||
|
|
9b2585b2bd | ||
|
|
4581ee4c5f | ||
|
|
56a7662b2e | ||
|
|
93d0021391 | ||
|
|
dfeeb0e4b2 | ||
|
|
ffb11490ce | ||
|
|
51994c6777 | ||
|
|
5e2007c899 | ||
|
|
4ddb17a27b | ||
|
|
48bd261f64 | ||
|
|
58f8805328 | ||
|
|
6800bcc339 | ||
|
|
562513099f | ||
|
|
e056fee378 | ||
|
|
2de242819e | ||
|
|
9ad8ade044 | ||
|
|
1eb074b361 | ||
|
|
716d6a2db5 | ||
|
|
4b0218169c | ||
|
|
ec3bcd0312 | ||
|
|
7dfc023360 | ||
|
|
900d5cd53c | ||
|
|
01f5de79b7 | ||
|
|
a7fbb2bddc | ||
|
|
eff82d1166 | ||
|
|
3dd4c8b9a3 | ||
|
|
60882f68ea | ||
|
|
dfff322541 | ||
|
|
40153a1da5 | ||
|
|
ed942d1c9b | ||
|
|
86fc06998b | ||
|
|
c37832d0a3 | ||
|
|
760bdf3016 | ||
|
|
d525d97f9e | ||
|
|
bf8c87046b | ||
|
|
edf2b70902 | ||
|
|
cb6e89b1ae | ||
|
|
f7efd475a4 | ||
|
|
48401b6201 | ||
|
|
e4e69ca060 | ||
|
|
a97f80e25e | ||
|
|
359bf1c21e | ||
|
|
cb172edfb7 | ||
|
|
d202849313 | ||
|
|
a099e1fffc | ||
|
|
f7e5319c3e | ||
|
|
98a17574e2 | ||
|
|
779d12f401 | ||
|
|
a7317fd518 | ||
|
|
5e3eaf9b9f | ||
|
|
4faa986e17 | ||
|
|
805e7bfdf4 | ||
|
|
8ae6aab1fb | ||
|
|
bbd981f255 | ||
|
|
c23c453597 | ||
|
|
7d6b81d367 | ||
|
|
d010e2ac47 | ||
|
|
04341f7258 | ||
|
|
e7ad2ca4c4 | ||
|
|
2e97525d4f | ||
|
|
ca1a71eaac | ||
|
|
9624f19a95 | ||
|
|
635353cb99 | ||
|
|
10da7f273e | ||
|
|
09e2eb05ce | ||
|
|
788d5cc949 | ||
|
|
f8efaf1806 | ||
|
|
41ad68a419 | ||
|
|
88694b2a6a | ||
|
|
8fc051ebd4 | ||
|
|
84b920d0da | ||
|
|
00402e0d56 | ||
|
|
2c254339cf | ||
|
|
10c69cec3f | ||
|
|
34aca42dc0 | ||
|
|
3db509a98a | ||
|
|
08cd3f624e | ||
|
|
bd74d47ec1 | ||
|
|
4d138a3f9b | ||
|
|
57cab74ecf | ||
|
|
c053888fcd | ||
|
|
b92b6d5fd3 | ||
|
|
b30b913c04 | ||
|
|
9d2b750d77 | ||
|
|
8cd3bafaff | ||
|
|
ce427a9a6c | ||
|
|
acd6c418d8 | ||
|
|
859ccb2ac1 | ||
|
|
2364664c2e | ||
|
|
eb56414cd7 | ||
|
|
26adfa4d46 | ||
|
|
a1ad965dcc | ||
|
|
160c344008 | ||
|
|
697b748aaf | ||
|
|
d174f3273c | ||
|
|
bcb9759a91 | ||
|
|
c039e8be7a | ||
|
|
45f1a47848 | ||
|
|
17835a8b7d | ||
|
|
e1e3786531 | ||
|
|
0c587f72cd | ||
|
|
dec80ff6cf | ||
|
|
3845da80e0 | ||
|
|
305a257ace | ||
|
|
ad50c56090 | ||
|
|
78a8432bed | ||
|
|
e2f5b88f2e | ||
|
|
d21922f814 | ||
|
|
1ff3aa8c14 | ||
|
|
919f9f14b3 | ||
|
|
58bbaf6a5f | ||
|
|
164c307bd1 | ||
|
|
5f22d21dc7 | ||
|
|
d9024ba983 | ||
|
|
0cb39fb6dc | ||
|
|
ee0cfa9891 | ||
|
|
2a0fb96ce6 | ||
|
|
7f09418f7c | ||
|
|
5e362add5b | ||
|
|
b6f6d04147 | ||
|
|
c0e4fc590c | ||
|
|
e9ca2b0448 | ||
|
|
c375f110a2 | ||
|
|
dccd63b75b | ||
|
|
03a026512e | ||
|
|
9616fe588d | ||
|
|
d52841240a | ||
|
|
d417157162 | ||
|
|
75bf1a0fb8 | ||
|
|
d28895b214 | ||
|
|
2ca7dc42d2 | ||
|
|
ca62b40929 | ||
|
|
5a2dff36c5 | ||
|
|
37051838a8 | ||
|
|
b3a770d1b1 | ||
|
|
34e85b9588 | ||
|
|
e44f95e368 | ||
|
|
a297859ec1 | ||
|
|
085af148e6 | ||
|
|
2debad388b | ||
|
|
682a23d712 | ||
|
|
d544791b3a | ||
|
|
35af82b5ed | ||
|
|
5c840a277f | ||
|
|
4202748cc6 | ||
|
|
36ed7fceb3 | ||
|
|
d81044c17b | ||
|
|
3f2c4fd970 | ||
|
|
3060834fda | ||
|
|
2353f725d1 | ||
|
|
0ce5ceedd5 | ||
|
|
19d35f7e33 | ||
|
|
e83dc59521 | ||
|
|
48c0b80f39 | ||
|
|
df9585499c | ||
|
|
d6fc86bb2e | ||
|
|
84367e795b | ||
|
|
3b42c1dbd4 | ||
|
|
a330f181b5 | ||
|
|
22be754296 | ||
|
|
a2fd30850b | ||
|
|
dffca699f0 | ||
|
|
42af40c299 | ||
|
|
a9e68e159d | ||
|
|
1946ecffe0 | ||
|
|
08ccb61737 | ||
|
|
4f6732da5d | ||
|
|
eeece5e14d | ||
|
|
c610bc377e | ||
|
|
3c684d239b | ||
|
|
95496f356f | ||
|
|
eeefedfc8f | ||
|
|
be63c6171d | ||
|
|
943ddd6b26 | ||
|
|
947430abbc | ||
|
|
b622c9e427 | ||
|
|
99257950d7 | ||
|
|
fe1ec64355 | ||
|
|
4dd222ab2c | ||
|
|
c0a954d4ab | ||
|
|
b045f74a86 | ||
|
|
1e3e34dd84 | ||
|
|
ee93b0c810 | ||
|
|
a3d3809e8b | ||
|
|
982760f401 | ||
|
|
9f8fb2c5c8 | ||
|
|
17b1dc8574 | ||
|
|
1d90eaf602 | ||
|
|
ba5e292487 | ||
|
|
504dae0886 | ||
|
|
ceb85bd251 | ||
|
|
b4e4f6251e | ||
|
|
210a4632f2 | ||
|
|
36f15b10ef | ||
|
|
394a900023 | ||
|
|
a35d0108d4 | ||
|
|
ee3efb7c6c | ||
|
|
d2cb61e097 | ||
|
|
60dba267ba | ||
|
|
d5390d9752 | ||
|
|
8568b224dc | ||
|
|
65702b3bb0 | ||
|
|
564c6b43f9 | ||
|
|
070fd261fc | ||
|
|
6575ad7bf3 | ||
|
|
48ca57ebe8 | ||
|
|
12189e5ddd | ||
|
|
b74e049360 | ||
|
|
636a890ed6 | ||
|
|
f0651bc0a8 | ||
|
|
07c29add11 | ||
|
|
9842ce38c4 | ||
|
|
7be9a1de24 | ||
|
|
5a1d55de22 | ||
|
|
4ed285cedd | ||
|
|
332f11ab7e | ||
|
|
478e5ef563 | ||
|
|
158845c374 | ||
|
|
a82442a987 | ||
|
|
8b25c32ce9 | ||
|
|
82ef80ece4 | ||
|
|
783b375617 | ||
|
|
a7406997e5 | ||
|
|
4daeae0cab | ||
|
|
0159606b6b | ||
|
|
f3c5a527d7 | ||
|
|
7fd1d8e254 | ||
|
|
2b543f5904 | ||
|
|
2cb3844011 | ||
|
|
9447d53d57 | ||
|
|
3d4454debe | ||
|
|
d749f4e031 | ||
|
|
e6a9b7d2b2 | ||
|
|
b7f1176838 | ||
|
|
f77fdc649f | ||
|
|
9d20e03cbb | ||
|
|
202328ffac | ||
|
|
bf26130fcc | ||
|
|
25bda423cc | ||
|
|
7c86d89810 | ||
|
|
70bcd45e5b | ||
|
|
316806c069 | ||
|
|
1f95937dcf | ||
|
|
26a5267ce7 | ||
|
|
207aefdf11 | ||
|
|
526b7e981e | ||
|
|
074a379b68 | ||
|
|
2aca116035 | ||
|
|
470c00a1f1 | ||
|
|
bf1e96d19e | ||
|
|
bd44303366 | ||
|
|
489f30d97c | ||
|
|
0b603354c4 | ||
|
|
34973dd7d7 | ||
|
|
e548d70356 | ||
|
|
193f84883e | ||
|
|
d860c328be | ||
|
|
2b04ed61e0 | ||
|
|
b316481acd | ||
|
|
f068607509 | ||
|
|
ca53d77fed | ||
|
|
727f5393af | ||
|
|
a14e3d19e7 | ||
|
|
32fb1d629c | ||
|
|
56b4c7d0b9 | ||
|
|
19dc423f21 | ||
|
|
7b1b789f3b | ||
|
|
85739b4766 | ||
|
|
e515eb904e | ||
|
|
2f187d7e94 | ||
|
|
0f428292f8 | ||
|
|
f9649cd870 | ||
|
|
e5f65736d6 | ||
|
|
e39d4b90a4 | ||
|
|
582bc0195d | ||
|
|
7a9f179020 | ||
|
|
0bca5b3477 | ||
|
|
b52fc163dd | ||
|
|
3471abb91e | ||
|
|
318ffb4532 | ||
|
|
b47a43e763 | ||
|
|
5abc9cf2c3 | ||
|
|
49c45c7166 | ||
|
|
e91476c1b8 | ||
|
|
1382c3ff50 | ||
|
|
89131fd84c | ||
|
|
697f7a59d2 | ||
|
|
6a85d4f6c6 | ||
|
|
7f6089f728 | ||
|
|
bcf7e10e6e | ||
|
|
638cecb991 | ||
|
|
c35a07bc9c | ||
|
|
6c9936a68c | ||
|
|
61bb05da0f | ||
|
|
3759f76e90 | ||
|
|
bf5a4cad47 | ||
|
|
3f62afa94f | ||
|
|
5323337d84 | ||
|
|
660544687e | ||
|
|
f71f40b61a | ||
|
|
d7bb49efbe | ||
|
|
4e107dadd3 | ||
|
|
1a90cf977d | ||
|
|
5ad3c18d34 | ||
|
|
7c5da3d520 | ||
|
|
dd538d9e44 | ||
|
|
c8bc272ab7 | ||
|
|
9766c9edae | ||
|
|
bc88c40201 | ||
|
|
147136ba52 | ||
|
|
195c891566 | ||
|
|
0a13754de7 | ||
|
|
b8a085b4bf | ||
|
|
dd280f74e7 | ||
|
|
2888c0cff0 | ||
|
|
d05b083c12 | ||
|
|
fef24552e9 | ||
|
|
649dfba360 | ||
|
|
a23ccfbd82 | ||
|
|
199a757618 | ||
|
|
3c21ef6485 | ||
|
|
30f5a79097 | ||
|
|
2de2e03409 | ||
|
|
6fb9e60dea | ||
|
|
498632cd7d | ||
|
|
6ac54c494e | ||
|
|
f125a4cf17 | ||
|
|
6afe931d01 | ||
|
|
2898e0f74a | ||
|
|
fe41e82a64 | ||
|
|
f475fc4491 | ||
|
|
75937b332a | ||
|
|
ca26a53e1b | ||
|
|
79a2c7fcbf | ||
|
|
1a42558d68 | ||
|
|
8edb115e08 | ||
|
|
b393000132 | ||
|
|
47c4ed4631 | ||
|
|
ce99e76a2b | ||
|
|
321323532b | ||
|
|
845194a65a | ||
|
|
e57eb45933 | ||
|
|
b148ada1d0 | ||
|
|
beb7497b5c | ||
|
|
3ab8b82683 | ||
|
|
79d0d6ea55 | ||
|
|
f4f99af890 | ||
|
|
c9d5ff46a5 | ||
|
|
15d9753789 | ||
|
|
439aca3eb0 | ||
|
|
31c493ed63 | ||
|
|
79a72586a6 | ||
|
|
2992621354 | ||
|
|
28857413b3 | ||
|
|
8f14f89098 | ||
|
|
412508534c | ||
|
|
0d5f3e9a3f | ||
|
|
a2af9320e0 | ||
|
|
c2547fd026 | ||
|
|
936afbfc96 | ||
|
|
c5de0a7709 | ||
|
|
a24a56fabf | ||
|
|
e74b118d04 | ||
|
|
d51b6338c0 | ||
|
|
5b7c3fcda2 | ||
|
|
5d0c83d05b | ||
|
|
683a703d85 | ||
|
|
4cb72e957d | ||
|
|
60ae60c1e1 | ||
|
|
f5c02b8340 | ||
|
|
128e777d9e | ||
|
|
c63bc1b9df | ||
|
|
4d9d9762ef | ||
|
|
46dd9135f0 | ||
|
|
b94c2ac804 | ||
|
|
612782ded9 | ||
|
|
7d36b552a0 | ||
|
|
b6629a75db | ||
|
|
2ff5c89e2b | ||
|
|
f474ff49c7 | ||
|
|
d38bdaf32c | ||
|
|
24b38f8163 | ||
|
|
5feae7757e | ||
|
|
1aef614890 | ||
|
|
4d4aae59db | ||
|
|
d6214bbdc1 | ||
|
|
98ec2aa6a3 | ||
|
|
71feebcde0 | ||
|
|
2d490c6fca | ||
|
|
1639c37ac3 | ||
|
|
18327ff883 | ||
|
|
711e300f1b | ||
|
|
1b95c91034 | ||
|
|
1e3e4c9f84 | ||
|
|
1e8a2d4dcc | ||
|
|
9698146b11 | ||
|
|
255cb6d487 | ||
|
|
efc16c4e98 | ||
|
|
bc4e47aa41 | ||
|
|
e4a5be4828 | ||
|
|
dc91a461d6 | ||
|
|
6d00c0665d | ||
|
|
b7e024ada0 | ||
|
|
2de0dc8399 | ||
|
|
4582104d08 | ||
|
|
f8fc93fbdd | ||
|
|
20c56fa265 | ||
|
|
3547898077 | ||
|
|
379ce709c4 | ||
|
|
12479e3d20 | ||
|
|
776b86dd79 | ||
|
|
5eb8bc9933 | ||
|
|
364be69754 | ||
|
|
4b9575d6fb | ||
|
|
fb8f4eee91 | ||
|
|
d0f37a2cae | ||
|
|
2e9d74167d | ||
|
|
380587e2be | ||
|
|
80edc64443 | ||
|
|
dd9a0b42dc | ||
|
|
91667051ff | ||
|
|
3b48ab0b9b | ||
|
|
dbc00031b0 | ||
|
|
fe7f63e669 | ||
|
|
caf76c8ef8 | ||
|
|
1e42fe6ea2 | ||
|
|
2d6b52799a | ||
|
|
60026aa61d | ||
|
|
5763a2f3de | ||
|
|
d38989a38a | ||
|
|
3d3e48af8b | ||
|
|
17d188ed88 | ||
|
|
1f483c35fa | ||
|
|
059adee7be | ||
|
|
d45cb98c67 | ||
|
|
759adabb9c | ||
|
|
58e24197b3 | ||
|
|
a2c359ea04 | ||
|
|
e87035ab04 | ||
|
|
08aad21265 | ||
|
|
76c0a5a907 | ||
|
|
3c7861fcd1 | ||
|
|
d9d0a008a6 | ||
|
|
dad4f9cb5b | ||
|
|
f118944787 | ||
|
|
d9f65c3b6c | ||
|
|
1914e986ed | ||
|
|
b14969cbfb | ||
|
|
880ef3f35f | ||
|
|
e5afddd14f | ||
|
|
0cd731621c | ||
|
|
36a1d6baa1 | ||
|
|
13c558311c | ||
|
|
56802a2021 | ||
|
|
e7ac2a4b7b | ||
|
|
6f0fb20bdb | ||
|
|
357a6d9f9f | ||
|
|
82e5c4bd5c | ||
|
|
e6e6e90d33 | ||
|
|
612288f0e0 | ||
|
|
c367afc473 | ||
|
|
d87d56b955 | ||
|
|
076f831975 | ||
|
|
1065a71c25 | ||
|
|
43e8d7fc20 | ||
|
|
0062590cba | ||
|
|
84184cbf6d | ||
|
|
ba50660c9e | ||
|
|
60a4dd52ef | ||
|
|
7fff6d1cce | ||
|
|
07f910dce3 | ||
|
|
5403af2838 | ||
|
|
119fc2820b | ||
|
|
629f6df984 | ||
|
|
6bfc6828ae | ||
|
|
acf2fb9409 | ||
|
|
29fa227fac | ||
|
|
d3f62d8228 | ||
|
|
d7889c5834 | ||
|
|
77d2a4744f | ||
|
|
aa4430501a | ||
|
|
d2f8755eb6 | ||
|
|
6445620d5e | ||
|
|
a10f18f0ed | ||
|
|
14bb1bf4ee | ||
|
|
0ffa410f1c | ||
|
|
77b9071b8a | ||
|
|
71fbe05e34 | ||
|
|
20e82a09ff | ||
|
|
fa5f9214f1 | ||
|
|
e8d2a637ab | ||
|
|
ca4db5f0cc | ||
|
|
901708278b | ||
|
|
2c54331081 | ||
|
|
0958dbb721 | ||
|
|
5f948cab13 | ||
|
|
8a9c9dc50f | ||
|
|
c82362283c | ||
|
|
f228a26b3e | ||
|
|
333d192eb7 | ||
|
|
0912bcf348 | ||
|
|
ba24038b92 | ||
|
|
5dcbaa5478 | ||
|
|
f2178b2db4 | ||
|
|
4a7e5f905f | ||
|
|
ad78966154 | ||
|
|
8e818f5fa8 | ||
|
|
2e2314633b | ||
|
|
b68f2d1fb8 | ||
|
|
6a5fcc67df | ||
|
|
e59a782705 | ||
|
|
36b9d99fcd |
976 changed files with 84828 additions and 50577 deletions
|
|
@ -5,32 +5,24 @@ description: Use when updating Codex Panel for a new Codex CLI or app-server API
|
||||||
|
|
||||||
# Codex Panel App-Server Update
|
# Codex Panel App-Server Update
|
||||||
|
|
||||||
Use this skill when Codex Panel needs to follow Codex CLI or experimental `codex app-server` API changes.
|
|
||||||
|
|
||||||
## Ground Rules
|
## Ground Rules
|
||||||
|
|
||||||
- Treat `src/generated/app-server/` as generated output. Do not hand-edit generated bindings.
|
- Treat `src/generated/app-server/` as generated output. Do not hand-edit generated bindings.
|
||||||
|
- Use `docs/design.md` for app-server boundary and source-of-truth decisions before adding compatibility layers or product behavior.
|
||||||
- Prefer current Codex CLI behavior over broad backward compatibility unless a concrete user need requires compatibility code.
|
- Prefer current Codex CLI behavior over broad backward compatibility unless a concrete user need requires compatibility code.
|
||||||
- Separate required compatibility fixes from optional cleanup or product improvements.
|
- Separate required compatibility fixes from optional cleanup or product improvements.
|
||||||
- Do not rely on local Git hooks. Run required generation and verification commands explicitly.
|
|
||||||
|
|
||||||
## Procedure
|
## Procedure
|
||||||
|
|
||||||
1. Read the README Compatibility and Development sections, `package.json`, and app-server-related source around requests, notifications, threads, approvals, Plan mode, hooks, and model listing.
|
1. Read the README Compatibility section and the relevant design and development guidance, then trace the affected app-server paths.
|
||||||
2. Compare the README Compatibility table's `codex.testedCliVersion` with the target `codex --version`.
|
2. Confirm the recorded, installed, and target Codex CLI versions. Check official release information when behavior is uncertain.
|
||||||
3. Check official Codex CLI or app-server release information when the target version is newer or behavior is uncertain.
|
3. Regenerate bindings with `npm run generate:app-server-types`.
|
||||||
4. Regenerate bindings with `npm run generate:app-server-types`.
|
4. Review generated diffs for protocol changes that affect runtime behavior. If mechanical normalization is needed, update the normalization code in `scripts/generate-app-server-types.mjs` and regenerate instead of patching generated files by hand.
|
||||||
5. Review generated diffs for protocol changes that affect runtime behavior. If mechanical normalization is needed, update `scripts/normalize-generated-types.mjs` and regenerate instead of patching generated files by hand.
|
5. Implement only the required compatibility changes and record the target version after validating it.
|
||||||
6. Implement only the compatibility changes needed for the target Codex CLI version.
|
6. Report optional cleanup or product follow-ups separately; do not implement them unless requested.
|
||||||
7. Update the README Compatibility table's `codex.testedCliVersion` only after validating against that version.
|
|
||||||
8. Identify optional follow-ups separately from required compatibility work:
|
|
||||||
- API changes that allow removing compatibility shims, fallback code, or local workarounds.
|
|
||||||
- New routes, notifications, fields, or lifecycle signals that could simplify existing implementation.
|
|
||||||
- New app-server capabilities that could enable Codex Panel product improvements.
|
|
||||||
9. Report optional follow-ups as proposals only, grouped separately from implemented required fixes unless the user explicitly asks to implement them.
|
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
- Run `npm run check` after regenerated bindings or compatibility changes.
|
- Run `npm run check` after regenerated bindings or compatibility changes.
|
||||||
- If the change must be reflected in Obsidian, run `npm run build` and verify the plugin reload path separately.
|
- Use the live Obsidian workflow when runtime behavior needs verification.
|
||||||
- Report the tested Codex CLI version, the generation command used, any app-server compatibility behavior that changed, and any optional simplification or feature proposals found.
|
- Report the tested Codex CLI version, changed compatibility behavior, and optional follow-ups.
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,11 @@ description: Use when validating Codex Panel inside a live Obsidian app with the
|
||||||
|
|
||||||
# Codex Panel Obsidian Dev
|
# Codex Panel Obsidian Dev
|
||||||
|
|
||||||
Use this skill for live Obsidian checks after building Codex Panel or when investigating behavior that unit tests cannot observe.
|
|
||||||
|
|
||||||
## Ground Rules
|
## Ground Rules
|
||||||
|
|
||||||
- Work from the Codex Panel repository root.
|
- Use `docs/development.md` for the build and generated-asset expectations before live Obsidian validation.
|
||||||
- Treat `obsidian plugin:reload id=codex-panel`, `obsidian reload`, `obsidian restart`, `obsidian devtools`, `obsidian eval`, `obsidian dev:cdp`, and `obsidian dev:mobile` as state-changing or intrusive. Ask the user before running them.
|
- Ask before altering or reloading the live Obsidian session, attaching a debugger, or running intrusive evaluation or input commands.
|
||||||
- Prefer read-only inspection first: `plugin`, `commands`, `dev:errors`, `dev:dom`, `dev:css`, `dev:console`, `dev:screenshot`, `version`.
|
- Prefer read-only inspection before state-changing commands.
|
||||||
- If `dev:console` reports that the debugger is not attached, ask before running `obsidian dev:debug on`.
|
|
||||||
- Do not clear console or error buffers unless the user approves; clearing can destroy useful failure context.
|
- Do not clear console or error buffers unless the user approves; clearing can destroy useful failure context.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
@ -23,13 +20,13 @@ Use this skill for live Obsidian checks after building Codex Panel or when inves
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Before reloading the live plugin, ask the user whether it is OK to reload their current Obsidian session. If approved:
|
2. If approved, reload the plugin when the live session needs the new build:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
obsidian plugin:reload id=codex-panel
|
obsidian plugin:reload id=codex-panel
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Open the relevant Codex Panel surface when needed:
|
3. Open the relevant surface when needed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
obsidian command id=codex-panel:open-panel
|
obsidian command id=codex-panel:open-panel
|
||||||
|
|
@ -46,54 +43,12 @@ Use this skill for live Obsidian checks after building Codex Panel or when inves
|
||||||
obsidian dev:screenshot path=<scratch-path>/codex-panel.png
|
obsidian dev:screenshot path=<scratch-path>/codex-panel.png
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Use focused selectors for the behavior under test. Prefer stable Codex Panel classes such as `.codex-panel`, `.codex-panel__composer`, `.codex-panel-threads`, and `.codex-panel-chat-turn-diff`.
|
5. Use focused Codex Panel-owned selectors found in the relevant implementation or styles.
|
||||||
|
|
||||||
## Dynamic UI Investigations
|
## Dynamic UI Investigations
|
||||||
|
|
||||||
When checking behavior that depends on browser layout, asynchronous rendering, or virtualized DOM state, measure the live UI rather than inferring from unit tests alone.
|
When behavior depends on layout, asynchronous rendering, or virtualized DOM state, capture the visible state and relevant DOM metrics before and after the real input path, then again after rendering settles. Compare framework or data state with the DOM where possible. Remove temporary probes and instrumentation before final validation and re-check runtime errors.
|
||||||
|
|
||||||
- Capture both the user-visible state and the underlying DOM metrics. Useful fields include `scrollTop`, `scrollHeight`, `clientHeight`, element bounding rects, rendered element counts, inline styles, and relevant computed styles.
|
|
||||||
- For virtualized or asynchronously rendered regions, compare framework state against DOM state where possible, such as virtualizer total size versus actual `scrollHeight`, rendered item count versus data count, or pre-event versus post-frame measurements.
|
|
||||||
- Prefer real input paths for input-sensitive bugs. Use `obsidian dev:cdp method=Input.dispatchMouseEvent ...` or focused keyboard events when a synthetic `dispatchEvent()` may skip browser/Electron behavior.
|
|
||||||
- Record measurements before the action, immediately after the action, after at least one animation frame, and after a short timeout when layout, markdown rendering, resize observers, or virtualizer settle loops may run later.
|
|
||||||
- If injecting temporary DOM probes with `obsidian eval`, remove them before finishing and re-check `dev:errors`. Avoid leaving long-running eval promises; use bounded `setTimeout`-based scripts that resolve.
|
|
||||||
- Treat console instrumentation as temporary. Remove debug logging before final validation, and search the touched files for probe markers or conflict markers before reporting.
|
|
||||||
|
|
||||||
## Common Checks
|
|
||||||
|
|
||||||
- Confirm the loaded version and enabled state:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
obsidian plugin id=codex-panel
|
|
||||||
```
|
|
||||||
|
|
||||||
- List available Codex Panel commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
obsidian commands filter=codex-panel
|
|
||||||
```
|
|
||||||
|
|
||||||
- Count rendered elements:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
obsidian dev:dom selector=.codex-panel__composer total
|
|
||||||
```
|
|
||||||
|
|
||||||
- Inspect text, attributes, or computed styles:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
obsidian dev:dom selector=.codex-panel__composer text
|
|
||||||
obsidian dev:dom selector=.codex-panel__composer attr=aria-label
|
|
||||||
obsidian dev:dom selector=.codex-panel__composer css=display
|
|
||||||
obsidian dev:css selector=.codex-panel__composer
|
|
||||||
```
|
|
||||||
|
|
||||||
- Capture a screenshot for visual review:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
obsidian dev:screenshot path=<scratch-path>/codex-panel.png
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reporting
|
## Reporting
|
||||||
|
|
||||||
Report the commands run, whether Obsidian was reloaded, key `dev:errors` or `dev:console` findings, and any screenshot path produced. If a command was skipped because it required user approval, state that plainly.
|
Report the observed behavior, runtime errors, intrusive actions taken, and evidence paths. State when an approval-gated check was skipped.
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,23 @@ description: Use when preparing, checking, committing, tagging, pushing, or repa
|
||||||
|
|
||||||
# Codex Panel Release
|
# Codex Panel Release
|
||||||
|
|
||||||
Use this skill when delegating Codex Panel release work to an agent. `docs/release.md` is the public procedure and source of truth for the release command sequence. This skill adds agent-facing gates, review duties, and failure handling.
|
`docs/release.md` is the public procedure and source of truth for the release command sequence. This skill adds agent-facing gates, review duties, and failure handling.
|
||||||
|
|
||||||
## Ground Rules
|
## Ground Rules
|
||||||
|
|
||||||
- Read and follow `docs/release.md`; do not maintain a parallel release command sequence here.
|
- Read and follow `docs/release.md`; do not maintain a parallel release command sequence here.
|
||||||
- Do not assume local Git hooks exist or ran. Run required verification commands explicitly.
|
|
||||||
- Do not create GitHub Releases locally with `gh release create`; the tag-triggered GitHub Actions workflow owns release creation and asset attachment.
|
- Do not create GitHub Releases locally with `gh release create`; the tag-triggered GitHub Actions workflow owns release creation and asset attachment.
|
||||||
- Keep internal validation notes, procedural details, and implementation reasoning out of release notes.
|
- Treat generated release notes as a draft. Review the full release diff and keep one short, public-facing `## Changes` section without internal or procedural detail.
|
||||||
- Release notes must be short, public-facing bullets under the single `## Changes` section required by `docs/release.md`.
|
|
||||||
|
|
||||||
## Delegation Procedure
|
## Delegation Procedure
|
||||||
|
|
||||||
1. Read `docs/release.md`, `package.json`, `manifest.json`, `versions.json`, and existing `.github/release-notes/` files.
|
1. Read `docs/release.md` and inspect the current version metadata and recent release notes.
|
||||||
2. Identify the target release version and the commit range since the previous released tag.
|
2. Identify the target release version and the commit range since the previous released tag.
|
||||||
3. Follow the preparation step in `docs/release.md`.
|
3. Inspect the full release diff, run preparation as documented, and edit the draft for completeness, accuracy, and user-facing wording.
|
||||||
4. Draft `.github/release-notes/X.Y.Z.md` from the full diff since the previous released tag, not only the latest commit.
|
4. Before committing, ask the user to approve the release version, reviewed release-note bullets, and included commit range.
|
||||||
5. Before committing, ask the user to approve the release version, release-note bullets, and included commit range.
|
5. After approval, follow `docs/release.md` through preflight, tag, and push. Let GitHub Actions create or update the GitHub Release.
|
||||||
6. After approval, follow `docs/release.md` for the commit, preflight, tag, and push sequence.
|
|
||||||
7. After pushing, let GitHub Actions create or update the GitHub Release.
|
|
||||||
|
|
||||||
## Failure Handling
|
## Failure Handling
|
||||||
|
|
||||||
- If a release script or preflight fails, fix the cause and rerun the explicit failed command.
|
|
||||||
- If the tag-triggered workflow fails before GitHub Release creation, use the repair procedure in `docs/release.md`.
|
- If the tag-triggered workflow fails before GitHub Release creation, use the repair procedure in `docs/release.md`.
|
||||||
- If a GitHub Release already exists or was partially created, inspect the state before taking action; do not assume local asset upload is the right recovery path.
|
- If a GitHub Release already exists or was partially created, inspect the state before taking action; do not assume local asset upload is the right recovery path.
|
||||||
|
|
|
||||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
|
|
@ -4,6 +4,8 @@ updates:
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
commit-message:
|
||||||
|
prefix: "ci(deps)"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 7
|
default-days: 7
|
||||||
|
|
||||||
|
|
@ -11,6 +13,9 @@ updates:
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore(deps)"
|
||||||
|
prefix-development: "chore(deps)"
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 7
|
default-days: 7
|
||||||
open-pull-requests-limit: 5
|
open-pull-requests-limit: 5
|
||||||
|
|
@ -24,3 +29,6 @@ updates:
|
||||||
update-types:
|
update-types:
|
||||||
- "version-update:semver-minor"
|
- "version-update:semver-minor"
|
||||||
- "version-update:semver-major"
|
- "version-update:semver-major"
|
||||||
|
- dependency-name: "typescript"
|
||||||
|
update-types:
|
||||||
|
- "version-update:semver-major"
|
||||||
|
|
|
||||||
5
.github/release-notes/3.2.7.md
vendored
Normal file
5
.github/release-notes/3.2.7.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Updates Codex App Server compatibility for Codex CLI 0.140.0.
|
||||||
|
- Adds a settings action to permanently delete archived threads you no longer need.
|
||||||
|
- Fixes settings screen refresh behavior and control styling, plus completed hook-run status rendering.
|
||||||
5
.github/release-notes/3.2.8.md
vendored
Normal file
5
.github/release-notes/3.2.8.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Improves in-progress chat rendering with smoother streaming Markdown updates.
|
||||||
|
- Fixes message stream settling and Work details collapse behavior near the bottom of the chat.
|
||||||
|
- Hardens app-server request handling, thread cleanup, and selection rewrite context checks.
|
||||||
4
.github/release-notes/3.2.9.md
vendored
Normal file
4
.github/release-notes/3.2.9.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Adds compatibility updates for Codex CLI 0.141.0 app-server changes.
|
||||||
|
- Shows Codex sleep history as wait status in the chat stream.
|
||||||
6
.github/release-notes/3.3.0.md
vendored
Normal file
6
.github/release-notes/3.3.0.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Supports MCP elicitation requests in the chat stream.
|
||||||
|
- Refines Codex Panel settings with Obsidian-style grouped rows and shorter descriptions.
|
||||||
|
- Keeps archived thread settings in sync when threads are archived, restored, or deleted elsewhere.
|
||||||
|
- Improves chat message streaming and release validation reliability.
|
||||||
5
.github/release-notes/3.4.0.md
vendored
Normal file
5
.github/release-notes/3.4.0.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Replaces `/mcp` with `/tools` for broader Codex capability diagnostics covering plugins, tool providers, MCP servers, and skills.
|
||||||
|
- Improves grouped slash-command and diagnostic output so structured command results are easier to scan.
|
||||||
|
- Streamlines the README usage guide around current panel, thread, composer, and diagnostics workflows.
|
||||||
5
.github/release-notes/3.4.1.md
vendored
Normal file
5
.github/release-notes/3.4.1.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Improves panel state consistency when switching threads, reusing panels, and handling pending requests.
|
||||||
|
- Hardens turn, rename, title-generation, and selection-rewrite lifecycles so stale status updates are less likely to leak into the UI.
|
||||||
|
- Improves file-change and runtime-setting status handling across app-server reconnects and diagnostics.
|
||||||
5
.github/release-notes/3.4.2.md
vendored
Normal file
5
.github/release-notes/3.4.2.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Improves chat scrolling so long threads stay anchored more reliably as messages stream in, expand, collapse, or change height.
|
||||||
|
- Fixes extra blank space that could remain after collapsing large message content.
|
||||||
|
- Tightens work-details spacing in the message stream.
|
||||||
7
.github/release-notes/3.4.3.md
vendored
Normal file
7
.github/release-notes/3.4.3.md
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Adds support for Codex CLI 0.142.0 and its updated app-server protocol.
|
||||||
|
- Aligns thread lists and the thread picker with Codex's recency-based ordering, including refreshes after new turns start.
|
||||||
|
- Improves restored panel handling so reopened thread panels appear in Threads view and hydrate when focused or foregrounded.
|
||||||
|
- Improves slash-command thread suggestions for `/archive`, `/rename`, `/resume`, and `/refer`, and lets `/reconnect` and `/compact` run while disconnected.
|
||||||
|
- Returns focus to the composer after responding to pending approvals, user inputs, or MCP elicitations.
|
||||||
3
.github/release-notes/3.4.4.md
vendored
Normal file
3
.github/release-notes/3.4.4.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Keep newly started or recently active threads visible and correctly ordered while Codex thread list snapshots catch up.
|
||||||
5
.github/release-notes/3.4.5.md
vendored
Normal file
5
.github/release-notes/3.4.5.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Improve composer and message-stream scrolling behavior, including smoother manual scrolling.
|
||||||
|
- Refine copied diagnostic details for clearer troubleshooting output.
|
||||||
|
- Improve runtime settings state handling.
|
||||||
4
.github/release-notes/3.4.6.md
vendored
Normal file
4
.github/release-notes/3.4.6.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Fixed Windows app-server launch when Codex is installed through npm and the configured executable is the `codex.cmd` shim (thanks @Lionel540, #16).
|
||||||
|
- Fixed capability diagnostics so Codex Panel no longer loads the full app catalog during connection or `/tools`, avoiding sustained Codex app-server CPU and energy use.
|
||||||
6
.github/release-notes/4.0.0.md
vendored
Normal file
6
.github/release-notes/4.0.0.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added `@active-note` and `@selection` composer completions for bringing the current Obsidian editor context into a Codex turn, including selected Markdown text as additional turn context.
|
||||||
|
- Added paste and drag-and-drop attachment support in the composer. Images are saved to the configured attachment folder as Obsidian embeds, while other files are inserted as wikilinks and sent to Codex as file mentions.
|
||||||
|
- Improved selection rewrite diffs and fixed settings dynamic-section display issues.
|
||||||
|
- Improved app-server diagnostics and cached metadata handling so transient probe failures and context changes do not wipe useful connection, tool, or thread information.
|
||||||
4
.github/release-notes/4.0.1.md
vendored
Normal file
4
.github/release-notes/4.0.1.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Fixed wikilink completion so Tab keeps the cursor before the closing brackets for heading references, while Enter accepts the completed link.
|
||||||
|
- Refreshed the README wording and screenshot.
|
||||||
3
.github/release-notes/4.0.2.md
vendored
Normal file
3
.github/release-notes/4.0.2.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added runtime permission details to the status panel, including the active profile, sandbox and network access, writable roots, approval policy, and reviewer.
|
||||||
4
.github/release-notes/4.1.0.md
vendored
Normal file
4
.github/release-notes/4.1.0.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added a `/permissions` slash command that shows the current permissions and approval settings.
|
||||||
|
- Refined the status panel permissions display into Permissions and Approvals sections, with connection diagnostics labeled consistently.
|
||||||
4
.github/release-notes/4.1.1.md
vendored
Normal file
4
.github/release-notes/4.1.1.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Fixed Windows connection failures when the configured Codex executable is an npm-installed command shim such as `codex.cmd`.
|
||||||
|
- Refined runtime permission and approval status resolution for more consistent diagnostics.
|
||||||
4
.github/release-notes/4.1.2.md
vendored
Normal file
4
.github/release-notes/4.1.2.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Let `/permissions` set the permission profile used for subsequent Codex turns, with composer completions for available profiles.
|
||||||
|
- Updated app-server compatibility for Codex CLI 0.142.5.
|
||||||
4
.github/release-notes/4.2.0.md
vendored
Normal file
4
.github/release-notes/4.2.0.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added an opt-in **Reference active note on send** setting that references the current Obsidian note when sending from the composer without changing the prompt text.
|
||||||
|
- Renamed sent-message context metadata to `Context · n items`, with implicit active-note context shown as `Active note · path/to/note.md`.
|
||||||
4
.github/release-notes/4.3.0.md
vendored
Normal file
4
.github/release-notes/4.3.0.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added `/clip <url> [message]` to save readable web page clippings as Markdown notes and send wikilink references from the composer.
|
||||||
|
- Added web clipping settings for the saved note folder, filename template, and tags.
|
||||||
5
.github/release-notes/4.3.1.md
vendored
Normal file
5
.github/release-notes/4.3.1.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Renamed active-note context to `@active` and updated related settings copy to refer to the active file consistently.
|
||||||
|
- Improved thread matching for composer thread commands and thread search so title and ID matches rank more predictably.
|
||||||
|
- Made saved vault paths for attachments, web clippings, and archived thread notes more consistent with their configured defaults.
|
||||||
4
.github/release-notes/4.4.0.md
vendored
Normal file
4
.github/release-notes/4.4.0.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Add Obsidian tag completions in the composer after `#`.
|
||||||
|
- Open Obsidian Search when clicking rendered tags in Codex Panel messages.
|
||||||
5
.github/release-notes/4.4.1.md
vendored
Normal file
5
.github/release-notes/4.4.1.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Match Obsidian's tag index and global search behavior for tag completions and rendered tag clicks.
|
||||||
|
- Show composer context as 0% before a thread starts instead of leaving it blank.
|
||||||
|
- Harden saved Markdown output for web clips and archived threads with safer YAML frontmatter handling.
|
||||||
6
.github/release-notes/4.5.0.md
vendored
Normal file
6
.github/release-notes/4.5.0.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Add an action to open subagent threads directly from agent summary messages.
|
||||||
|
- Show reasoning effort descriptions in composer suggestions.
|
||||||
|
- Update app-server compatibility for Codex CLI 0.143.0.
|
||||||
|
- Polish toolbar and slash command wording, and fix composer suggestion matching.
|
||||||
3
.github/release-notes/4.5.1.md
vendored
Normal file
3
.github/release-notes/4.5.1.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Polish chat panel wording around conversations, context, and composer guidance.
|
||||||
6
.github/release-notes/4.5.2.md
vendored
Normal file
6
.github/release-notes/4.5.2.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Update app-server compatibility for Codex CLI 0.144.0 and improve reconnection, metadata, and MCP request handling.
|
||||||
|
- Load thread history incrementally in Threads View while retaining complete thread-picker search results.
|
||||||
|
- Fix web clips to save Defuddle's Markdown output and refresh collapsed-content controls after layout changes.
|
||||||
|
- Prevent stale asynchronous UI updates after panels, settings, or the plugin close.
|
||||||
4
.github/release-notes/4.6.0.md
vendored
Normal file
4
.github/release-notes/4.6.0.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Add `@today`, `@tomorrow`, and `@yesterday` composer completions that resolve through the configured Daily Notes or Periodic Notes folder and date format.
|
||||||
|
- Improve `@active` and `@selection` suggestions with clearer note, range, and selected-text previews.
|
||||||
3
.github/release-notes/4.6.1.md
vendored
Normal file
3
.github/release-notes/4.6.1.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Align DOM creation and daily-note date handling with Obsidian's bundled APIs for Community plugin review compatibility.
|
||||||
5
.github/release-notes/4.7.0.md
vendored
Normal file
5
.github/release-notes/4.7.0.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Add `/btw` side chats that open an ephemeral conversation in a new panel without interrupting the source thread.
|
||||||
|
- Open subagent threads as read-only panels and hide conversation actions that do not apply to them.
|
||||||
|
- Improve reliability around Vault file destinations, web clipping, app-server connections, and asynchronous panel cleanup.
|
||||||
4
.github/release-notes/4.7.1.md
vendored
Normal file
4
.github/release-notes/4.7.1.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Improve chat and side-chat stability during concurrent actions, panel restoration, and cleanup.
|
||||||
|
- Prevent stale app-server data and rapid settings changes from overwriting newer state.
|
||||||
5
.github/release-notes/5.0.0.md
vendored
Normal file
5
.github/release-notes/5.0.0.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Replace `/clip` with `/web <url> [message]` for temporary web context without saving a note. For durable archives—especially rendered or signed-in pages—use Obsidian Web Clipper and reference the saved note.
|
||||||
|
- Improve hook controls so trusting a hook no longer enables it automatically, and only trusted unmanaged hooks can be enabled or disabled.
|
||||||
|
- Prevent an in-flight automatic thread title from overwriting a newer manual rename.
|
||||||
5
.github/release-notes/5.0.1.md
vendored
Normal file
5
.github/release-notes/5.0.1.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- You can now cancel an in-progress `/web` import and keep the original prompt to edit or resend it.
|
||||||
|
- Changing the Codex executable in settings now reconnects open panels cleanly.
|
||||||
|
- File links containing backticks are now preserved when exporting archived threads.
|
||||||
5
.github/release-notes/5.0.2.md
vendored
Normal file
5
.github/release-notes/5.0.2.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Persistent threads now reconnect cleanly after temporary Codex disconnects or app-server setting changes, while temporary side chats no longer resume incorrectly.
|
||||||
|
- Slash-command suggestions now immediately respect the restrictions for temporary side chats and subagent threads.
|
||||||
|
- Thread rename and archive results are now kept within the Codex context where the operation started, preventing stale updates after settings change.
|
||||||
3
.github/release-notes/5.0.3.md
vendored
Normal file
3
.github/release-notes/5.0.3.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Release assets now carry individually verifiable provenance attestations.
|
||||||
3
.github/release-notes/5.0.4.md
vendored
Normal file
3
.github/release-notes/5.0.4.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Restored availability of downloadable install assets while upstream attestation verification catches up with GitHub's API changes.
|
||||||
6
.github/release-notes/5.1.0.md
vendored
Normal file
6
.github/release-notes/5.1.0.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Added search to Codex Panel settings.
|
||||||
|
- Large web, thread, selection, and vault references are now bounded predictably, report truncation when needed, and keep their context details after reopening a thread.
|
||||||
|
- Paste and drop attachments now preserve their intended insertion point while files are being saved, with clearer composer editing state.
|
||||||
|
- Improved reliability when switching threads, leaving running subagents, or changing runtime and app-server settings, preventing stale results and lost updates.
|
||||||
6
.github/release-notes/5.1.1.md
vendored
Normal file
6
.github/release-notes/5.1.1.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Thread history now stays in sync across the chat toolbar, thread picker, and Threads view while loading older threads or refreshing.
|
||||||
|
- Thread search now uses fuzzy title matching across the complete active history instead of matching thread IDs.
|
||||||
|
- Running subagent summaries now show each agent's latest activity instead of only its initial status.
|
||||||
|
- Opening the thread picker again now closes the previous picker, and accepted MCP links are preserved in conversation history.
|
||||||
6
.github/release-notes/5.1.2.md
vendored
Normal file
6
.github/release-notes/5.1.2.md
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Thread rename and auto-name controls now prevent conflicting edits while work is in progress, and cancelling auto-name returns to the existing draft.
|
||||||
|
- Actions with known unmet prerequisites are now disabled before execution, including unavailable auto-name and selection rewrite actions.
|
||||||
|
- Thread operation errors no longer appear as entries in navigation lists.
|
||||||
|
- Resumed conversations now hide Codex Panel's internal context metadata while preserving their context attachments.
|
||||||
4
.github/release-notes/5.2.0.md
vendored
Normal file
4
.github/release-notes/5.2.0.md
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Thread references created with `/refer` now remain as readable, clickable links in conversation history and open the referenced thread in an available Codex Panel.
|
||||||
|
- Opening an existing thread in a new panel is now faster because resume no longer waits for background metadata and thread-list refreshes.
|
||||||
5
.github/release-notes/5.2.1.md
vendored
Normal file
5
.github/release-notes/5.2.1.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Updated compatibility for Codex CLI 0.145.0.
|
||||||
|
- Subagent conversations that support replies can now be continued directly from their panel.
|
||||||
|
- Fixed malformed links in exported thread archives.
|
||||||
61
.github/workflows/check.yml
vendored
61
.github/workflows/check.yml
vendored
|
|
@ -11,23 +11,76 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: npm run check:ci
|
name: npm run check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Detect compatibility changes
|
||||||
|
id: compatibility
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
|
||||||
|
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||||
|
run: |
|
||||||
|
if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- \
|
||||||
|
.github/workflows/check.yml \
|
||||||
|
README.md \
|
||||||
|
manifest.json \
|
||||||
|
package.json \
|
||||||
|
package-lock.json \
|
||||||
|
scripts/api-baseline.mjs \
|
||||||
|
scripts/app-server-compatibility.mjs \
|
||||||
|
scripts/generate-app-server-types.mjs \
|
||||||
|
src/app-server/connection/compatibility.json \
|
||||||
|
src/generated/app-server \
|
||||||
|
versions.json; then
|
||||||
|
echo "required=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "required=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 26
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Install recorded Codex CLI
|
||||||
|
if: steps.compatibility.outputs.required == 'true'
|
||||||
|
run: npm install --global "@openai/codex@$(node scripts/app-server-compatibility.mjs --tested-cli-version)"
|
||||||
|
|
||||||
|
- name: Check API baselines
|
||||||
|
if: steps.compatibility.outputs.required == 'true'
|
||||||
|
run: npm run api:baseline
|
||||||
|
|
||||||
|
- name: Check generated app-server bindings
|
||||||
|
if: steps.compatibility.outputs.required == 'true'
|
||||||
|
run: npm run generate:app-server-types:check
|
||||||
|
|
||||||
|
- name: Check pull request commit messages
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
run: npm run commitlint -- --from "$BASE_SHA" --to "$HEAD_SHA" --verbose
|
||||||
|
|
||||||
|
- name: Check pushed commit messages
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.before }}
|
||||||
|
HEAD_SHA: ${{ github.sha }}
|
||||||
|
run: npm run commitlint -- --from "$BASE_SHA" --to "$HEAD_SHA" --verbose
|
||||||
|
|
||||||
|
- name: Check release metadata
|
||||||
|
run: npm run release:check
|
||||||
|
|
||||||
- name: Check
|
- name: Check
|
||||||
run: npm run check:ci
|
run: npm run check
|
||||||
|
|
|
||||||
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
|
|
@ -7,8 +7,6 @@ on:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
|
@ -17,34 +15,44 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with:
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 26
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Install recorded Codex CLI
|
||||||
|
run: npm install --global "@openai/codex@$(node scripts/app-server-compatibility.mjs --tested-cli-version)"
|
||||||
|
|
||||||
|
- name: Check API baselines
|
||||||
|
run: npm run api:baseline
|
||||||
|
|
||||||
|
- name: Check generated app-server bindings
|
||||||
|
run: npm run generate:app-server-types:check
|
||||||
|
|
||||||
- name: Check release version
|
- name: Check release version
|
||||||
run: npm run release:check
|
run: npm run release:check
|
||||||
env:
|
env:
|
||||||
RELEASE_VERSION: ${{ github.ref_name }}
|
RELEASE_VERSION: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Run checks and build
|
- name: Check release commit messages
|
||||||
run: npm run check:ci
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
PREVIOUS_TAG="$(node -p 'Object.keys(require("./versions.json")).at(-2)')"
|
||||||
|
git rev-parse --verify "refs/tags/$PREVIOUS_TAG"
|
||||||
|
npm run commitlint -- --from "$PREVIOUS_TAG" --to "$TAG" --verbose
|
||||||
|
|
||||||
- name: Generate artifact attestations
|
- name: Run checks and build
|
||||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4
|
run: npm run check
|
||||||
with:
|
|
||||||
subject-path: |
|
|
||||||
main.js
|
|
||||||
manifest.json
|
|
||||||
styles.css
|
|
||||||
|
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,4 +1,6 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
.stryker-tmp/
|
||||||
.jj/
|
.jj/
|
||||||
.codex/
|
.codex/
|
||||||
main.js
|
main.js
|
||||||
|
|
@ -7,4 +9,6 @@ data.json
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist/
|
dist/
|
||||||
|
coverage/
|
||||||
|
reports/
|
||||||
tmp/
|
tmp/
|
||||||
|
|
|
||||||
1
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
26
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
node_modules/
|
|
||||||
.jj/
|
|
||||||
.codex/
|
|
||||||
main.js
|
|
||||||
styles.css
|
|
||||||
data.json
|
|
||||||
dist/
|
|
||||||
tmp/
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"printWidth": 140,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
25
AGENTS.md
25
AGENTS.md
|
|
@ -1,9 +1,22 @@
|
||||||
This repository contains the Codex Panel Obsidian plugin. Use the tracked docs as the source of truth: `README.md` for user-facing behavior, `docs/development.md` for development workflow, and `docs/release.md` for releases.
|
This repository contains the Codex Panel Obsidian plugin.
|
||||||
|
|
||||||
Jujutsu is the recommended local change-management workflow when available.
|
## Working Principles
|
||||||
|
|
||||||
## Working Rules
|
- Start from evidence. Reproduce defects before fixing them; for product changes, reconstruct the current workflow, user need, alternatives, and losses before deciding what should change. Treat theoretical failure modes and reviewer agreement as hypotheses, not evidence of meaningful user or operational harm.
|
||||||
|
- Use design documents as context, not as a substitute for current behavior and user expectations. Prefer coherent user-facing behavior and clear ownership, and remove needless abstraction or obsolete compatibility instead of preserving it by default.
|
||||||
|
- Treat implementation, tests, documentation, lint policy, and final history as one deliverable: inspect each for drift, but update only the durable contracts that changed. Documentation should record user-facing behavior, long-term design, or reusable workflow; do not add it merely to narrate an implementation change or duplicate tests and tooling.
|
||||||
|
- Parallelize only substantial concerns that can be implemented and tested independently with little shared-file contention. Keep small, tightly coupled, sequential, or coordination-heavy work together.
|
||||||
|
|
||||||
- Follow the documented ownership and source layout boundaries.
|
## What To Read
|
||||||
- Do not hand-edit generated app-server bindings.
|
|
||||||
- Keep internal validation notes and reasoning out of public release notes.
|
- Read `README.md` for user-facing behavior, requirements, commands, privacy, and compatibility.
|
||||||
|
- Read `docs/design.md` when changing responsibility boundaries, runtime ownership, app-server source-of-truth behavior, UI ownership, or testing philosophy.
|
||||||
|
- Read `docs/development.md` before implementation work, generated binding work, source layout decisions, validation, or compatibility baseline changes.
|
||||||
|
- Read `docs/release.md` for release preparation, release notes, preflight, tagging, pushing, and release repair.
|
||||||
|
- Use the repo-local skills in `.agents/skills/` when a task matches a more specific workflow.
|
||||||
|
|
||||||
|
## Changes And Validation
|
||||||
|
|
||||||
|
- Jujutsu is the recommended local change-management workflow when available. Make each final change a coherent review unit with an honest description.
|
||||||
|
- Before publishing, inspect and reorganize the graph when needed rather than preserving implementation chronology: normally fold corrective follow-ups into the concern they complete, split mixed changes, and keep a follow-up separate only when it remains meaningful on its own.
|
||||||
|
- Use Conventional Commits for new commits and follow `docs/development.md` for repository rules and validation. Re-run relevant validation after history edits and before handoff or publication.
|
||||||
|
|
|
||||||
167
README.md
167
README.md
|
|
@ -1,179 +1,80 @@
|
||||||
# Codex Panel
|
# Codex Panel
|
||||||
|
|
||||||
Codex Panel brings your existing Codex setup into an Obsidian sidebar. It starts `codex app-server` locally, uses the current vault root as the Codex working directory, and leaves Codex in charge of models, reasoning effort, sandbox modes, approval policies, MCP servers, hooks, skills, providers, network access, and thread history.
|
Codex Panel brings Codex into an Obsidian sidebar. It keeps Codex threads beside your notes, helps you add vault context to prompts, and lets you handle approvals and file changes without switching windows.
|
||||||
|
|
||||||
The plugin is intentionally thin: it is an Obsidian surface for Codex, not a separate AI client with its own runtime policy. If your Codex CLI or Codex.app environment is already configured, Codex Panel uses that same runtime behavior in a persistent Obsidian pane, with note-aware support for links, threads, archives, and editor selections.
|
If the Codex CLI is already installed and authenticated, Codex Panel uses that local setup.
|
||||||
|
|
||||||
Use it when you want Codex beside your notes, not in a separate terminal or app window.
|

|
||||||
|
|
||||||
## Why use it
|
## Why use it
|
||||||
|
|
||||||
- Keep Codex next to the notes, specs, plans, and project context you are editing in Obsidian.
|
- Keep Codex work next to the notes or projects it is about.
|
||||||
- Reuse Codex configuration instead of configuring models, sandboxing, approvals, MCP servers, hooks, skills, providers, or credentials again.
|
- Bring notes, selections, links, and attachments into prompts.
|
||||||
- Work with persistent Codex threads in Obsidian panes, including multiple panels side by side.
|
- Manage threads and respond to Codex requests inside Obsidian.
|
||||||
- Navigate threads from an Obsidian sidebar view, with live status for currently open threads.
|
- Review file changes, rewrite selected note text, and archive useful threads as notes.
|
||||||
- Use vault-aware composing: wikilink suggestions come from Obsidian files, sent wikilinks resolve to Codex file mentions when possible, and rendered vault file links open in Obsidian.
|
|
||||||
- Archive Codex threads as Markdown notes in your vault.
|
|
||||||
- Rewrite selected note text with Codex, then review a selection-scoped diff before applying the replacement.
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
Codex Panel runs `codex app-server` as a local child process and talks to it over stdio. Each open chat panel has its own app-server connection, active thread, pending requests, and composer draft. The vault root is passed to Codex as the working directory.
|
Codex Panel starts `codex app-server` locally. Each open panel keeps its own active thread and draft, with Codex working from the vault root.
|
||||||
|
|
||||||
Codex remains the runtime authority:
|
Codex Panel stores panel preferences only, not API keys or provider credentials.
|
||||||
|
|
||||||
- Model and reasoning defaults come from Codex configuration.
|
|
||||||
- Sandbox mode, approval policy, approval reviewer, network policy, MCP servers, hooks, skills, provider settings, and thread history are owned by Codex.
|
|
||||||
- Codex Panel can display and adjust supported per-thread runtime controls, but it does not replace Codex configuration.
|
|
||||||
- Codex Panel stores panel preferences only. It does not store API keys or provider credentials.
|
|
||||||
|
|
||||||
That boundary is also the privacy and security boundary. Data sent from the panel is handled by the configured Codex CLI according to your Codex configuration.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Obsidian desktop app 1.12.0 or newer.
|
- Obsidian desktop app 1.12.0 or newer.
|
||||||
- Codex CLI installed, authenticated, and available as `codex`, or configured with an absolute executable path in Codex Panel settings.
|
- Codex CLI installed, authenticated, and available as `codex`, or configured with an absolute executable path in Codex Panel settings.
|
||||||
- A vault where Codex is allowed to work according to your Codex CLI configuration.
|
- A local vault where Codex is allowed to work.
|
||||||
|
|
||||||
Codex Panel does not support Obsidian mobile.
|
Codex Panel does not support Obsidian mobile.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install Codex Panel from Obsidian's Community plugins browser:
|
Install Codex Panel from Obsidian's Community plugins browser by searching for **Codex Panel**, then install and enable the plugin.
|
||||||
|
|
||||||
1. Open **Settings**.
|
|
||||||
2. Go to **Community plugins**.
|
|
||||||
3. Select **Browse**.
|
|
||||||
4. Search for **Codex Panel**.
|
|
||||||
5. Install and enable the plugin.
|
|
||||||
|
|
||||||
You can also open the plugin page directly: <https://community.obsidian.md/plugins/codex-panel>.
|
You can also open the plugin page directly: <https://community.obsidian.md/plugins/codex-panel>.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
1. Open the command palette and run **Codex Panel: Open panel**, or select the ribbon icon.
|
1. Open the note or project you want to work on.
|
||||||
2. If Obsidian cannot find `codex`, set **Settings -> Codex Panel -> Codex executable** to an absolute path such as `/opt/homebrew/bin/codex`.
|
2. Run **Codex Panel: Open panel** from the command palette, or select the ribbon icon.
|
||||||
3. Ask Codex to work on the vault or project from the composer.
|
3. In the composer, reference what matters and describe the outcome you want. For example: `@active Summarize this note and suggest the next three actions.`
|
||||||
4. Use **Codex Panel: Open thread...** or **Codex Panel: Open threads view** to return to recent non-archived Codex threads.
|
4. Follow the turn in the panel, respond to requests when needed, and review any file changes before continuing.
|
||||||
5. Select text in a Markdown note and run **Codex Panel: Rewrite selection** when you want a note-local rewrite with a reviewable diff.
|
|
||||||
|
|
||||||
By default, `Enter` sends and `Shift+Enter` inserts a newline. You can switch sending to `Cmd/Ctrl+Enter` in Codex Panel settings.
|
If Obsidian cannot find `codex`, set **Settings -> Codex Panel -> Codex executable** to an absolute path such as `/opt/homebrew/bin/codex`.
|
||||||
|
|
||||||
## What you can do
|
## Working with Codex Panel
|
||||||
|
|
||||||
### Panels and threads
|
### Keep work separate or pick it up later
|
||||||
|
|
||||||
- Open one Codex panel in the right sidebar, or open multiple panels for multiple threads.
|
A panel is an independent working surface with its own active thread and draft. Open multiple panels when different tasks should stay visible and separate. Regular Codex threads remain available after a panel closes; return to them from **Codex Panel: Open thread...**, panel history, or the Threads view.
|
||||||
- Start a fresh thread in the current panel.
|
|
||||||
- Resume, rename, auto-name, fork, roll back, compact, and archive Codex threads.
|
|
||||||
- Use the left sidebar Threads view to browse non-archived threads and see which ones are open, running, or waiting for input.
|
|
||||||
- Forks open in a new right-sidebar pane so the source thread stays visible; fork-and-archive replaces the source panel with the forked thread.
|
|
||||||
|
|
||||||
### Compose and steer turns
|
For a one-off question alongside the current thread, use `/btw` to open a temporary side chat. To carry context between regular threads without merging their histories, use `/refer <thread> <message>`. The submitted message keeps a normal link to the referenced thread; clicking it opens the thread in an available Codex Panel, creating one when needed.
|
||||||
|
|
||||||
- Compose with completions for slash commands, enabled skills (`$skill-name`), recent threads, models, and supported reasoning efforts.
|
### Bring in the right context
|
||||||
- Reference another non-archived thread without switching away from the current one.
|
|
||||||
- Follow a running turn as Codex streams assistant messages, reasoning, commands, tool calls, hooks, file changes, plan updates, and agent activity.
|
|
||||||
- Respond to Codex questions, command approvals, file approvals, MCP requests, and permission requests.
|
|
||||||
- Send steering messages while a turn is running, or interrupt when the composer is empty.
|
|
||||||
- Show the active Codex goal above the message stream, then edit, pause, resume, or clear it from the panel.
|
|
||||||
|
|
||||||
### Runtime controls and diagnostics
|
The composer lets you point Codex to relevant material without pasting it into the prompt. Type wikilinks to reference vault files while keeping their names readable. Use `@active` for the active file or `@selection` for the current Markdown selection.
|
||||||
|
|
||||||
- Inspect context usage from the composer status row.
|
When Obsidian Daily Notes or the daily section of Periodic Notes is enabled, `@today`, `@tomorrow`, and `@yesterday` resolve through its folder and date format. Paste or drop files to save them in the configured attachment folder and reference them from the same prompt. For material outside the vault, `/web <url> [message]` fetches readable page content and attaches it to the next turn without creating a note.
|
||||||
- Toggle Plan mode, fast mode, and approval auto-review for subsequent turns.
|
|
||||||
- Set the model and reasoning effort for subsequent turns when supported by Codex.
|
|
||||||
- Inspect usage limits, connection diagnostics, MCP servers, enabled skills, runtime debug details, and discovered hooks from the toolbar, settings, or slash commands.
|
|
||||||
|
|
||||||
### Obsidian-native workflows
|
### Guide a running turn
|
||||||
|
|
||||||
- Use wikilink suggestions backed by Obsidian file search and recent notes.
|
As a turn runs, the panel keeps the conversation together with requests that need your attention. You can answer questions, approve or reject actions, steer the turn with another message, or interrupt it. Plans, goals, tool activity, and agent work remain available when you need to inspect how the task is progressing.
|
||||||
- Send resolved wikilinks as Codex file mentions when the target exists.
|
|
||||||
- Open rendered Markdown links to vault files in Obsidian.
|
|
||||||
- Review Codex file changes in an Obsidian diff view with changed files and a copy-diff action.
|
|
||||||
- Archive threads as Markdown notes with configurable folder, filename template, and tags, or archive without saving.
|
|
||||||
- Rewrite selected note text using the current editor buffer as context, then review the replacement diff before applying it.
|
|
||||||
|
|
||||||
Rollback restores the latest prompt in the Codex thread. It does not undo file edits in your vault.
|
### Review and keep the result
|
||||||
|
|
||||||
## Commands
|
When Codex changes files, open the Obsidian diff view to review the changes and copy the patch. For a focused edit, select Markdown text and run **Codex Panel: Rewrite selection** to request a replacement and review a selection-scoped diff before applying it.
|
||||||
|
|
||||||
### Obsidian commands
|
Archive a finished thread with or without saving it as a Markdown note. Saved notes use the configured folder, filename template, and tags; archived threads can later be restored or permanently deleted from settings.
|
||||||
|
|
||||||
The command palette entries are ordered by the workflow they support: opening panels, finding threads, managing the active chat, and editing note text.
|
Use `/help` for the current slash command list.
|
||||||
|
|
||||||
| Command | What it does |
|
|
||||||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| **Codex Panel: Open panel** | Open or focus a Codex panel. |
|
|
||||||
| **Codex Panel: Open new panel** | Open another Codex panel. |
|
|
||||||
| **Codex Panel: Open thread...** | Search non-archived threads from a picker. Press `Enter` to open in the current panel, or `Cmd/Ctrl+Enter` to open in a new panel. |
|
|
||||||
| **Codex Panel: Open threads view** | Open the left sidebar thread list. |
|
|
||||||
| **Codex Panel: Start new chat** | Clear the current panel for a fresh Codex thread. |
|
|
||||||
| **Codex Panel: Rewrite selection** | Rewrite selected Markdown note text with Codex and review a selection-scoped diff before applying it. |
|
|
||||||
|
|
||||||
### Slash commands
|
|
||||||
|
|
||||||
Use `/help` in the composer to show the available slash commands. The list below follows the same groups shown in help.
|
|
||||||
|
|
||||||
#### Panel actions
|
|
||||||
|
|
||||||
| Command | What it does |
|
|
||||||
| ------------------------- | ------------------------------------------------------------- |
|
|
||||||
| `/clear` | Clear the current panel and start a fresh Codex thread. |
|
|
||||||
| `/resume [thread]` | Resume a recent Codex thread. |
|
|
||||||
| `/reconnect` | Reconnect to `codex app-server` and resume the active thread. |
|
|
||||||
| `/fork` | Fork the active Codex thread. |
|
|
||||||
| `/rollback` | Roll back the latest turn and restore its prompt. |
|
|
||||||
| `/compact` | Compact the current thread context. |
|
|
||||||
| `/archive <thread>` | Archive the selected Codex thread. |
|
|
||||||
| `/rename <thread> <name>` | Rename the selected Codex thread. |
|
|
||||||
|
|
||||||
#### Thread settings
|
|
||||||
|
|
||||||
| Command | What it does |
|
|
||||||
| ----------------------------- | -------------------------------------------------- |
|
|
||||||
| `/auto-review` | Toggle approval auto-review. |
|
|
||||||
| `/fast` | Toggle fast service tier for subsequent turns. |
|
|
||||||
| `/plan [message]` | Toggle Plan mode, optionally with a message. |
|
|
||||||
| `/goal` | Show the current thread goal. |
|
|
||||||
| `/goal set <objective>` | Create or update the current thread goal. |
|
|
||||||
| `/goal edit` | Load the current thread goal into the composer. |
|
|
||||||
| `/goal pause` | Pause the current thread goal. |
|
|
||||||
| `/goal resume` | Resume the current thread goal. |
|
|
||||||
| `/goal clear` | Clear the current thread goal. |
|
|
||||||
| `/model [model\|default]` | Show or set the model for subsequent turns. |
|
|
||||||
| `/reasoning [level\|default]` | Show or set reasoning effort for subsequent turns. |
|
|
||||||
|
|
||||||
#### Diagnostics
|
|
||||||
|
|
||||||
| Command | What it does |
|
|
||||||
| --------- | ------------------------------------------------ |
|
|
||||||
| `/status` | Show current thread, context, and usage limits. |
|
|
||||||
| `/doctor` | Show Codex CLI and Codex App Server diagnostics. |
|
|
||||||
| `/mcp` | Show MCP servers reported by Codex App Server. |
|
|
||||||
| `/help` | Show available slash commands. |
|
|
||||||
|
|
||||||
#### Composition
|
|
||||||
|
|
||||||
| Command | What it does |
|
|
||||||
| --------------------------- | ------------------------------------------------------------------ |
|
|
||||||
| `/refer <thread> <message>` | Send a message with recent turns from another non-archived thread. |
|
|
||||||
|
|
||||||
## Privacy and security
|
|
||||||
|
|
||||||
Codex Panel acts as a local Obsidian client for Codex App Server:
|
|
||||||
|
|
||||||
- It does not make network requests itself. It talks to the configured `codex app-server` process over stdio.
|
|
||||||
- User input, approvals, resolved file mentions, and thread/configuration operations are sent to Codex App Server.
|
|
||||||
- It stores only panel-specific preferences, not API keys or provider credentials.
|
|
||||||
- Network access, sandbox behavior, approval policy, and other runtime policy are controlled by your Codex CLI configuration.
|
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
| Key | Version | Policy |
|
| Key | Version | Policy |
|
||||||
| ------------------------ | --------- | ---------------------------------------------------------- |
|
| -------------------------------------- | --------- | --------------------------------------------------------------------------------------------------- |
|
||||||
| `obsidian.minAppVersion` | `1.12.0` | Track the latest patch for this Obsidian minor. |
|
| `manifest.minAppVersion` | `1.12.0` | Minimum Obsidian desktop version declared for plugin loading. |
|
||||||
| `codex.testedCliVersion` | `0.139.0` | Track app-server compatibility by Codex CLI minor version. |
|
| `obsidian` API types | `1.12.3` | TypeScript API package used for compile-time checks; kept in the same minor as `manifest` baseline. |
|
||||||
|
| `codexAppServer.testedCliVersion` | `0.145.0` | Exact CLI patch used to generate and verify bindings; compatibility is tracked by minor version. |
|
||||||
|
|
||||||
Codex Panel depends on the experimental `codex app-server` API.
|
Codex Panel depends on the experimental `codex app-server` API.
|
||||||
|
|
||||||
|
|
|
||||||
BIN
assets/screenshot.webp
Normal file
BIN
assets/screenshot.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
221
biome.jsonc
Normal file
221
biome.jsonc
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.5.1/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
// Project-wide source shape and placement.
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/source-shape/no-handwritten-reexports.grit",
|
||||||
|
"includes": ["**/*.{ts,tsx,mjs}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/source-shape/no-responsibility-root-module-files.grit",
|
||||||
|
"includes": ["**/src/{app-server,domain}/*.{ts,tsx}", "**/src/features/{chat,threads}/*.{ts,tsx}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/source-shape/no-misplaced-tsx.grit",
|
||||||
|
"includes": [
|
||||||
|
"**/src/**/*.tsx",
|
||||||
|
"!**/src/{settings,shared/ui}/**",
|
||||||
|
"!**/src/features/chat/{panel,ui}/**",
|
||||||
|
"!**/src/features/{selection-rewrite,threads-view,turn-diff}/*.dom.tsx",
|
||||||
|
"!**/src/shared/dom/*.dom.tsx",
|
||||||
|
"!**/src/shared/obsidian/*.obsidian.tsx"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/source-shape/no-self-referential-initializer-callback.grit",
|
||||||
|
"includes": ["**/*.{ts,tsx,mjs}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/source-shape/no-unsafe-iterator-value.grit",
|
||||||
|
"includes": ["**/*.{ts,tsx,mjs}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/source-shape/no-uncontrolled-preact-form-state.grit",
|
||||||
|
"includes": ["**/src/**/*.tsx"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cross-cutting source-layer import boundaries.
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-domain-outer-layer-imports.grit",
|
||||||
|
"includes": ["**/src/domain/**/*.ts", "**/src/features/chat/domain/**/*.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-lower-level-feature-imports.grit",
|
||||||
|
"includes": ["**/src/app-server/**/*.ts", "**/src/shared/**/*.{ts,tsx}"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// App-server and generated protocol boundaries.
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-app-server-subfolder-root-imports.grit",
|
||||||
|
"includes": ["**/src/app-server/**/*.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-app-server-connection-boundary-imports.grit",
|
||||||
|
"includes": [
|
||||||
|
"**/src/app-server/protocol/**/*.ts",
|
||||||
|
"**/src/domain/**/*.ts",
|
||||||
|
"**/src/shared/**/*.{ts,tsx}",
|
||||||
|
"**/src/features/selection-rewrite/**/*.{ts,tsx}",
|
||||||
|
"!**/src/features/selection-rewrite/app-server-transport.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-app-server-protocol-boundary-imports.grit",
|
||||||
|
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/app-server/**", "!**/src/features/chat/app-server/mappers/thread-stream/turn-items.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-chat-turn-item-non-turn-protocol-imports.grit",
|
||||||
|
"includes": ["**/src/features/chat/app-server/mappers/thread-stream/turn-items.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-generated-app-server-boundary-imports.grit",
|
||||||
|
"includes": [
|
||||||
|
"**/src/**/*.{ts,tsx}",
|
||||||
|
"!**/src/app-server/connection/**",
|
||||||
|
"!**/src/app-server/protocol/server-requests.ts",
|
||||||
|
"!**/src/app-server/protocol/thread.ts",
|
||||||
|
"!**/src/app-server/protocol/turn.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/runtime/no-app-server-direct-rpcs.grit",
|
||||||
|
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/app-server/**", "!**/src/features/chat/app-server/**"]
|
||||||
|
},
|
||||||
|
// Workspace and feature coordination boundaries.
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-chat-workspace-boundary-imports.grit",
|
||||||
|
"includes": ["**/src/features/chat/**/*.{ts,tsx}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-feature-workspace-boundary-imports.grit",
|
||||||
|
"includes": ["**/src/features/**/*.{ts,tsx}", "!**/src/features/chat/**"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-workspace-chat-internal-imports.grit",
|
||||||
|
"includes": ["**/src/workspace/**/*.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-thread-workflow-app-server-imports.grit",
|
||||||
|
"includes": ["**/src/features/threads/workflows/**/*.ts"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chat feature layer boundaries.
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-chat-application-outer-layer-imports.grit",
|
||||||
|
"includes": ["**/src/features/chat/application/**/*.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-chat-app-server-outer-layer-imports.grit",
|
||||||
|
"includes": ["**/src/features/chat/app-server/**/*.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-chat-host-rendering-layer-imports.grit",
|
||||||
|
"includes": ["**/src/features/chat/host/**/*.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-chat-panel-runtime-boundary-imports.grit",
|
||||||
|
"includes": ["**/src/features/chat/panel/**/*.{ts,tsx}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-chat-presentation-outer-layer-imports.grit",
|
||||||
|
"includes": ["**/src/features/chat/presentation/**/*.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/import-boundaries/no-chat-ui-outer-layer-imports.grit",
|
||||||
|
"includes": ["**/src/features/chat/ui/**/*.{ts,tsx}"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Chat state and runtime ownership.
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/runtime/no-state-module-side-effects.grit",
|
||||||
|
"includes": ["**/src/features/chat/application/state/**/*.ts"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// DOM bridge and Preact root boundaries.
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/dom/no-implicit-dom-bridges.grit",
|
||||||
|
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/**/*.{dom,obsidian,measure}.{ts,tsx}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/dom/no-dom-events-imports.grit",
|
||||||
|
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/**/*.{dom,obsidian,measure}.{ts,tsx}"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/dom/no-preact-root-imports.grit",
|
||||||
|
"includes": ["**/src/**/*.{ts,tsx}", "!**/src/**/*.{dom,obsidian}.{ts,tsx}"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// CSS source policy.
|
||||||
|
{
|
||||||
|
"path": "./scripts/grit/no-restricted-css-policy.grit",
|
||||||
|
"includes": ["**/src/styles/**/*.css"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"files": {
|
||||||
|
"includes": [
|
||||||
|
"*.config.mjs",
|
||||||
|
"*.config.ts",
|
||||||
|
"*.css",
|
||||||
|
"*.json",
|
||||||
|
"*.jsonc",
|
||||||
|
"*.mjs",
|
||||||
|
"scripts/**/*.{json,jsonc}",
|
||||||
|
"scripts/**/*.grit",
|
||||||
|
"scripts/**/*.mjs",
|
||||||
|
"src/**/*.{css,ts,tsx}",
|
||||||
|
"tests/**/*.{mjs,ts,tsx}",
|
||||||
|
"!src/generated"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"indentStyle": "space",
|
||||||
|
"lineWidth": 140
|
||||||
|
},
|
||||||
|
"assist": {
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"noDuplicateClasses": "on",
|
||||||
|
"organizeImports": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"domains": {
|
||||||
|
"react": "recommended",
|
||||||
|
"test": "recommended"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"preset": "recommended",
|
||||||
|
"complexity": {
|
||||||
|
"useLiteralKeys": "off"
|
||||||
|
},
|
||||||
|
"correctness": {
|
||||||
|
"useExhaustiveDependencies": {
|
||||||
|
"level": "error",
|
||||||
|
"options": {
|
||||||
|
"reportUnnecessaryDependencies": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nursery": {
|
||||||
|
"noExcessiveSelectorClasses": {
|
||||||
|
"level": "error",
|
||||||
|
"options": {
|
||||||
|
"maxClasses": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"noCommonJs": "error"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noImportCycles": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
commitlint.config.mjs
Normal file
9
commitlint.config.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default {
|
||||||
|
defaultIgnores: false,
|
||||||
|
extends: ["@commitlint/config-conventional"],
|
||||||
|
ignores: [(message) => /^Merge pull request #\d+ from \S+/.test(message)],
|
||||||
|
rules: {
|
||||||
|
"subject-case": [0],
|
||||||
|
"type-enum": [2, "always", ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "test"]],
|
||||||
|
},
|
||||||
|
};
|
||||||
101
docs/design.md
Normal file
101
docs/design.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Design
|
||||||
|
|
||||||
|
Codex Panel is an Obsidian surface for Codex. It exists to put Codex beside vault notes without becoming a separate AI client, runtime policy editor, terminal, search product, or writing suite.
|
||||||
|
|
||||||
|
This document records durable design direction. User-facing behavior belongs in `README.md`; daily workflow, source layout, generated files, and compatibility checks belong in `docs/development.md`.
|
||||||
|
|
||||||
|
## Product Boundary
|
||||||
|
|
||||||
|
Keep the panel thin. Codex Panel owns the Obsidian experience around Codex: panels, composing, vault-aware link handoff, approvals and user input, file-change review, archive export, panel preferences, diagnostics, and selection rewrite.
|
||||||
|
|
||||||
|
Codex owns runtime behavior and thread state: models, credentials, sandboxing, approval policy, MCP servers, hooks, providers, agent-initiated network access, thread history, archived state, goals, and runtime settings. The panel should read and update that state through `codex app-server`, not redefine it.
|
||||||
|
|
||||||
|
The panel may provide a narrow management surface when direct configuration of Codex-owned state is impractical and app-server still owns the semantics. Hook controls serve this role: help users recognize and manage hooks they added or changed without presenting the panel as a code-audit surface.
|
||||||
|
|
||||||
|
The panel may acquire prompt context through an explicit user action. `/web` fetches the requested page through Obsidian and attaches the extracted content as untrusted turn context; it is not a search surface or agent network policy.
|
||||||
|
|
||||||
|
Panel-acquired context remains reference material rather than current user authorization. Send its bounded payload as untrusted app-server context without adding Panel-owned metadata envelopes to Codex history. Apply physical byte and part limits at the app-server boundary; keep source-specific selection and truncation policy with the context producer. Keep read-only compatibility for envelopes written by older Panel versions isolated at history projection boundaries.
|
||||||
|
|
||||||
|
Vault file references are prompt handoff data, not durable thread metadata. Keep their display state local; ordinary wikilinks remain represented by the user-authored message text. Thread references persist as ordinary, human-readable `codex://threads/` Markdown links, while their bounded transcript payload remains ephemeral turn context.
|
||||||
|
|
||||||
|
Panel settings should store only panel-specific preferences. Do not mirror Codex configuration in Obsidian settings just to display or inspect it.
|
||||||
|
|
||||||
|
## Sources of Truth
|
||||||
|
|
||||||
|
`codex app-server` is the source of truth for Codex state. Panel-side caches exist to keep the UI stable across transient failures; failed reads or stale panels must not become authoritative empty state.
|
||||||
|
|
||||||
|
The app-server API is experimental. The project tracks the supported Codex CLI minor and favors a clean current flow over broad old-protocol compatibility.
|
||||||
|
|
||||||
|
Runtime controls should express visible user intent for the active thread rather than copy Codex configuration. Diagnostics should expose only actionable troubleshooting facts.
|
||||||
|
|
||||||
|
An app-server context is the pair of Codex executable and Vault root. Replacing it must invalidate context-bound connections and work before publishing the new context, keep events attributed to their source context, and discard old runtime metadata. Preserve last-known-good state only across transient failures within the same context.
|
||||||
|
|
||||||
|
Vault root is the Panel-owned workspace root for that context. Panel operations always use it as the thread working directory and do not project mutable protocol thread cwd values into panel state.
|
||||||
|
|
||||||
|
## Code Boundaries
|
||||||
|
|
||||||
|
Raw app-server protocol belongs at the app-server boundary. Boundary code should adapt protocol payloads into panel-owned domain models or small projections before those values reach features, workspace coordination, settings, or UI.
|
||||||
|
|
||||||
|
Application workflows should depend on feature-owned contracts rather than app-server clients, RPC details, connection checks, vault-path wiring, or raw protocol projections.
|
||||||
|
|
||||||
|
Turn stream conversion is the main exception: raw app-server stream payloads may be consumed at the conversion boundary because the event set is broad and changes with Codex. The converter should still reduce them into panel-owned display and diagnostic models before they reach chat state or UI.
|
||||||
|
|
||||||
|
Server request adapters should turn app-server requests into coarse panel models before they reach pending request state. The UI handles user-facing intent; app-server-specific decisions and response payloads stay boundary-owned.
|
||||||
|
|
||||||
|
Source modules should be organized by reason to change, not by the single Obsidian plugin entrypoint. Boundaries should stay close to the state, lifecycle, or external API they own.
|
||||||
|
|
||||||
|
Do not hide complexity behind forwarding layers. Add an abstraction only when it owns a lifecycle, boundary, state transition, or reusable domain capability.
|
||||||
|
|
||||||
|
## UI Ownership
|
||||||
|
|
||||||
|
Runtime UI composition is Preact-owned. Preact components should render the panel shell, toolbar, thread stream, composer, and request controls.
|
||||||
|
|
||||||
|
Obsidian and app-server boundaries stay outside Preact components. External lifecycles, app-server connections, editor/workspace APIs, and rendering bridges belong in boundary modules.
|
||||||
|
|
||||||
|
Chat-visible state should have one authoritative owner. Components should consume narrow projections of that state rather than mirror it into another reactive store.
|
||||||
|
|
||||||
|
TanStack Query is the single panel-side owner of cached app-server resources. Features may project authoritative event results into that state, but should not introduce parallel cache or synchronization mechanisms. Partial read models are reconciled at explicit lifecycle boundaries rather than kept globally and continuously consistent.
|
||||||
|
|
||||||
|
Thread lifecycle changes should be projected from completed operation facts into the shared read model. When one user action changes multiple visible projections, publish them coherently without delaying live operational state or coupling independent panels.
|
||||||
|
|
||||||
|
Reads with different completeness requirements have separate lifecycles. Complete operation-local reads must not replace bounded shared history, and transient activity should come from its owning live state rather than forcing history refreshes.
|
||||||
|
|
||||||
|
Imperative UI bridges are allowed only where the host platform requires them. They should remain narrow boundary adapters, not a second UI composition system inside Preact-owned surfaces.
|
||||||
|
|
||||||
|
## Interaction Principles
|
||||||
|
|
||||||
|
Multiple panels are separate Obsidian leaves. Treat each panel as its own Codex working surface with independent connection, thread, turn state, composer, and pending requests.
|
||||||
|
|
||||||
|
Long-running actions must preserve the user intent that started them. Panel-local results may affect a panel only while that intent still owns its target; Codex facts completed in the current app-server context remain shared truth even if the initiating panel has moved on.
|
||||||
|
|
||||||
|
Coordinate conflicting work at the narrowest shared semantic owner. Independent panels should not block one another, and stale reads or writes must not overwrite newer shared state. Coalesce work only when replacement is part of the operation's meaning.
|
||||||
|
|
||||||
|
Foreground reveal and focus are the narrow workspace-wide exception: the latest user intent wins without serializing unrelated panel or app-server work. Cleanup obligations created by a committed state transition must survive the UI action that initiated them.
|
||||||
|
|
||||||
|
Do not use explanatory copy as the default remedy for an unclear or incoherent interface. Before adding instructions, warnings, status text, validation messages, confirmations, or diagnostics, determine why the interface does not communicate the necessary behavior through its structure and state. Prefer improving information hierarchy, labels, defaults, available choices, affordances, validation timing, progress indication, and feedback. Add text only when irreducible information remains and it materially helps the user decide, act, or recover; do not expose implementation concepts merely because they explain the current behavior.
|
||||||
|
|
||||||
|
Subagent threads opened from agent activity remain persistent and restorable but stay outside ordinary thread history. Use the app-server direct-input capability when available, falling back to read-only subagent panels for older servers, while preserving parent and agent provenance for other specialized behavior.
|
||||||
|
|
||||||
|
Mode-derived restrictions for the active panel thread must remain consistent across actions and UI. Keep connection state, turn busy state, and operations targeting another listed thread in their owning workflows.
|
||||||
|
|
||||||
|
Thread history and other app-server resources should follow app-server semantics. Panel-side views are read models over app-server snapshots and lifecycle events; stale or partial refreshes must not overwrite newer state. Obsidian integrations such as archive note export are convenience views, not replacements for Codex history.
|
||||||
|
|
||||||
|
Routine thread lists should load a bounded recent set and paginate older threads on demand instead of eagerly fetching complete inventories.
|
||||||
|
|
||||||
|
Selection rewrite is intentionally scoped to a focused edit-and-review workflow. Avoid expanding it into a broader writing assistant without a separate design decision.
|
||||||
|
|
||||||
|
Server requests should become panel UI only when the user can naturally answer them in context. Unknown or unsupported requests should stay diagnostic instead of pretending to be normal conversation text.
|
||||||
|
|
||||||
|
Thread stream display should separate primary conversation from diagnostic detail and progress/status. Preserve stable item identity across history, streaming, and rendering updates.
|
||||||
|
|
||||||
|
Codex Panel UI should feel native inside Obsidian. Prefer Obsidian variables, standard classes, and side-panel patterns. Add custom visual treatment only when Codex-specific state would otherwise be hard to read.
|
||||||
|
|
||||||
|
## Testing Direction
|
||||||
|
|
||||||
|
Tests should protect user expectations, app-server/panel responsibility boundaries, and state-transition invariants.
|
||||||
|
|
||||||
|
Prefer tests for visible behavior and state-transition invariants across panels, threads, requests, streams, and display fallbacks.
|
||||||
|
|
||||||
|
Avoid tests that freeze incidental implementation details unless those details directly protect a user-visible invariant.
|
||||||
|
|
||||||
|
Panel tests may define how received structured values are displayed, retained, or normalized. They should not redefine Codex-owned runtime policy, model lists, sandbox behavior, approval policy, or thread history semantics.
|
||||||
|
|
@ -1,60 +1,93 @@
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
Use this document for day-to-day implementation mechanics: commands, generated files, loaded artifacts, source layout, naming, validation, and common pitfalls. For product boundaries and design rationale, see `docs/design.md`.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm ci
|
npm ci
|
||||||
npm run format
|
npm run fix
|
||||||
npm run check
|
npm run check
|
||||||
npm run build
|
npm run test:coverage
|
||||||
|
npm run test:mutation
|
||||||
```
|
```
|
||||||
|
|
||||||
Run `npm run format` after edits and before `npm run check` so Prettier-only issues are fixed upfront. `npm run check` runs TypeScript type checking, unit tests, ESLint, Prettier check, and a production esbuild bundle.
|
Use the Node.js version in `.node-version`.
|
||||||
The local `npm run check` path runs checks in parallel and uses local caches for TypeScript, Vitest, ESLint, and Prettier. Use `npm run check:ci` when you need to reproduce the sequential, non-cached CI validation path.
|
|
||||||
|
|
||||||
`main.js`, `styles.css`, `data.json`, and `node_modules/` are ignored by Git. `main.js` and `styles.css` are still the files Obsidian loads, so run `npm run build` after source changes.
|
Use `npm run fix` for mechanical cleanup, review its diff, and run `npm run check` before handoff.
|
||||||
CSS is authored in `src/styles/` and generated into the ignored root `styles.css` release asset. Use `npm run build:styles` when only regenerating CSS, or `npm run build:styles:check` to verify that the authored CSS can be rendered.
|
|
||||||
|
|
||||||
## Source Layout
|
Use `npm run test:coverage` to identify source modules and branches that lack exercised behavior. It reports every authored TypeScript source file, including files not imported by tests, while excluding generated app-server bindings. Open `coverage/index.html` to inspect line-level gaps. Coverage is diagnostic and has no pass/fail threshold; prioritize user-visible behavior and state-transition invariants rather than raising the aggregate percentage.
|
||||||
|
|
||||||
The source tree is organized by responsibility rather than by the original single Obsidian plugin entrypoint:
|
Use `npm run test:mutation` for an exploratory mutation test of domain-owned logic, app-server protocol normalization, thread-stream event mapping, and the chat root reducer. Review surviving mutants individually instead of treating the aggregate score as a quality gate: add tests for meaningful behavior gaps, simplify equivalent or redundant code, and leave mutants alone when neither change improves the durable contract. The run ignores expensive module-initialization mutants, is intentionally manual, and writes its ignored HTML report to `reports/mutation/mutation.html`.
|
||||||
|
|
||||||
- `src/main.ts` registers Obsidian views, commands, settings, and lifecycle hooks.
|
## Commit Messages
|
||||||
- `src/app-server/` owns the app-server boundary: transport, connection lifecycle, compatibility helpers, runtime payload helpers, RPC facades, protocol-to-panel adapters, and shared app-server-backed cache state.
|
|
||||||
- `src/features/chat/` owns the main Codex chat surface: Obsidian `ItemView` classes, panel orchestration, request/app-server controllers, composer behavior, display models, chat-only UI renderers, runtime state and status projection, approvals, user input, thread actions, and panel-specific state.
|
|
||||||
- `src/features/threads-view/` owns the dedicated Obsidian thread list view, including app-server thread list rendering and live open-panel snapshot aggregation.
|
|
||||||
- `src/features/selection-rewrite/` owns the Markdown editor selection rewrite command, popover, prompt/output handling, and ephemeral rewrite thread runner.
|
|
||||||
- `src/workspace/` owns Obsidian workspace coordination across Codex surfaces, including panel discovery/opening, open-panel snapshots, and thread surface broadcasts.
|
|
||||||
- `src/shared/` contains feature-neutral helpers, including reusable DOM pieces and unified diff display helpers shared by chat and selection rewrite.
|
|
||||||
- `src/settings/` contains Obsidian settings models, settings-tab rendering, and app-server-backed dynamic settings data.
|
|
||||||
- `src/domain/catalog/` contains generated-independent model catalog and reasoning effort helpers shared outside app-server transport.
|
|
||||||
- `src/domain/threads/` contains thread title, reference, transcript, naming, and archive export helpers shared outside the chat feature. Some helpers still read app-server turn/item types directly when they are pure protocol interpreters, but new shared UI/workspace/settings-facing behavior should prefer panel-owned models or small adapters at the app-server boundary.
|
|
||||||
- `src/styles/` contains the authored CSS modules and `order.json` concatenation order. `scripts/build-styles.mjs` concatenates them into the ignored root `styles.css`, which remains the Obsidian release asset.
|
|
||||||
|
|
||||||
Keep new code near the state or API it owns. Feature-to-feature imports are allowed when the imported feature owns a concrete capability or integration point, such as thread export, thread title generation, or an Obsidian view. Do not use another feature as a generic utility bucket; move truly feature-neutral behavior to `src/shared/`, `src/domain/`, or `src/app-server/`. Lower-level modules in `src/app-server/`, `src/domain/`, and `src/shared/` must remain independent from `src/features/`; ESLint enforces that dependency direction.
|
Use [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) for new commits:
|
||||||
|
|
||||||
Codex Panel's runtime UI is Preact-owned through direct Preact APIs. Keep chat panel UI, the Threads view, and the selection rewrite popover in Preact components; use imperative DOM writes only for explicit bridge modules or Obsidian-owned API boundaries such as `MarkdownRenderer`, virtualizer measurement glue, diff rendering, icon rendering, `SuggestModal`, and the Obsidian `Setting`-based settings tab.
|
```text
|
||||||
|
feat(composer): add daily note context suggestions
|
||||||
|
fix: prevent manual titles from being overwritten
|
||||||
|
```
|
||||||
|
|
||||||
Chat panel state belongs in `ChatStateStore`. Panel-visible state changes should dispatch named reducer actions and flow through the shell-state adapter; do not add controller-owned signals, ad hoc Preact state mirrors, generic state patch actions, or a non-Preact UI runtime without revisiting the project design notes.
|
Use one of `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, or `test`. Scopes are optional. Keep the description concise and omit a trailing period; capitalization is unrestricted. Use `!` before the colon or a `BREAKING CHANGE:` footer for a disruptive change.
|
||||||
|
|
||||||
## App-Server Bindings
|
CI checks commits introduced by pull requests and direct pushes; GitHub-generated pull-request merge commits are exempt. To check a range locally, run `npm run commitlint -- --from <base> --to <head> --verbose`.
|
||||||
|
|
||||||
|
Use focused scripts while iterating, but run `npm run check` before handoff. CI and release preflight use the same check.
|
||||||
|
|
||||||
|
Keep rule suppressions local and include the Obsidian-specific reason when a native Obsidian UI pattern intentionally diverges from a generic browser rule.
|
||||||
|
|
||||||
|
Grit policy cases protect matcher semantics rather than diagnostic wording, source locations, or the exact shape of Biome configuration. Keep one valid and invalid source case for each policy, and change a case only when that policy's accepted or rejected source shape intentionally changes.
|
||||||
|
|
||||||
|
## Generated and Loaded Files
|
||||||
|
|
||||||
|
`main.js`, `styles.css`, `data.json`, and `node_modules/` are ignored by Git. `main.js` and `styles.css` are still the files Obsidian loads, so run `npm run build` before live Obsidian validation if you have not already run `npm run check` after the source change.
|
||||||
|
|
||||||
|
CSS is authored in `src/styles/` and generated into the ignored root `styles.css` release asset. Use `npm run build:styles` when only regenerating CSS; it also verifies the authored CSS order before writing `styles.css`.
|
||||||
|
|
||||||
The app-server TypeScript bindings in `src/generated/app-server/` are generated from the installed Codex CLI:
|
The app-server TypeScript bindings in `src/generated/app-server/` are generated from the installed Codex CLI:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run generate:app-server-types
|
npm run generate:app-server-types
|
||||||
|
npm run generate:app-server-types:check
|
||||||
npm run check
|
npm run check
|
||||||
```
|
```
|
||||||
|
|
||||||
The generation script uses `codex app-server generate-ts --experimental` because the panel depends on experimental app-server fields such as collaboration mode and generated v2 types.
|
Do not hand-edit the bindings. `src/app-server/connection/compatibility.json` records their CLI patch and generation arguments; the check command regenerates and compares them without replacing tracked files.
|
||||||
|
|
||||||
|
## Placement Rules
|
||||||
|
|
||||||
|
Follow the ownership boundaries in `docs/design.md` and the import and source-shape lint rules. Put behavior where its reason to change lives, and update the lint policy and its tests when intentionally changing a boundary.
|
||||||
|
|
||||||
|
Keep generated protocol types behind app-server adapters and expose panel-owned contracts or projections to features. Keep direct DOM work in explicit bridge, measurement, or rendering modules; use the established filename suffixes for those boundaries and reserve `.tsx` for rendering.
|
||||||
|
|
||||||
|
## CSS Rules
|
||||||
|
|
||||||
|
CSS should stay native to Obsidian. Prefer Obsidian variables and Codex Panel tokens for color, typography, spacing, and layout dimensions instead of hardcoded values.
|
||||||
|
|
||||||
|
Keep selectors shallow and scoped to Codex Panel classes. Add new authored CSS files to `src/styles/order.json`, and remove unused or test-only classes.
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
Name modules by owned responsibility. Use lifecycle or boundary nouns only when the object owns that lifecycle or boundary, and passive-data names for values.
|
||||||
|
|
||||||
|
Prefer functions and factories. Reserve classes for mutable resource ownership, external class APIs, and `Error` types.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Build before Obsidian validation. Obsidian loads ignored root assets, not the TypeScript or authored CSS sources directly.
|
||||||
|
- Preserve last-known-good app-server state on refresh failure. Do not turn disconnected reads into authoritative empty thread lists, settings snapshots, hook inventories, or diagnostics.
|
||||||
|
- Normalize optional and nullable app-server values before display. Users should not see raw `undefined`, `null`, protocol enum gaps, or fallback labels that imply Panel owns a Codex runtime setting.
|
||||||
|
- Do not use DOM order as thread history state. Thread stream DOM is a presentation surface, and delayed Markdown rendering can change heights after initial render.
|
||||||
|
|
||||||
## API Baselines
|
## API Baselines
|
||||||
|
|
||||||
Use the local API baseline report when checking whether the development environment matches the supported API policy:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run api:baseline
|
npm run api:baseline
|
||||||
npm run api:baseline:check
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Obsidian API compatibility follows semver within the guaranteed minor. `manifest.json` and `versions.json` define the minimum supported Obsidian app minor with a `.0` patch version, while the `obsidian` npm dev dependency tracks the latest patch in that same minor using a `~` range. When the Obsidian API minor is raised, update `manifest.json`, `versions.json`, README requirements, and the `obsidian` dev dependency together.
|
Obsidian runtime compatibility is declared through `manifest.json` and `versions.json`. The `obsidian` npm package provides compile-time TypeScript API definitions; it is not runtime validation for an Obsidian app-version matrix. Because the project does not run app-version smoke tests, keep the API type package in the same minor as `manifest.minAppVersion` and use the latest patch in that minor for local type checking. `npm run api:baseline` exits non-zero when the local environment or recorded baselines drift. Raise `manifest.minAppVersion` only when intentionally adopting a newer Obsidian app/API minor.
|
||||||
|
|
||||||
Codex app-server compatibility is managed by Codex CLI minor version. README records the tested Codex CLI patch version, and the baseline check verifies that the local `codex --version` is in the same minor before app-server binding or compatibility work.
|
Codex app-server compatibility is managed by CLI minor version. `src/app-server/connection/compatibility.json` is the source of truth for the exact generation patch and app-server capabilities; README displays that patch.
|
||||||
|
|
||||||
|
Local compatibility work should run `npm run api:baseline` and `npm run generate:app-server-types:check`; CI runs the same verification with the recorded CLI.
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ GitHub Releases attach only `main.js`, `manifest.json`, and `styles.css` as Obsi
|
||||||
|
|
||||||
Release work is Jujutsu-first in a colocated Git repository; Git is still used to push the tag that triggers the GitHub Release workflow. If the checkout has not been initialized for Jujutsu yet, run `jj git init --colocate` and `jj bookmark track main --remote=origin` once.
|
Release work is Jujutsu-first in a colocated Git repository; Git is still used to push the tag that triggers the GitHub Release workflow. If the checkout has not been initialized for Jujutsu yet, run `jj git init --colocate` and `jj bookmark track main --remote=origin` once.
|
||||||
|
|
||||||
Create a release by preparing the next version, editing the generated release notes, committing the release changes, then running the preflight before pushing the matching tag:
|
Plugin versions are for Obsidian distribution, not a library API compatibility contract. Use patch releases for workflow-preserving changes, minor releases for visible capabilities or raised runtime baselines, and major releases for disruptive workflow, settings, storage, or support-policy changes.
|
||||||
|
|
||||||
|
Create a release by preparing the next version, reviewing and editing the generated release notes, committing the release changes, then running the preflight before pushing the matching tag:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run release:prepare -- X.Y.Z
|
npm run release:prepare -- X.Y.Z
|
||||||
# Edit .github/release-notes/X.Y.Z.md.
|
# Review and edit .github/release-notes/X.Y.Z.md.
|
||||||
jj status
|
jj status
|
||||||
jj commit -m "Bump version to X.Y.Z"
|
jj commit -m "chore(release): X.Y.Z"
|
||||||
jj bookmark move main --to @-
|
jj bookmark move main --to @-
|
||||||
npm run release:preflight
|
npm run release:preflight
|
||||||
jj tag set X.Y.Z -r main
|
jj tag set X.Y.Z -r main
|
||||||
|
|
@ -18,6 +20,10 @@ jj git push --remote origin --bookmark main
|
||||||
git push origin X.Y.Z
|
git push origin X.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
`release:prepare` updates the version files and creates a `## Changes` release notes template. `release:preflight` verifies the local Jujutsu/Git state, release metadata, lockfile, and full build once after the release commit is on `main`.
|
`release:prepare` updates version files and drafts `## Changes` from selected Conventional Commits since the previous version. Use those commits to locate candidate changes, then compare the resulting user-visible behavior with the previous release before choosing the version. Review the draft against the full diff for complete, user-facing notes; consolidate and reorder entries as needed, and replace an empty placeholder.
|
||||||
|
|
||||||
The release workflow runs `npm ci`, `npm run release:check`, `npm run check:ci`, attaches the install assets, and generates GitHub artifact attestations for them. The release notes file is required and must contain a single `## Changes` section. If a tag-triggered release fails before creating the GitHub Release, fix the commit, move the local tag with `jj tag set --allow-move -r main X.Y.Z`, then update the remote tag with `git push --force origin X.Y.Z`.
|
Run `release:preflight` after the release commit is on `main`; it validates the commit range and release readiness.
|
||||||
|
|
||||||
|
Release notes should normally include only user-facing changes. Internal implementation changes, validation details, and release procedure notes should be omitted when minor; when they are important enough to mention, group them into at most one concise bullet.
|
||||||
|
|
||||||
|
The release notes file must contain a single `## Changes` section. The tag-triggered workflow validates and publishes the install assets. If it fails before creating the GitHub Release, fix the commit, move the local tag with `jj tag set --allow-move -r main X.Y.Z`, then update the remote tag with `git push --force origin X.Y.Z`.
|
||||||
|
|
|
||||||
|
|
@ -1,601 +1,34 @@
|
||||||
import js from "@eslint/js";
|
import json from "@eslint/json";
|
||||||
import { defineConfig } from "eslint/config";
|
import { defineConfig } from "eslint/config";
|
||||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
|
||||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||||
import reactHooks from "eslint-plugin-react-hooks";
|
import { PlainTextParser } from "eslint-plugin-obsidianmd/dist/lib/plainTextParser.js";
|
||||||
import ts from "typescript";
|
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
const typeScriptFiles = ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"];
|
const sourceTypeScriptFiles = ["src/**/*.{ts,tsx}"];
|
||||||
const nodeJavaScriptFiles = ["*.mjs", "scripts/**/*.mjs"];
|
|
||||||
const typeScriptConfigFiles = ["*.config.ts"];
|
|
||||||
const lintedTypeScriptFiles = [...typeScriptFiles, ...typeScriptConfigFiles];
|
|
||||||
const unsafeTypeScriptRules = {
|
|
||||||
"@typescript-eslint/no-unsafe-argument": "error",
|
|
||||||
"@typescript-eslint/no-unsafe-assignment": "error",
|
|
||||||
"@typescript-eslint/no-unsafe-call": "error",
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": "error",
|
|
||||||
};
|
|
||||||
const projectTypeScriptRules = {
|
|
||||||
"@typescript-eslint/consistent-type-imports": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
|
||||||
"@typescript-eslint/no-floating-promises": "error",
|
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
||||||
...unsafeTypeScriptRules,
|
|
||||||
"react-hooks/exhaustive-deps": "error",
|
|
||||||
"react-hooks/rules-of-hooks": "error",
|
|
||||||
};
|
|
||||||
const testTypeScriptRelaxations = {
|
|
||||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-call": "off",
|
|
||||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
|
||||||
"@typescript-eslint/require-await": "off",
|
|
||||||
};
|
|
||||||
const preactFormRestrictions = [
|
|
||||||
{
|
|
||||||
selector: "JSXAttribute[name.name=/^(defaultValue|defaultChecked)$/]",
|
|
||||||
message: "Keep Preact form state explicit with controlled value or checked props.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const removedChatStateEscapeHatchRestrictions = [
|
|
||||||
{
|
|
||||||
selector: "Literal[value='state/patched']",
|
|
||||||
message: "Use a named ChatAction instead of reintroducing the generic state patch escape hatch.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const chatSignalAdapterRestrictions = [
|
|
||||||
{
|
|
||||||
selector: "ImportDeclaration[source.value='@preact/signals']",
|
|
||||||
message: "Keep chat signals in panel/shell-state.tsx as a reducer-to-Preact notification adapter.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const chatDomainLayerRestrictions = [
|
|
||||||
{
|
|
||||||
selector:
|
|
||||||
"ImportDeclaration[source.value=/^(?:\\.\\.\\/)+(?:app-server|application|host|panel|presentation|ui)(?:\\/|$)|^src\\/features\\/chat\\/(?:app-server|application|host|panel|presentation|ui)(?:\\/|$)/]",
|
|
||||||
message:
|
|
||||||
"Keep chat/domain as Panel-owned meaning models and pure derivations; app-server, application, host, panel, presentation, and UI layers may depend on domain, not the reverse.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const uiRootImportRestrictions = [
|
|
||||||
{
|
|
||||||
selector: "ImportDeclaration[source.value=/shared\\/ui\\/ui-root$/]",
|
|
||||||
message: "Import the Preact root adapter only from explicit root bridge files.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const generatedAppServerSourceImportPatterns = importBoundaryPatterns("generated/app-server", "src/generated/app-server", 6);
|
|
||||||
const generatedAppServerTestImportPatterns = importBoundaryPatterns("src/generated/app-server", "src/generated/app-server", 6);
|
|
||||||
const lowerLevelFeatureImportPatterns = importBoundaryPatterns("features", "src/features", 6);
|
|
||||||
const nonAppServerBannedAppServerProtocolModules = [
|
|
||||||
"catalog",
|
|
||||||
"diagnostics",
|
|
||||||
"initialization",
|
|
||||||
"request-input",
|
|
||||||
"runtime-config",
|
|
||||||
"runtime-metrics",
|
|
||||||
"runtime-policy",
|
|
||||||
"thread",
|
|
||||||
"thread-goal",
|
|
||||||
"thread-settings",
|
|
||||||
"turn-history",
|
|
||||||
];
|
|
||||||
const nonAppServerBannedAppServerProtocolImportPatterns = nonAppServerBannedAppServerProtocolModules.flatMap((moduleName) =>
|
|
||||||
importBoundaryPatterns(`app-server/protocol/${moduleName}`, `src/app-server/protocol/${moduleName}`, 6),
|
|
||||||
);
|
|
||||||
const generatedAppServerThreadImportRestrictions = [
|
|
||||||
{
|
|
||||||
selector:
|
|
||||||
"ImportDeclaration[source.value=/generated\\/app-server\\/v2\\/Thread$/] ImportSpecifier[imported.name='Thread'][local.name='Thread']",
|
|
||||||
message: "Import generated app-server Thread as AppServerThread, or use the Panel-owned domain Thread model.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const unsafeIteratorRestrictions = [
|
|
||||||
{
|
|
||||||
selector: "MemberExpression[property.name='value'][object.type='CallExpression'][object.callee.property.name='next']",
|
|
||||||
message: "Avoid reading iterator.next().value directly; use for...of or inspect the typed IteratorResult first.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const imperativeDomWriteMethods = new Set([
|
|
||||||
"addClass",
|
|
||||||
"addClasses",
|
|
||||||
"append",
|
|
||||||
"appendChild",
|
|
||||||
"after",
|
|
||||||
"before",
|
|
||||||
"createDiv",
|
|
||||||
"createEl",
|
|
||||||
"createSpan",
|
|
||||||
"empty",
|
|
||||||
"hide",
|
|
||||||
"insertAdjacentElement",
|
|
||||||
"insertAdjacentHTML",
|
|
||||||
"insertAdjacentText",
|
|
||||||
"insertBefore",
|
|
||||||
"prepend",
|
|
||||||
"removeClass",
|
|
||||||
"removeClasses",
|
|
||||||
"remove",
|
|
||||||
"removeChild",
|
|
||||||
"replaceChildren",
|
|
||||||
"replaceWith",
|
|
||||||
"setCssProps",
|
|
||||||
"setCssStyles",
|
|
||||||
"setText",
|
|
||||||
"show",
|
|
||||||
"setAttr",
|
|
||||||
"toggleClass",
|
|
||||||
]);
|
|
||||||
const imperativeDomEventMethods = new Set(["addEventListener", "removeEventListener"]);
|
|
||||||
const imperativeDomAssignmentProperties = new Set([
|
|
||||||
"checked",
|
|
||||||
"innerHTML",
|
|
||||||
"onblur",
|
|
||||||
"onchange",
|
|
||||||
"onclick",
|
|
||||||
"ondblclick",
|
|
||||||
"onfocus",
|
|
||||||
"oninput",
|
|
||||||
"onkeydown",
|
|
||||||
"onkeyup",
|
|
||||||
"onmousedown",
|
|
||||||
"onmousemove",
|
|
||||||
"onmouseup",
|
|
||||||
"onpointerdown",
|
|
||||||
"onpointerup",
|
|
||||||
"onscroll",
|
|
||||||
"onselect",
|
|
||||||
"outerHTML",
|
|
||||||
"textContent",
|
|
||||||
"value",
|
|
||||||
]);
|
|
||||||
const pureChatStateRestrictions = [
|
|
||||||
{
|
|
||||||
selector: "CallExpression[callee.object.name='Date'][callee.property.name='now']",
|
|
||||||
message: "Keep chat state transforms deterministic; generate IDs or timestamps at the controller/view boundary.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "NewExpression[callee.name='Date']",
|
|
||||||
message: "Keep chat state transforms deterministic; pass dates in from the controller/view boundary.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "CallExpression[callee.object.name='Math'][callee.property.name='random']",
|
|
||||||
message: "Keep chat state transforms deterministic; generate IDs at the controller/view boundary.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "NewExpression[callee.name=/^(AppServerClient|ConnectionManager|Notice)$/]",
|
|
||||||
message: "Keep app-server and Obsidian side effects out of chat state transforms.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "CallExpression[callee.property.name=/^(setTimeout|clearTimeout|requestAnimationFrame)$/]",
|
|
||||||
message: "Keep scheduling side effects out of chat state transforms.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "MemberExpression[object.name=/^(document|localStorage|sessionStorage)$/]",
|
|
||||||
message: "Keep browser globals out of chat state transforms.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const chatExternalDomBridgeFiles = [
|
|
||||||
"src/features/chat/ui/message-stream/markdown-renderer.ts",
|
|
||||||
"src/features/chat/ui/message-stream/virtualizer.ts",
|
|
||||||
];
|
|
||||||
const chatPreactDomBridgeFiles = [
|
|
||||||
"src/features/chat/ui/message-stream/text-item.tsx",
|
|
||||||
"src/features/chat/ui/message-stream/tool-result.tsx",
|
|
||||||
"src/features/chat/ui/message-stream/viewport.tsx",
|
|
||||||
"src/features/chat/ui/composer-dom.ts",
|
|
||||||
"src/features/chat/panel/shell.tsx",
|
|
||||||
"src/features/chat/ui/turn-diff/render.tsx",
|
|
||||||
];
|
|
||||||
const chatImperativeDomBridgeFiles = [...chatExternalDomBridgeFiles, ...chatPreactDomBridgeFiles];
|
|
||||||
const uiRootBridgeFiles = [
|
|
||||||
"src/features/chat/panel/shell.tsx",
|
|
||||||
"src/features/chat/ui/turn-diff/render.tsx",
|
|
||||||
"src/features/chat/ui/turn-diff/view.ts",
|
|
||||||
"src/features/selection-rewrite/popover.tsx",
|
|
||||||
"src/features/threads-view/renderer.tsx",
|
|
||||||
];
|
|
||||||
const nonChatImperativeDomBridgeFiles = [
|
|
||||||
"src/features/selection-rewrite/popover.tsx",
|
|
||||||
"src/features/thread-picker/modal.ts",
|
|
||||||
"src/features/threads-view/renderer.tsx",
|
|
||||||
"src/settings/dynamic-sections.ts",
|
|
||||||
"src/settings/tab.ts",
|
|
||||||
"src/shared/diff/render.ts",
|
|
||||||
"src/shared/ui/components.tsx",
|
|
||||||
"src/shared/ui/textarea-autogrow.ts",
|
|
||||||
"src/shared/ui/textarea-caret.ts",
|
|
||||||
"src/shared/ui/ui-root.tsx",
|
|
||||||
];
|
|
||||||
const nonUiEventListenerFiles = ["src/shared/lifecycle/abortable.ts", "src/shared/ui/dom-events.ts"];
|
|
||||||
const baseSourceSyntaxRestrictions = [
|
|
||||||
...removedChatStateEscapeHatchRestrictions,
|
|
||||||
...generatedAppServerThreadImportRestrictions,
|
|
||||||
...unsafeIteratorRestrictions,
|
|
||||||
...preactFormRestrictions,
|
|
||||||
];
|
|
||||||
const sourceSyntaxRestrictionsWithoutUiRoot = [...baseSourceSyntaxRestrictions, ...chatSignalAdapterRestrictions];
|
|
||||||
const sourceSyntaxRestrictions = [...sourceSyntaxRestrictionsWithoutUiRoot, ...uiRootImportRestrictions];
|
|
||||||
const chatSourceSyntaxRestrictions = sourceSyntaxRestrictions;
|
|
||||||
const chatDomBridgeSyntaxRestrictions = sourceSyntaxRestrictions;
|
|
||||||
const nonChatDomBridgeSyntaxRestrictions = sourceSyntaxRestrictions;
|
|
||||||
const pureChatStateSyntaxRestrictions = [...sourceSyntaxRestrictions, ...pureChatStateRestrictions];
|
|
||||||
const codexPanelEslintPlugin = {
|
|
||||||
rules: {
|
|
||||||
"no-self-referential-initializer-callback": {
|
|
||||||
meta: {
|
|
||||||
type: "problem",
|
|
||||||
docs: {
|
|
||||||
description: "Disallow callbacks in variable initializers from referencing the variable being initialized.",
|
|
||||||
},
|
|
||||||
messages: {
|
|
||||||
selfReference: "Avoid referencing '{{name}}' from a callback inside its own initializer; declare it first with an explicit type.",
|
|
||||||
},
|
|
||||||
schema: [],
|
|
||||||
},
|
|
||||||
create(context) {
|
|
||||||
return {
|
|
||||||
VariableDeclarator(node) {
|
|
||||||
if (node.id.type !== "Identifier" || !node.init) return;
|
|
||||||
if (node.init.type !== "NewExpression") return;
|
|
||||||
const reference = findInitializerCallbackReference(node.init, node.id.name);
|
|
||||||
if (reference) context.report({ node: reference, messageId: "selfReference", data: { name: node.id.name } });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"no-chat-state-direct-mutation": {
|
|
||||||
meta: {
|
|
||||||
type: "problem",
|
|
||||||
docs: {
|
|
||||||
description: "Disallow direct ChatState mutation in chat modules.",
|
|
||||||
},
|
|
||||||
messages: {
|
|
||||||
assign: "Route ChatState updates through ChatStateStore.dispatch().",
|
|
||||||
mutateCollection: "Clone ChatState collections and update them through ChatStateStore.dispatch().",
|
|
||||||
},
|
|
||||||
schema: [],
|
|
||||||
},
|
|
||||||
create(context) {
|
|
||||||
const mutatingCollectionMethods = new Set(["add", "clear", "delete", "push", "set"]);
|
|
||||||
const chatStateAliasVariables = new WeakSet();
|
|
||||||
let parserServices = null;
|
|
||||||
let checker = null;
|
|
||||||
const typeChecker = () => {
|
|
||||||
if (!parserServices) {
|
|
||||||
parserServices = parserServicesFromContext(context);
|
|
||||||
checker = parserServices.program.getTypeChecker();
|
|
||||||
}
|
|
||||||
return checker;
|
|
||||||
};
|
|
||||||
const variableForIdentifier = (node) => {
|
|
||||||
let scope = context.sourceCode.getScope(node);
|
|
||||||
while (scope) {
|
|
||||||
const variable = scope.variables.find((item) => item.name === node.name);
|
|
||||||
if (variable) return variable;
|
|
||||||
scope = scope.upper;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const markChatStateAlias = (node) => {
|
|
||||||
const variable = variableForIdentifier(node);
|
|
||||||
if (variable) chatStateAliasVariables.add(variable);
|
|
||||||
};
|
|
||||||
const isChatStateAlias = (node) => {
|
|
||||||
if (node?.type !== "Identifier") return false;
|
|
||||||
const variable = variableForIdentifier(node);
|
|
||||||
return Boolean(variable && chatStateAliasVariables.has(variable));
|
|
||||||
};
|
|
||||||
const typeAt = (node) => {
|
|
||||||
const services = parserServices ?? parserServicesFromContext(context);
|
|
||||||
if (!parserServices) {
|
|
||||||
parserServices = services;
|
|
||||||
checker = services.program.getTypeChecker();
|
|
||||||
}
|
|
||||||
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
||||||
return typeChecker().getTypeAtLocation(tsNode);
|
|
||||||
};
|
|
||||||
const isChatStateValue = (node) => typeIncludesChatState(typeAt(node), typeChecker());
|
|
||||||
const isChatStateAliasSource = (node) =>
|
|
||||||
(isChatStateTarget(node) || isChatStateValue(node)) && typeCanCarryChatStateMutation(typeAt(node));
|
|
||||||
const isChatStateTarget = (node) => {
|
|
||||||
if (isChatStateAlias(node)) return true;
|
|
||||||
if (!isMemberExpression(node)) return false;
|
|
||||||
if (isChatStateMember(node)) return true;
|
|
||||||
const root = rootMemberObject(node);
|
|
||||||
if (!root) return false;
|
|
||||||
if (isChatStateAlias(root)) return true;
|
|
||||||
return typeIncludesChatState(typeAt(root), typeChecker());
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
VariableDeclarator(node) {
|
|
||||||
if (node.id.type !== "Identifier" || !node.init) return;
|
|
||||||
if (isChatStateAliasSource(node.init)) markChatStateAlias(node.id);
|
|
||||||
},
|
|
||||||
AssignmentExpression(node) {
|
|
||||||
if (isMemberExpression(node.left) && isChatStateTarget(node.left)) context.report({ node: node.left, messageId: "assign" });
|
|
||||||
},
|
|
||||||
CallExpression(node) {
|
|
||||||
if (!isMemberExpression(node.callee)) return;
|
|
||||||
const method = staticPropertyName(node.callee.property);
|
|
||||||
if (!method || !mutatingCollectionMethods.has(method)) return;
|
|
||||||
if (isChatStateTarget(node.callee.object)) context.report({ node: node.callee, messageId: "mutateCollection" });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"no-imperative-dom": {
|
|
||||||
meta: {
|
|
||||||
type: "problem",
|
|
||||||
docs: {
|
|
||||||
description: "Disallow imperative DOM writes and event wiring outside explicit bridge files.",
|
|
||||||
},
|
|
||||||
messages: {
|
|
||||||
event: "Keep imperative DOM event wiring in an explicit bridge module or Obsidian-owned UI boundary.",
|
|
||||||
write: "Keep imperative DOM writes in an explicit bridge module or Obsidian-owned UI boundary.",
|
|
||||||
},
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
allowEvents: { type: "boolean" },
|
|
||||||
allowWrites: { type: "boolean" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
create(context) {
|
|
||||||
const options = context.options[0] ?? {};
|
|
||||||
const allowEvents = options.allowEvents === true;
|
|
||||||
const allowWrites = options.allowWrites === true;
|
|
||||||
let parserServices = null;
|
|
||||||
let checker = null;
|
|
||||||
|
|
||||||
const typeChecker = () => {
|
|
||||||
if (!parserServices) {
|
|
||||||
parserServices = parserServicesFromContext(context);
|
|
||||||
checker = parserServices.program.getTypeChecker();
|
|
||||||
}
|
|
||||||
return checker;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDomTarget = (node) => {
|
|
||||||
const services = parserServices ?? parserServicesFromContext(context);
|
|
||||||
if (!parserServices) {
|
|
||||||
parserServices = services;
|
|
||||||
checker = services.program.getTypeChecker();
|
|
||||||
}
|
|
||||||
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
||||||
return typeIncludesDom(typeChecker().getTypeAtLocation(tsNode), typeChecker());
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
AssignmentExpression(node) {
|
|
||||||
if (allowWrites || !isMemberExpression(node.left)) return;
|
|
||||||
const property = staticPropertyName(node.left.property);
|
|
||||||
if (!property || !imperativeDomAssignmentProperties.has(property)) return;
|
|
||||||
if (isDomTarget(node.left.object)) context.report({ node: node.left, messageId: "write" });
|
|
||||||
},
|
|
||||||
CallExpression(node) {
|
|
||||||
if (!isMemberExpression(node.callee)) return;
|
|
||||||
const method = staticPropertyName(node.callee.property);
|
|
||||||
if (!method) return;
|
|
||||||
if (!allowWrites && imperativeDomWriteMethods.has(method) && isDomTarget(node.callee.object)) {
|
|
||||||
context.report({ node: node.callee, messageId: "write" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!allowEvents && imperativeDomEventMethods.has(method) && isDomTarget(node.callee.object)) {
|
|
||||||
context.report({ node: node.callee, messageId: "event" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function importBoundaryPatterns(relativeTarget, absoluteTarget, maxParentDepth) {
|
|
||||||
const targets = [absoluteTarget, ...Array.from({ length: maxParentDepth }, (_, index) => `${"../".repeat(index + 1)}${relativeTarget}`)];
|
|
||||||
return targets.flatMap((target) => [target, `${target}/**`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function restrictedSyntaxRule(restrictions) {
|
|
||||||
return {
|
|
||||||
"no-restricted-syntax": ["error", ...restrictions],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isChatStateMember(node) {
|
|
||||||
if (!isMemberExpression(node)) return false;
|
|
||||||
|
|
||||||
let current = node;
|
|
||||||
while (isMemberExpression(current)) {
|
|
||||||
if (isIdentifier(current.object, "state")) return true;
|
|
||||||
if (isThisStateMember(current.object)) return true;
|
|
||||||
current = current.object;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isThisStateMember(node) {
|
|
||||||
return isMemberExpression(node) && node.object?.type === "ThisExpression" && staticPropertyName(node.property) === "state";
|
|
||||||
}
|
|
||||||
|
|
||||||
function rootMemberObject(node) {
|
|
||||||
if (!isMemberExpression(node)) return null;
|
|
||||||
let current = node;
|
|
||||||
while (isMemberExpression(current)) current = current.object;
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMemberExpression(node) {
|
|
||||||
return node?.type === "MemberExpression";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIdentifier(node, name) {
|
|
||||||
return node?.type === "Identifier" && node.name === name;
|
|
||||||
}
|
|
||||||
|
|
||||||
function staticPropertyName(node) {
|
|
||||||
return node?.type === "Identifier" ? node.name : node?.type === "Literal" && typeof node.value === "string" ? node.value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parserServicesFromContext(context) {
|
|
||||||
const services = context.sourceCode.parserServices;
|
|
||||||
if (!services?.program || !services.esTreeNodeToTSNodeMap) {
|
|
||||||
throw new Error("codex-panel/no-imperative-dom requires TypeScript parser services.");
|
|
||||||
}
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeIncludesDom(type, checker, seen = new Set()) {
|
|
||||||
if (!type || seen.has(type.id)) return false;
|
|
||||||
seen.add(type.id);
|
|
||||||
|
|
||||||
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return false;
|
|
||||||
if (type.isUnionOrIntersection()) return type.types.some((item) => typeIncludesDom(item, checker, seen));
|
|
||||||
|
|
||||||
const typeName = checker.typeToString(type);
|
|
||||||
if (domTypeName(typeName)) return true;
|
|
||||||
|
|
||||||
const symbolName = type.getSymbol()?.getName() ?? type.aliasSymbol?.getName() ?? "";
|
|
||||||
if (domTypeName(symbolName)) return true;
|
|
||||||
|
|
||||||
const apparent = checker.getApparentType(type);
|
|
||||||
if (apparent !== type && typeIncludesDom(apparent, checker, seen)) return true;
|
|
||||||
|
|
||||||
const bases = typeof type.getBaseTypes === "function" ? (type.getBaseTypes() ?? []) : [];
|
|
||||||
return bases.some((base) => typeIncludesDom(base, checker, seen));
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeIncludesChatState(type, checker, seen = new Set()) {
|
|
||||||
if (!type || seen.has(type.id)) return false;
|
|
||||||
seen.add(type.id);
|
|
||||||
|
|
||||||
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return false;
|
|
||||||
if (type.isUnionOrIntersection()) return type.types.some((item) => typeIncludesChatState(item, checker, seen));
|
|
||||||
|
|
||||||
const typeName = checker.typeToString(type);
|
|
||||||
if (chatStateTypeName(typeName)) return true;
|
|
||||||
|
|
||||||
const symbolName = type.getSymbol()?.getName() ?? type.aliasSymbol?.getName() ?? "";
|
|
||||||
if (chatStateTypeName(symbolName)) return true;
|
|
||||||
|
|
||||||
const apparent = checker.getApparentType(type);
|
|
||||||
if (apparent !== type && typeIncludesChatState(apparent, checker, seen)) return true;
|
|
||||||
|
|
||||||
const bases = typeof type.getBaseTypes === "function" ? (type.getBaseTypes() ?? []) : [];
|
|
||||||
return bases.some((base) => typeIncludesChatState(base, checker, seen));
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeCanCarryChatStateMutation(type, seen = new Set()) {
|
|
||||||
if (!type || seen.has(type.id)) return false;
|
|
||||||
seen.add(type.id);
|
|
||||||
|
|
||||||
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) return false;
|
|
||||||
if (type.isUnionOrIntersection()) return type.types.some((item) => typeCanCarryChatStateMutation(item, seen));
|
|
||||||
|
|
||||||
const primitiveLikeFlags =
|
|
||||||
ts.TypeFlags.StringLike |
|
|
||||||
ts.TypeFlags.NumberLike |
|
|
||||||
ts.TypeFlags.BooleanLike |
|
|
||||||
ts.TypeFlags.BigIntLike |
|
|
||||||
ts.TypeFlags.ESSymbolLike |
|
|
||||||
ts.TypeFlags.Null |
|
|
||||||
ts.TypeFlags.Undefined |
|
|
||||||
ts.TypeFlags.Void;
|
|
||||||
return (type.flags & primitiveLikeFlags) === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function domTypeName(name) {
|
|
||||||
return /\b(?:AbortSignal|Document|Element|EventTarget|HTML[A-Za-z]*Element|HTMLElement|Node|SVG[A-Za-z]*Element|SVGElement|Window)\b/.test(
|
|
||||||
name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function chatStateTypeName(name) {
|
|
||||||
return /\bChatState\b/.test(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findInitializerCallbackReference(root, name) {
|
|
||||||
let reference = null;
|
|
||||||
|
|
||||||
const visit = (node, inCallback) => {
|
|
||||||
if (!node || reference) return;
|
|
||||||
if (Array.isArray(node)) {
|
|
||||||
for (const item of node) visit(item, inCallback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof node !== "object" || typeof node.type !== "string") return;
|
|
||||||
|
|
||||||
if (isFunctionNode(node)) {
|
|
||||||
if (functionShadowsName(node, name)) return;
|
|
||||||
visit(node.body, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inCallback && node.type === "Identifier" && node.name === name) {
|
|
||||||
reference = node;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(node)) {
|
|
||||||
if (key === "parent") continue;
|
|
||||||
if (node.type === "MemberExpression" && key === "property" && !node.computed) continue;
|
|
||||||
if (node.type === "Property" && key === "key" && !node.computed) continue;
|
|
||||||
visit(value, inCallback);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
visit(root, false);
|
|
||||||
return reference;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFunctionNode(node) {
|
|
||||||
return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression";
|
|
||||||
}
|
|
||||||
|
|
||||||
function functionShadowsName(node, name) {
|
|
||||||
return node.params.some((param) => patternContainsName(param, name)) || patternContainsName(node.id, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function patternContainsName(node, name) {
|
|
||||||
if (!node) return false;
|
|
||||||
if (Array.isArray(node)) return node.some((item) => patternContainsName(item, name));
|
|
||||||
if (typeof node !== "object" || typeof node.type !== "string") return false;
|
|
||||||
if (node.type === "Identifier") return node.name === name;
|
|
||||||
return Object.entries(node).some(([key, value]) => key !== "parent" && patternContainsName(value, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
ignores: ["main.js", "node_modules/**", "src/generated/**"],
|
ignores: ["main.js", "node_modules/**", "src/generated/**"],
|
||||||
},
|
},
|
||||||
js.configs.recommended,
|
{
|
||||||
|
basePath: "src",
|
||||||
|
plugins: {
|
||||||
|
obsidianmd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...obsidianmd.configs.recommended.map(obsidianSourceRecommendedConfig).filter(Boolean),
|
||||||
...tseslint.configs.strictTypeChecked.map((config) => ({
|
...tseslint.configs.strictTypeChecked.map((config) => ({
|
||||||
...config,
|
...config,
|
||||||
files: lintedTypeScriptFiles,
|
files: sourceTypeScriptFiles,
|
||||||
})),
|
|
||||||
...tseslint.configs.stylisticTypeChecked.map((config) => ({
|
|
||||||
...config,
|
|
||||||
files: lintedTypeScriptFiles,
|
|
||||||
})),
|
|
||||||
...obsidianmd.configs.recommended.map((config) => ({
|
|
||||||
...config,
|
|
||||||
basePath: "src",
|
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
files: lintedTypeScriptFiles,
|
files: sourceTypeScriptFiles,
|
||||||
plugins: {
|
|
||||||
"react-hooks": reactHooks,
|
|
||||||
},
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2022,
|
ecmaVersion: 2022,
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
projectService: true,
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
projectService: true,
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
AbortSignal: "readonly",
|
AbortSignal: "readonly",
|
||||||
|
|
@ -611,159 +44,72 @@ export default defineConfig([
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...projectTypeScriptRules,
|
// Biome import restrictions do not cover `import("...").T`; keep that type syntax out before source-boundary lint runs.
|
||||||
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
disallowTypeAnnotations: true,
|
||||||
|
fixStyle: "separate-type-imports",
|
||||||
|
prefer: "type-imports",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/require-await": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: nodeJavaScriptFiles,
|
files: ["manifest.json"],
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2022,
|
|
||||||
sourceType: "module",
|
|
||||||
globals: {
|
|
||||||
URL: "readonly",
|
|
||||||
console: "readonly",
|
|
||||||
process: "readonly",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["tests/**/*.{ts,tsx}"],
|
|
||||||
rules: {
|
|
||||||
...testTypeScriptRelaxations,
|
|
||||||
...restrictedSyntaxRule(generatedAppServerThreadImportRestrictions),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["src/**/*.ts", "src/**/*.tsx"],
|
|
||||||
plugins: {
|
plugins: {
|
||||||
"codex-panel": codexPanelEslintPlugin,
|
json,
|
||||||
obsidianmd,
|
obsidianmd,
|
||||||
},
|
},
|
||||||
|
language: "json/json",
|
||||||
rules: {
|
rules: {
|
||||||
"codex-panel/no-self-referential-initializer-callback": "error",
|
"obsidianmd/validate-manifest": "error",
|
||||||
"obsidianmd/ui/sentence-case": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
acronyms: ["MCP"],
|
|
||||||
brands: ["Codex", "Codex Panel", "Obsidian"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["src/**/*.{ts,tsx}"],
|
files: ["LICENSE"],
|
||||||
ignores: ["src/features/chat/**/*.{ts,tsx}", ...nonChatImperativeDomBridgeFiles, ...nonUiEventListenerFiles],
|
plugins: {
|
||||||
|
obsidianmd,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parser: PlainTextParser,
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...restrictedSyntaxRule(sourceSyntaxRestrictions),
|
"obsidianmd/validate-license": "error",
|
||||||
"codex-panel/no-imperative-dom": "error",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
files: ["src/features/chat/**/*.{ts,tsx}"],
|
|
||||||
ignores: ["src/features/chat/panel/shell-state.tsx", ...chatImperativeDomBridgeFiles],
|
|
||||||
rules: {
|
|
||||||
...restrictedSyntaxRule(chatSourceSyntaxRestrictions),
|
|
||||||
"codex-panel/no-imperative-dom": "error",
|
|
||||||
"codex-panel/no-chat-state-direct-mutation": "error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["src/features/chat/panel/shell-state.tsx"],
|
|
||||||
rules: {
|
|
||||||
...restrictedSyntaxRule(baseSourceSyntaxRestrictions),
|
|
||||||
"codex-panel/no-imperative-dom": "error",
|
|
||||||
"codex-panel/no-chat-state-direct-mutation": "error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: chatImperativeDomBridgeFiles,
|
|
||||||
rules: {
|
|
||||||
...restrictedSyntaxRule(chatDomBridgeSyntaxRestrictions),
|
|
||||||
"codex-panel/no-chat-state-direct-mutation": "error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: nonChatImperativeDomBridgeFiles,
|
|
||||||
rules: restrictedSyntaxRule(nonChatDomBridgeSyntaxRestrictions),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: nonUiEventListenerFiles,
|
|
||||||
rules: {
|
|
||||||
...restrictedSyntaxRule(sourceSyntaxRestrictions),
|
|
||||||
"codex-panel/no-imperative-dom": ["error", { allowEvents: true }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: uiRootBridgeFiles,
|
|
||||||
rules: restrictedSyntaxRule(sourceSyntaxRestrictionsWithoutUiRoot),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["src/features/chat/application/state/**/*.{ts,tsx}"],
|
|
||||||
rules: {
|
|
||||||
...restrictedSyntaxRule(pureChatStateSyntaxRestrictions),
|
|
||||||
"codex-panel/no-imperative-dom": "error",
|
|
||||||
"codex-panel/no-chat-state-direct-mutation": "error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["src/features/chat/domain/**/*.{ts,tsx}"],
|
|
||||||
rules: restrictedSyntaxRule([...sourceSyntaxRestrictions, ...chatDomainLayerRestrictions]),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["src/app-server/**/*.{ts,tsx}", "src/domain/**/*.{ts,tsx}", "src/shared/**/*.{ts,tsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: lowerLevelFeatureImportPatterns,
|
|
||||||
message: "Lower-level modules must not import feature modules. Move shared behavior to shared, domain, or app-server.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["src/**/*.{ts,tsx}"],
|
|
||||||
ignores: ["src/app-server/**/*.{ts,tsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: nonAppServerBannedAppServerProtocolImportPatterns,
|
|
||||||
message:
|
|
||||||
"Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: generatedAppServerSourceImportPatterns,
|
|
||||||
message: "Keep generated app-server types behind src/app-server; expose Panel-owned models to feature, UI, and reducer code.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["tests/**/*.{ts,tsx}"],
|
|
||||||
ignores: ["tests/app-server/**/*.{ts,tsx}"],
|
|
||||||
rules: {
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: generatedAppServerTestImportPatterns,
|
|
||||||
message:
|
|
||||||
"Keep generated app-server types behind src/app-server and tests/app-server; feature tests should use Panel-owned models.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
eslintConfigPrettier,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function obsidianSourceRecommendedConfig(config) {
|
||||||
|
const rules = Object.fromEntries(
|
||||||
|
Object.entries(config.rules ?? {})
|
||||||
|
.filter(([ruleName]) => {
|
||||||
|
return (
|
||||||
|
ruleName.startsWith("obsidianmd/") && ruleName !== "obsidianmd/validate-manifest" && ruleName !== "obsidianmd/validate-license"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(([ruleName, ruleConfig]) => {
|
||||||
|
if (ruleName !== "obsidianmd/ui/sentence-case") return [ruleName, ruleConfig];
|
||||||
|
return [
|
||||||
|
ruleName,
|
||||||
|
[
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
enforceCamelCaseLower: true,
|
||||||
|
acronyms: ["MCP"],
|
||||||
|
brands: ["Codex", "Codex Panel", "Obsidian"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (Object.keys(rules).length === 0) return null;
|
||||||
|
const obsidianConfig = {
|
||||||
|
basePath: "src",
|
||||||
|
rules,
|
||||||
|
};
|
||||||
|
if (config.files) obsidianConfig.files = config.files;
|
||||||
|
if (config.ignores) obsidianConfig.ignores = config.ignores;
|
||||||
|
return obsidianConfig;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
"entry": ["src/main.ts", "scripts/**/*.mjs", "*.config.{ts,mjs}", "tests/**/*.test.{ts,tsx}"],
|
"entry": ["src/main.ts", "scripts/**/*.mjs", "*.config.{ts,mjs}", "tests/**/*.test.{ts,tsx}"],
|
||||||
"project": ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}", "scripts/**/*.mjs", "*.config.{ts,mjs}"],
|
"project": ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}", "scripts/**/*.mjs", "*.config.{ts,mjs}"],
|
||||||
"ignore": ["src/generated/**"],
|
"ignore": ["src/generated/**"],
|
||||||
"ignoreBinaries": ["codex"]
|
"ignoreBinaries": ["codex", "taskkill"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "codex-panel",
|
"id": "codex-panel",
|
||||||
"name": "Codex Panel",
|
"name": "Codex Panel",
|
||||||
"version": "3.2.6",
|
"version": "5.2.1",
|
||||||
"minAppVersion": "1.12.0",
|
"minAppVersion": "1.12.0",
|
||||||
"description": "Codex in your sidebar.",
|
"description": "Codex in your sidebar.",
|
||||||
"author": "murashit",
|
"author": "murashit",
|
||||||
|
|
|
||||||
5458
package-lock.json
generated
5458
package-lock.json
generated
File diff suppressed because it is too large
Load diff
86
package.json
86
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "codex-panel",
|
"name": "codex-panel",
|
||||||
"version": "3.2.6",
|
"version": "5.2.1",
|
||||||
"description": "Codex in your Obsidian sidebar.",
|
"description": "Codex in your Obsidian sidebar.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"author": "murashit",
|
"author": "murashit",
|
||||||
|
|
@ -13,57 +13,65 @@
|
||||||
"url": "https://github.com/murashit/codex-panel/issues"
|
"url": "https://github.com/murashit/codex-panel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/murashit/codex-panel#readme",
|
"homepage": "https://github.com/murashit/codex-panel#readme",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=26"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:styles": "node scripts/build-styles.mjs",
|
|
||||||
"build:styles:check": "node scripts/build-styles.mjs --check",
|
|
||||||
"build": "node esbuild.config.mjs",
|
|
||||||
"format": "node scripts/format.mjs",
|
|
||||||
"format:check": "node scripts/format.mjs --check --cache",
|
|
||||||
"format:check:ci": "node scripts/format.mjs --check",
|
|
||||||
"api:baseline": "node scripts/api-baseline.mjs",
|
"api:baseline": "node scripts/api-baseline.mjs",
|
||||||
"api:baseline:check": "node scripts/api-baseline.mjs --check",
|
"build": "node esbuild.config.mjs",
|
||||||
|
"build:styles": "node scripts/build-styles.mjs",
|
||||||
|
"commitlint": "commitlint",
|
||||||
|
"check": "concurrently --group --pad-prefix 'node:check:*' && node --run build",
|
||||||
|
"check:biome": "biome check --diagnostic-level=warn --error-on-warnings",
|
||||||
|
"check:css-usage": "node scripts/check-css-usage.mjs",
|
||||||
|
"check:eslint": "eslint src manifest.json LICENSE --max-warnings=0",
|
||||||
|
"check:test": "node --run test",
|
||||||
|
"check:typecheck": "node --run typecheck",
|
||||||
|
"check:unused": "knip --no-progress",
|
||||||
|
"fix": "biome check --fix --skip=plugin --diagnostic-level=warn --error-on-warnings && knip --fix --no-progress",
|
||||||
"generate:app-server-types": "node scripts/generate-app-server-types.mjs",
|
"generate:app-server-types": "node scripts/generate-app-server-types.mjs",
|
||||||
"lint": "node scripts/run-parallel.mjs lint:ts lint:css",
|
"generate:app-server-types:check": "node scripts/generate-app-server-types.mjs --check",
|
||||||
"lint:css": "stylelint \"src/**/*.css\" --max-warnings=0",
|
|
||||||
"lint:css:usage": "node scripts/check-css-usage.mjs",
|
|
||||||
"lint:css:usage:check": "node scripts/check-css-usage.mjs --fail-on-candidates",
|
|
||||||
"lint:deps": "node scripts/check-import-cycles.mjs",
|
|
||||||
"lint:ts": "eslint src tests scripts \"*.config.ts\" \"*.config.mjs\" --max-warnings=0",
|
|
||||||
"lint:ts:ci": "eslint src tests scripts \"*.config.ts\" \"*.config.mjs\" --max-warnings=0",
|
|
||||||
"release:check": "node scripts/release/check.mjs",
|
"release:check": "node scripts/release/check.mjs",
|
||||||
|
"release:notes": "node scripts/release/notes.mjs",
|
||||||
"release:preflight": "node scripts/release/preflight.mjs",
|
"release:preflight": "node scripts/release/preflight.mjs",
|
||||||
"release:prepare": "node scripts/release/prepare.mjs",
|
"release:prepare": "node scripts/release/prepare.mjs",
|
||||||
"test": "vitest run --cache",
|
"test": "vitest run --pool threads --maxWorkers 6",
|
||||||
"test:ci": "vitest run",
|
"test:coverage": "vitest run --pool threads --maxWorkers 6 --coverage",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit --incremental --tsBuildInfoFile node_modules/.cache/typescript/tsconfig.tsbuildinfo",
|
"test:mutation": "stryker run",
|
||||||
"typecheck:ci": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit --incremental --tsBuildInfoFile node_modules/.cache/typescript/tsconfig.tsbuildinfo"
|
||||||
"unused": "npm run unused:exports",
|
|
||||||
"unused:exports": "knip --no-progress",
|
|
||||||
"check": "node scripts/check.mjs",
|
|
||||||
"check:ci": "node scripts/check.mjs --ci"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@biomejs/biome": "^2.5.3",
|
||||||
"@types/node": "^25.6.0",
|
"@commitlint/cli": "^21.2.1",
|
||||||
|
"@commitlint/config-conventional": "^21.2.0",
|
||||||
|
"@stryker-mutator/core": "^9.6.1",
|
||||||
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
||||||
|
"@types/mdast": "^4.0.4",
|
||||||
|
"@types/node": "^26.1.1",
|
||||||
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
|
"concurrently": "^9.2.4",
|
||||||
|
"conventional-changelog-conventionalcommits": "^10.2.1",
|
||||||
|
"conventional-commits-parser": "^7.1.0",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^10.4.1",
|
"eslint": "^10.6.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-plugin-obsidianmd": "^0.4.1",
|
||||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
"knip": "^6.16.1",
|
"knip": "^6.25.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"obsidian": "~1.12.3",
|
"obsidian": "~1.12.3",
|
||||||
"prettier": "^3.8.3",
|
|
||||||
"stylelint": "^17.12.0",
|
|
||||||
"stylelint-config-standard": "^40.0.0",
|
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.60.1",
|
"typescript-eslint": "^8.64.0",
|
||||||
"vitest": "^4.1.8"
|
"vitest": "^4.1.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@preact/signals": "^2.9.1",
|
"@tanstack/query-core": "^5.101.2",
|
||||||
"@tanstack/query-core": "^5.101.0",
|
"defuddle": "^0.19.1",
|
||||||
"@tanstack/virtual-core": "3.17.0",
|
"diff": "^9.0.0",
|
||||||
"preact": "^10.29.2"
|
"mdast-util-from-markdown": "^2.0.3",
|
||||||
|
"mdast-util-to-markdown": "^2.1.2",
|
||||||
|
"micromark": "^4.0.2",
|
||||||
|
"obsidian-daily-notes-interface": "^0.9.5",
|
||||||
|
"preact": "^10.29.7",
|
||||||
|
"unist-util-visit": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,28 @@
|
||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { readJson } from "./release/utils.mjs";
|
import { readFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const args = new Set(process.argv.slice(2));
|
const validArgs = new Set(["--json"]);
|
||||||
const shouldCheck = args.has("--check");
|
|
||||||
const asJson = args.has("--json");
|
|
||||||
|
|
||||||
function fail(message) {
|
if (isMain()) {
|
||||||
failures.push(message);
|
const args = new Set(process.argv.slice(2));
|
||||||
}
|
const asJson = args.has("--json");
|
||||||
|
for (const arg of args) {
|
||||||
|
if (!validArgs.has(arg)) {
|
||||||
|
console.error("Usage: node scripts/api-baseline.mjs [--json]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function warn(message) {
|
const report = await createApiBaselineReport();
|
||||||
warnings.push(message);
|
if (asJson) {
|
||||||
|
console.log(JSON.stringify(report, null, 2));
|
||||||
|
} else {
|
||||||
|
printReport(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.failures.length > 0) process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSemver(value) {
|
function parseSemver(value) {
|
||||||
|
|
@ -39,12 +50,139 @@ function readCodexVersion() {
|
||||||
return parseSemver(`${result.stdout}\n${result.stderr}`)?.version ?? null;
|
return parseSemver(`${result.stdout}\n${result.stderr}`)?.version ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createApiBaselineReport(options = {}) {
|
||||||
|
const failures = [];
|
||||||
|
const fail = (message) => {
|
||||||
|
failures.push(message);
|
||||||
|
};
|
||||||
|
const inputs = await readBaselineInputs(options.cwd ?? process.cwd());
|
||||||
|
const readmeBaselines = readCompatibilityBaselines(inputs.readme);
|
||||||
|
|
||||||
|
const codexReadmeVersion = readmeBaselines.codexTestedCliVersion;
|
||||||
|
const codexRecordedVersion = inputs.appServerCompatibilityJson.codexAppServer?.testedCliVersion ?? null;
|
||||||
|
const codexLocalVersion = options.readCodexVersion ? await options.readCodexVersion() : readCodexVersion();
|
||||||
|
const codexReadmeSemver = parseSemver(codexReadmeVersion);
|
||||||
|
const codexRecordedSemver = parseSemver(codexRecordedVersion);
|
||||||
|
const codexLocalSemver = parseSemver(codexLocalVersion);
|
||||||
|
|
||||||
|
const obsidianMinVersion = inputs.manifestJson.minAppVersion;
|
||||||
|
const obsidianReadmeApiTypesVersion = readmeBaselines.obsidianApiTypesVersion;
|
||||||
|
const obsidianReadmeMinVersion = readmeBaselines.obsidianMinAppVersion;
|
||||||
|
const obsidianVersionEntry = inputs.versionsJson[inputs.packageJson.version] ?? null;
|
||||||
|
const obsidianSpec = inputs.packageJson.devDependencies?.obsidian ?? null;
|
||||||
|
const obsidianLockVersion = inputs.packageLockJson.packages?.["node_modules/obsidian"]?.version ?? null;
|
||||||
|
const obsidianSpecSemver = parseSemver(obsidianSpec);
|
||||||
|
const obsidianLockSemver = parseSemver(obsidianLockVersion);
|
||||||
|
const obsidianMinSemver = parseSemver(obsidianMinVersion);
|
||||||
|
|
||||||
|
const generationArguments = inputs.appServerCompatibilityJson.codexAppServer?.typeGeneration?.arguments ?? null;
|
||||||
|
const expectedGenerationArguments = ["app-server", "generate-ts", "--experimental"];
|
||||||
|
const appServerGenerationArgumentsDeclared =
|
||||||
|
Array.isArray(generationArguments) && generationArguments.every((argument) => typeof argument === "string");
|
||||||
|
const appServerGenerationArgumentsSupported =
|
||||||
|
appServerGenerationArgumentsDeclared && arraysEqual(generationArguments, expectedGenerationArguments);
|
||||||
|
const initializeCapabilities = inputs.appServerCompatibilityJson.codexAppServer?.initialize?.capabilities ?? {};
|
||||||
|
const initializeExperimentalApi = initializeCapabilities.experimentalApi === true;
|
||||||
|
const initializeRequestAttestationDisabled = initializeCapabilities.requestAttestation === false;
|
||||||
|
|
||||||
|
if (!codexReadmeSemver) {
|
||||||
|
fail("README.md Compatibility table must define `codexAppServer.testedCliVersion` as X.Y.Z.");
|
||||||
|
}
|
||||||
|
if (!codexRecordedSemver || codexRecordedSemver.version !== codexRecordedVersion) {
|
||||||
|
fail("src/app-server/connection/compatibility.json must declare codexAppServer.testedCliVersion as X.Y.Z.");
|
||||||
|
}
|
||||||
|
if (!codexLocalSemver) {
|
||||||
|
fail("local codex --version could not be read.");
|
||||||
|
}
|
||||||
|
if (codexReadmeVersion && codexRecordedVersion && codexReadmeVersion !== codexRecordedVersion) {
|
||||||
|
fail(`README Codex CLI ${codexReadmeVersion} does not match recorded tested CLI ${codexRecordedVersion}.`);
|
||||||
|
}
|
||||||
|
if (codexLocalSemver && codexRecordedSemver && codexLocalSemver.version !== codexRecordedSemver.version) {
|
||||||
|
fail(`local Codex CLI ${codexLocalSemver.version} does not match recorded tested CLI ${codexRecordedSemver.version}.`);
|
||||||
|
}
|
||||||
|
if (!appServerGenerationArgumentsDeclared) {
|
||||||
|
fail("src/app-server/connection/compatibility.json must declare codexAppServer.typeGeneration.arguments as a string array.");
|
||||||
|
} else if (!appServerGenerationArgumentsSupported) {
|
||||||
|
fail(`app-server type generation arguments must be ${expectedGenerationArguments.join(" ")}.`);
|
||||||
|
}
|
||||||
|
if (!initializeExperimentalApi) {
|
||||||
|
fail("src/app-server/connection/compatibility.json must declare codexAppServer.initialize.capabilities.experimentalApi: true.");
|
||||||
|
}
|
||||||
|
if (!initializeRequestAttestationDisabled) {
|
||||||
|
fail("src/app-server/connection/compatibility.json must declare codexAppServer.initialize.capabilities.requestAttestation: false.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!obsidianMinSemver) fail("manifest.json minAppVersion must be X.Y.Z.");
|
||||||
|
if (obsidianMinSemver && obsidianMinSemver.patch !== 0) {
|
||||||
|
fail(`manifest.json minAppVersion should be the minor baseline patch 0, got ${obsidianMinVersion}.`);
|
||||||
|
}
|
||||||
|
if (!parseSemver(obsidianReadmeMinVersion)) {
|
||||||
|
fail("README.md Compatibility table must define `manifest.minAppVersion` as X.Y.Z.");
|
||||||
|
} else if (obsidianReadmeMinVersion !== obsidianMinVersion) {
|
||||||
|
fail(`README Obsidian baseline ${displayValue(obsidianReadmeMinVersion)} does not match manifest ${obsidianMinVersion}.`);
|
||||||
|
}
|
||||||
|
if (obsidianVersionEntry !== obsidianMinVersion) {
|
||||||
|
fail(`versions.json must map ${inputs.packageJson.version} to manifest minAppVersion ${obsidianMinVersion}.`);
|
||||||
|
}
|
||||||
|
if (!obsidianSpecSemver) fail("package.json devDependency obsidian must include an X.Y.Z version.");
|
||||||
|
if (!obsidianLockSemver) fail("package-lock.json must lock node_modules/obsidian to X.Y.Z.");
|
||||||
|
if (packageRangeKind(obsidianSpec) !== "patch") {
|
||||||
|
fail(`package.json devDependency obsidian should use a patch-only '~' range, got ${displayValue(obsidianSpec)}.`);
|
||||||
|
}
|
||||||
|
if (obsidianMinSemver && obsidianSpecSemver && minorKey(obsidianSpecSemver) !== minorKey(obsidianMinSemver)) {
|
||||||
|
fail(`obsidian devDependency minor ${minorKey(obsidianSpecSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
|
||||||
|
}
|
||||||
|
if (obsidianMinSemver && obsidianLockSemver && minorKey(obsidianLockSemver) !== minorKey(obsidianMinSemver)) {
|
||||||
|
fail(`locked obsidian minor ${minorKey(obsidianLockSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
|
||||||
|
}
|
||||||
|
if (!parseSemver(obsidianReadmeApiTypesVersion)) {
|
||||||
|
fail("README.md Compatibility table must define `obsidian` API types as X.Y.Z.");
|
||||||
|
} else if (obsidianReadmeApiTypesVersion !== obsidianLockVersion) {
|
||||||
|
fail(
|
||||||
|
`README Obsidian API types ${displayValue(obsidianReadmeApiTypesVersion)} does not match package-lock obsidian ${displayValue(obsidianLockVersion)}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
codex: {
|
||||||
|
policy: "compatibility is managed by minor; generated bindings are proven against an exact CLI patch",
|
||||||
|
recordedTestedCliVersion: codexRecordedVersion,
|
||||||
|
readmeTestedCliVersion: codexReadmeVersion,
|
||||||
|
readmeTestedMinor: minorKey(codexReadmeSemver),
|
||||||
|
localCliVersion: codexLocalVersion,
|
||||||
|
localCliMinor: minorKey(codexLocalSemver),
|
||||||
|
readmeMatchesRecordedVersion: codexReadmeVersion && codexRecordedVersion ? codexReadmeVersion === codexRecordedVersion : false,
|
||||||
|
localCliMatchesRecordedVersion:
|
||||||
|
codexLocalSemver && codexRecordedSemver ? codexLocalSemver.version === codexRecordedSemver.version : null,
|
||||||
|
generationArguments,
|
||||||
|
appServerGenerationArgumentsSupported,
|
||||||
|
initializeExperimentalApi,
|
||||||
|
initializeRequestAttestationDisabled,
|
||||||
|
},
|
||||||
|
obsidian: {
|
||||||
|
policy: "manifest declares the minimum app version; obsidian npm provides compile-time API types in the same minor",
|
||||||
|
minAppVersion: obsidianMinVersion,
|
||||||
|
minAppVersionMinor: minorKey(obsidianMinSemver),
|
||||||
|
readmeMinAppVersion: obsidianReadmeMinVersion,
|
||||||
|
versionsJsonCurrentMinAppVersion: obsidianVersionEntry,
|
||||||
|
readmeApiTypesVersion: obsidianReadmeApiTypesVersion,
|
||||||
|
packageDependency: obsidianSpec,
|
||||||
|
packageDependencyRange: packageRangeKind(obsidianSpec),
|
||||||
|
packageDependencyMinor: minorKey(obsidianSpecSemver),
|
||||||
|
lockedPackageVersion: obsidianLockVersion,
|
||||||
|
lockedPackageMinor: minorKey(obsidianLockSemver),
|
||||||
|
},
|
||||||
|
failures,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function readCompatibilityBaselines(readme) {
|
function readCompatibilityBaselines(readme) {
|
||||||
const section = markdownSection(readme, "Compatibility");
|
const section = markdownSection(readme, "Compatibility");
|
||||||
const table = readMarkdownTableValues(section);
|
const table = readMarkdownTableValues(section);
|
||||||
return {
|
return {
|
||||||
codexTestedCliVersion: table.get("codex.testedCliVersion") ?? null,
|
codexTestedCliVersion: table.get("codexAppServer.testedCliVersion") ?? null,
|
||||||
obsidianMinAppVersion: table.get("obsidian.minAppVersion") ?? null,
|
obsidianApiTypesVersion: table.get("obsidian") ?? null,
|
||||||
|
obsidianMinAppVersion: table.get("manifest.minAppVersion") ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +202,7 @@ function readMarkdownTableValues(markdown) {
|
||||||
const header = splitMarkdownTableRow(lines[index]);
|
const header = splitMarkdownTableRow(lines[index]);
|
||||||
if (!header) continue;
|
if (!header) continue;
|
||||||
const separator = splitMarkdownTableRow(lines[index + 1]);
|
const separator = splitMarkdownTableRow(lines[index + 1]);
|
||||||
if (!separator || !separator.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
if (!separator?.every((cell) => /^:?-{3,}:?$/.test(cell))) continue;
|
||||||
|
|
||||||
const keyColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "key");
|
const keyColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "key");
|
||||||
const versionColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "version");
|
const versionColumn = header.findIndex((cell) => normalizeTableHeader(cell) === "version");
|
||||||
|
|
@ -109,118 +247,49 @@ function displayValue(value) {
|
||||||
return value ?? "(missing)";
|
return value ?? "(missing)";
|
||||||
}
|
}
|
||||||
|
|
||||||
const failures = [];
|
function arraysEqual(left, right) {
|
||||||
const warnings = [];
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
||||||
|
|
||||||
const packageJson = await readJson("package.json");
|
|
||||||
const packageLockJson = await readJson("package-lock.json");
|
|
||||||
const manifestJson = await readJson("manifest.json");
|
|
||||||
const versionsJson = await readJson("versions.json");
|
|
||||||
const readme = await readFile("README.md", "utf8");
|
|
||||||
const clientSource = await readFile("src/app-server/connection/client.ts", "utf8");
|
|
||||||
const appServerGenerateSource = await readFile("scripts/generate-app-server-types.mjs", "utf8");
|
|
||||||
const readmeBaselines = readCompatibilityBaselines(readme);
|
|
||||||
|
|
||||||
const codexReadmeVersion = readmeBaselines.codexTestedCliVersion;
|
|
||||||
const codexLocalVersion = readCodexVersion();
|
|
||||||
const codexReadmeSemver = parseSemver(codexReadmeVersion);
|
|
||||||
const codexLocalSemver = parseSemver(codexLocalVersion);
|
|
||||||
|
|
||||||
const obsidianMinVersion = manifestJson.minAppVersion;
|
|
||||||
const obsidianReadmeMinVersion = readmeBaselines.obsidianMinAppVersion;
|
|
||||||
const obsidianVersionEntry = versionsJson[packageJson.version] ?? null;
|
|
||||||
const obsidianSpec = packageJson.devDependencies?.obsidian ?? null;
|
|
||||||
const obsidianLockVersion = packageLockJson.packages?.["node_modules/obsidian"]?.version ?? null;
|
|
||||||
const obsidianSpecSemver = parseSemver(obsidianSpec);
|
|
||||||
const obsidianLockSemver = parseSemver(obsidianLockVersion);
|
|
||||||
const obsidianMinSemver = parseSemver(obsidianMinVersion);
|
|
||||||
|
|
||||||
const appServerGenerationExperimental =
|
|
||||||
appServerGenerateSource.includes("app-server") &&
|
|
||||||
appServerGenerateSource.includes("generate-ts") &&
|
|
||||||
appServerGenerateSource.includes("--experimental");
|
|
||||||
const initializeExperimentalApi = /experimentalApi:\s*true/.test(clientSource);
|
|
||||||
const initializeRequestAttestationDisabled = /requestAttestation:\s*false/.test(clientSource);
|
|
||||||
|
|
||||||
if (!codexReadmeSemver) {
|
|
||||||
fail("README.md Compatibility table must define `codex.testedCliVersion` as X.Y.Z.");
|
|
||||||
}
|
|
||||||
if (!codexLocalSemver) {
|
|
||||||
const message = "local codex --version could not be read.";
|
|
||||||
if (shouldCheck) fail(message);
|
|
||||||
else warn(message);
|
|
||||||
}
|
|
||||||
if (codexReadmeSemver && codexLocalSemver && minorKey(codexReadmeSemver) !== minorKey(codexLocalSemver)) {
|
|
||||||
fail(`local Codex CLI minor ${minorKey(codexLocalSemver)} does not match compatibility table minor ${minorKey(codexReadmeSemver)}.`);
|
|
||||||
}
|
|
||||||
if (!appServerGenerationExperimental) fail("generate:app-server-types must use codex app-server generate-ts --experimental.");
|
|
||||||
if (!initializeExperimentalApi) fail("app-server initialize must declare experimentalApi: true.");
|
|
||||||
if (!initializeRequestAttestationDisabled) fail("app-server initialize must declare requestAttestation: false.");
|
|
||||||
|
|
||||||
if (!obsidianMinSemver) fail("manifest.json minAppVersion must be X.Y.Z.");
|
|
||||||
if (obsidianMinSemver && obsidianMinSemver.patch !== 0) {
|
|
||||||
fail(`manifest.json minAppVersion should be the minor baseline patch 0, got ${obsidianMinVersion}.`);
|
|
||||||
}
|
|
||||||
if (!parseSemver(obsidianReadmeMinVersion)) {
|
|
||||||
fail("README.md Compatibility table must define `obsidian.minAppVersion` as X.Y.Z.");
|
|
||||||
} else if (obsidianReadmeMinVersion !== obsidianMinVersion) {
|
|
||||||
fail(`README Obsidian baseline ${displayValue(obsidianReadmeMinVersion)} does not match manifest ${obsidianMinVersion}.`);
|
|
||||||
}
|
|
||||||
if (obsidianVersionEntry !== obsidianMinVersion) {
|
|
||||||
fail(`versions.json must map ${packageJson.version} to manifest minAppVersion ${obsidianMinVersion}.`);
|
|
||||||
}
|
|
||||||
if (!obsidianSpecSemver) fail("package.json devDependency obsidian must include an X.Y.Z version.");
|
|
||||||
if (!obsidianLockSemver) fail("package-lock.json must lock node_modules/obsidian to X.Y.Z.");
|
|
||||||
if (packageRangeKind(obsidianSpec) !== "patch") {
|
|
||||||
fail(`package.json devDependency obsidian should use a patch-only '~' range, got ${displayValue(obsidianSpec)}.`);
|
|
||||||
}
|
|
||||||
if (obsidianMinSemver && obsidianSpecSemver && minorKey(obsidianSpecSemver) !== minorKey(obsidianMinSemver)) {
|
|
||||||
fail(`obsidian devDependency minor ${minorKey(obsidianSpecSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
|
|
||||||
}
|
|
||||||
if (obsidianMinSemver && obsidianLockSemver && minorKey(obsidianLockSemver) !== minorKey(obsidianMinSemver)) {
|
|
||||||
fail(`locked obsidian minor ${minorKey(obsidianLockSemver)} does not match minAppVersion minor ${minorKey(obsidianMinSemver)}.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const report = {
|
async function readBaselineInputs(cwd) {
|
||||||
codex: {
|
const [packageJson, packageLockJson, manifestJson, versionsJson, readme, appServerCompatibilityJson] = await Promise.all([
|
||||||
policy: "managed by minor version",
|
readJson(cwd, "package.json"),
|
||||||
readmeTestedCliVersion: codexReadmeVersion,
|
readJson(cwd, "package-lock.json"),
|
||||||
readmeTestedMinor: minorKey(codexReadmeSemver),
|
readJson(cwd, "manifest.json"),
|
||||||
localCliVersion: codexLocalVersion,
|
readJson(cwd, "versions.json"),
|
||||||
localCliMinor: minorKey(codexLocalSemver),
|
readFile(path.join(cwd, "README.md"), "utf8"),
|
||||||
localCliMatchesTestedMinor: codexReadmeSemver && codexLocalSemver ? minorKey(codexReadmeSemver) === minorKey(codexLocalSemver) : null,
|
readJson(cwd, "src/app-server/connection/compatibility.json"),
|
||||||
appServerGenerationExperimental,
|
]);
|
||||||
initializeExperimentalApi,
|
|
||||||
initializeRequestAttestationDisabled,
|
|
||||||
},
|
|
||||||
obsidian: {
|
|
||||||
policy: "track latest patch within the guaranteed minor; raise minAppVersion when the minor changes",
|
|
||||||
minAppVersion: obsidianMinVersion,
|
|
||||||
minAppVersionMinor: minorKey(obsidianMinSemver),
|
|
||||||
readmeMinAppVersion: obsidianReadmeMinVersion,
|
|
||||||
versionsJsonCurrentMinAppVersion: obsidianVersionEntry,
|
|
||||||
packageDependency: obsidianSpec,
|
|
||||||
packageDependencyRange: packageRangeKind(obsidianSpec),
|
|
||||||
packageDependencyMinor: minorKey(obsidianSpecSemver),
|
|
||||||
lockedPackageVersion: obsidianLockVersion,
|
|
||||||
lockedPackageMinor: minorKey(obsidianLockSemver),
|
|
||||||
},
|
|
||||||
warnings,
|
|
||||||
failures,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (asJson) {
|
return {
|
||||||
console.log(JSON.stringify(report, null, 2));
|
appServerCompatibilityJson,
|
||||||
} else {
|
manifestJson,
|
||||||
|
packageJson,
|
||||||
|
packageLockJson,
|
||||||
|
readme,
|
||||||
|
versionsJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJson(cwd, file) {
|
||||||
|
return JSON.parse(await readFile(path.join(cwd, file), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMain() {
|
||||||
|
return process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReport(report) {
|
||||||
console.log("API baseline");
|
console.log("API baseline");
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Codex app-server");
|
console.log("Codex app-server");
|
||||||
console.log(` policy: ${report.codex.policy}`);
|
console.log(` policy: ${report.codex.policy}`);
|
||||||
console.log(` compatibility table CLI: ${displayValue(report.codex.readmeTestedCliVersion)}`);
|
console.log(` compatibility table CLI: ${displayValue(report.codex.readmeTestedCliVersion)}`);
|
||||||
|
console.log(` recorded tested CLI: ${displayValue(report.codex.recordedTestedCliVersion)}`);
|
||||||
console.log(` compatibility table minor: ${displayValue(report.codex.readmeTestedMinor)}`);
|
console.log(` compatibility table minor: ${displayValue(report.codex.readmeTestedMinor)}`);
|
||||||
console.log(` local codex CLI: ${displayValue(report.codex.localCliVersion)}`);
|
console.log(` local codex CLI: ${displayValue(report.codex.localCliVersion)}`);
|
||||||
console.log(` local codex minor: ${displayValue(report.codex.localCliMinor)}`);
|
console.log(` local codex minor: ${displayValue(report.codex.localCliMinor)}`);
|
||||||
console.log(` generate-ts --experimental: ${report.codex.appServerGenerationExperimental ? "yes" : "no"}`);
|
console.log(` generation arguments: ${report.codex.generationArguments?.join(" ") ?? "(missing)"}`);
|
||||||
console.log(` initialize experimentalApi: ${report.codex.initializeExperimentalApi ? "yes" : "no"}`);
|
console.log(` initialize experimentalApi: ${report.codex.initializeExperimentalApi ? "yes" : "no"}`);
|
||||||
console.log(` initialize requestAttestation disabled: ${report.codex.initializeRequestAttestationDisabled ? "yes" : "no"}`);
|
console.log(` initialize requestAttestation disabled: ${report.codex.initializeRequestAttestationDisabled ? "yes" : "no"}`);
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|
@ -229,19 +298,13 @@ if (asJson) {
|
||||||
console.log(` manifest minAppVersion: ${displayValue(report.obsidian.minAppVersion)}`);
|
console.log(` manifest minAppVersion: ${displayValue(report.obsidian.minAppVersion)}`);
|
||||||
console.log(` compatibility table minAppVersion: ${displayValue(report.obsidian.readmeMinAppVersion)}`);
|
console.log(` compatibility table minAppVersion: ${displayValue(report.obsidian.readmeMinAppVersion)}`);
|
||||||
console.log(` versions.json current minAppVersion: ${displayValue(report.obsidian.versionsJsonCurrentMinAppVersion)}`);
|
console.log(` versions.json current minAppVersion: ${displayValue(report.obsidian.versionsJsonCurrentMinAppVersion)}`);
|
||||||
|
console.log(` compatibility table API types: ${displayValue(report.obsidian.readmeApiTypesVersion)}`);
|
||||||
console.log(` package obsidian: ${displayValue(report.obsidian.packageDependency)}`);
|
console.log(` package obsidian: ${displayValue(report.obsidian.packageDependency)}`);
|
||||||
console.log(` package range: ${report.obsidian.packageDependencyRange}`);
|
console.log(` package range: ${report.obsidian.packageDependencyRange}`);
|
||||||
console.log(` package-lock obsidian: ${displayValue(report.obsidian.lockedPackageVersion)}`);
|
console.log(` package-lock obsidian: ${displayValue(report.obsidian.lockedPackageVersion)}`);
|
||||||
if (warnings.length > 0) {
|
if (report.failures.length > 0) {
|
||||||
console.log("");
|
|
||||||
console.log("Warnings");
|
|
||||||
for (const message of warnings) console.log(` - ${message}`);
|
|
||||||
}
|
|
||||||
if (failures.length > 0) {
|
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("Failures");
|
console.log("Failures");
|
||||||
for (const message of failures) console.log(` - ${message}`);
|
for (const message of report.failures) console.log(` - ${message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldCheck && failures.length > 0) process.exit(1);
|
|
||||||
|
|
|
||||||
49
scripts/app-server-compatibility.mjs
Normal file
49
scripts/app-server-compatibility.mjs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const compatibilityRelativePath = "src/app-server/connection/compatibility.json";
|
||||||
|
const exactSemverPattern = /^\d+\.\d+\.\d+$/;
|
||||||
|
|
||||||
|
if (isMain()) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.length !== 1 || args[0] !== "--tested-cli-version") {
|
||||||
|
console.error("Usage: node scripts/app-server-compatibility.mjs --tested-cli-version");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const policy = await readAppServerGenerationPolicy(process.cwd());
|
||||||
|
console.log(policy.testedCliVersion);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readAppServerGenerationPolicy(cwd) {
|
||||||
|
const compatibilityPath = path.join(cwd, compatibilityRelativePath);
|
||||||
|
const compatibility = JSON.parse(await readFile(compatibilityPath, "utf8"));
|
||||||
|
const testedCliVersion = compatibility.codexAppServer?.testedCliVersion;
|
||||||
|
const generationArguments = compatibility.codexAppServer?.typeGeneration?.arguments;
|
||||||
|
|
||||||
|
if (typeof testedCliVersion !== "string" || !exactSemverPattern.test(testedCliVersion)) {
|
||||||
|
throw new Error(`${compatibilityRelativePath} must declare codexAppServer.testedCliVersion as X.Y.Z.`);
|
||||||
|
}
|
||||||
|
if (!isStringArray(generationArguments) || generationArguments.length === 0) {
|
||||||
|
throw new Error(`${compatibilityRelativePath} must declare codexAppServer.typeGeneration.arguments as a non-empty string array.`);
|
||||||
|
}
|
||||||
|
if (generationArguments.includes("--out")) {
|
||||||
|
throw new Error(`${compatibilityRelativePath} must not include --out in codexAppServer.typeGeneration.arguments.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { generationArguments, testedCliVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringArray(value) {
|
||||||
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMain() {
|
||||||
|
return process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
|
||||||
|
}
|
||||||
|
|
@ -5,35 +5,23 @@ import { fileURLToPath } from "node:url";
|
||||||
const sourceDir = path.join("src", "styles");
|
const sourceDir = path.join("src", "styles");
|
||||||
const orderPath = path.join(sourceDir, "order.json");
|
const orderPath = path.join(sourceDir, "order.json");
|
||||||
const outputPath = "styles.css";
|
const outputPath = "styles.css";
|
||||||
const checkMode = process.argv.includes("--check");
|
|
||||||
const validArgs = new Set(["--check"]);
|
|
||||||
|
|
||||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
for (const arg of process.argv.slice(2)) {
|
if (process.argv.length > 2) {
|
||||||
if (!validArgs.has(arg)) {
|
console.error("Usage: node scripts/build-styles.mjs");
|
||||||
console.error("Usage: node scripts/build-styles.mjs [--check]");
|
process.exit(1);
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkMode) {
|
|
||||||
await checkStyles();
|
|
||||||
} else {
|
|
||||||
await buildStyles();
|
|
||||||
}
|
}
|
||||||
|
await buildStyles();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildStyles() {
|
export async function buildStyles() {
|
||||||
|
await checkStyleOrder();
|
||||||
await mkdir(path.dirname(outputPath), { recursive: true });
|
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||||
await writeFile(outputPath, await renderStyles());
|
await writeFile(outputPath, await renderStyles());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkStyles() {
|
|
||||||
await checkStyleOrder();
|
|
||||||
await renderStyles();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renderStyles() {
|
export async function renderStyles() {
|
||||||
|
const { version } = JSON.parse(await readFile("package.json", "utf8"));
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
|
||||||
const sourceFiles = await readStyleOrder();
|
const sourceFiles = await readStyleOrder();
|
||||||
|
|
@ -42,7 +30,7 @@ export async function renderStyles() {
|
||||||
parts.push(content.trimEnd());
|
parts.push(content.trimEnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${parts.join("\n\n")}\n`;
|
return `/* Codex Panel v${version} */\n\n${parts.join("\n\n")}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readStyleOrder() {
|
async function readStyleOrder() {
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,9 @@ import path from "node:path";
|
||||||
|
|
||||||
const root = process.cwd();
|
const root = process.cwd();
|
||||||
const stylesDir = path.join(root, "src", "styles");
|
const stylesDir = path.join(root, "src", "styles");
|
||||||
const args = new Set(process.argv.slice(2));
|
if (process.argv.length > 2) {
|
||||||
const failOnCandidates = args.has("--fail-on-candidates");
|
console.error("Usage: node scripts/check-css-usage.mjs");
|
||||||
const validArgs = new Set(["--fail-on-candidates"]);
|
process.exit(1);
|
||||||
|
|
||||||
for (const arg of args) {
|
|
||||||
if (!validArgs.has(arg)) {
|
|
||||||
console.error("Usage: node scripts/check-css-usage.mjs [--fail-on-candidates]");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssFiles = await orderedCssFiles();
|
const cssFiles = await orderedCssFiles();
|
||||||
|
|
@ -21,28 +15,20 @@ const sourceFiles = await filesInTree(path.join(root, "src"), new Set([".ts", ".
|
||||||
const testFiles = await filesInTree(path.join(root, "tests"), new Set([".ts", ".tsx"]));
|
const testFiles = await filesInTree(path.join(root, "tests"), new Set([".ts", ".tsx"]));
|
||||||
|
|
||||||
const cssClasses = await collectCssClasses(cssFiles);
|
const cssClasses = await collectCssClasses(cssFiles);
|
||||||
|
const cssCustomProperties = await collectCssCustomProperties(cssFiles);
|
||||||
|
const referencedCssCustomProperties = await collectReferencedCssCustomProperties(cssFiles);
|
||||||
const sourceTexts = await readTexts(sourceFiles);
|
const sourceTexts = await readTexts(sourceFiles);
|
||||||
const testTexts = await readTexts(testFiles);
|
const testTexts = await readTexts(testFiles);
|
||||||
const sourceText = sourceTexts.map((item) => item.text).join("\n");
|
const sourceText = sourceTexts.map((item) => item.text).join("\n");
|
||||||
const dynamicPrefixes = collectDynamicClassPrefixes(sourceText);
|
const dynamicPrefixes = collectDynamicClassPrefixes(sourceText);
|
||||||
|
|
||||||
const exactSourceUsed = [];
|
|
||||||
const dynamicSourceUsed = [];
|
|
||||||
const testOnlyCandidates = [];
|
const testOnlyCandidates = [];
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
|
const unusedCustomProperties = [];
|
||||||
|
|
||||||
for (const [className, locations] of cssClasses) {
|
for (const [className, locations] of cssClasses) {
|
||||||
const sourceMatches = locationsInTexts(sourceTexts, className);
|
const sourceMatches = locationsInTexts(sourceTexts, className);
|
||||||
if (sourceMatches.length > 0) {
|
if (sourceMatches.length > 0) continue;
|
||||||
exactSourceUsed.push({ className, locations, sourceMatches });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingPrefix = dynamicPrefixes.find((prefix) => className.startsWith(prefix));
|
|
||||||
if (matchingPrefix) {
|
|
||||||
dynamicSourceUsed.push({ className, locations, matchingPrefix });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testMatches = locationsInTexts(testTexts, className);
|
const testMatches = locationsInTexts(testTexts, className);
|
||||||
if (testMatches.length > 0) {
|
if (testMatches.length > 0) {
|
||||||
|
|
@ -53,16 +39,15 @@ for (const [className, locations] of cssClasses) {
|
||||||
candidates.push({ className, locations });
|
candidates.push({ className, locations });
|
||||||
}
|
}
|
||||||
|
|
||||||
printReport({
|
for (const [propertyName, locations] of cssCustomProperties) {
|
||||||
total: cssClasses.size,
|
if (referencedCssCustomProperties.has(propertyName) || locationsInTexts(sourceTexts, propertyName).length > 0) continue;
|
||||||
exactSourceUsed,
|
unusedCustomProperties.push({ propertyName, locations });
|
||||||
dynamicSourceUsed,
|
}
|
||||||
testOnlyCandidates,
|
|
||||||
candidates,
|
|
||||||
dynamicPrefixes,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (failOnCandidates && candidates.length + testOnlyCandidates.length > 0) process.exit(1);
|
if (candidates.length + testOnlyCandidates.length + unusedCustomProperties.length + dynamicPrefixes.length > 0) {
|
||||||
|
printCandidates({ testOnlyCandidates, candidates, unusedCustomProperties, dynamicPrefixes });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
async function orderedCssFiles() {
|
async function orderedCssFiles() {
|
||||||
const orderPath = path.join(stylesDir, "order.json");
|
const orderPath = path.join(stylesDir, "order.json");
|
||||||
|
|
@ -76,12 +61,11 @@ async function orderedCssFiles() {
|
||||||
async function collectCssClasses(files) {
|
async function collectCssClasses(files) {
|
||||||
const result = new Map();
|
const result = new Map();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const text = await readFile(file, "utf8");
|
const text = stripCssComments(await readFile(file, "utf8"));
|
||||||
const lines = text.split("\n");
|
const lines = text.split("\n");
|
||||||
for (const [index, line] of lines.entries()) {
|
for (const [index, line] of lines.entries()) {
|
||||||
const withoutComment = line.replace(/\/\*.*?\*\//g, "");
|
|
||||||
const classPattern = /(^|[^\\])\.([_a-zA-Z][\w-]*)/g;
|
const classPattern = /(^|[^\\])\.([_a-zA-Z][\w-]*)/g;
|
||||||
for (const match of withoutComment.matchAll(classPattern)) {
|
for (const match of line.matchAll(classPattern)) {
|
||||||
const className = match[2];
|
const className = match[2];
|
||||||
if (!className.startsWith("codex-panel")) continue;
|
if (!className.startsWith("codex-panel")) continue;
|
||||||
const locations = result.get(className) ?? [];
|
const locations = result.get(className) ?? [];
|
||||||
|
|
@ -93,6 +77,38 @@ async function collectCssClasses(files) {
|
||||||
return new Map([...result.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
return new Map([...result.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function collectCssCustomProperties(files) {
|
||||||
|
const result = new Map();
|
||||||
|
for (const file of files) {
|
||||||
|
const text = stripCssComments(await readFile(file, "utf8"));
|
||||||
|
const lines = text.split("\n");
|
||||||
|
for (const [index, line] of lines.entries()) {
|
||||||
|
for (const match of line.matchAll(/(--codex-panel-[\w-]+)\s*:/g)) {
|
||||||
|
const propertyName = match[1];
|
||||||
|
const locations = result.get(propertyName) ?? [];
|
||||||
|
locations.push(`${relative(file)}:${index + 1}`);
|
||||||
|
result.set(propertyName, locations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Map([...result.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectReferencedCssCustomProperties(files) {
|
||||||
|
const result = new Set();
|
||||||
|
for (const file of files) {
|
||||||
|
const text = stripCssComments(await readFile(file, "utf8"));
|
||||||
|
for (const match of text.matchAll(/var\(\s*(--codex-panel-[\w-]+)/g)) {
|
||||||
|
result.add(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripCssComments(text) {
|
||||||
|
return text.replace(/\/\*[\s\S]*?\*\//g, (comment) => comment.replace(/[^\n]/g, ""));
|
||||||
|
}
|
||||||
|
|
||||||
function collectDynamicClassPrefixes(text) {
|
function collectDynamicClassPrefixes(text) {
|
||||||
const prefixes = new Set();
|
const prefixes = new Set();
|
||||||
const templatePrefixPattern = /codex-panel(?:-[a-z]+)?__[a-z0-9-]+--\$\{/g;
|
const templatePrefixPattern = /codex-panel(?:-[a-z]+)?__[a-z0-9-]+--\$\{/g;
|
||||||
|
|
@ -112,9 +128,12 @@ async function readTexts(files) {
|
||||||
|
|
||||||
function locationsInTexts(texts, needle) {
|
function locationsInTexts(texts, needle) {
|
||||||
const locations = [];
|
const locations = [];
|
||||||
|
const escapedNeedle = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
const pattern = new RegExp(`(^|[^\\w-])${escapedNeedle}(?![\\w-])`);
|
||||||
for (const { file, text } of texts) {
|
for (const { file, text } of texts) {
|
||||||
let offset = text.indexOf(needle);
|
const match = pattern.exec(text);
|
||||||
if (offset === -1) continue;
|
if (!match) continue;
|
||||||
|
const offset = match.index + match[1].length;
|
||||||
const line = text.slice(0, offset).split("\n").length;
|
const line = text.slice(0, offset).split("\n").length;
|
||||||
locations.push(`${relative(file)}:${line}`);
|
locations.push(`${relative(file)}:${line}`);
|
||||||
}
|
}
|
||||||
|
|
@ -142,35 +161,41 @@ async function collectFiles(directory, extensions, excludedPrefixes, result) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printReport({ total, exactSourceUsed, dynamicSourceUsed, testOnlyCandidates, candidates, dynamicPrefixes }) {
|
function printCandidates({ testOnlyCandidates, candidates, unusedCustomProperties, dynamicPrefixes }) {
|
||||||
const totalCandidates = candidates.length + testOnlyCandidates.length;
|
console.error("CSS usage check failed.");
|
||||||
|
|
||||||
console.log("CSS usage audit");
|
|
||||||
console.log(` CSS classes: ${total}`);
|
|
||||||
console.log(` Source literals: ${exactSourceUsed.length}`);
|
|
||||||
console.log(` Source dynamic modifiers: ${dynamicSourceUsed.length}`);
|
|
||||||
console.log(` Test-only candidates: ${testOnlyCandidates.length}`);
|
|
||||||
console.log(` Unused candidates: ${totalCandidates}`);
|
|
||||||
|
|
||||||
if (dynamicPrefixes.length > 0) {
|
if (dynamicPrefixes.length > 0) {
|
||||||
console.log("\nDynamic modifier prefixes:");
|
console.error("");
|
||||||
for (const prefix of dynamicPrefixes) console.log(` ${prefix}*`);
|
console.error("Dynamic CSS class prefixes are not allowed:");
|
||||||
|
for (const prefix of dynamicPrefixes) {
|
||||||
|
console.error(` ${prefix}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (testOnlyCandidates.length > 0) {
|
if (testOnlyCandidates.length > 0) {
|
||||||
console.log("\nTest-only unused CSS class candidates:");
|
console.error("");
|
||||||
|
console.error("Test-only unused CSS class candidates:");
|
||||||
for (const item of testOnlyCandidates) {
|
for (const item of testOnlyCandidates) {
|
||||||
console.log(` ${item.className}`);
|
console.error(` ${item.className}`);
|
||||||
console.log(` css: ${item.locations.join(", ")}`);
|
console.error(` css: ${item.locations.join(", ")}`);
|
||||||
console.log(` tests: ${item.testMatches.join(", ")}`);
|
console.error(` tests: ${item.testMatches.join(", ")}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidates.length > 0) {
|
if (candidates.length > 0) {
|
||||||
console.log("\nUnused CSS class candidates:");
|
console.error("");
|
||||||
|
console.error("Unused CSS class candidates:");
|
||||||
for (const item of candidates) {
|
for (const item of candidates) {
|
||||||
console.log(` ${item.className}`);
|
console.error(` ${item.className}`);
|
||||||
console.log(` css: ${item.locations.join(", ")}`);
|
console.error(` css: ${item.locations.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unusedCustomProperties.length > 0) {
|
||||||
|
console.error("");
|
||||||
|
console.error("Unused CSS custom property candidates:");
|
||||||
|
for (const item of unusedCustomProperties) {
|
||||||
|
console.error(` ${item.propertyName}`);
|
||||||
|
console.error(` css: ${item.locations.join(", ")}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
import path from "node:path";
|
|
||||||
import ts from "typescript";
|
|
||||||
|
|
||||||
if (process.argv.length > 2) {
|
|
||||||
console.error("Usage: node scripts/check-import-cycles.mjs");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const configPath = ts.findConfigFile(cwd, ts.sys.fileExists, "tsconfig.json");
|
|
||||||
if (!configPath) {
|
|
||||||
console.error("Could not find tsconfig.json.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
||||||
if (configFile.error) {
|
|
||||||
reportDiagnostic(configFile.error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath));
|
|
||||||
if (parsedConfig.errors.length > 0) {
|
|
||||||
for (const diagnostic of parsedConfig.errors) reportDiagnostic(diagnostic);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceFiles = parsedConfig.fileNames.map((file) => path.resolve(file)).filter((file) => isCheckedSourceFile(relativePath(file)));
|
|
||||||
const sourceFileSet = new Set(sourceFiles);
|
|
||||||
const compilerHost = ts.createCompilerHost(parsedConfig.options, true);
|
|
||||||
const moduleResolutionCache = ts.createModuleResolutionCache(cwd, (file) => file, parsedConfig.options);
|
|
||||||
const graph = new Map(sourceFiles.map((file) => [relativePath(file), new Set()]));
|
|
||||||
|
|
||||||
for (const sourceFile of sourceFiles) {
|
|
||||||
const sourceText = ts.sys.readFile(sourceFile);
|
|
||||||
if (sourceText === undefined) continue;
|
|
||||||
|
|
||||||
const parsedSource = ts.createSourceFile(sourceFile, sourceText, parsedConfig.options.target ?? ts.ScriptTarget.ES2022, true);
|
|
||||||
const from = relativePath(sourceFile);
|
|
||||||
|
|
||||||
for (const edge of importEdges(parsedSource)) {
|
|
||||||
const resolved = ts.resolveModuleName(
|
|
||||||
edge.specifier,
|
|
||||||
sourceFile,
|
|
||||||
parsedConfig.options,
|
|
||||||
compilerHost,
|
|
||||||
moduleResolutionCache,
|
|
||||||
).resolvedModule;
|
|
||||||
if (!resolved) continue;
|
|
||||||
|
|
||||||
const resolvedFile = path.resolve(resolved.resolvedFileName);
|
|
||||||
if (!sourceFileSet.has(resolvedFile)) continue;
|
|
||||||
|
|
||||||
graph.get(from)?.add(relativePath(resolvedFile));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cycleComponents = stronglyConnectedComponents(graph)
|
|
||||||
.filter((component) => component.length > 1 || hasSelfDependency(graph, component[0]))
|
|
||||||
.sort((left, right) => left[0].localeCompare(right[0]));
|
|
||||||
|
|
||||||
if (cycleComponents.length === 0) {
|
|
||||||
console.log("No import cycles found.");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`Import cycle check failed: found ${cycleComponents.length} cycle${cycleComponents.length === 1 ? "" : "s"}.`);
|
|
||||||
for (const [index, component] of cycleComponents.entries()) {
|
|
||||||
const cycle = sampleCycle(graph, component);
|
|
||||||
console.error(`${index + 1}. ${cycle.join(" -> ")}`);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
|
|
||||||
function importEdges(sourceFile) {
|
|
||||||
const edges = [];
|
|
||||||
|
|
||||||
for (const statement of sourceFile.statements) {
|
|
||||||
if (ts.isImportDeclaration(statement) && hasRuntimeImportEdge(statement) && stringLiteralText(statement.moduleSpecifier)) {
|
|
||||||
edges.push({
|
|
||||||
specifier: stringLiteralText(statement.moduleSpecifier),
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
ts.isExportDeclaration(statement) &&
|
|
||||||
hasRuntimeExportEdge(statement) &&
|
|
||||||
statement.moduleSpecifier &&
|
|
||||||
stringLiteralText(statement.moduleSpecifier)
|
|
||||||
) {
|
|
||||||
edges.push({
|
|
||||||
specifier: stringLiteralText(statement.moduleSpecifier),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return edges;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasRuntimeImportEdge(statement) {
|
|
||||||
const importClause = statement.importClause;
|
|
||||||
if (!importClause) return true;
|
|
||||||
if (importClause.isTypeOnly) return false;
|
|
||||||
if (importClause.name) return true;
|
|
||||||
|
|
||||||
const namedBindings = importClause.namedBindings;
|
|
||||||
if (!namedBindings) return false;
|
|
||||||
if (ts.isNamespaceImport(namedBindings)) return true;
|
|
||||||
return namedBindings.elements.some((element) => !element.isTypeOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasRuntimeExportEdge(statement) {
|
|
||||||
if (statement.isTypeOnly) return false;
|
|
||||||
|
|
||||||
const exportClause = statement.exportClause;
|
|
||||||
if (!exportClause) return true;
|
|
||||||
if (ts.isNamespaceExport(exportClause)) return true;
|
|
||||||
return exportClause.elements.some((element) => !element.isTypeOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringLiteralText(node) {
|
|
||||||
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stronglyConnectedComponents(graph) {
|
|
||||||
let nextIndex = 0;
|
|
||||||
const stack = [];
|
|
||||||
const onStack = new Set();
|
|
||||||
const indices = new Map();
|
|
||||||
const lowLinks = new Map();
|
|
||||||
const components = [];
|
|
||||||
|
|
||||||
for (const node of graph.keys()) {
|
|
||||||
if (!indices.has(node)) visit(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
return components.map((component) => component.sort());
|
|
||||||
|
|
||||||
function visit(node) {
|
|
||||||
indices.set(node, nextIndex);
|
|
||||||
lowLinks.set(node, nextIndex);
|
|
||||||
nextIndex += 1;
|
|
||||||
stack.push(node);
|
|
||||||
onStack.add(node);
|
|
||||||
|
|
||||||
for (const dependency of graph.get(node) ?? []) {
|
|
||||||
if (!indices.has(dependency)) {
|
|
||||||
visit(dependency);
|
|
||||||
lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(dependency)));
|
|
||||||
} else if (onStack.has(dependency)) {
|
|
||||||
lowLinks.set(node, Math.min(lowLinks.get(node), indices.get(dependency)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowLinks.get(node) !== indices.get(node)) return;
|
|
||||||
|
|
||||||
const component = [];
|
|
||||||
let item;
|
|
||||||
do {
|
|
||||||
item = stack.pop();
|
|
||||||
onStack.delete(item);
|
|
||||||
component.push(item);
|
|
||||||
} while (item !== node);
|
|
||||||
components.push(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sampleCycle(graph, component) {
|
|
||||||
const componentSet = new Set(component);
|
|
||||||
|
|
||||||
for (const start of component) {
|
|
||||||
if (graph.get(start)?.has(start)) return [start, start];
|
|
||||||
|
|
||||||
for (const dependency of graph.get(start) ?? []) {
|
|
||||||
if (!componentSet.has(dependency)) continue;
|
|
||||||
const pathToStart = findPath(graph, componentSet, dependency, start, new Set([start]));
|
|
||||||
if (pathToStart) return [start, ...pathToStart];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPath(graph, allowedNodes, current, target, seen) {
|
|
||||||
if (current === target) return [current];
|
|
||||||
if (seen.has(current)) return null;
|
|
||||||
seen.add(current);
|
|
||||||
|
|
||||||
for (const dependency of graph.get(current) ?? []) {
|
|
||||||
if (!allowedNodes.has(dependency)) continue;
|
|
||||||
const path = findPath(graph, allowedNodes, dependency, target, seen);
|
|
||||||
if (path) return [current, ...path];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSelfDependency(graph, node) {
|
|
||||||
return Boolean(node && graph.get(node)?.has(node));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCheckedSourceFile(file) {
|
|
||||||
return (
|
|
||||||
file.startsWith("src/") &&
|
|
||||||
!file.startsWith("src/generated/") &&
|
|
||||||
(file.endsWith(".ts") || file.endsWith(".tsx")) &&
|
|
||||||
!file.endsWith(".d.ts")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function relativePath(file) {
|
|
||||||
return path.relative(cwd, file).split(path.sep).join("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportDiagnostic(diagnostic) {
|
|
||||||
console.error(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
|
|
||||||
const args = new Set(process.argv.slice(2));
|
|
||||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
||||||
const checks = [
|
|
||||||
{ local: "typecheck", ci: "typecheck:ci", localPhase: "parallel" },
|
|
||||||
{ local: "test", ci: "test:ci", localPhase: "parallel" },
|
|
||||||
{ local: "lint:ts", ci: "lint:ts:ci", localPhase: "parallel" },
|
|
||||||
{ local: "lint:css", ci: "lint:css", localPhase: "parallel" },
|
|
||||||
{ local: "lint:css:usage:check", ci: null, localPhase: "parallel" },
|
|
||||||
{ local: "lint:deps", ci: "lint:deps", localPhase: "parallel" },
|
|
||||||
{ local: "format:check", ci: "format:check:ci", localPhase: "parallel" },
|
|
||||||
{ local: "build:styles:check", ci: "build:styles:check", localPhase: "parallel" },
|
|
||||||
{ local: "unused", ci: "unused", localPhase: "parallel" },
|
|
||||||
{ local: "build", ci: "build", localPhase: "sequential" },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const arg of args) {
|
|
||||||
if (arg !== "--ci") {
|
|
||||||
console.error("Usage: node scripts/check.mjs [--ci]");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.has("--ci")) {
|
|
||||||
for (const check of checks) {
|
|
||||||
if (check.ci === null) continue;
|
|
||||||
run(npmCommand, ["run", check.ci]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const parallelScripts = checks.filter((check) => check.localPhase === "parallel").map((check) => check.local);
|
|
||||||
run("node", ["scripts/run-parallel.mjs", ...parallelScripts]);
|
|
||||||
for (const check of checks.filter((check) => check.localPhase === "sequential")) run(npmCommand, ["run", check.local]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function run(command, args) {
|
|
||||||
const result = spawnSync(command, args, {
|
|
||||||
stdio: "inherit",
|
|
||||||
shell: false,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
console.error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
||||||
}
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
import { createHash } from "node:crypto";
|
|
||||||
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
import prettier from "prettier";
|
|
||||||
|
|
||||||
const sourceExtensions = new Set([".ts", ".tsx", ".js", ".mjs", ".json", ".css", ".md"]);
|
|
||||||
const scriptExtensions = new Set([".js", ".mjs"]);
|
|
||||||
const rootExtensions = new Set([".ts", ".json", ".mjs", ".md", ".css"]);
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const shouldCheck = args.includes("--check");
|
|
||||||
const shouldUseCache = args.includes("--cache");
|
|
||||||
const cacheLocation = argumentValue("--cache-location") ?? "node_modules/.cache/prettier/.prettier-cache";
|
|
||||||
const root = process.cwd();
|
|
||||||
const files = await formatFiles();
|
|
||||||
const cache = shouldCheck && shouldUseCache ? await readCache(cacheLocation) : new Map();
|
|
||||||
const nextCache = new Map(cache);
|
|
||||||
const unformattedFiles = [];
|
|
||||||
let hadError = false;
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
try {
|
|
||||||
const info = await prettier.getFileInfo(file, { ignorePath: path.join(root, ".prettierignore") });
|
|
||||||
if (info.ignored || info.inferredParser === null) continue;
|
|
||||||
|
|
||||||
const input = await readFile(file, "utf8");
|
|
||||||
const config = (await prettier.resolveConfig(file)) ?? {};
|
|
||||||
const cacheKey = path.relative(root, file);
|
|
||||||
const cacheHash = hashFormatInput(input, config, info.inferredParser);
|
|
||||||
if (shouldCheck && cache.get(cacheKey) === cacheHash) continue;
|
|
||||||
|
|
||||||
const output = await prettier.format(input, { ...config, filepath: file });
|
|
||||||
|
|
||||||
if (output === input) {
|
|
||||||
nextCache.set(cacheKey, cacheHash);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (shouldCheck) {
|
|
||||||
nextCache.delete(cacheKey);
|
|
||||||
unformattedFiles.push(cacheKey);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await writeFile(file, output);
|
|
||||||
console.log(path.relative(root, file));
|
|
||||||
} catch (error) {
|
|
||||||
hadError = true;
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
console.error(`${path.relative(root, file)}: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldCheck && shouldUseCache) await writeCache(cacheLocation, nextCache);
|
|
||||||
if (unformattedFiles.length > 0) {
|
|
||||||
console.error("Unformatted files:");
|
|
||||||
for (const file of unformattedFiles) console.error(file);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (hadError) process.exit(1);
|
|
||||||
|
|
||||||
function argumentValue(name) {
|
|
||||||
const index = args.indexOf(name);
|
|
||||||
if (index === -1) return null;
|
|
||||||
return args[index + 1] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readCache(file) {
|
|
||||||
try {
|
|
||||||
const value = JSON.parse(await readFile(path.join(root, file), "utf8"));
|
|
||||||
return new Map(Object.entries(value.files ?? {}));
|
|
||||||
} catch {
|
|
||||||
return new Map();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeCache(file, cacheEntries) {
|
|
||||||
const absoluteFile = path.join(root, file);
|
|
||||||
await mkdir(path.dirname(absoluteFile), { recursive: true });
|
|
||||||
const files = Object.fromEntries([...cacheEntries.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
||||||
await writeFile(absoluteFile, `${JSON.stringify({ version: 1, files })}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashFormatInput(input, config, inferredParser) {
|
|
||||||
return createHash("sha256")
|
|
||||||
.update(prettier.version)
|
|
||||||
.update("\0")
|
|
||||||
.update(inferredParser ?? "")
|
|
||||||
.update("\0")
|
|
||||||
.update(JSON.stringify(config))
|
|
||||||
.update("\0")
|
|
||||||
.update(input)
|
|
||||||
.digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function formatFiles() {
|
|
||||||
const result = [
|
|
||||||
...(await filesInTree("src", sourceExtensions, { excludedPrefixes: ["src/generated"] })),
|
|
||||||
...(await filesInTree("tests", sourceExtensions)),
|
|
||||||
...(await filesInTree("scripts", scriptExtensions)),
|
|
||||||
...(await rootFiles(rootExtensions)),
|
|
||||||
];
|
|
||||||
return [...new Set(result)].sort((a, b) => path.relative(root, a).localeCompare(path.relative(root, b)));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rootFiles(extensions) {
|
|
||||||
const entries = await readdir(root, { withFileTypes: true });
|
|
||||||
return entries.filter((entry) => entry.isFile() && extensions.has(path.extname(entry.name))).map((entry) => path.join(root, entry.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function filesInTree(relativeDirectory, extensions, options = {}) {
|
|
||||||
const directory = path.join(root, relativeDirectory);
|
|
||||||
const excludedPrefixes = options.excludedPrefixes?.map((prefix) => path.join(root, prefix)) ?? [];
|
|
||||||
const result = [];
|
|
||||||
await collectFiles(directory, extensions, excludedPrefixes, result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectFiles(directory, extensions, excludedPrefixes, result) {
|
|
||||||
if (excludedPrefixes.some((prefix) => directory === prefix || directory.startsWith(`${prefix}${path.sep}`))) return;
|
|
||||||
|
|
||||||
const entries = await readdir(directory, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const file = path.join(directory, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
await collectFiles(file, extensions, excludedPrefixes, result);
|
|
||||||
} else if (entry.isFile() && extensions.has(path.extname(entry.name))) {
|
|
||||||
result.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,105 @@
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
import { mkdir, mkdtemp, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { readAppServerGenerationPolicy } from "./app-server-compatibility.mjs";
|
||||||
|
|
||||||
const generatedDir = path.resolve("src/generated/app-server");
|
const generatedRelativeDir = "src/generated/app-server";
|
||||||
const generatedHeader = "// GENERATED CODE! DO NOT MODIFY BY HAND!";
|
const generatedHeader = "// GENERATED CODE! DO NOT MODIFY BY HAND!";
|
||||||
const normalizationNotice = "// This file was mechanically normalized after generation by scripts/generate-app-server-types.mjs.";
|
const normalizationNotice = "// This file was mechanically normalized after generation by scripts/generate-app-server-types.mjs.";
|
||||||
|
|
||||||
await cleanGeneratedTypes();
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
run("codex", ["app-server", "generate-ts", "--experimental", "--out", "src/generated/app-server"]);
|
try {
|
||||||
await normalizeGeneratedTypes();
|
const args = process.argv.slice(2);
|
||||||
|
if (args.some((arg) => arg !== "--check") || args.length > 1) {
|
||||||
async function cleanGeneratedTypes() {
|
throw new Error("Usage: node scripts/generate-app-server-types.mjs [--check]");
|
||||||
await rm(generatedDir, { recursive: true, force: true });
|
}
|
||||||
await mkdir(generatedDir, { recursive: true });
|
await generateAppServerTypes({ check: args.includes("--check") });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function normalizeGeneratedTypes() {
|
export async function generateAppServerTypes(options = {}) {
|
||||||
|
const cwd = options.cwd ?? process.cwd();
|
||||||
|
const generatedDir = path.resolve(cwd, generatedRelativeDir);
|
||||||
|
const runCommand = options.runCommand ?? run;
|
||||||
|
const readCodexVersion = options.readCodexVersion ?? readInstalledCodexVersion;
|
||||||
|
const policy = await readAppServerGenerationPolicy(cwd);
|
||||||
|
const installedCliVersion = await readCodexVersion(cwd);
|
||||||
|
if (installedCliVersion !== policy.testedCliVersion) {
|
||||||
|
throw new Error(
|
||||||
|
`Codex CLI ${policy.testedCliVersion} is required to generate app-server bindings; found ${installedCliVersion ?? "an unreadable version"}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const generatedParent = path.dirname(generatedDir);
|
||||||
|
await mkdir(generatedParent, { recursive: true });
|
||||||
|
const stagedDir = await mkdtemp(path.join(generatedParent, ".app-server-"));
|
||||||
|
try {
|
||||||
|
const stagedRelativeDir = path.relative(cwd, stagedDir);
|
||||||
|
await runCommand("codex", [...policy.generationArguments, "--out", stagedRelativeDir], { cwd });
|
||||||
|
await normalizeGeneratedTypes(stagedDir);
|
||||||
|
if (options.check === true) {
|
||||||
|
const differences = await compareGeneratedTrees(generatedDir, stagedDir);
|
||||||
|
if (differences.length > 0) {
|
||||||
|
throw new Error(`generated app-server bindings are out of date:\n${differences.map((difference) => ` ${difference}`).join("\n")}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await replaceGeneratedTypes(generatedDir, stagedDir);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await rm(stagedDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compareGeneratedTrees(trackedDir, stagedDir) {
|
||||||
|
const [trackedFiles, stagedFiles] = await Promise.all([listFiles(trackedDir), listFiles(stagedDir)]);
|
||||||
|
const trackedSet = new Set(trackedFiles);
|
||||||
|
const stagedSet = new Set(stagedFiles);
|
||||||
|
const differences = [];
|
||||||
|
|
||||||
|
for (const file of stagedFiles) {
|
||||||
|
if (!trackedSet.has(file)) {
|
||||||
|
differences.push(`added: ${file}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [trackedSource, stagedSource] = await Promise.all([readFile(path.join(trackedDir, file)), readFile(path.join(stagedDir, file))]);
|
||||||
|
if (!trackedSource.equals(stagedSource)) differences.push(`changed: ${file}`);
|
||||||
|
}
|
||||||
|
for (const file of trackedFiles) {
|
||||||
|
if (!stagedSet.has(file)) differences.push(`removed: ${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return differences;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replaceGeneratedTypes(generatedDir, stagedDir) {
|
||||||
|
const backupDir = `${generatedDir}.backup-${process.pid.toString()}-${Date.now().toString()}`;
|
||||||
|
let hasBackup = false;
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await rename(generatedDir, backupDir);
|
||||||
|
hasBackup = true;
|
||||||
|
} catch (error) {
|
||||||
|
if (!isMissingPathError(error)) throw error;
|
||||||
|
}
|
||||||
|
await rename(stagedDir, generatedDir);
|
||||||
|
if (hasBackup) await rm(backupDir, { recursive: true, force: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (hasBackup) {
|
||||||
|
await rm(generatedDir, { recursive: true, force: true });
|
||||||
|
await rename(backupDir, generatedDir);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMissingPathError(error) {
|
||||||
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function normalizeGeneratedTypes(generatedDir) {
|
||||||
const files = await listTypeScriptFiles(generatedDir);
|
const files = await listTypeScriptFiles(generatedDir);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|
@ -40,6 +124,24 @@ async function listTypeScriptFiles(dir) {
|
||||||
return files.flat();
|
return files.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listFiles(dir, relativeDir = "") {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = await readdir(path.join(dir, relativeDir), { withFileTypes: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (isMissingPathError(error)) return [];
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const files = await Promise.all(
|
||||||
|
entries.map((entry) => {
|
||||||
|
const relativePath = path.join(relativeDir, entry.name);
|
||||||
|
if (entry.isDirectory()) return listFiles(dir, relativePath);
|
||||||
|
return entry.isFile() ? [relativePath.replaceAll(path.sep, "/")] : [];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return files.flat().sort();
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSource(source) {
|
function normalizeSource(source) {
|
||||||
let normalized = source;
|
let normalized = source;
|
||||||
do {
|
do {
|
||||||
|
|
@ -55,14 +157,25 @@ function addNormalizationNotice(source) {
|
||||||
return source.replace(generatedHeader, `${generatedHeader}\n${normalizationNotice}`);
|
return source.replace(generatedHeader, `${generatedHeader}\n${normalizationNotice}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function run(command, args) {
|
function run(command, args, options = {}) {
|
||||||
const result = spawnSync(command, args, {
|
const result = spawnSync(command, args, {
|
||||||
|
cwd: options.cwd ?? process.cwd(),
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
shell: false,
|
shell: false,
|
||||||
});
|
});
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`);
|
throw new Error(`Failed to run ${command} ${args.join(" ")}: ${result.error.message}`);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} exited with status ${(result.status ?? 1).toString()}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInstalledCodexVersion(cwd) {
|
||||||
|
const result = spawnSync("codex", ["--version"], {
|
||||||
|
cwd,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: "pipe",
|
||||||
|
shell: false,
|
||||||
|
});
|
||||||
|
if (result.error || result.status !== 0) return null;
|
||||||
|
return `${result.stdout}\n${result.stderr}`.match(/\b\d+\.\d+\.\d+\b/)?.[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
scripts/grit/dom/no-dom-events-imports.grit
Normal file
13
scripts/grit/dom/no-dom-events-imports.grit
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: r"^[\"'].*shared/dom/events\.dom[\"']$" },
|
||||||
|
register_diagnostic(span=$stmt, message="Import DOM event listener helpers only from explicit .dom, .obsidian, or .measure bridge files.", severity="error")
|
||||||
|
}
|
||||||
56
scripts/grit/dom/no-implicit-dom-bridges.grit
Normal file
56
scripts/grit/dom/no-implicit-dom-bridges.grit
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
or {
|
||||||
|
`$target.createElement($...)` as $stmt,
|
||||||
|
`$target.createTextNode($...)` as $stmt,
|
||||||
|
`$target.createEl($...)` as $stmt,
|
||||||
|
`$target.createDiv($...)` as $stmt,
|
||||||
|
`$target.createSpan($...)` as $stmt,
|
||||||
|
`$target.empty()` as $stmt,
|
||||||
|
`$target.append($...)` as $stmt,
|
||||||
|
`$target.prepend($...)` as $stmt,
|
||||||
|
`$target.before($...)` as $stmt,
|
||||||
|
`$target.after($...)` as $stmt,
|
||||||
|
`$target.appendChild($...)` as $stmt,
|
||||||
|
`$target.insertBefore($...)` as $stmt,
|
||||||
|
`$target.replaceChildren($...)` as $stmt,
|
||||||
|
`$target.remove()` as $stmt,
|
||||||
|
`$target.insertAdjacentHTML($...)` as $stmt,
|
||||||
|
`$target.setAttribute($...)` as $stmt,
|
||||||
|
`$target.setAttr($...)` as $stmt,
|
||||||
|
`$target.addClass($...)` as $stmt,
|
||||||
|
`$target.removeClass($...)` as $stmt,
|
||||||
|
`$target.toggleClass($...)` as $stmt,
|
||||||
|
`$target.setText($...)` as $stmt,
|
||||||
|
`$target.classList.add($...)` as $stmt,
|
||||||
|
`$target.classList.remove($...)` as $stmt,
|
||||||
|
`$target.classList.toggle($...)` as $stmt,
|
||||||
|
`$target.addEventListener($...)` as $stmt,
|
||||||
|
`$target.removeEventListener($...)` as $stmt,
|
||||||
|
`$target.querySelector($...)` as $stmt,
|
||||||
|
`$target.querySelectorAll($...)` as $stmt,
|
||||||
|
`$target.closest($...)` as $stmt,
|
||||||
|
`$target.contains($...)` as $stmt,
|
||||||
|
`$target.getBoundingClientRect()` as $stmt,
|
||||||
|
`$target.focus($...)` as $stmt,
|
||||||
|
`$target.select()` as $stmt,
|
||||||
|
`$target.setSelectionRange($...)` as $stmt,
|
||||||
|
`$target.setCssProps($...)` as $stmt,
|
||||||
|
`$target.scrollHeight` as $stmt,
|
||||||
|
`$target.scrollWidth` as $stmt,
|
||||||
|
`$target.clientHeight` as $stmt,
|
||||||
|
`$target.clientWidth` as $stmt,
|
||||||
|
`$target.offsetTop` as $stmt,
|
||||||
|
`$target.offsetHeight` as $stmt,
|
||||||
|
`$target.selectionStart` as $stmt,
|
||||||
|
`$target.selectionEnd` as $stmt,
|
||||||
|
`$target.selectionDirection` as $stmt,
|
||||||
|
`$target.activeElement` as $stmt,
|
||||||
|
`$target.ownerDocument` as $stmt,
|
||||||
|
`$target.defaultView` as $stmt,
|
||||||
|
`$target.style.$property` as $stmt
|
||||||
|
} where {
|
||||||
|
not { $stmt <: `$target.addEventListener("abort", $...)` },
|
||||||
|
not { $stmt <: `$target.removeEventListener("abort", $...)` },
|
||||||
|
register_diagnostic(span=$stmt, message="Keep DOM reads, writes, measurements, hit-tests, focus, and event wiring in files named with a .dom, .obsidian, or .measure suffix.", severity="error")
|
||||||
|
}
|
||||||
13
scripts/grit/dom/no-preact-root-imports.grit
Normal file
13
scripts/grit/dom/no-preact-root-imports.grit
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: r"^[\"'].*shared/dom/preact-root\.dom[\"']$" },
|
||||||
|
register_diagnostic(span=$stmt, message="Import the Preact root adapter only from explicit root bridge files.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pattern app_server_protocol_connection_source() { r"^[\"'](?:\.\./)+connection(?:/.*)?[\"']$" }
|
||||||
|
|
||||||
|
private pattern root_app_server_connection_source() {
|
||||||
|
or {
|
||||||
|
r"^[\"'](?:\.\./)+app-server/connection(?:/.*)?[\"']$",
|
||||||
|
r"^[\"']src/app-server/connection(?:/.*)?[\"']$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pattern app_server_connection_boundary_source() {
|
||||||
|
or {
|
||||||
|
app_server_protocol_connection_source(),
|
||||||
|
root_app_server_connection_source()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: app_server_connection_boundary_source() },
|
||||||
|
register_diagnostic(span=$stmt, message="Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pattern app_server_protocol_source() { r"^[\"'](?:(?:\.\./)+app-server/protocol|src/app-server/protocol)/[^/\"']+(?:/.*)?[\"']$" }
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: app_server_protocol_source() },
|
||||||
|
register_diagnostic(span=$stmt, message="Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol at its app-server boundary; feature state and UI must use Panel-owned models.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: r"^[\"']\.\./[^/\"']+[\"']$" },
|
||||||
|
register_diagnostic(span=$stmt, message="App-server subfolders must not import sibling root modules; move the dependency into a responsibility subfolder.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where {
|
||||||
|
$source <: r"^[\"'](?:(?:\.\./)+(?:host|panel|presentation|ui)|src/features/chat/(?:host|panel|presentation|ui))(?:/.*)?[\"']$"
|
||||||
|
},
|
||||||
|
register_diagnostic(span=$stmt, message="Chat app-server adapters must not import chat host, panel, presentation, or UI layers.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where {
|
||||||
|
$source <: r"^[\"'](?:(?:\.\./)+(?:app-server|host|panel|presentation|ui)|(?:\.\./){2,}(?:selection-rewrite|thread-picker|threads|threads-view|turn-diff)|src/(?:app-server|features/(?:chat/(?:app-server|host|panel|presentation|ui)|selection-rewrite|thread-picker|threads|threads-view|turn-diff)))(?:/.*)?[\"']$"
|
||||||
|
},
|
||||||
|
register_diagnostic(span=$stmt, message="Chat application modules must not import app-server, sibling feature, host, panel, presentation, or UI layers; expose state and workflow contracts instead.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where {
|
||||||
|
$source <: r"^[\"'](?:(?:\.\./)+(?:presentation|ui)|src/features/chat/(?:presentation|ui))(?:/.*)?[\"']$"
|
||||||
|
},
|
||||||
|
register_diagnostic(span=$stmt, message="Chat host modules must not import chat presentation or UI layers; route render-facing projections through panel-owned adapters.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where {
|
||||||
|
$source <: r"^[\"'](?:(?:\.\./)+(?:app-server|host)|src/(?:app-server|features/chat/(?:app-server|host)))(?:/.*)?[\"']$"
|
||||||
|
},
|
||||||
|
register_diagnostic(span=$stmt, message="Chat panel modules must not import app-server adapters or chat host internals.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where {
|
||||||
|
$source <: r"^[\"'](?:(?:\.\./)+(?:application|app-server|host|panel|ui)|src/(?:app-server|features/chat/(?:application|app-server|host|panel|ui)))(?:/.*)?[\"']$"
|
||||||
|
},
|
||||||
|
register_diagnostic(span=$stmt, message="Chat presentation modules must stay pure view-model projection; keep application, app-server, host, panel, and UI dependencies outward.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pattern app_server_protocol_source() { r"^[\"'](?:(?:\.\./)+app-server/protocol|src/app-server/protocol)/[^/\"']+(?:/.*)?[\"']$" }
|
||||||
|
|
||||||
|
private pattern turn_protocol_source() { r"^[\"'](?:(?:\.\./)+app-server/protocol/turn|src/app-server/protocol/turn)(?:/.*)?[\"']$" }
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: app_server_protocol_source(), not { $source <: turn_protocol_source() } },
|
||||||
|
register_diagnostic(span=$stmt, message="Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol at its app-server boundary; feature state and UI must use Panel-owned models.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where {
|
||||||
|
$source <: r"^[\"'](?:(?:\.\./)+(?:application|app-server|host|panel)|src/(?:app-server|features/chat/(?:application|app-server|host|panel)))(?:/.*)?[\"']$"
|
||||||
|
},
|
||||||
|
register_diagnostic(span=$stmt, message="Chat UI modules must not import application, app-server, host, or panel layers; pass render-ready props and actions through UI contracts.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+workspace|src/workspace)(?:/.*)?[\"']$" },
|
||||||
|
register_diagnostic(span=$stmt, message="Chat modules must not import workspace modules; pass workspace capabilities through chat host contracts.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pattern root_outer_layer_source() {
|
||||||
|
or {
|
||||||
|
r"^[\"'](?:\.\./)+(?:app-server|settings|workspace|shared|features)(?:/.*)?[\"']$",
|
||||||
|
r"^[\"']src/(?:app-server|settings|workspace|shared|features)(?:/.*)?[\"']$",
|
||||||
|
r"^[\"']obsidian[\"']$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pattern chat_domain_outer_layer_source() { r"^[\"'](?:\.\./)+(?:application|host|panel|presentation|ui)(?:/.*)?[\"']$" }
|
||||||
|
|
||||||
|
private pattern domain_outer_layer_source() {
|
||||||
|
or {
|
||||||
|
root_outer_layer_source(),
|
||||||
|
chat_domain_outer_layer_source()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: domain_outer_layer_source() },
|
||||||
|
register_diagnostic(span=$stmt, message="Domain modules must stay pure; outer layers may depend on domain, not the reverse.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+workspace|src/workspace)(?:/.*)?[\"']$" },
|
||||||
|
register_diagnostic(span=$stmt, message="Feature modules must not import workspace modules; pass workspace capabilities through feature host contracts.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pattern generated_app_server_source() { r"^[\"'].*(?:src/)?generated/app-server(?:/.*)?[\"']$" }
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: generated_app_server_source() },
|
||||||
|
register_diagnostic(span=$stmt, message="Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where { $source <: r"^[\"'](?:(?:\.\./)+features|src/features)(?:/.*)?[\"']$" },
|
||||||
|
register_diagnostic(span=$stmt, message="Do not import feature modules from this layer. Move reusable behavior to domain, shared DOM/Obsidian/runtime/UI, or app-server adapters.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where {
|
||||||
|
$source <: r"^[\"'](?:(?:\.\./)+app-server/(?:connection|services)|src/app-server/(?:connection|services))(?:/.*)?[\"']$"
|
||||||
|
},
|
||||||
|
register_diagnostic(span=$stmt, message="Thread workflows must depend on feature-owned ports instead of app-server clients or services.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
private pattern js_module_reference() {
|
||||||
|
or {
|
||||||
|
JsImport(),
|
||||||
|
JsImportCallExpression()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js_module_reference() as $stmt where {
|
||||||
|
$stmt <: contains `$source` where {
|
||||||
|
$source <: r"^[\"'](?:\.\./features/chat/(?:application|app-server|domain|panel|presentation|ui)|src/features/chat/(?:application|app-server|domain|panel|presentation|ui))(?:/.*)?[\"']$"
|
||||||
|
},
|
||||||
|
register_diagnostic(span=$stmt, message="Workspace modules may coordinate chat only through chat host contracts and Obsidian views.", severity="error")
|
||||||
|
}
|
||||||
53
scripts/grit/no-restricted-css-policy.grit
Normal file
53
scripts/grit/no-restricted-css-policy.grit
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
engine biome(1.0)
|
||||||
|
language css
|
||||||
|
|
||||||
|
or {
|
||||||
|
// Keep colors on Obsidian or Codex Panel design tokens.
|
||||||
|
CssDeclarationWithSemicolon() as $decl where {
|
||||||
|
$decl <: r"\s*(?:background(?:-color)?|border(?:-(?:block|inline|top|right|bottom|left))?(?:-color)?|box-shadow|caret-color|color|fill|outline(?:-color)?|stroke|text-shadow): .*?(?:#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\(|\b(?:black|white)\b).*",
|
||||||
|
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel design tokens instead of hardcoded colors.")
|
||||||
|
},
|
||||||
|
CssDeclarationWithSemicolon() as $decl where {
|
||||||
|
$decl <: r".*--codex-panel-.*(?:accent|background|border|color|danger|error|faint|muted|ring|success|surface|text|warning).*: .*?(?:#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\(|\b(?:black|white)\b).*",
|
||||||
|
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel design tokens instead of hardcoded colors.")
|
||||||
|
},
|
||||||
|
CssDeclarationWithSemicolon() as $decl where {
|
||||||
|
$decl <: r".*\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\(.*",
|
||||||
|
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel design tokens instead of color functions.")
|
||||||
|
},
|
||||||
|
// Keep typography and layout dimensions on shared variables.
|
||||||
|
CssDeclarationWithSemicolon() as $decl where {
|
||||||
|
$decl <: r"\s*font-size: .*?\b\d*\.?\d+(?:px|rem|em|%)\b.*",
|
||||||
|
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel typography tokens instead of hardcoded font sizes.")
|
||||||
|
},
|
||||||
|
CssDeclarationWithSemicolon() as $decl where {
|
||||||
|
$decl <: r"\s*font-weight:\s*(?:[1-9]00|bold|bolder|lighter|normal)\s*;.*",
|
||||||
|
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel typography tokens instead of hardcoded font weights.")
|
||||||
|
},
|
||||||
|
CssDeclarationWithSemicolon() as $decl where {
|
||||||
|
$decl <: r"\s*line-height: .*?(?:\b\d*\.?\d+\b|\b\d*\.?\d+(?:px|rem|em|%)\b).*",
|
||||||
|
register_diagnostic(span=$decl, message="Use Obsidian or Codex Panel typography tokens instead of hardcoded line heights.")
|
||||||
|
},
|
||||||
|
CssDeclarationWithSemicolon() as $decl where {
|
||||||
|
$decl <: r"\s*(?:border(?:-(?:block|inline|top|right|bottom|left))?-width|border-radius|bottom|column-gap|gap|height|inset|inset-(?:block|inline)|left|margin|margin-(?:block|inline|top|right|bottom|left)|max-height|max-width|min-height|min-width|padding|padding-(?:block|inline|top|right|bottom|left)|right|row-gap|top|width): .*?\b\d*\.?\d+(?:px|rem|em)\b.*",
|
||||||
|
register_diagnostic(span=$decl, message="Prefer Obsidian or Codex Panel spacing and size tokens for layout dimensions.")
|
||||||
|
},
|
||||||
|
// Keep selectors local and predictable.
|
||||||
|
CssPseudoClassSelector() as $selector where {
|
||||||
|
$selector <: r".*:has.*",
|
||||||
|
register_diagnostic(span=$selector, message="Avoid :has() because it can cause broad selector invalidation.")
|
||||||
|
},
|
||||||
|
CssPseudoClassSelector() as $selector where {
|
||||||
|
$selector <: r".*:where[(].*(?:[.]|#|\[).*",
|
||||||
|
register_diagnostic(span=$selector, message="Do not hide class, id, or attribute selectors inside :where().")
|
||||||
|
},
|
||||||
|
CssIdSelector() as $selector where { register_diagnostic(span=$selector, message="Avoid ID selectors in Codex Panel CSS.") },
|
||||||
|
CssUniversalSelector() as $selector where {
|
||||||
|
register_diagnostic(span=$selector, message="Avoid universal selectors in Codex Panel CSS.")
|
||||||
|
},
|
||||||
|
CssAtRule() as $rule where {
|
||||||
|
$rule <: r"@keyframes ([a-zA-Z0-9_-]+)[\s\S]*"($name),
|
||||||
|
not $name <: r"codex-panel-[a-z0-9-]+",
|
||||||
|
register_diagnostic(span=$rule, message="Prefix keyframes with codex-panel-.")
|
||||||
|
}
|
||||||
|
}
|
||||||
5
scripts/grit/runtime/no-app-server-direct-rpcs.grit
Normal file
5
scripts/grit/runtime/no-app-server-direct-rpcs.grit
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
`$target.request($...)` as $stmt where {
|
||||||
|
register_diagnostic(span=$stmt, message="Keep direct app-server RPC calls behind app-server services and chat app-server adapters.", severity="error")
|
||||||
|
}
|
||||||
18
scripts/grit/runtime/no-state-module-side-effects.grit
Normal file
18
scripts/grit/runtime/no-state-module-side-effects.grit
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
or {
|
||||||
|
`Date.now()` as $stmt,
|
||||||
|
`new Date()` as $stmt,
|
||||||
|
`Math.random()` as $stmt,
|
||||||
|
`new AppServerClient($...)` as $stmt,
|
||||||
|
`new ConnectionManager($...)` as $stmt,
|
||||||
|
`new Notice($...)` as $stmt,
|
||||||
|
`setTimeout($...)` as $stmt,
|
||||||
|
`clearTimeout($...)` as $stmt,
|
||||||
|
`requestAnimationFrame($...)` as $stmt,
|
||||||
|
`document.$property` as $stmt,
|
||||||
|
`localStorage.$property` as $stmt,
|
||||||
|
`sessionStorage.$property` as $stmt
|
||||||
|
} where {
|
||||||
|
register_diagnostic(span=$stmt, message="Keep this state module deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", severity="error")
|
||||||
|
}
|
||||||
8
scripts/grit/source-shape/no-handwritten-reexports.grit
Normal file
8
scripts/grit/source-shape/no-handwritten-reexports.grit
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
or {
|
||||||
|
JsExportNamedFromClause() as $stmt,
|
||||||
|
JsExportFromClause() as $stmt
|
||||||
|
} where {
|
||||||
|
register_diagnostic(span=$stmt, message="Do not add hand-written re-exports. Import from the owning module directly.", severity="error")
|
||||||
|
}
|
||||||
5
scripts/grit/source-shape/no-misplaced-tsx.grit
Normal file
5
scripts/grit/source-shape/no-misplaced-tsx.grit
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
`$program` where {
|
||||||
|
register_diagnostic(span=$program, message="Keep TSX files in rendering-owned source folders; non-rendering source should use .ts.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
`$program` where {
|
||||||
|
register_diagnostic(span=$program, message="Keep responsibility-split source roots free of module files; add modules to the matching subfolder instead of the root.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
`const $name = new $constructor($callback)` as $stmt where {
|
||||||
|
or { $callback <: `() => $body`, $callback <: `function() { $body }` },
|
||||||
|
$body <: contains `$name`,
|
||||||
|
register_diagnostic(span=$stmt, message="Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
JsxAttribute(name=$name) as $stmt where {
|
||||||
|
or { $name <: `defaultValue`, $name <: `defaultChecked` },
|
||||||
|
register_diagnostic(span=$stmt, message="Keep Preact form state explicit with controlled value or checked props.", severity="error")
|
||||||
|
}
|
||||||
5
scripts/grit/source-shape/no-unsafe-iterator-value.grit
Normal file
5
scripts/grit/source-shape/no-unsafe-iterator-value.grit
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
language js
|
||||||
|
|
||||||
|
`$iterator.next().value` as $stmt where {
|
||||||
|
register_diagnostic(span=$stmt, message="Avoid reading iterator.next().value directly; use for...of or inspect the typed IteratorResult first.", severity="error")
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { compareVersions, isExpectedNextVersion, parseVersion, readJson } from "./utils.mjs";
|
import { compareVersions, isExpectedNextVersion, parseVersion } from "./versioning.mjs";
|
||||||
|
|
||||||
function fail(message) {
|
function fail(message) {
|
||||||
console.error(`release check failed: ${message}`);
|
console.error(`release check failed: ${message}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageJson = await readJson("package.json");
|
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
|
||||||
const packageLockJson = await readJson("package-lock.json");
|
const packageLockJson = JSON.parse(await readFile("package-lock.json", "utf8"));
|
||||||
const manifestJson = await readJson("manifest.json");
|
const manifestJson = JSON.parse(await readFile("manifest.json", "utf8"));
|
||||||
const versionsJson = await readJson("versions.json");
|
const versionsJson = JSON.parse(await readFile("versions.json", "utf8"));
|
||||||
|
|
||||||
const releaseVersion = process.env.RELEASE_VERSION || packageJson.version;
|
const releaseVersion = process.env.RELEASE_VERSION || packageJson.version;
|
||||||
const currentVersion = parseVersion(releaseVersion);
|
const currentVersion = parseVersion(releaseVersion);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue