telstore 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -14,6 +14,8 @@ development tools; `login` asks for those, your phone number and the verificatio
14
14
  npx telstore login # once only
15
15
  npx telstore config chat @my_backups # where backups go, from now on
16
16
  npx telstore data.tar # split it and send it there
17
+ npx telstore a.tar b.tar c.tar # or several: one backup each, one after another
18
+ npx telstore ./backups # or a folder: every file one level inside it
17
19
  npx telstore list # what is already in the destination
18
20
  npx telstore restore telstore-20260905-7f3a91
19
21
  ```
@@ -23,11 +25,11 @@ npx telstore restore telstore-20260905-7f3a91
23
25
  | Command | What it does |
24
26
  |---|---|
25
27
  | `telstore login` | Log in to Telegram. Add `--token` to log in with a session token instead. |
26
- | `telstore <file>` | Split the file and upload it. Prints the `backupId` you restore with. |
28
+ | `telstore <file\|folder\|pattern>...` | Split each file and upload it. Prints the `backupId` you restore with. |
27
29
  | `telstore list` | The backups stored in the destination, newest first. |
28
- | `telstore restore <backup-id>` | Download every chunk and reassemble the file. |
29
- | `telstore delete <backup-id>` | Remove a backup's chunks and manifest from the chat, for good. |
30
- | `telstore status` | Account, destination, and unfinished backups. |
30
+ | `telstore restore <backup-id>...` | Download every chunk and reassemble the file. Several ids run one after another. |
31
+ | `telstore delete <backup-id>...` | Remove a backup's chunks and manifest from the chat, for good. Several ids are listed and confirmed once. |
32
+ | `telstore status` | Account, destination, and unfinished uploads and restores. |
31
33
  | `telstore config` | Show or change settings. |
32
34
  | `telstore token` | Print a session token for a machine you do not trust. |
33
35
  | `telstore logout` | Remove the locally stored session. |
@@ -35,7 +37,7 @@ npx telstore restore telstore-20260905-7f3a91
35
37
  ## Settings
36
38
 
37
39
  `config` writes; flags do not. A flag applies to the run you typed it on and changes nothing
38
- on disk, so `--to @elsewhere` sends one backup elsewhere without moving the destination for
40
+ on disk, so `--chat @elsewhere` sends one backup elsewhere without moving the destination for
39
41
  the next one.
40
42
 
41
43
  ```bash
@@ -46,7 +48,7 @@ npx telstore config chunkSize --unset # back to the default
46
48
 
47
49
  | Setting | Flag | Default | Meaning |
48
50
  |---|---|---|---|
49
- | `chat` | `--to` | none | `@username`, `-100123…`, or `me` |
51
+ | `chat` | `--chat` | none | `@username`, `-100123…`, or `me` |
50
52
  | `chunkSize` | `--chunk-size` | `1800MB` | e.g. `1.8GB`, `500MB`. Ceiling 1950MB. |
51
53
  | `uploadConcurrency` | `--upload-concurrency` | `32` | 512KB parts in parallel while uploading, 1–64 |
52
54
  | `downloadConcurrency` | `--download-concurrency` | `8` | 8MB slices in parallel while restoring, 1–64 |
@@ -54,7 +56,8 @@ npx telstore config chunkSize --unset # back to the default
54
56
  | `verbose` | `--verbose` | off | Show the Telegram client's own connection logs |
55
57
 
56
58
  Three flags have no setting behind them: `--out <path>` names where one restore writes,
57
- `--yes` skips the confirmation `delete` asks, and `--token` takes no value — a token written
59
+ `--yes` skips the confirmation an upload batch and `delete` ask for, and `--token` takes no
60
+ value — a token written
58
61
  on the command line would sit in `ps` and in that machine's shell history, so it is pasted at
59
62
  a prompt that does not echo.
60
63
 
@@ -74,6 +77,61 @@ telstore-20260901-9de447 photos.zip 940.3 MB 1 2026-09-01
74
77
  2 backups. Restore with: npx telstore restore <backup-id>
