@leemour/max-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/bin/max.d.ts +3 -0
- package/dist/bin/max.d.ts.map +1 -0
- package/dist/bin/max.js +14 -0
- package/dist/bin/max.js.map +1 -0
- package/dist/cache/driver.d.ts +38 -0
- package/dist/cache/driver.d.ts.map +1 -0
- package/dist/cache/driver.js +26 -0
- package/dist/cache/driver.js.map +1 -0
- package/dist/cache/drivers/bun-sqlite.d.ts +8 -0
- package/dist/cache/drivers/bun-sqlite.d.ts.map +1 -0
- package/dist/cache/drivers/bun-sqlite.js +23 -0
- package/dist/cache/drivers/bun-sqlite.js.map +1 -0
- package/dist/cache/drivers/node-sqlite.d.ts +8 -0
- package/dist/cache/drivers/node-sqlite.d.ts.map +1 -0
- package/dist/cache/drivers/node-sqlite.js +22 -0
- package/dist/cache/drivers/node-sqlite.js.map +1 -0
- package/dist/cache/index.d.ts +22 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +33 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/open.d.ts +14 -0
- package/dist/cache/open.d.ts.map +1 -0
- package/dist/cache/open.js +26 -0
- package/dist/cache/open.js.map +1 -0
- package/dist/cache/schema.d.ts +14 -0
- package/dist/cache/schema.d.ts.map +1 -0
- package/dist/cache/schema.js +95 -0
- package/dist/cache/schema.js.map +1 -0
- package/dist/cache/store.d.ts +32 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +153 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/client.d.ts +103 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +383 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.d.ts.map +1 -0
- package/dist/commands/account.js +24 -0
- package/dist/commands/account.js.map +1 -0
- package/dist/commands/cache.d.ts +10 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +36 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/chats.d.ts +3 -0
- package/dist/commands/chats.d.ts.map +1 -0
- package/dist/commands/chats.js +28 -0
- package/dist/commands/chats.js.map +1 -0
- package/dist/commands/contacts.d.ts +10 -0
- package/dist/commands/contacts.d.ts.map +1 -0
- package/dist/commands/contacts.js +33 -0
- package/dist/commands/contacts.js.map +1 -0
- package/dist/commands/context.d.ts +34 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +40 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/messages.d.ts +3 -0
- package/dist/commands/messages.d.ts.map +1 -0
- package/dist/commands/messages.js +62 -0
- package/dist/commands/messages.js.map +1 -0
- package/dist/commands/runs.d.ts +9 -0
- package/dist/commands/runs.d.ts.map +1 -0
- package/dist/commands/runs.js +68 -0
- package/dist/commands/runs.js.map +1 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +74 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +83 -0
- package/dist/config.js.map +1 -0
- package/dist/domain/map.d.ts +22 -0
- package/dist/domain/map.d.ts.map +1 -0
- package/dist/domain/map.js +107 -0
- package/dist/domain/map.js.map +1 -0
- package/dist/domain/models.d.ts +57 -0
- package/dist/domain/models.d.ts.map +1 -0
- package/dist/domain/models.js +7 -0
- package/dist/domain/models.js.map +1 -0
- package/dist/generated/client.generated.d.ts +30 -0
- package/dist/generated/client.generated.d.ts.map +1 -0
- package/dist/generated/client.generated.js +26 -0
- package/dist/generated/client.generated.js.map +1 -0
- package/dist/generated/opcodes.generated.d.ts +44 -0
- package/dist/generated/opcodes.generated.d.ts.map +1 -0
- package/dist/generated/opcodes.generated.js +44 -0
- package/dist/generated/opcodes.generated.js.map +1 -0
- package/dist/generated/operations.generated.d.ts +76 -0
- package/dist/generated/operations.generated.d.ts.map +1 -0
- package/dist/generated/operations.generated.js +15 -0
- package/dist/generated/operations.generated.js.map +1 -0
- package/dist/output.d.ts +22 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +34 -0
- package/dist/output.js.map +1 -0
- package/dist/profile.d.ts +33 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +60 -0
- package/dist/profile.js.map +1 -0
- package/dist/program.d.ts +29 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +130 -0
- package/dist/program.js.map +1 -0
- package/dist/protocol/connection.d.ts +60 -0
- package/dist/protocol/connection.d.ts.map +1 -0
- package/dist/protocol/connection.js +142 -0
- package/dist/protocol/connection.js.map +1 -0
- package/dist/protocol/frame.d.ts +44 -0
- package/dist/protocol/frame.d.ts.map +1 -0
- package/dist/protocol/frame.js +68 -0
- package/dist/protocol/frame.js.map +1 -0
- package/dist/runs/events.d.ts +92 -0
- package/dist/runs/events.d.ts.map +1 -0
- package/dist/runs/events.js +85 -0
- package/dist/runs/events.js.map +1 -0
- package/dist/runs/recording.d.ts +35 -0
- package/dist/runs/recording.d.ts.map +1 -0
- package/dist/runs/recording.js +53 -0
- package/dist/runs/recording.js.map +1 -0
- package/dist/runs/run.d.ts +81 -0
- package/dist/runs/run.d.ts.map +1 -0
- package/dist/runs/run.js +175 -0
- package/dist/runs/run.js.map +1 -0
- package/dist/session/handshake.d.ts +26 -0
- package/dist/session/handshake.d.ts.map +1 -0
- package/dist/session/handshake.js +32 -0
- package/dist/session/handshake.js.map +1 -0
- package/dist/session/prompt.d.ts +18 -0
- package/dist/session/prompt.d.ts.map +1 -0
- package/dist/session/prompt.js +41 -0
- package/dist/session/prompt.js.map +1 -0
- package/dist/session/store.d.ts +46 -0
- package/dist/session/store.d.ts.map +1 -0
- package/dist/session/store.js +80 -0
- package/dist/session/store.js.map +1 -0
- package/dist/spec/check.d.ts +16 -0
- package/dist/spec/check.d.ts.map +1 -0
- package/dist/spec/check.js +44 -0
- package/dist/spec/check.js.map +1 -0
- package/dist/spec/define.d.ts +95 -0
- package/dist/spec/define.d.ts.map +1 -0
- package/dist/spec/define.js +34 -0
- package/dist/spec/define.js.map +1 -0
- package/dist/spec/identity.d.ts +23 -0
- package/dist/spec/identity.d.ts.map +1 -0
- package/dist/spec/identity.js +23 -0
- package/dist/spec/identity.js.map +1 -0
- package/dist/spec/index.d.ts +15 -0
- package/dist/spec/index.d.ts.map +1 -0
- package/dist/spec/index.js +30 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/spec/operations/chats.d.ts +25 -0
- package/dist/spec/operations/chats.d.ts.map +1 -0
- package/dist/spec/operations/chats.js +49 -0
- package/dist/spec/operations/chats.js.map +1 -0
- package/dist/spec/operations/contacts.d.ts +9 -0
- package/dist/spec/operations/contacts.d.ts.map +1 -0
- package/dist/spec/operations/contacts.js +39 -0
- package/dist/spec/operations/contacts.js.map +1 -0
- package/dist/spec/operations/messages.d.ts +19 -0
- package/dist/spec/operations/messages.d.ts.map +1 -0
- package/dist/spec/operations/messages.js +37 -0
- package/dist/spec/operations/messages.js.map +1 -0
- package/dist/spec/operations/session.d.ts +69 -0
- package/dist/spec/operations/session.d.ts.map +1 -0
- package/dist/spec/operations/session.js +106 -0
- package/dist/spec/operations/session.js.map +1 -0
- package/dist/spec/scalars.d.ts +16 -0
- package/dist/spec/scalars.d.ts.map +1 -0
- package/dist/spec/scalars.js +28 -0
- package/dist/spec/scalars.js.map +1 -0
- package/dist/testing/mock-max.d.ts +29 -0
- package/dist/testing/mock-max.d.ts.map +1 -0
- package/dist/testing/mock-max.js +46 -0
- package/dist/testing/mock-max.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Viacheslav Ptsarev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# max-cli
|
|
2
|
+
|
|
3
|
+
Читать и писать в свой личный аккаунт [MAX Messenger](https://max.ru) из терминала или из
|
|
4
|
+
скрипта — чаты, сообщения, контакты, — не открывая браузер.
|
|
5
|
+
|
|
6
|
+
Сделано **в первую очередь для агентов и скриптов**: любой результат доступен как одно
|
|
7
|
+
детерминированное значение JSON, у любого отказа есть код, на который можно ветвиться, а stdout
|
|
8
|
+
никогда не несёт ничего, кроме данных. Та же дисциплина делает его приятным для человека: в
|
|
9
|
+
терминале таблицы и цвет, и ничего не происходит само.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
max session start # один раз: токен уходит в ключницу
|
|
13
|
+
max chats list --limit 5
|
|
14
|
+
max messages list "Иван Петров"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> ⚠ **Версия `0.1.0` ещё не опубликована.** Под именем `@leemour/max-cli` на npm пока нет ничего.
|
|
18
|
+
> Пока — [установка из исходников](docs/installation.md).
|
|
19
|
+
|
|
20
|
+
## Что это даёт
|
|
21
|
+
|
|
22
|
+
- **Один запуск — одна операция.** Соединиться, сделать, напечатать, отключиться. Ничего не висит
|
|
23
|
+
фоном, не держит сокет и не слушает события: команда, которая напечатала ответ и не вышла, — это
|
|
24
|
+
дефект, а не особенность.
|
|
25
|
+
- **Чтение ничего не помечает прочитанным.** В протоколе «получить историю» и «отметить
|
|
26
|
+
прочитанным» — разные операции; вторая не отправляется никогда, и это проверяется тестом, а не
|
|
27
|
+
обещается в README.
|
|
28
|
+
- **Токен не касается ни файла, ни истории оболочки.** Он спрашивается без эха или читается из
|
|
29
|
+
трубы и уходит в ключницу операционной системы. В схеме настроек нет поля, куда его можно было
|
|
30
|
+
бы положить.
|
|
31
|
+
- **Отправка, которая не врёт об исходе.** Если ответ не пришёл, это `outcome_unknown`, а не
|
|
32
|
+
«ошибка» и не «отправлено»: сообщение могло уйти. Повтор возможен только с тем же `--cid`, и
|
|
33
|
+
MAX схлопывает дубль — это измерено на реальном аккаунте, а не предположено.
|
|
34
|
+
- **Имя чата не угадывается.** Часть названия, подходящая к двум чатам, — это отказ со списком
|
|
35
|
+
кандидатов. Отправить не в тот разговор нельзя отменить.
|
|
36
|
+
- **Диагностика, в которой нет содержимого.** `--verbose` показывает по строке на запрос,
|
|
37
|
+
`--record` кладёт их в каталог запуска на 30 дней. Операция, опкод, идентификаторы, байты,
|
|
38
|
+
длительности — да. Название чата, имя, текст, телефон, токен — никогда, ни обрезанными, ни
|
|
39
|
+
хэшем.
|
|
40
|
+
- **По умолчанию не записывается ничего.** Мессенджер, который сам собирает каталог с историей
|
|
41
|
+
того, кого вы читали, — это чужая жизнь в чужом логе; запись включается флагом.
|
|
42
|
+
- **Два рантайма, и это проверяется.** Node 22+ и Bun; под обоими в CI выполняется собранная
|
|
43
|
+
команда, а не только проверка типов.
|
|
44
|
+
- **Мы выглядим как официальный клиент.** На проводе нет ни нашего имени, ни своего user-agent:
|
|
45
|
+
поля, которыми клиент представляется, копируют веб-клиент MAX.
|
|
46
|
+
|
|
47
|
+
## Содержание
|
|
48
|
+
|
|
49
|
+
- [Установка](#установка)
|
|
50
|
+
- [Вход](#вход)
|
|
51
|
+
- [Использование](#использование)
|
|
52
|
+
- [Для скриптов и агентов](#для-скриптов-и-агентов)
|
|
53
|
+
- [Документация](#документация)
|
|
54
|
+
- [Разработка](#разработка)
|
|
55
|
+
- [Лицензия](#лицензия)
|
|
56
|
+
|
|
57
|
+
## Установка
|
|
58
|
+
|
|
59
|
+
Пакет — **`@leemour/max-cli`**, команда, которую он ставит, — **`max`**. Имя без области занято
|
|
60
|
+
чужим пакетом с 2018 года.
|
|
61
|
+
|
|
62
|
+
Пока `0.1.0` не опубликован, ставится из исходников:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
git clone git@github.com:leemour/max-cli.git
|
|
66
|
+
cd max-cli && pnpm install && pnpm build && pnpm link --global
|
|
67
|
+
max --version # 0.1.0
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Нужен **Node 22 или новее**, либо **Bun 1.3+**. Подробности, переменные окружения и то, куда
|
|
71
|
+
ложатся файлы, — [docs/installation.md](docs/installation.md).
|
|
72
|
+
|
|
73
|
+
## Вход
|
|
74
|
+
|
|
75
|
+
Своего входа по номеру телефона пока нет: `max session start` импортирует токен, полученный в
|
|
76
|
+
официальном клиенте, и кладёт его в ключницу.
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
max session start # спросит токен, не отображая ввод
|
|
80
|
+
pass show max/token | max session start # или из трубы
|
|
81
|
+
max account show # кто вы
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Несколько аккаунтов — несколько профилей, и профиль называется **первым словом**, а не флагом:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
max chats list # профиль default
|
|
88
|
+
max personal chats list # профиль personal
|
|
89
|
+
export MAX_PROFILE=personal # или на всю сессию оболочки
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Использование
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
max chats list --limit 5
|
|
96
|
+
max contacts list
|
|
97
|
+
max messages list 0 --limit 50 # по id чата
|
|
98
|
+
max messages list "Иван Петров" # или по части названия
|
|
99
|
+
max messages send 0 "текст"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Что делала команда:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
max chats list --verbose # показать по строке на запрос, ничего не сохраняя
|
|
106
|
+
max chats list --record # сохранить, ничего не показывая
|
|
107
|
+
max runs list # что делалось, новое сверху
|
|
108
|
+
max runs show <id>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Локальная копия — чтобы было чем ответить без сети:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
max chats list --offline
|
|
115
|
+
max cache clear
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Полностью: [docs/usage.md](docs/usage.md). Каждая команда и каждая опция:
|
|
119
|
+
[docs/commands.md](docs/commands.md) — эта страница собирается из самой программы, поэтому описать
|
|
120
|
+
версию, которой не существует, она не может.
|
|
121
|
+
|
|
122
|
+
## Для скриптов и агентов
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
max chats list --json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`--json` — это **ровно одно значение JSON на stdout и больше ничего**: ни спиннера, ни галочки, ни
|
|
129
|
+
предупреждения. То же самое включается само, когда stdout не терминал. Ошибка уходит на stderr, а
|
|
130
|
+
stdout остаётся пустым, поэтому отказ невозможно принять за результат:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{"error":{"code":"authentication_error","message":"no session for profile \"default\" — run `max session start`"}}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Ветвиться надо по коду возврата: `4` — нет сессии, `6` — не найдено, `9` — таймаут, `14` — исход
|
|
137
|
+
неизвестен, `130` — прервано. Вся таблица — в [docs/commands.md](docs/commands.md).
|
|
138
|
+
|
|
139
|
+
## Документация
|
|
140
|
+
|
|
141
|
+
| | |
|
|
142
|
+
|---|---|
|
|
143
|
+
| [docs/installation.md](docs/installation.md) | установка, обновление, куда что ложится |
|
|
144
|
+
| [docs/usage.md](docs/usage.md) | вход, чтение, отправка, машинный режим |
|
|
145
|
+
| [docs/sessions.md](docs/sessions.md) | токен, ключница, профили |
|
|
146
|
+
| [docs/configuration.md](docs/configuration.md) | настройки, переменные, порядок разрешения |
|
|
147
|
+
| [docs/diagnostics.md](docs/diagnostics.md) | `--verbose`, `--record`, `max runs` |
|
|
148
|
+
| [docs/security.md](docs/security.md) | что пишется на диск, а что никогда |
|
|
149
|
+
| [docs/troubleshooting.md](docs/troubleshooting.md) | по симптому: что делать, когда не работает |
|
|
150
|
+
| [docs/commands.md](docs/commands.md) | каждая команда и опция — **генерируется** из программы |
|
|
151
|
+
| [docs/protocol.md](docs/protocol.md) | каждый опкод и откуда известна его форма — **генерируется** |
|
|
152
|
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | как это устроено и какие швы нельзя пересекать |
|
|
153
|
+
| [docs/DECISIONS.md](docs/DECISIONS.md) | что и почему решено — читать прежде, чем «чинить» странное |
|
|
154
|
+
| [docs/BACKLOG.md](docs/BACKLOG.md) | что осталось |
|
|
155
|
+
|
|
156
|
+
Оглавление целиком — [docs/README.md](docs/README.md).
|
|
157
|
+
|
|
158
|
+
## Разработка
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
pnpm install
|
|
162
|
+
pnpm lint && pnpm typecheck && pnpm test
|
|
163
|
+
pnpm build && pnpm smoke:bun # собранная команда под вторым рантаймом
|
|
164
|
+
pnpm generate # переписать сгенерированные файлы
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Протокол MAX неофициальный и разобран обратной инженерией: это не Bot API. Каждый опкод и форма
|
|
168
|
+
каждого сообщения объявлены один раз в `src/spec/`, а реестр, типизированный клиент и
|
|
169
|
+
[docs/protocol.md](docs/protocol.md) из них генерируются — и CI падает, если дерево устарело.
|
|
170
|
+
|
|
171
|
+
Половина, не имеющая отношения к MAX — потоки вывода, рендерер, коды ошибок, ключница, часы, —
|
|
172
|
+
вынесена в [`@leemour/cli-core`](https://github.com/leemour/cli-core) и общая с `braze-cli`.
|
|
173
|
+
|
|
174
|
+
## Лицензия
|
|
175
|
+
|
|
176
|
+
MIT — см. [LICENSE](LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max.d.ts","sourceRoot":"","sources":["../../src/bin/max.ts"],"names":[],"mappings":""}
|
package/dist/bin/max.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from "../program.js";
|
|
3
|
+
// `max chats --json | head` closes the pipe while we are still writing, and an unhandled EPIPE
|
|
4
|
+
// makes Node print a stack trace over the output of the command that just worked. A reader that
|
|
5
|
+
// stopped reading is not an error: leave quietly, as every other Unix tool does.
|
|
6
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
7
|
+
stream.on("error", (error) => {
|
|
8
|
+
if (error.code === "EPIPE")
|
|
9
|
+
process.exit(0);
|
|
10
|
+
throw error;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
14
|
+
//# sourceMappingURL=max.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max.js","sourceRoot":"","sources":["../../src/bin/max.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAEnC,+FAA+F;AAC/F,gGAAgG;AAChG,iFAAiF;AACjF,KAAK,MAAM,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACtD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE;QAClD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAC3C,MAAM,KAAK,CAAA;IACb,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,OAAO,CAAC,QAAQ,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam the two runtimes meet at.
|
|
3
|
+
*
|
|
4
|
+
* `node:sqlite` and `bun:sqlite` are different modules with different names, so a static import of
|
|
5
|
+
* either one breaks the other runtime **at load time** — before any code runs and before anything
|
|
6
|
+
* can catch it. The database is therefore handed in, the way the keyring and the clock already are
|
|
7
|
+
* (`CONVENTIONS.md`: core takes its environment as arguments).
|
|
8
|
+
*
|
|
9
|
+
* The surface is deliberately tiny. Everything above it is ordinary SQL, so replacing the driver —
|
|
10
|
+
* or adding a third — is writing thirty lines, not learning an abstraction.
|
|
11
|
+
*/
|
|
12
|
+
/** What SQLite itself can hold. Not `any`: a value it cannot store must fail to compile. */
|
|
13
|
+
export type SqlValue = string | number | bigint | null | Uint8Array;
|
|
14
|
+
export interface CacheStatement {
|
|
15
|
+
/** For writes. `changes` is how the fetch lease decides who won. */
|
|
16
|
+
run(...parameters: SqlValue[]): {
|
|
17
|
+
changes: number;
|
|
18
|
+
};
|
|
19
|
+
get(...parameters: SqlValue[]): Record<string, unknown> | undefined;
|
|
20
|
+
all(...parameters: SqlValue[]): Record<string, unknown>[];
|
|
21
|
+
}
|
|
22
|
+
export interface CacheDatabase {
|
|
23
|
+
exec(sql: string): void;
|
|
24
|
+
prepare(sql: string): CacheStatement;
|
|
25
|
+
close(): void;
|
|
26
|
+
}
|
|
27
|
+
export type OpenDatabase = (path: string) => CacheDatabase;
|
|
28
|
+
/**
|
|
29
|
+
* **Write-ahead mode, and a timeout rather than an instant failure.**
|
|
30
|
+
*
|
|
31
|
+
* Without the first, one writer freezes out every reader for the length of its write, and several
|
|
32
|
+
* `max` invocations at once is the normal case here rather than the exception. Without the second,
|
|
33
|
+
* a writer that finds the database busy fails immediately instead of waiting the moment out.
|
|
34
|
+
*
|
|
35
|
+
* `foreign_keys` is off by default in SQLite, which surprises everyone once.
|
|
36
|
+
*/
|
|
37
|
+
export declare const PRAGMAS = "\n PRAGMA journal_mode = WAL;\n PRAGMA busy_timeout = 5000;\n PRAGMA foreign_keys = ON;\n";
|
|
38
|
+
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/cache/driver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,4FAA4F;AAC5F,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,UAAU,CAAA;AAEnE,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,GAAG,CAAC,GAAG,UAAU,EAAE,QAAQ,EAAE,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACnD,GAAG,CAAC,GAAG,UAAU,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IACnE,GAAG,CAAC,GAAG,UAAU,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;CAC1D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAA;IACpC,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,aAAa,CAAA;AAE1D;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,iGAInB,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam the two runtimes meet at.
|
|
3
|
+
*
|
|
4
|
+
* `node:sqlite` and `bun:sqlite` are different modules with different names, so a static import of
|
|
5
|
+
* either one breaks the other runtime **at load time** — before any code runs and before anything
|
|
6
|
+
* can catch it. The database is therefore handed in, the way the keyring and the clock already are
|
|
7
|
+
* (`CONVENTIONS.md`: core takes its environment as arguments).
|
|
8
|
+
*
|
|
9
|
+
* The surface is deliberately tiny. Everything above it is ordinary SQL, so replacing the driver —
|
|
10
|
+
* or adding a third — is writing thirty lines, not learning an abstraction.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* **Write-ahead mode, and a timeout rather than an instant failure.**
|
|
14
|
+
*
|
|
15
|
+
* Without the first, one writer freezes out every reader for the length of its write, and several
|
|
16
|
+
* `max` invocations at once is the normal case here rather than the exception. Without the second,
|
|
17
|
+
* a writer that finds the database busy fails immediately instead of waiting the moment out.
|
|
18
|
+
*
|
|
19
|
+
* `foreign_keys` is off by default in SQLite, which surprises everyone once.
|
|
20
|
+
*/
|
|
21
|
+
export const PRAGMAS = `
|
|
22
|
+
PRAGMA journal_mode = WAL;
|
|
23
|
+
PRAGMA busy_timeout = 5000;
|
|
24
|
+
PRAGMA foreign_keys = ON;
|
|
25
|
+
`;
|
|
26
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.js","sourceRoot":"","sources":["../../src/cache/driver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAoBH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG;;;;CAItB,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
/**
|
|
3
|
+
* The specifier is a variable on purpose: written inline, TypeScript tries to resolve `bun:sqlite`
|
|
4
|
+
* and fails, because Bun's types are not installed and would only be right for one of the two
|
|
5
|
+
* runtimes anyway. The import is reached only under Bun, so Node never evaluates it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const openWithBunSqlite: (path: string) => Promise<CacheDatabase>;
|
|
8
|
+
//# sourceMappingURL=bun-sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bun-sqlite.d.ts","sourceRoot":"","sources":["../../../src/cache/drivers/bun-sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,cAAc,CAAA;AAc3D;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,aAAa,CAiB3E,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The specifier is a variable on purpose: written inline, TypeScript tries to resolve `bun:sqlite`
|
|
3
|
+
* and fails, because Bun's types are not installed and would only be right for one of the two
|
|
4
|
+
* runtimes anyway. The import is reached only under Bun, so Node never evaluates it.
|
|
5
|
+
*/
|
|
6
|
+
export const openWithBunSqlite = async (path) => {
|
|
7
|
+
const specifier = "bun:sqlite";
|
|
8
|
+
const { Database } = (await import(specifier));
|
|
9
|
+
const database = new Database(path);
|
|
10
|
+
return {
|
|
11
|
+
exec: (sql) => database.exec(sql),
|
|
12
|
+
prepare: (sql) => {
|
|
13
|
+
const statement = database.query(sql);
|
|
14
|
+
return {
|
|
15
|
+
run: (...parameters) => ({ changes: Number(statement.run(...parameters).changes) }),
|
|
16
|
+
get: (...parameters) => statement.get(...parameters),
|
|
17
|
+
all: (...parameters) => statement.all(...parameters),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
close: () => database.close(),
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=bun-sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bun-sqlite.js","sourceRoot":"","sources":["../../../src/cache/drivers/bun-sqlite.ts"],"names":[],"mappings":"AAcA;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,IAAY,EAA0B,EAAE;IAC9E,MAAM,SAAS,GAAG,YAAY,CAAA;IAC9B,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,CAAoD,CAAA;IACjG,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAA;IAEnC,OAAO;QACL,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;QACjC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACf,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YACrC,OAAO;gBACL,GAAG,EAAE,CAAC,GAAG,UAAsB,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/F,GAAG,EAAE,CAAC,GAAG,UAAsB,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,UAAU,CAAwC;gBACvG,GAAG,EAAE,CAAC,GAAG,UAAsB,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,UAAU,CAA8B;aAC9F,CAAA;QACH,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE;KAC9B,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
/**
|
|
3
|
+
* ⚠ On Node 22 the import itself prints `ExperimentalWarning: SQLite is an experimental feature`
|
|
4
|
+
* to stderr. Node 24 is silent. Left alone deliberately (`NEED-59`): suppressing warnings
|
|
5
|
+
* process-wide to hide one line costs more than it saves.
|
|
6
|
+
*/
|
|
7
|
+
export declare const openWithNodeSqlite: (path: string) => CacheDatabase;
|
|
8
|
+
//# sourceMappingURL=node-sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-sqlite.d.ts","sourceRoot":"","sources":["../../../src/cache/drivers/node-sqlite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,cAAc,CAAA;AAE3D;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,aAejD,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
/**
|
|
3
|
+
* ⚠ On Node 22 the import itself prints `ExperimentalWarning: SQLite is an experimental feature`
|
|
4
|
+
* to stderr. Node 24 is silent. Left alone deliberately (`NEED-59`): suppressing warnings
|
|
5
|
+
* process-wide to hide one line costs more than it saves.
|
|
6
|
+
*/
|
|
7
|
+
export const openWithNodeSqlite = (path) => {
|
|
8
|
+
const database = new DatabaseSync(path);
|
|
9
|
+
return {
|
|
10
|
+
exec: (sql) => database.exec(sql),
|
|
11
|
+
prepare: (sql) => {
|
|
12
|
+
const statement = database.prepare(sql);
|
|
13
|
+
return {
|
|
14
|
+
run: (...parameters) => ({ changes: Number(statement.run(...parameters).changes) }),
|
|
15
|
+
get: (...parameters) => statement.get(...parameters),
|
|
16
|
+
all: (...parameters) => statement.all(...parameters),
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
close: () => database.close(),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=node-sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-sqlite.js","sourceRoot":"","sources":["../../../src/cache/drivers/node-sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAG1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAY,EAAiB,EAAE;IAChE,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,CAAA;IAEvC,OAAO;QACL,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;QACjC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACf,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YACvC,OAAO;gBACL,GAAG,EAAE,CAAC,GAAG,UAAsB,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/F,GAAG,EAAE,CAAC,GAAG,UAAsB,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,UAAU,CAAwC;gBACvG,GAAG,EAAE,CAAC,GAAG,UAAsB,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,UAAU,CAA8B;aAC9F,CAAA;QACH,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE;KAC9B,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CacheStore } from "./store.js";
|
|
2
|
+
export type { CacheStore } from "./store.js";
|
|
3
|
+
export interface ProfileCacheOptions {
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
/**
|
|
6
|
+
* Where a cache that could not be opened is reported.
|
|
7
|
+
*
|
|
8
|
+
* ⚠ **Silence here hid the feature being entirely off for a day.** The `catch` was deliberate —
|
|
9
|
+
* a cache must never fail the command it was meant to help — but swallowing the reason meant the
|
|
10
|
+
* directory never being created looked identical to everything working. Failing quietly is
|
|
11
|
+
* right; failing *invisibly* is not.
|
|
12
|
+
*/
|
|
13
|
+
onProblem?: (message: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Opens this profile's record, **or gives up without failing the command**.
|
|
17
|
+
*
|
|
18
|
+
* The file holds message text, so the directory is `0700` and the database `0600`. It lives under
|
|
19
|
+
* the cache directory rather than the state one: deleting it must lose nothing that was typed.
|
|
20
|
+
*/
|
|
21
|
+
export declare const openProfileCache: (profile: string, { env, onProblem }?: ProfileCacheOptions) => Promise<CacheStore | undefined>;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,YAAY,CAAA;AAEvD,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,MAAM,EACf,qBAAkC,mBAAwB,KACzD,OAAO,CAAC,UAAU,GAAG,SAAS,CAehC,CAAA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolvePaths } from "@leemour/cli-core";
|
|
4
|
+
import { openCache } from "./open.js";
|
|
5
|
+
import { openStore } from "./store.js";
|
|
6
|
+
/**
|
|
7
|
+
* Opens this profile's record, **or gives up without failing the command**.
|
|
8
|
+
*
|
|
9
|
+
* The file holds message text, so the directory is `0700` and the database `0600`. It lives under
|
|
10
|
+
* the cache directory rather than the state one: deleting it must lose nothing that was typed.
|
|
11
|
+
*/
|
|
12
|
+
export const openProfileCache = async (profile, { env = process.env, onProblem } = {}) => {
|
|
13
|
+
const paths = resolvePaths({ appName: "max-cli", prefix: "MAX", env });
|
|
14
|
+
const file = join(paths.cache, `${profile}.db`);
|
|
15
|
+
try {
|
|
16
|
+
// SQLite will not create a file in a directory that does not exist, and on a fresh machine
|
|
17
|
+
// this one never does.
|
|
18
|
+
mkdirSync(paths.cache, { recursive: true, mode: 0o700 });
|
|
19
|
+
const store = openStore({ database: await openCache(file) });
|
|
20
|
+
chmodSync(file, 0o600);
|
|
21
|
+
return store;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
onProblem?.(`the local record is unavailable, so this ran against MAX: ${asReason(error)}`);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** The reason, never the path — a profile name and a home directory are nobody else's business. */
|
|
29
|
+
const asReason = (error) => {
|
|
30
|
+
const code = error?.code;
|
|
31
|
+
return typeof code === "string" ? code : error instanceof Error ? error.name : "an unknown problem";
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAA;AAiBvD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,EACnC,OAAe,EACf,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,SAAS,KAA0B,EAAE,EACzB,EAAE;IACnC,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAA;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,OAAO,KAAK,CAAC,CAAA;IAE/C,IAAI,CAAC;QACH,2FAA2F;QAC3F,uBAAuB;QACvB,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACxD,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC5D,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,SAAS,EAAE,CAAC,6DAA6D,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAC3F,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC,CAAA;AAED,mGAAmG;AACnG,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAU,EAAE;IAC1C,MAAM,IAAI,GAAI,KAA4B,EAAE,IAAI,CAAA;IAChD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAA;AACrG,CAAC,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CacheDatabase } from "./driver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Opens the cache with whichever SQLite the runtime actually has.
|
|
4
|
+
*
|
|
5
|
+
* **Both imports are dynamic, and that is the whole point.** A static `import` of
|
|
6
|
+
* `node:sqlite` fails under Bun at load time, and one of `bun:sqlite` fails under Node the same
|
|
7
|
+
* way — before any code runs, so nothing can catch it and no test can reach it. Reaching the
|
|
8
|
+
* import only inside the branch that can satisfy it is what makes one build serve both runtimes.
|
|
9
|
+
*
|
|
10
|
+
* Measured 2026-09-19: `node:sqlite` exists on Node 22.23.2 and 24 and not in Bun; `bun:sqlite`
|
|
11
|
+
* is the reverse (`NEED-11`).
|
|
12
|
+
*/
|
|
13
|
+
export declare const openCache: (path: string) => Promise<CacheDatabase>;
|
|
14
|
+
//# sourceMappingURL=open.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/cache/open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAW,MAAM,aAAa,CAAA;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,aAAa,CAInE,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PRAGMAS } from "./driver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Opens the cache with whichever SQLite the runtime actually has.
|
|
4
|
+
*
|
|
5
|
+
* **Both imports are dynamic, and that is the whole point.** A static `import` of
|
|
6
|
+
* `node:sqlite` fails under Bun at load time, and one of `bun:sqlite` fails under Node the same
|
|
7
|
+
* way — before any code runs, so nothing can catch it and no test can reach it. Reaching the
|
|
8
|
+
* import only inside the branch that can satisfy it is what makes one build serve both runtimes.
|
|
9
|
+
*
|
|
10
|
+
* Measured 2026-09-19: `node:sqlite` exists on Node 22.23.2 and 24 and not in Bun; `bun:sqlite`
|
|
11
|
+
* is the reverse (`NEED-11`).
|
|
12
|
+
*/
|
|
13
|
+
export const openCache = async (path) => {
|
|
14
|
+
const database = "Bun" in globalThis ? await openUnderBun(path) : await openUnderNode(path);
|
|
15
|
+
database.exec(PRAGMAS);
|
|
16
|
+
return database;
|
|
17
|
+
};
|
|
18
|
+
const openUnderBun = async (path) => {
|
|
19
|
+
const { openWithBunSqlite } = await import("./drivers/bun-sqlite.js");
|
|
20
|
+
return await openWithBunSqlite(path);
|
|
21
|
+
};
|
|
22
|
+
const openUnderNode = async (path) => {
|
|
23
|
+
const { openWithNodeSqlite } = await import("./drivers/node-sqlite.js");
|
|
24
|
+
return openWithNodeSqlite(path);
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=open.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"open.js","sourceRoot":"","sources":["../../src/cache/open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,OAAO,EAAE,MAAM,aAAa,CAAA;AAEzD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,IAAY,EAA0B,EAAE;IACtE,MAAM,QAAQ,GAAG,KAAK,IAAI,UAAU,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAA;IAC3F,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACtB,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,YAAY,GAAG,KAAK,EAAE,IAAY,EAA0B,EAAE;IAClE,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAA;IACrE,OAAO,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAA;AACtC,CAAC,CAAA;AAED,MAAM,aAAa,GAAG,KAAK,EAAE,IAAY,EAA0B,EAAE;IACnE,MAAM,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;IACvE,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAA;AACjC,CAAC,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CacheDatabase } from "./driver.js";
|
|
2
|
+
/**
|
|
3
|
+
* **Raise this on every change to the statements below.** The second schema change is the one that
|
|
4
|
+
* corrupts somebody's file, because the first is always made while the only copy is your own.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SCHEMA_VERSION = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Brings a file up to date, and **refuses a file from the future** rather than writing to it.
|
|
9
|
+
*
|
|
10
|
+
* A newer `max` may have added a column this one does not know about. Reading it is survivable;
|
|
11
|
+
* writing to it is how one version quietly destroys what another stored.
|
|
12
|
+
*/
|
|
13
|
+
export declare const migrate: (database: CacheDatabase) => void;
|
|
14
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEhD;;;GAGG;AACH,eAAO,MAAM,cAAc,IAAI,CAAA;AAiF/B;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,UAAU,aAAa,KAAG,IAcjD,CAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Raise this on every change to the statements below.** The second schema change is the one that
|
|
3
|
+
* corrupts somebody's file, because the first is always made while the only copy is your own.
|
|
4
|
+
*/
|
|
5
|
+
export const SCHEMA_VERSION = 1;
|
|
6
|
+
/**
|
|
7
|
+
* Everything the cache holds, and the indexes are part of it rather than an afterthought — each
|
|
8
|
+
* one exists for a query that is actually made.
|
|
9
|
+
*
|
|
10
|
+
* **Times are epoch milliseconds**, the way MAX sends them: an integer sorts, compares and ranges
|
|
11
|
+
* correctly without a format to agree on. The domain model's ISO strings are built on the way out.
|
|
12
|
+
*
|
|
13
|
+
* Ids are text even though MAX sends numbers. They are 64-bit, they are identifiers rather than
|
|
14
|
+
* arithmetic, and `INTEGER` here would put the rounding this project already fixed back again.
|
|
15
|
+
*/
|
|
16
|
+
const STATEMENTS = [
|
|
17
|
+
`CREATE TABLE IF NOT EXISTS chats (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
title TEXT,
|
|
20
|
+
kind TEXT NOT NULL,
|
|
21
|
+
unread_count INTEGER,
|
|
22
|
+
last_message_at INTEGER,
|
|
23
|
+
participants_count INTEGER,
|
|
24
|
+
generation INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
fetched_at INTEGER NOT NULL
|
|
26
|
+
)`,
|
|
27
|
+
`CREATE INDEX IF NOT EXISTS chats_by_recency ON chats (last_message_at DESC)`,
|
|
28
|
+
`CREATE TABLE IF NOT EXISTS contacts (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
name TEXT,
|
|
31
|
+
username TEXT,
|
|
32
|
+
description TEXT,
|
|
33
|
+
fetched_at INTEGER NOT NULL
|
|
34
|
+
)`,
|
|
35
|
+
`CREATE TABLE IF NOT EXISTS messages (
|
|
36
|
+
chat_id TEXT NOT NULL,
|
|
37
|
+
id TEXT NOT NULL,
|
|
38
|
+
sender_id TEXT,
|
|
39
|
+
sender_name TEXT,
|
|
40
|
+
time INTEGER NOT NULL,
|
|
41
|
+
update_time INTEGER,
|
|
42
|
+
text TEXT NOT NULL,
|
|
43
|
+
outgoing INTEGER,
|
|
44
|
+
attachments TEXT NOT NULL,
|
|
45
|
+
fetched_at INTEGER NOT NULL,
|
|
46
|
+
PRIMARY KEY (chat_id, id)
|
|
47
|
+
)`,
|
|
48
|
+
`CREATE INDEX IF NOT EXISTS messages_by_time ON messages (chat_id, time DESC)`,
|
|
49
|
+
`CREATE INDEX IF NOT EXISTS messages_by_update ON messages (chat_id, update_time)`,
|
|
50
|
+
/**
|
|
51
|
+
* The windows we claim to hold **completely**, which is what makes "absent means deleted" safe
|
|
52
|
+
* to believe. A message outside every range is a message we have never looked for; a message
|
|
53
|
+
* missing from inside one has been deleted.
|
|
54
|
+
*/
|
|
55
|
+
`CREATE TABLE IF NOT EXISTS ranges (
|
|
56
|
+
chat_id TEXT NOT NULL,
|
|
57
|
+
from_time INTEGER NOT NULL,
|
|
58
|
+
to_time INTEGER NOT NULL,
|
|
59
|
+
PRIMARY KEY (chat_id, from_time)
|
|
60
|
+
)`,
|
|
61
|
+
`CREATE INDEX IF NOT EXISTS ranges_by_chat ON ranges (chat_id, from_time)`,
|
|
62
|
+
/**
|
|
63
|
+
* When a whole collection was last known to be complete. A `fetched_at` on each row cannot say
|
|
64
|
+
* this: it tells you when a chat was seen, never that the list of chats has no newcomer missing
|
|
65
|
+
* from it.
|
|
66
|
+
*/
|
|
67
|
+
`CREATE TABLE IF NOT EXISTS fetched (
|
|
68
|
+
kind TEXT PRIMARY KEY,
|
|
69
|
+
at INTEGER NOT NULL
|
|
70
|
+
)`,
|
|
71
|
+
`CREATE TABLE IF NOT EXISTS fetch_lease (
|
|
72
|
+
chat_id TEXT NOT NULL,
|
|
73
|
+
anchor TEXT NOT NULL,
|
|
74
|
+
holder TEXT NOT NULL,
|
|
75
|
+
expires_at INTEGER NOT NULL,
|
|
76
|
+
PRIMARY KEY (chat_id, anchor)
|
|
77
|
+
)`,
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Brings a file up to date, and **refuses a file from the future** rather than writing to it.
|
|
81
|
+
*
|
|
82
|
+
* A newer `max` may have added a column this one does not know about. Reading it is survivable;
|
|
83
|
+
* writing to it is how one version quietly destroys what another stored.
|
|
84
|
+
*/
|
|
85
|
+
export const migrate = (database) => {
|
|
86
|
+
const current = Number(database.prepare("PRAGMA user_version").get()?.user_version ?? 0);
|
|
87
|
+
if (current > SCHEMA_VERSION) {
|
|
88
|
+
throw new Error(`this cache was written by a newer max (schema ${current}, this one speaks ${SCHEMA_VERSION}) — ` +
|
|
89
|
+
"run `max cache clear`, or use the newer version");
|
|
90
|
+
}
|
|
91
|
+
for (const statement of STATEMENTS)
|
|
92
|
+
database.exec(statement);
|
|
93
|
+
database.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/cache/schema.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAA;AAE/B;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG;IACjB;;;;;;;;;KASG;IACH,6EAA6E;IAE7E;;;;;;KAMG;IAEH;;;;;;;;;;;;KAYG;IACH,8EAA8E;IAC9E,kFAAkF;IAElF;;;;OAIG;IACH;;;;;KAKG;IACH,0EAA0E;IAE1E;;;;OAIG;IACH;;;KAGG;IAEH;;;;;;KAMG;CACJ,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,QAAuB,EAAQ,EAAE;IACvD,MAAM,OAAO,GAAG,MAAM,CACnB,QAAQ,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,GAAG,EAAgC,EAAE,YAAY,IAAI,CAAC,CAChG,CAAA;IAED,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,iDAAiD,OAAO,qBAAqB,cAAc,MAAM;YAC/F,iDAAiD,CACpD,CAAA;IACH,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,UAAU;QAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC5D,QAAQ,CAAC,IAAI,CAAC,yBAAyB,cAAc,EAAE,CAAC,CAAA;AAC1D,CAAC,CAAA"}
|