@tgcloud/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 +53 -0
- package/bin/tgcloud.js +2 -0
- package/package.json +36 -0
- package/src/api/client.js +136 -0
- package/src/api/endpoints.js +124 -0
- package/src/cli.js +51 -0
- package/src/commands/add.js +133 -0
- package/src/commands/completion.js +298 -0
- package/src/commands/deploy.js +319 -0
- package/src/commands/diff.js +57 -0
- package/src/commands/fetch.js +23 -0
- package/src/commands/init.js +226 -0
- package/src/commands/login.js +56 -0
- package/src/commands/migrate.js +142 -0
- package/src/commands/pull.js +156 -0
- package/src/commands/reset.js +95 -0
- package/src/commands/run.js +145 -0
- package/src/commands/status.js +72 -0
- package/src/commands/webhook.js +139 -0
- package/src/core/capabilities.js +83 -0
- package/src/core/credentials.js +229 -0
- package/src/core/db-changes.js +221 -0
- package/src/core/file-diff.js +11 -0
- package/src/core/hasher.js +11 -0
- package/src/core/local-diff.js +57 -0
- package/src/core/migration-flow.js +356 -0
- package/src/core/project-layout.js +294 -0
- package/src/core/project-root.js +47 -0
- package/src/core/run-output.js +134 -0
- package/src/core/scanner.js +115 -0
- package/src/core/snapshot.js +132 -0
- package/src/core/state.js +92 -0
- package/src/core/sync.js +50 -0
- package/src/templates/AGENTS.md +95 -0
- package/src/templates/docs/tgcloud-sdk.md +347 -0
- package/src/templates/gitignore +2 -0
- package/src/templates/handlers/_default_.js +6 -0
- package/src/templates/handlers/message.js +13 -0
- package/src/templates/lib/_default_.js +2 -0
- package/src/templates/schema.js +10 -0
- package/src/utils/logger.js +54 -0
- package/src/utils/pager.js +76 -0
- package/src/utils/prompt.js +220 -0
- package/src/utils/spinner.js +5 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# tgcloud SDK reference
|
|
2
|
+
|
|
3
|
+
Everything your bot's modules can import from `sdk`: the database (`db`), the
|
|
4
|
+
Telegram Bot API (`api`), HTTP (`fetch`), and `console` logging. The database is
|
|
5
|
+
the largest surface, so it comes first; `api`, `fetch`, and `console` are at the
|
|
6
|
+
end.
|
|
7
|
+
|
|
8
|
+
> Orientation and project rules live in [../AGENTS.md](../AGENTS.md). This file is
|
|
9
|
+
> the API reference.
|
|
10
|
+
|
|
11
|
+
## `sdk` at a glance
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { db, api, fetch, BotApiError } from 'sdk'
|
|
15
|
+
// or from submodules:
|
|
16
|
+
import { table, integer, text, eq, sql } from 'sdk/db'
|
|
17
|
+
import { api } from 'sdk/api'
|
|
18
|
+
import { fetch } from 'sdk/fetch'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **`db`** — SQLite query builder + schema DSL. → most of this file.
|
|
22
|
+
- **`api`** — Telegram Bot API (`api.sendMessage(...)`). → *Telegram Bot API* below.
|
|
23
|
+
- **`fetch`** — outbound HTTP. → *HTTP* below.
|
|
24
|
+
- **`console`** — logging that surfaces in `npx tgcloud run` output. → *Logging* below.
|
|
25
|
+
|
|
26
|
+
## Rules that bite
|
|
27
|
+
|
|
28
|
+
The runtime is not stock Node — these defaults don't hold. Detail is in the
|
|
29
|
+
sections below; project rules are in [../AGENTS.md](../AGENTS.md).
|
|
30
|
+
|
|
31
|
+
- **Import by bare module name** — `from 'schema'`, `from 'lib/cart'`, `from 'sdk/db'`. Never a relative path or a `.js` extension.
|
|
32
|
+
- **Every DB call is async — always `await`.** `.all()`/`.get()`/`.values()`/`.run()`, `db.$count()`, and raw `db.run/all/get` all return Promises. A forgotten `await` returns the builder, not rows.
|
|
33
|
+
- **No foreign keys.** `.references()` and table-level `foreignKey()` **throw when declared** — a schema using them won't deploy. Enforce integrity in app code.
|
|
34
|
+
- **Drops happen only via `.deprecated('reason')`** on a column/table/index. Deleting the declaration does *not* drop it; type changes are manual (`db.run(...)`).
|
|
35
|
+
- **A handler's `export default (input, ctx)`** gets the update's *payload* as `input` — `handlers/message` receives the `Message` (i.e. `update.message`), `handlers/callback_query` the `CallbackQuery`. The full `Update` (with `update_id`) is `ctx.update`.
|
|
36
|
+
|
|
37
|
+
# Database (`db`)
|
|
38
|
+
|
|
39
|
+
## Importing
|
|
40
|
+
|
|
41
|
+
`db` (the default export of `sdk/db`) holds the whole API; the same functions are
|
|
42
|
+
also named exports.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { table, integer, text, eq, sql } from 'sdk/db' // named imports
|
|
46
|
+
import db, { sql } from 'sdk/db' // or namespace + names
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Import your own modules by **bare name** — `from 'schema'`, `from 'lib/cart'` —
|
|
50
|
+
never a relative path or `.js` extension.
|
|
51
|
+
|
|
52
|
+
## Defining the schema (`schema.js`)
|
|
53
|
+
|
|
54
|
+
Tables are **named exports**. `table()` builds a descriptor at runtime (no DB call);
|
|
55
|
+
the platform discovers the exported tables and runs migrations when you deploy
|
|
56
|
+
`schema.js`.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { table, integer, text, boolean, json, index, check, sql } from 'sdk/db'
|
|
60
|
+
|
|
61
|
+
export const users = table('users', {
|
|
62
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
63
|
+
tgId: integer('tg_id').unique(),
|
|
64
|
+
name: text('name').notNull(),
|
|
65
|
+
lang: text('lang').default('en'),
|
|
66
|
+
isAdmin: boolean('is_admin').default(false),
|
|
67
|
+
prefs: json('prefs'),
|
|
68
|
+
created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
|
|
69
|
+
}, (t) => ({
|
|
70
|
+
createdIdx: index('idx_users_created').on(t.created),
|
|
71
|
+
}))
|
|
72
|
+
|
|
73
|
+
export const todos = table('todos', {
|
|
74
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
75
|
+
userId: integer('user_id').notNull(), // logical link to users.id — FKs are NOT enforced
|
|
76
|
+
text: text('text').notNull(),
|
|
77
|
+
done: boolean('done').default(false),
|
|
78
|
+
priority: integer('priority').default(0),
|
|
79
|
+
}, (t) => ({
|
|
80
|
+
userDoneIdx: index('idx_todos_user_done').on(t.userId, t.done),
|
|
81
|
+
priorityCheck: check('priority_check', sql`${t.priority} >= 0`),
|
|
82
|
+
}))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The third argument to `table()` is a callback `(t) => ({...})` where `t` exposes the
|
|
86
|
+
columns (`t.userId` → a column ref); declare indexes and table-level constraints there.
|
|
87
|
+
|
|
88
|
+
## Column types
|
|
89
|
+
|
|
90
|
+
| factory | SQLite | notes |
|
|
91
|
+
|-------------|--------|------------------------------------------------|
|
|
92
|
+
| `text()` | TEXT | |
|
|
93
|
+
| `integer()` | INTEGER| |
|
|
94
|
+
| `real()` | REAL | alias `float()` |
|
|
95
|
+
| `numeric()` | NUMERIC| |
|
|
96
|
+
| `blob()` | BLOB | takes/returns `Uint8Array` |
|
|
97
|
+
| `boolean()` | INTEGER| stored 0/1, read as `true`/`false` |
|
|
98
|
+
| `json()` | TEXT | auto `JSON.stringify` / `JSON.parse` |
|
|
99
|
+
|
|
100
|
+
Signature: `integer(name?, opts?)`. The name is optional — if omitted it's taken
|
|
101
|
+
from the JS key. `opts.mode` sets runtime conversion:
|
|
102
|
+
|
|
103
|
+
| mode | stored as | JS value |
|
|
104
|
+
|----------------|--------------------|-----------------------|
|
|
105
|
+
| `boolean` | INTEGER 0/1 | `boolean` |
|
|
106
|
+
| `json` | TEXT (JSON) | any object/array |
|
|
107
|
+
| `timestamp` | INTEGER (unix sec) | `Date` |
|
|
108
|
+
| `timestamp_ms` | INTEGER (unix ms) | `Date` |
|
|
109
|
+
| `bytes` | BLOB | `Uint8Array` |
|
|
110
|
+
|
|
111
|
+
`boolean()` and `json()` are sugar over `integer(name, { mode: 'boolean' })` and
|
|
112
|
+
`text(name, { mode: 'json' })`. A `blob()` column takes and returns a `Uint8Array`
|
|
113
|
+
automatically — that's handled at the wire level, so it needs no mode.
|
|
114
|
+
|
|
115
|
+
## Column modifiers (chainable)
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
integer('id').primaryKey()
|
|
119
|
+
integer('id').primaryKey({ autoIncrement: true })
|
|
120
|
+
text('name').notNull()
|
|
121
|
+
text('tg').unique()
|
|
122
|
+
text('lang').default('en')
|
|
123
|
+
integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`)
|
|
124
|
+
text('slug').generatedAlwaysAs(sql`lower(name)`, { mode: 'stored' }) // 'virtual' | 'stored'
|
|
125
|
+
text('name').constraint('COLLATE NOCASE') // arbitrary column-level DDL
|
|
126
|
+
text('email').deprecated('replaced by login') // marks for drop; invisible at runtime
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- `.default()` on a `json()` column encodes the value automatically; a `` sql`...` ``
|
|
130
|
+
default is passed through verbatim.
|
|
131
|
+
- `.deprecated()` is terminal — don't chain methods after it.
|
|
132
|
+
|
|
133
|
+
## No foreign keys
|
|
134
|
+
|
|
135
|
+
The runtime runs with `PRAGMA foreign_keys` **off**, so a declared FK would be
|
|
136
|
+
silently inert (no cascades, no orphan protection). To make that impossible,
|
|
137
|
+
`.references()` and table-level `foreignKey()` **throw when declared** — a schema
|
|
138
|
+
using them won't deploy. `REFERENCES`/`FOREIGN KEY` smuggled in via `.constraint()`
|
|
139
|
+
or a raw `db.run('CREATE TABLE …')` stay inert too. Enforce integrity in app code:
|
|
140
|
+
delete children before parents, insert parents before children, sweep orphans with
|
|
141
|
+
`LEFT JOIN … WHERE parent.id IS NULL`.
|
|
142
|
+
|
|
143
|
+
## Table-level constraints & indexes (in the callback)
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
table('t', { ... }, (t) => ({
|
|
147
|
+
pk: primaryKey({ columns: [t.a, t.b] }),
|
|
148
|
+
uq: unique('uq_email').on(t.email),
|
|
149
|
+
chk: check('chk_done', sql`${t.done} in (0, 1)`),
|
|
150
|
+
idx: index('idx_name').on(t.col),
|
|
151
|
+
uidx: uniqueIndex('uidx_email').on(t.email),
|
|
152
|
+
lower: index('idx_lower').on(sql`lower(${t.email})`), // expression index
|
|
153
|
+
active:index('idx_active').on(t.userId).where(sql`done = 0`), // partial index
|
|
154
|
+
// foreignKey(...) is NOT supported — it throws (see "No foreign keys")
|
|
155
|
+
}))
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Table modifiers (chained after `table(...)`): `.strict()`, `.withoutRowid()`,
|
|
159
|
+
`.constraint('CHECK (x > 0)', 'chk_x')`, `.deprecated('reason')`.
|
|
160
|
+
|
|
161
|
+
## Query builder
|
|
162
|
+
|
|
163
|
+
### select
|
|
164
|
+
|
|
165
|
+
`select(projection?)` → `.from(table)` → builder. No projection = `SELECT *`.
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
await db.select().from(todos).all() // all rows
|
|
169
|
+
await db.select().from(todos).where(eq(todos.id, 1)).get() // first row or null
|
|
170
|
+
await db.$count(todos) // COUNT(*)
|
|
171
|
+
|
|
172
|
+
await db.select().from(todos)
|
|
173
|
+
.where(and(eq(todos.userId, uid), eq(todos.done, false)))
|
|
174
|
+
.orderBy(desc(todos.priority), asc(todos.id))
|
|
175
|
+
.limit(10).offset(20)
|
|
176
|
+
.all()
|
|
177
|
+
|
|
178
|
+
// projection: { alias: colRef | sqlExpr }
|
|
179
|
+
await db.select({ id: todos.id, title: todos.text, n: sql`count(*)` })
|
|
180
|
+
.from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Chain: `.where()` `.orderBy()` `.limit()` `.offset()` `.groupBy()` `.having()`
|
|
184
|
+
`.distinct()`; terminals `.all()` / `.get()` / `.values()`.
|
|
185
|
+
|
|
186
|
+
The builder is **awaitable** — `await db.select().from(todos)` (with `.where(…)`
|
|
187
|
+
etc. as needed) runs `.all()` and resolves to the row array, so `.all()` is
|
|
188
|
+
optional. Use `.get()`/`.values()` for the other shapes; count rows with
|
|
189
|
+
`db.$count(table, where?)`.
|
|
190
|
+
|
|
191
|
+
### insert / update / delete
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
await db.insert(todos).values({ userId: 1, text: 'Buy milk' }).run()
|
|
195
|
+
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run() // batch
|
|
196
|
+
await db.insert(todos).values({ text: 'X' }).returning().run() // RETURNING *
|
|
197
|
+
|
|
198
|
+
await db.insert(users).values({ tgId: 42, name: 'Ann' })
|
|
199
|
+
.onConflictDoNothing({ target: users.tgId }).run()
|
|
200
|
+
await db.insert(users).values({ tgId: 42, name: 'Ann' })
|
|
201
|
+
.onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run()
|
|
202
|
+
|
|
203
|
+
await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run() // .set() required
|
|
204
|
+
await db.delete(todos).where(eq(todos.id, 1)).run()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
A plain insert/update/delete resolves to `[]` — there's no insert id or row count.
|
|
208
|
+
Add `.returning()` (→ `RETURNING *`) or `.returning({ id: todos.id })` to get the
|
|
209
|
+
affected rows back (converted, since they're bound to the table). Like select,
|
|
210
|
+
these builders are **awaitable**: `await db.insert(todos).values({ … })` runs
|
|
211
|
+
without an explicit `.run()`.
|
|
212
|
+
|
|
213
|
+
### Raw SQL — `db.run` / `db.all` / `db.get`
|
|
214
|
+
|
|
215
|
+
Mode is by method, not auto-detected: `run` = write/exec, `all` = all rows,
|
|
216
|
+
`get`/`one` = first row (or `null`). Each takes a `` sql`…` `` object or a
|
|
217
|
+
`(queryString, params)` pair.
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
await db.run('UPDATE todos SET done = 1 WHERE id = :id', { ':id': 5 })
|
|
221
|
+
await db.all(sql`SELECT * FROM todos WHERE done = ${false}`)
|
|
222
|
+
await db.get(sql`SELECT count(*) AS c FROM todos`)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
> Raw methods are **not bound to a table**, so rows come back without mode
|
|
226
|
+
> conversion (boolean/json/timestamp arrive as 0/1, a JSON string, unix seconds).
|
|
227
|
+
> Only the table-bound builder converts.
|
|
228
|
+
|
|
229
|
+
`db.raw.read(sql, params)` / `db.raw.write(sql, params)` are the low-level escape
|
|
230
|
+
hatch (params default `{}`); prefer `run`/`all`/`get` above.
|
|
231
|
+
|
|
232
|
+
## `sql` tagged template
|
|
233
|
+
|
|
234
|
+
`` sql`...` `` interpolates values as named parameters, column refs as identifiers,
|
|
235
|
+
and splices nested `sql`.
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
sql`count = ${n}` // count = :p1
|
|
239
|
+
sql`${todos.priority} > ${min}` // priority > :p2 (column as identifier)
|
|
240
|
+
sql`WHERE ${cond}` // nested sql spliced in
|
|
241
|
+
sql.raw('datetime("now")') // literal, no parameters
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
In DDL contexts (DEFAULT / CHECK / GENERATED) parameters don't work — use literal SQL.
|
|
245
|
+
|
|
246
|
+
## Operators
|
|
247
|
+
|
|
248
|
+
Import from `sdk/db`:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
import {
|
|
252
|
+
eq, ne, gt, gte, lt, lte,
|
|
253
|
+
like, notLike,
|
|
254
|
+
isNull, isNotNull, and, or, not,
|
|
255
|
+
between, notBetween, inArray, notInArray,
|
|
256
|
+
count, sum, avg, min, max,
|
|
257
|
+
asc, desc,
|
|
258
|
+
} from 'sdk/db'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`.where(e1, e2)` with multiple args is equivalent to `and(e1, e2)`.
|
|
262
|
+
|
|
263
|
+
## Migrations (summary)
|
|
264
|
+
|
|
265
|
+
- **Adding** columns/tables/indexes — applied automatically on `npx tgcloud migrate`
|
|
266
|
+
after you deploy `schema.js`.
|
|
267
|
+
- **Dropping** — only via `.deprecated()`; the CLI shows what will be removed.
|
|
268
|
+
- **Changing a column's type** — not automatic; do it manually with `db.run`.
|
|
269
|
+
|
|
270
|
+
`npx tgcloud push` reports pending DB changes but never applies them. Run
|
|
271
|
+
`npx tgcloud migrate` to apply.
|
|
272
|
+
|
|
273
|
+
# Telegram Bot API (`api`)
|
|
274
|
+
|
|
275
|
+
`import { api, BotApiError } from 'sdk'`. Call any Bot API method as
|
|
276
|
+
`api.<method>(params)` — a Proxy dispatches the name, so every current (and
|
|
277
|
+
future) method works with no SDK update.
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
const me = await api.getMe() // → the unwrapped `result`
|
|
281
|
+
await api.sendMessage({ chat_id: id, text: 'Hello!' })
|
|
282
|
+
await api.editMessageText({ chat_id, message_id, text: 'Updated' })
|
|
283
|
+
await api.answerCallbackQuery({ callback_query_id, text: 'Done' })
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- **The envelope is unwrapped.** On `{ ok: true }` the call resolves to `result`
|
|
287
|
+
directly — no `.ok`/`.result` wrapper. Params are one object using the Bot API's
|
|
288
|
+
snake_case names (`chat_id`, `message_id`, …).
|
|
289
|
+
- **Failures throw `BotApiError`.** On `{ ok: false }` it throws; the error carries
|
|
290
|
+
`.code` (Bot API `error_code` — 400/403/429/…), `.description`, `.method`, and
|
|
291
|
+
`.parameters` (e.g. `retry_after` on 429, `migrate_to_chat_id`). Catch and inspect
|
|
292
|
+
`.code` to handle an expected failure:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
try {
|
|
296
|
+
await api.deleteMessage({ chat_id, message_id });
|
|
297
|
+
} catch (e) {
|
|
298
|
+
if (e.code !== 400) throw e; // 400 = already gone; anything else is a real error
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
# HTTP (`fetch`)
|
|
303
|
+
|
|
304
|
+
`import { fetch } from 'sdk'`. A `fetch`-like client for outbound HTTP.
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
const res = await fetch('https://api.example.com/users', {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
headers: { 'Content-Type': 'application/json' },
|
|
310
|
+
body: JSON.stringify({ name: 'Alice' }),
|
|
311
|
+
});
|
|
312
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
313
|
+
const data = await res.json();
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The response: `res.status`, `res.statusText`, `res.ok` (true for 200–299),
|
|
317
|
+
`res.url`, `res.headers` (`.get()` / `.has()` / `.keys()` / `.entries()`), and body
|
|
318
|
+
readers `await res.json()` / `await res.text()`. Or stream:
|
|
319
|
+
`for await (const chunk of res.body)`.
|
|
320
|
+
|
|
321
|
+
Body helpers set the matching `Content-Type` for you:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
await fetch(url, { method: 'POST', body: fetch.body.json({ a: 1 }) }); // application/json
|
|
325
|
+
await fetch(url, { method: 'POST', body: fetch.body.form({ a: 1 }) }); // x-www-form-urlencoded
|
|
326
|
+
await fetch(url, { method: 'POST', body: fetch.body.text('hi') }); // text/plain
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Notes: a body can be read **once** — a second `.json()`/`.text()`/stream throws
|
|
330
|
+
`TypeError: body used already` (check `res.bodyUsed`). Redirects are followed
|
|
331
|
+
automatically (web-parity) — `res.url` is the final URL after any hops. A 404 (or
|
|
332
|
+
any HTTP status) resolves normally with `res.ok === false`; only real network
|
|
333
|
+
errors (bad host, invalid URL) reject.
|
|
334
|
+
|
|
335
|
+
# Logging (`console`)
|
|
336
|
+
|
|
337
|
+
Plain `console` works, and its output shows up in `npx tgcloud run`:
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
console.log('processing', { chatId: id }); // log / debug — plain
|
|
341
|
+
console.info('started'); // info — blue
|
|
342
|
+
console.warn('rate limited'); // warn — yellow
|
|
343
|
+
console.error(err); // error — red, includes a stack
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Each line is tagged with its `[file:line]`. `console.error` and `console.trace`
|
|
347
|
+
append a full stack trace; `console.warn` does not.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// handlers/message.js — runs on each `message` update. The file name is the
|
|
2
|
+
// Telegram update type; the platform calls the default export with the matching
|
|
3
|
+
// payload (here, a Message). The full update is available as `ctx.update`.
|
|
4
|
+
|
|
5
|
+
import { api } from 'sdk';
|
|
6
|
+
|
|
7
|
+
export default async function (message, ctx) {
|
|
8
|
+
// Echo the text back — replace with your own logic.
|
|
9
|
+
await api.sendMessage({
|
|
10
|
+
chat_id: message.chat.id,
|
|
11
|
+
text: `You said: ${message.text ?? '(no text)'}`,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { table, integer, text } from 'sdk/db';
|
|
2
|
+
|
|
3
|
+
// Your database tables go here as named exports. `npx tgcloud push` deploys this
|
|
4
|
+
// file; `npx tgcloud migrate` applies the changes to the database.
|
|
5
|
+
|
|
6
|
+
// Uncomment to define your first table:
|
|
7
|
+
// export const users = table('users', {
|
|
8
|
+
// id: integer('id').primaryKey({ autoIncrement: true }),
|
|
9
|
+
// name: text('name').notNull(),
|
|
10
|
+
// });
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function success(msg) {
|
|
4
|
+
console.log(chalk.green(' ✓ ') + msg);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Non-fatal error line. Convention: only the "Error:" word is colored, the
|
|
8
|
+
// message stays default — matches fatal(). Glyphs (✓/✗) are reserved for
|
|
9
|
+
// formatted lists (created files, deploy status), not message-level output.
|
|
10
|
+
export function error(msg) {
|
|
11
|
+
// Errors go to stderr (like fatal()), so a script capturing stdout doesn't mix
|
|
12
|
+
// error text into normal output.
|
|
13
|
+
console.error(chalk.red('Error: ') + msg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function warn(msg) {
|
|
17
|
+
console.log(chalk.yellow('Warning: ') + msg);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function info(msg) {
|
|
21
|
+
console.log(msg);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function modified(file) {
|
|
25
|
+
console.log(chalk.yellow(' ↑ ') + file + chalk.dim(' — modified'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function newFile(file) {
|
|
29
|
+
console.log(chalk.green(' + ') + file + chalk.dim(' — new'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function unchanged(file) {
|
|
33
|
+
console.log(chalk.green(' ✓ ') + file + chalk.dim(' — unchanged'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function deleted(file) {
|
|
37
|
+
console.log(chalk.red(' - ') + file + chalk.dim(' — deleted'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function skipped(file, reason) {
|
|
41
|
+
console.log(chalk.red(' ✗ ') + file + chalk.dim(` — ${reason}`));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A completed per-file action: green ✓, the path, then a dim "— status" suffix.
|
|
45
|
+
// Pairs with skipped() (red ✗). Use for deploy/pull/reset result lines so the
|
|
46
|
+
// status reads as dim metadata, consistent with modified()/newFile()/deleted().
|
|
47
|
+
export function done(file, status) {
|
|
48
|
+
console.log(chalk.green(' ✓ ') + file + (status ? chalk.dim(` — ${status}`) : ''));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function fatal(msg) {
|
|
52
|
+
console.error(chalk.red('Error: ') + msg);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Launch a pager (like git does) if:
|
|
5
|
+
* - stdout is a TTY,
|
|
6
|
+
* - not explicitly disabled (via options.disabled),
|
|
7
|
+
* - PAGER env var is not empty / not set to something trivial.
|
|
8
|
+
*
|
|
9
|
+
* Returns an object with `write(str)` and async `close()`.
|
|
10
|
+
* Fallback to plain stdout if the pager can't be launched.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const pager = createPager({ disabled: options.pager === false });
|
|
14
|
+
* pager.write('line\n');
|
|
15
|
+
* await pager.close();
|
|
16
|
+
*/
|
|
17
|
+
export function createPager({ disabled = false } = {}) {
|
|
18
|
+
// Fallback: write directly to stdout, no pager involvement.
|
|
19
|
+
const stdoutFallback = {
|
|
20
|
+
write: (s) => process.stdout.write(s),
|
|
21
|
+
close: async () => {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (disabled) return stdoutFallback;
|
|
25
|
+
if (process.env.TGCLOUD_NO_PAGER === '1') return stdoutFallback;
|
|
26
|
+
if (!process.stdout.isTTY) return stdoutFallback;
|
|
27
|
+
|
|
28
|
+
// Resolve pager command.
|
|
29
|
+
// Convention: empty/unset PAGER → default to `less`. "cat" disables paging.
|
|
30
|
+
const raw = process.env.PAGER;
|
|
31
|
+
if (raw === '' || raw === 'cat') return stdoutFallback;
|
|
32
|
+
|
|
33
|
+
const cmd = raw && raw.trim() ? raw : 'less';
|
|
34
|
+
|
|
35
|
+
// Split into argv if PAGER had args; otherwise add sensible defaults for less.
|
|
36
|
+
let argv;
|
|
37
|
+
if (raw && raw.includes(' ')) {
|
|
38
|
+
const parts = raw.split(/\s+/).filter(Boolean);
|
|
39
|
+
argv = parts;
|
|
40
|
+
} else if (cmd === 'less') {
|
|
41
|
+
// -R: keep ANSI colors, -F: quit-if-one-screen, -X: no alt-screen clear
|
|
42
|
+
argv = ['less', '-R', '-F', '-X'];
|
|
43
|
+
} else {
|
|
44
|
+
argv = [cmd];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let child;
|
|
48
|
+
try {
|
|
49
|
+
child = spawn(argv[0], argv.slice(1), { stdio: ['pipe', 'inherit', 'inherit'] });
|
|
50
|
+
} catch {
|
|
51
|
+
return stdoutFallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If user quits the pager with `q` while we're still writing, stdin closes
|
|
55
|
+
// and further writes raise EPIPE. Swallow those — expected behavior.
|
|
56
|
+
let closed = false;
|
|
57
|
+
child.stdin.on('error', (err) => {
|
|
58
|
+
if (err?.code === 'EPIPE') { closed = true; return; }
|
|
59
|
+
// Other errors — leave to default handler.
|
|
60
|
+
});
|
|
61
|
+
child.on('exit', () => { closed = true; });
|
|
62
|
+
child.on('error', () => { closed = true; });
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
write: (s) => {
|
|
66
|
+
if (closed) return;
|
|
67
|
+
try { child.stdin.write(s); } catch { /* EPIPE */ }
|
|
68
|
+
},
|
|
69
|
+
close: () => new Promise((resolve) => {
|
|
70
|
+
if (closed) { resolve(); return; }
|
|
71
|
+
try { child.stdin.end(); } catch {}
|
|
72
|
+
if (child.exitCode != null) { resolve(); return; }
|
|
73
|
+
child.once('exit', resolve);
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
}
|