75
78
  ```
76
79
 
80
+ ## Several files at once
81
+
82
+ `telstore a.tar b.tar c.tar` uploads them one after another over a single connection. Each
83
+ file becomes its own backup with its own `backupId`, exactly as three separate runs would
84
+ have produced.
85
+
86
+ A **folder** stands for the files one level inside it — subfolders are named on screen and
87
+ left alone, hidden files are skipped. A **pattern** stands for the names it matches:
88
+
89
+ ```bash
90
+ npx telstore ./backups # every file directly inside ./backups
91
+ npx telstore 'logs/*.tar' # quoted, so telstore matches it rather than the shell
92
+ npx telstore logs/abc* # unquoted: your shell expands it first, same result
93
+ ```
94
+
95
+ More than one file is listed, added up and confirmed before the first byte goes out:
96
+
97
+ ```
98
+ 3 files, 4.20 GB, to https://web.telegram.org/k/#@my_backups
99
+
100
+ a.tar 1.20 GB
101
+ b.tar 2.00 GB
102
+ c.tar 1.00 GB
103
+
104
+ Upload these 3 files? [y/N]
105
+ ```
106
+
107
+ `--yes` skips the question; without a terminal to ask in, the run stops and says so rather
108
+ than reading an empty line as "no". Names that do not exist, and a file named twice, are
109
+ refused before anything is sent; a file that fails mid-transfer does not stop the ones after
110
+ it, and the run ends with a line per file and a non-zero exit code:
111
+
112
+ ```
113
+ 3 files: 2 uploaded, 1 failed.
114
+
115
+ a.tar telstore-20260905-7f3a91 (12 chunks)
116
+ b.tar failed: connection dropped mid-transfer
117
+ c.tar telstore-20260905-9de447 (1 chunk)
118
+ ```
119
+
120
+ ## Several backups at once
121
+
122
+ `restore` and `delete` take a list of ids the same way, over one connection, with a summary
123
+ and a non-zero exit code if any of them failed:
124
+
125
+ ```bash
126
+ npx telstore restore telstore-20260905-7f3a91 telstore-20260901-9de447
127
+ npx telstore delete telstore-20260905-7f3a91 telstore-20260901-9de447
128
+ ```
129
+
130
+ With several ids, `restore` writes each file under the name in its own manifest, so `--out`
131
+ — which names exactly one file — is refused rather than quietly used three times. `delete`
132
+ looks every id up first and shows the whole list before asking once; an id that is nowhere to
133
+ be found stops the run before anything is destroyed.
134
+
77
135
  ## Resuming an upload
78
136
 
79
137
  Progress lives in `~/.telstore/state/` (the 20 most recent), so running the same command
@@ -81,7 +139,18 @@ again skips the finished chunks and keeps the same `backupId`. A resumed backup
81
139
  chunk size it started with; passing a `--chunk-size` or a destination that differs from its
82
140
  own makes telstore refuse to run rather than re-cut or redirect it silently.
83
141
 
84
- **Restore keeps no state** — `Ctrl-C` mid-restore saves nothing, running again starts over.
142
+ After a batch, run telstore again with **only the files that are left**: the finished ones
143
+ have had their records cleared, so repeating the whole command would upload them a second
144
+ time as new backups. `npx telstore status` lists what is unfinished.
145
+
146
+ ## Resuming a restore
147
+
148
+ `Ctrl-C` mid-restore keeps the `<target>.partial` file rather than throwing it away. Running
149
+ the same command again hashes each chunk-sized region of it against the manifest, in order,
150
+ and carries on from the first one that does not match — nothing already on disk is trusted
151
+ just because it is there. `npx telstore status` lists unfinished restores alongside
152
+ unfinished uploads, with a resume command for each.
153
+
85
154
  And `delete` has **no undo**: Telegram is the only copy.
86
155
 
87
156
  ## Running on a machine you do not trust
package/bin/telstore.js CHANGED
@@ -14,12 +14,18 @@ const SIGINT_EXIT_CODE = 130
14
14
  let currentCommand = null
15
15
  let currentBackupId = null
16
16
 
17
+ // A batch clears each finished item's record as it goes, so by the time Ctrl-C lands these
18
+ // are transfers no second run should touch. Ctrl-C needs their names to say so.
19
+ const finished = []
20
+
17
21
  process.on('SIGINT', () => {
18
22
  // A passphrase prompt has stdin in raw mode, and process.exit skips readline's own cleanup.
19
23
  // Without this, Ctrl-C hands back a shell that no longer echoes what is typed into it.
20
24
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
21
25
 
22
- process.stderr.write(interruptMessage(currentCommand, { backupId: currentBackupId }))
26
+ process.stderr.write(
27
+ interruptMessage(currentCommand, { backupId: currentBackupId, done: finished }),
28
+ )
23
29
  process.exit(SIGINT_EXIT_CODE)
24
30
  })
25
31
 
@@ -88,13 +94,23 @@ async function main() {
88
94
  }
89
95
 
90
96
  case 'upload': {
91
- const { runUpload } = await import('../src/commands/upload.js')
97
+ const { runUploads } = await import('../src/commands/upload.js')
92
98
 
93
- await runUpload(parsed.args[0], parsed.options, {
99
+ const { failed } = await runUploads(parsed.args, parsed.options, {
100
+ // Only route saw the command line, and an unquoted note is told apart from a plain
101
+ // missing file by where the words sat on it.
102
+ filesAfterNote: parsed.filesAfterNote,
94
103
  onBackupId: (id) => {
95
104
  currentBackupId = id
96
105
  },
106
+ onFileDone: (file) => {
107
+ if (file.id) finished.push(file)
108
+ },
97
109
  })
110
+
111
+ // A batch reports its own failures by name and has already said so on stdout; the exit
112
+ // code is what carries that out to whatever ran telstore.
113
+ if (failed > 0) process.exitCode = 1
98
114
  return
99
115
  }
100
116
 
@@ -103,9 +119,18 @@ async function main() {
103
119
  throw new Error('Missing backup id. Example: npx telstore restore telstore-20260905-7f3a91')
104
120
  }
105
121
 
106
- const { runRestore } = await import('../src/commands/restore.js')
122
+ const { runRestores } = await import('../src/commands/restore.js')
123
+
124
+ const { failed } = await runRestores(parsed.args, parsed.options, {
125
+ onBackupId: (id) => {
126
+ currentBackupId = id
127
+ },
128
+ onRestoreDone: (item) => {
129
+ finished.push(item)
130
+ },
131
+ })
107
132
 
108
- await runRestore(parsed.args[0], parsed.options)
133
+ if (failed > 0) process.exitCode = 1
109
134
  return
110
135
  }
111
136
 
@@ -114,9 +139,11 @@ async function main() {
114
139
  throw new Error('Missing backup id. Example: npx telstore delete telstore-20260905-7f3a91')
115
140
  }
116
141
 
117
- const { runDelete } = await import('../src/commands/delete.js')
142
+ const { runDeletes } = await import('../src/commands/delete.js')
143
+
144
+ const { failed } = await runDeletes(parsed.args, parsed.options)
118
145
 
119
- await runDelete(parsed.args[0], parsed.options)
146
+ if (failed > 0) process.exitCode = 1
120
147
  return
121
148
  }
122
149
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telstore",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Split large files into chunks and store them on Telegram",
5
5
  "keywords": [
6
6
  "telegram",
package/src/caption.js CHANGED
@@ -14,6 +14,37 @@ function oneLine(name) {
14
14
  return String(name).replace(/\s+/g, ' ').trim()
15
15
  }
16
16
 
17
+ // Telegram takes 1024 characters in a caption, and the card around the note already spends
18
+ // some of them — a file name alone may be 255. 500 leaves both room to spare.
19
+ export const MAX_NOTE_LENGTH = 500
20
+
21
+ // A note is written at a shell prompt and read in two places: the manifest body and the card
22
+ // in the chat. Folding it here, once, is what keeps those two from holding slightly different
23
+ // notes and leaving nobody able to say which one was typed.
24
+ export function parseNote(raw) {
25
+ if (raw === undefined || raw === null) return null
26
+
27
+ const note = oneLine(raw)
28
+
29
+ if (note === '') {
30
+ throw new Error(
31
+ '--note is empty. Write the note itself, or leave the flag off — a backup with a blank ' +
32
+ 'note is one telstore had something to say about and did not.',
33
+ )
34
+ }
35
+
36
+ if (note.length > MAX_NOTE_LENGTH) {
37
+ throw new Error(
38
+ `--note is ${note.length} characters, and a caption has only room for ${MAX_NOTE_LENGTH} ` +
39
+ 'once the rest of the card has had its share. Shorten it: telstore will not cut it ' +
40
+ 'short by itself, because half a note read as a whole one is exactly the kind of ' +
41
+ 'plausible wrong answer this tool exists to refuse.',
42
+ )
43
+ }
44
+
45
+ return note
46
+ }
47
+
17
48
  function utcMinutes(createdAt) {
18
49
  return `${new Date(createdAt).toISOString().slice(0, 16).replace('T', ' ')} UTC`
19
50
  }
@@ -22,12 +53,15 @@ export function chunkCaption({ id, number, total }) {
22
53
  return `📦 ${id} · ${number}/${total}`
23
54
  }
24
55
 
25
- export function manifestCaption({ id, name, size, chunks, createdAt }) {
56
+ export function manifestCaption({ id, name, size, chunks, createdAt, note = null }) {
26
57
  return [
27
58
  `📄 ${oneLine(name)}`,
28
59
  `💾 ${formatBytes(size)} · ${chunks} chunk${chunks === 1 ? '' : 's'}`,
29
60
  `🆔 ${id}`,
30
61
  `📅 ${utcMinutes(createdAt)}`,
62
+ // Below the facts telstore knows, above the line that says how to get the file back:
63
+ // the note is the one part of the card a person wrote, so it reads last of the four.
64
+ ...(note ? [`📝 ${oneLine(note)}`] : []),
31
65
  '',
32
66
  `↩ npx telstore restore ${id}`,
33
67
  MANIFEST_TAG,
@@ -50,11 +84,15 @@ export function parseManifestCaption(text) {
50
84
  const id = marker(lines, '🆔')
51
85
  const createdAt = marker(lines, '📅')
52
86
 
87
+ // Every card telstore wrote before --note existed is a complete card, so the note is the
88
+ // one marker whose absence means "there is no note" rather than "this is not a card".
89
+ const note = marker(lines, '📝')
90
+
53
91
  if (!name || !totals || !id || !createdAt) return null
54
92
 
55
93
  const match = /^(.+) · (\d+) chunks?$/.exec(totals)
56
94
 
57
95
  if (!match) return null
58
96
 
59
- return { id, name, size: match[1], chunks: Number(match[2]), createdAt }
97
+ return { id, name, size: match[1], chunks: Number(match[2]), createdAt, note }
60
98
  }
package/src/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { basename } from 'node:path'
1
2
  import { parseArgs } from 'node:util'
2
3
 
3
4
  const SUBCOMMANDS = new Set([
@@ -13,11 +14,12 @@ const SUBCOMMANDS = new Set([
13
14
  ])
14
15
 
15
16
  const OPTIONS = {
16
- to: { type: 'string' },
17
+ chat: { type: 'string' },
17
18
  'chunk-size': { type: 'string' },
18
19
  'upload-concurrency': { type: 'string' },
19
20
  'download-concurrency': { type: 'string' },
20
21
  out: { type: 'string' },
22
+ note: { type: 'string' },
21
23
  limit: { type: 'string' },
22
24
  verbose: { type: 'boolean' },
23
25
  unset: { type: 'boolean' },
@@ -30,11 +32,11 @@ export const HELP = `telstore — split large files into chunks and store them o
30
32
 
31
33
  Usage:
32
34
  npx telstore login Log in to Telegram, only needed once
33
- npx telstore <file> Split a file and upload it to Telegram
35
+ npx telstore <file|folder|pattern>... Split files and upload them to Telegram
34
36
  npx telstore list List the backups stored in the destination
35
- npx telstore restore <backup-id> Download the chunks and reassemble the file
36
- npx telstore delete <backup-id> Remove a backup's chunks and manifest from the chat
37
- npx telstore status Show the account, the destination and unfinished backups
37
+ npx telstore restore <backup-id>... Download the chunks and reassemble the files
38
+ npx telstore delete <backup-id>... Remove backups' chunks and manifests from the chat
39
+ npx telstore status Show the account, the destination and unfinished uploads and restores
38
40
  npx telstore config Show every setting and where its value comes from
39
41
  npx telstore logout Remove the saved session
40
42
 
@@ -42,6 +44,15 @@ Running on a machine you do not trust:
42
44
  npx telstore token Print a session token for another machine
43
45
  npx telstore login --token Log in there by pasting one, session stays sealed
44
46
 
47
+ Several files in one run go one after another over a single connection, each becoming its own
48
+ backup. A folder means the files one level inside it, and a pattern means the names it matches
49
+ — the shell usually expands those itself, so quote one to hand it to telstore intact. More than
50
+ one file is listed and confirmed before the first byte goes out. Run telstore again with only
51
+ the files that are left to carry on after an interruption.
52
+
53
+ restore and delete take several ids the same way: one connection, one line each, and an exit
54
+ code that reports any that failed. delete shows everything it is about to destroy and asks once.
55
+
45
56
  Settings:
46
57
  npx telstore config <name> Print one setting's value
47
58
  npx telstore config <name> <value> Change it for good
@@ -55,19 +66,25 @@ Settings:
55
66
  verbose Show Telegram connection logs, default false.
56
67
 
57
68
  Options apply to one run and are never saved. Use config to change a setting for good.
58
- --to <chat> Destination for this run only.
69
+ --chat <chat> Destination for this run only.
59
70
  --chunk-size <n> Chunk size for this run only. An unfinished backup keeps the
60
71
  size it started with.
61
72
  --upload-concurrency <n> 512KB parts in parallel while uploading, this run only.
62
73
  --download-concurrency <n> 8MB slices in parallel while restoring, this run only.
63
74
  --out <path> Where to write the restored file. Defaults to the basename in
64
- the manifest.
75
+ the manifest, and works with one backup id only.
76
+ --note <text> A note to store with the upload. It goes into the manifest and
77
+ onto the manifest message, where Telegram's own search can find
78
+ it, and every file of a batch gets the same one. A note with
79
+ spaces in it has to be quoted — --note "march archive" — or the
80
+ shell hands the words after the first to telstore as more files
81
+ to upload.
65
82
  --limit <n> How many backups list shows this run.
66
83
  --token Log in by pasting a session token. It takes no value on
67
84
  purpose: a token written on the command line would sit in
68
85
  "ps" for the whole life of the command, and stay in that
69
86
  machine's shell history afterwards.
70
- --yes Delete without asking to confirm first.
87
+ --yes Upload a batch, or delete, without being asked to confirm.
71
88
  --verbose Show Telegram connection logs for this run.
72
89
  -h, --help Show this help.
73
90
  `
@@ -76,18 +93,65 @@ Options apply to one run and are never saved. Use config to change a setting for
76
93
  // finished chunk to a state file, restore has not. Naming the backup matters because the
77
94
  // id is what `status` lists and what a later `restore` needs — the chunks are already in
78
95
  // the chat under that id, whether or not this run ever finishes.
79
- export function interruptMessage(command, { backupId } = {}) {
96
+ export function interruptMessage(command, { backupId, done = [] } = {}) {
80
97
  if (command === 'upload') {
81
98
  const backup = backupId ? `Backup ${backupId} is saved` : 'Progress is saved'
82
99
 
100
+ // A batch is where "run the same command again" turns into a lie: the files it already
101
+ // finished have had their records cleared, so repeating the whole line would upload them
102
+ // a second time under new ids. Name them, and ask for the ones that are left instead.
103
+ if (done.length > 0) {
104
+ const width = Math.max(...done.map((file) => basename(file.path).length))
105
+ const finished = done
106
+ .map((file) => ` ${basename(file.path).padEnd(width)} ${file.id}`)
107
+ .join('\n')
108
+
109
+ return (
110
+ `\n${backup}. These are finished and need no second run:\n${finished}\n` +
111
+ 'Run telstore again with only the files that are left — repeating the whole command ' +
112
+ 'would upload the finished ones again as new backups. "npx telstore status" shows ' +
113
+ 'what is unfinished.\n'
114
+ )
115
+ }
116
+
83
117
  return (
84
118
  `\n${backup} — run the same command again to continue, ` +
85
119
  'or "npx telstore status" to see what is left.\n'
86
120
  )
87
121
  }
88
122
 
123
+ // A restore keeps its .partial now, and the next run proves each chunk in it against the
124
+ // manifest before trusting a byte — so "running again starts over", which this said while
125
+ // there was nothing to resume from, would now be false.
89
126
  if (command === 'restore') {
90
- return '\nStopped. Download progress is not saved, running again starts over.\n'
127
+ // Finished ids have been renamed to their real names and their records removed, so
128
+ // repeating the whole command line would meet an overwrite prompt and then download
129
+ // them again from nothing. Name them and ask for the rest, exactly as a batch upload does.
130
+ if (done.length > 0) {
131
+ const width = Math.max(...done.map((item) => basename(item.path).length))
132
+ const finished = done
133
+ .map((item) => ` ${basename(item.path).padEnd(width)} ${item.id}`)
134
+ .join('\n')
135
+
136
+ return (
137
+ `\nStopped. These are finished and need no second run:\n${finished}\n` +
138
+ 'Run telstore again with only the ids that are left — their .partial files are kept, ' +
139
+ 'so those carry on where they stopped. "npx telstore status" shows what is unfinished.\n'
140
+ )
141
+ }
142
+
143
+ // With onBackupId firing only once the .partial is open, an id here means there is a
144
+ // file to carry on from. Without one, this run stopped before it wrote anything, and
145
+ // saying a .partial was kept would be the same lie this message was rewritten to stop
146
+ // telling — just from the other side.
147
+ if (!backupId) {
148
+ return '\nStopped before anything was written. Run the same command again to start.\n'
149
+ }
150
+
151
+ return (
152
+ `\nBackup ${backupId} kept its .partial file — run the same command again from this ` +
153
+ 'directory to carry on, or "npx telstore status" to see what is left.\n'
154
+ )
91
155
  }
92
156
 
93
157
  // A delete has already destroyed messages for good by the time Ctrl-C lands, and the
@@ -107,8 +171,8 @@ export function interruptMessage(command, { backupId } = {}) {
107
171
  // parseArgs rejects anything starting with a dash as an option, and reports it as one:
108
172
  // `config chat -100123` fails with "Unknown option '-1'", naming a flag nobody typed.
109
173
  //
110
- // Two shapes need rescuing, and they are rescued differently. As a flag value, `--to -100123`
111
- // is joined into `--to=-100123`; only a bare negative integer qualifies, so `--to --verbose`
174
+ // Two shapes need rescuing, and they are rescued differently. As a flag value, `--chat -100123`
175
+ // is joined into `--chat=-100123`; only a bare negative integer qualifies, so `--chat --verbose`
112
176
  // still reports the missing value instead of eating the next flag. As a positional —
113
177
  // `config chat -100123` — there is nothing to join it to, so `--` goes in front and the rest
114
178
  // of the line is handed over verbatim. That is greedy on purpose: a flag written after the
@@ -125,8 +189,8 @@ function protectNegativeChatIds(argv) {
125
189
  return safe
126
190
  }
127
191
 
128
- if (argv[i] === '--to' && /^-\d+$/.test(argv[i + 1] ?? '')) {
129
- safe.push(`--to=${argv[i + 1]}`)
192
+ if (argv[i] === '--chat' && /^-\d+$/.test(argv[i + 1] ?? '')) {
193
+ safe.push(`--chat=${argv[i + 1]}`)
130
194
  i += 1
131
195
  continue
132
196
  }
@@ -142,33 +206,52 @@ function protectNegativeChatIds(argv) {
142
206
  return safe
143
207
  }
144
208
 
209
+ // An unquoted note is gone by the time node starts: the shell hands `--note ghi chu` over as
210
+ // three arguments and nothing can put the quotes back. The one trace it leaves is its own
211
+ // tail — every word after the first sits here as a positional, after the flag — and upload
212
+ // needs that to tell the mistake from a plain missing file. The shape of the command line is
213
+ // already this file's business, so the observation is made here rather than guessed at later.
214
+ //
215
+ // The last --note is the one parseArgs kept, so it is the one whose position counts.
216
+ function filesNamedAfterNote(tokens) {
217
+ const note = tokens.findLast((token) => token.kind === 'option' && token.name === 'note')
218
+
219
+ if (!note) return false
220
+
221
+ return tokens.some((token) => token.kind === 'positional' && token.index > note.index)
222
+ }
223
+
145
224
  export function route(argv) {
146
- const { values, positionals } = parseArgs({
225
+ const { values, positionals, tokens } = parseArgs({
147
226
  args: protectNegativeChatIds(argv),
148
227
  options: OPTIONS,
149
228
  allowPositionals: true,
229
+ tokens: true,
150
230
  })
151
231
 
152
232
  const [first, ...rest] = positionals
233
+ const filesAfterNote = filesNamedAfterNote(tokens)
153
234
 
154
- // `telstore --to @chan` with no file used to mean "remember this destination". Flags no
235
+ // `telstore --chat @chan` with no file used to mean "remember this destination". Flags no
155
236
  // longer write anything, so that line now asks for a run that has nothing to upload —
156
237
  // say where the destination actually lives instead of printing help at someone who was
157
238
  // perfectly clear about what they wanted.
158
- if (first === undefined && values.to && !values.help) {
239
+ if (first === undefined && values.chat && !values.help) {
159
240
  throw new Error(
160
- `Nothing to upload. To change the destination for good, run "npx telstore config chat ${values.to}". ` +
161
- 'To use it for one run, pass --to alongside a file or a command.',
241
+ `Nothing to upload. To change the destination for good, run "npx telstore config chat ${values.chat}". ` +
242
+ 'To use it for one run, pass --chat alongside a file or a command.',
162
243
  )
163
244
  }
164
245
 
165
246
  if (values.help || first === undefined || first === 'help') {
166
- return { command: 'help', args: [], options: values }
247
+ return { command: 'help', args: [], options: values, filesAfterNote }
167
248
  }
168
249
 
169
250
  if (SUBCOMMANDS.has(first)) {
170
- return { command: first, args: rest, options: values }
251
+ return { command: first, args: rest, options: values, filesAfterNote }
171
252
  }
172
253
 
173
- return { command: 'upload', args: [first], options: values }
254
+ // Every positional, not just the first: `telstore a b c` used to upload `a` and drop the
255
+ // rest without a word, which is the one thing this project never does.
256
+ return { command: 'upload', args: positionals, options: values, filesAfterNote }
174
257
  }