xcli-v2 0.1.0__tar.gz
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.
- xcli_v2-0.1.0/.gitignore +7 -0
- xcli_v2-0.1.0/.tasks/TODO.md +23 -0
- xcli_v2-0.1.0/.tasks/backlog.md +67 -0
- xcli_v2-0.1.0/.tasks/notes/2026-02-13-testing-checklist.md +36 -0
- xcli_v2-0.1.0/CONTRIBUTING.md +29 -0
- xcli_v2-0.1.0/LICENSE +21 -0
- xcli_v2-0.1.0/PKG-INFO +131 -0
- xcli_v2-0.1.0/README.md +102 -0
- xcli_v2-0.1.0/pyproject.toml +74 -0
- xcli_v2-0.1.0/src/xcli/__init__.py +5 -0
- xcli_v2-0.1.0/src/xcli/cli.py +38 -0
- xcli_v2-0.1.0/src/xcli/cmd/__init__.py +1 -0
- xcli_v2-0.1.0/src/xcli/cmd/auth.py +124 -0
- xcli_v2-0.1.0/src/xcli/cmd/compose.py +33 -0
- xcli_v2-0.1.0/src/xcli/cmd/posts.py +105 -0
- xcli_v2-0.1.0/src/xcli/cmd/publish.py +202 -0
- xcli_v2-0.1.0/src/xcli/cmd/timeline.py +69 -0
- xcli_v2-0.1.0/src/xcli/core/__init__.py +1 -0
- xcli_v2-0.1.0/src/xcli/core/config.py +51 -0
- xcli_v2-0.1.0/src/xcli/core/errors.py +17 -0
- xcli_v2-0.1.0/src/xcli/core/output.py +36 -0
- xcli_v2-0.1.0/src/xcli/core/posting.py +71 -0
- xcli_v2-0.1.0/src/xcli/core/session.py +51 -0
- xcli_v2-0.1.0/src/xcli/core/text_input.py +48 -0
- xcli_v2-0.1.0/src/xcli/core/token_store.py +44 -0
- xcli_v2-0.1.0/src/xcli/core/x_auth.py +158 -0
- xcli_v2-0.1.0/src/xcli/core/x_client.py +342 -0
- xcli_v2-0.1.0/tests/test_posting.py +38 -0
- xcli_v2-0.1.0/tests/test_text_input.py +30 -0
- xcli_v2-0.1.0/tests/test_token_store.py +45 -0
- xcli_v2-0.1.0/tests/test_x_client.py +35 -0
- xcli_v2-0.1.0/uv.lock +755 -0
xcli_v2-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## 2026-02-13
|
|
4
|
+
|
|
5
|
+
- [x] Audit current CLI surface and relevant XDK endpoints.
|
|
6
|
+
- [x] Expand `.tasks/backlog.md` into a detailed roadmap (phases, endpoint mapping, sequencing).
|
|
7
|
+
- [x] Implement `xcli posts mine` and `xcli posts get --id <tweet_id>`.
|
|
8
|
+
- [x] Implement `xcli timeline --user <handle>`.
|
|
9
|
+
- [x] Implement auth UX improvements: `xcli auth status` and `xcli auth logout`.
|
|
10
|
+
- [x] Implement media upload + attachment support in post/reply flows.
|
|
11
|
+
- [x] Investigate tweet scheduling endpoint availability in public XDK/OpenAPI.
|
|
12
|
+
- [x] Add tests for new functionality and run validation (`pytest`, `ruff`, `mypy`).
|
|
13
|
+
|
|
14
|
+
### Notes
|
|
15
|
+
|
|
16
|
+
- Existing commands are currently `compose`, `post`, `reply`, `quote`, and `auth` (`login`, `whoami`).
|
|
17
|
+
- XDK supports required read endpoints: `users.get_posts`, `posts.get_by_id`, `users.get_timeline`, and `users.get_by_username`.
|
|
18
|
+
- XDK OpenAPI includes Spaces `scheduled` state but no tweet scheduling field/path in post create APIs.
|
|
19
|
+
- Media upload endpoint requires OAuth scope `media.write`; existing tokens without it must re-login.
|
|
20
|
+
|
|
21
|
+
### Testing checklist
|
|
22
|
+
|
|
23
|
+
- See `.tasks/notes/2026-02-13-testing-checklist.md` for the detailed checklist and live command validations.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# xcli backlog
|
|
2
|
+
|
|
3
|
+
## Roadmap
|
|
4
|
+
|
|
5
|
+
### Endpoint inventory (XDK 0.8.x)
|
|
6
|
+
|
|
7
|
+
Confirmed in the installed XDK client:
|
|
8
|
+
|
|
9
|
+
- `client.users.get_me` -> authenticated user lookup.
|
|
10
|
+
- `client.users.get_posts` -> user-authored posts (paginated iterator).
|
|
11
|
+
- `client.posts.get_by_id` -> single post lookup.
|
|
12
|
+
- `client.users.get_by_username` -> resolve `@handle` -> user id.
|
|
13
|
+
- `client.users.get_timeline` -> reverse-chronological home timeline for a user id (paginated iterator).
|
|
14
|
+
- `client.posts.create` -> create post/reply/quote.
|
|
15
|
+
|
|
16
|
+
Scheduling note:
|
|
17
|
+
|
|
18
|
+
- No native "scheduled post at timestamp" endpoint is exposed for posts in this XDK version.
|
|
19
|
+
- `schedule/scheduled` terms currently appear in Spaces search state, not tweet create/update.
|
|
20
|
+
- Treat tweet scheduling as API-capability investigation (possible private/limited-scope endpoint), not local queue emulation.
|
|
21
|
+
|
|
22
|
+
### Phase 1: read + auth UX (active)
|
|
23
|
+
|
|
24
|
+
1. Pull own recent tweets: `xcli posts mine`.
|
|
25
|
+
2. Fetch a single tweet by id: `xcli posts get --id <tweet_id>`.
|
|
26
|
+
3. Timeline lookup for a handle: `xcli timeline --user <handle>`.
|
|
27
|
+
4. Better auth UX: `xcli auth logout` and `xcli auth status`.
|
|
28
|
+
|
|
29
|
+
Done when:
|
|
30
|
+
|
|
31
|
+
- Commands support both human output and `--json`.
|
|
32
|
+
- Errors map to friendly messages (bad id, auth missing/expired, API errors).
|
|
33
|
+
- Timeline and post-list commands respect a limit (`--limit`, sane defaults).
|
|
34
|
+
|
|
35
|
+
### Phase 2: publish enhancements (active)
|
|
36
|
+
|
|
37
|
+
1. Media upload + attach media to posts/replies.
|
|
38
|
+
2. Validate media constraints and produce clear error messages.
|
|
39
|
+
3. Keep `--dry-run` behavior useful when media is provided.
|
|
40
|
+
|
|
41
|
+
Done when:
|
|
42
|
+
|
|
43
|
+
- `xcli post ... --media <path> [--media <path>]` works for supported media.
|
|
44
|
+
- Upload + create flow is atomic enough for CLI usage and surfaces upload failures.
|
|
45
|
+
- Reply flow supports media attachment with same behavior.
|
|
46
|
+
|
|
47
|
+
### Phase 3: stretch features
|
|
48
|
+
|
|
49
|
+
1. Notifications/inbox command (investigate API support and tier access limits).
|
|
50
|
+
2. Tweet scheduling command if/when API support is confirmed.
|
|
51
|
+
3. Thread composer (`xcli thread create ...`).
|
|
52
|
+
4. Shell completion + richer TUI-style previews.
|
|
53
|
+
|
|
54
|
+
### OSS/release chores
|
|
55
|
+
|
|
56
|
+
1. Add CI workflow for `ruff`, `mypy`, and `pytest`.
|
|
57
|
+
2. Add changelog and release checklist.
|
|
58
|
+
3. Add issue templates for bugs and feature requests.
|
|
59
|
+
|
|
60
|
+
## Implementation notes
|
|
61
|
+
|
|
62
|
+
- Keep command names lowercase and concise.
|
|
63
|
+
- Reuse existing auth refresh and output helpers where possible.
|
|
64
|
+
- Prefer atomic tasks with clean commit boundaries:
|
|
65
|
+
- `phase-1` command surface
|
|
66
|
+
- `phase-2` media upload + publish integration
|
|
67
|
+
- tests + docs updates
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Testing checklist (2026-02-13)
|
|
2
|
+
|
|
3
|
+
## Automated checks
|
|
4
|
+
|
|
5
|
+
- [x] `ruff check .`
|
|
6
|
+
- [x] `python -m mypy src` (run via `.venv/bin/python -m mypy src`)
|
|
7
|
+
- [x] `python -m pytest` (18 tests passing)
|
|
8
|
+
|
|
9
|
+
## CLI surface checks
|
|
10
|
+
|
|
11
|
+
- [x] `python -m xcli.cli --help`
|
|
12
|
+
- [x] `python -m xcli.cli post --help`
|
|
13
|
+
- [x] `python -m xcli.cli posts --help`
|
|
14
|
+
- [x] `python -m xcli.cli auth --help`
|
|
15
|
+
|
|
16
|
+
## Live API checks
|
|
17
|
+
|
|
18
|
+
- [x] `xcli auth status --json`
|
|
19
|
+
- [x] `xcli auth whoami --json`
|
|
20
|
+
- [x] `xcli posts mine --limit 3 --json`
|
|
21
|
+
- [x] `xcli posts get --id <id_from_mine> --json`
|
|
22
|
+
- [x] `xcli timeline --user dremnik --limit 3 --json`
|
|
23
|
+
|
|
24
|
+
## Optional follow-up checks
|
|
25
|
+
|
|
26
|
+
- [ ] `xcli post "test" --media <image.png> --yes --json`
|
|
27
|
+
- [ ] `xcli reply "test" --to <tweet_id> --media <image.png> --yes --json`
|
|
28
|
+
- [ ] `xcli auth logout --json` then `xcli auth status --json`
|
|
29
|
+
|
|
30
|
+
### Notes
|
|
31
|
+
|
|
32
|
+
- Previous token scopes were missing `media.write` (`tweet.write users.read tweet.read offline.access`).
|
|
33
|
+
- Added default scope `media.write` in config and CLI now returns a clear error if scope is missing.
|
|
34
|
+
- After re-login, token includes `media.write`.
|
|
35
|
+
- Media upload flow updated to use multipart form upload directly against `/2/media/upload`.
|
|
36
|
+
- To complete media live checks: rerun the two media commands above with a normal local image file.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping improve `xcli`.
|
|
4
|
+
|
|
5
|
+
## Local setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e .[dev]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Before opening a PR
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ruff check
|
|
15
|
+
mypy src
|
|
16
|
+
pytest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commit style
|
|
20
|
+
|
|
21
|
+
- Keep changes focused and atomic.
|
|
22
|
+
- Add or update tests for behavior changes.
|
|
23
|
+
- Update docs for any user-facing command changes.
|
|
24
|
+
|
|
25
|
+
## Release process
|
|
26
|
+
|
|
27
|
+
- Bump version in `pyproject.toml`.
|
|
28
|
+
- Update `CHANGELOG.md`.
|
|
29
|
+
- Tag the release in git.
|
xcli_v2-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Twitter CLI Contributors
|
|
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.
|
xcli_v2-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xcli-v2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-source CLI for composing and publishing X posts
|
|
5
|
+
Author: Twitter CLI Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: automation,cli,social,twitter,x
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Internet
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: platformdirs>=4.2.2
|
|
21
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
22
|
+
Requires-Dist: typer>=0.12.3
|
|
23
|
+
Requires-Dist: xdk>=0.8.1
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.11.1; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.3.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.6.4; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# xcli
|
|
31
|
+
|
|
32
|
+
`xcli` is a command line tool for interacting with the X API.
|
|
33
|
+
|
|
34
|
+
It is designed for local-first usage, safe publishing defaults, and open-source packaging.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Install from PyPI:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install xcli-v2
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
1. Set OAuth app credentials:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export TWITTER_CLIENT_ID="..."
|
|
50
|
+
export TWITTER_CLIENT_SECRET="..."
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
2. Run login flow:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
xcli auth login
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
3. Draft content:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
xcli compose "shipping small daily"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
4. Post from terminal text (default):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
xcli post "shipping small daily"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
5. Preview without posting:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
xcli post "shipping small daily" --dry-run
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
6. Post from file:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
xcli post --file draft.txt
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
7. Attach media (repeat `--media` up to 4 files):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
xcli post "launch day" --media image1.png --media image2.jpg
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Currently supported media types are image uploads accepted by the X media upload endpoint (jpeg, png, webp, bmp, tiff).
|
|
90
|
+
Media upload requires OAuth scope `media.write`; if you logged in before this was added,
|
|
91
|
+
run `xcli auth login` again to refresh token scopes.
|
|
92
|
+
|
|
93
|
+
## Commands
|
|
94
|
+
|
|
95
|
+
- `xcli auth login`
|
|
96
|
+
- `xcli auth whoami`
|
|
97
|
+
- `xcli auth status`
|
|
98
|
+
- `xcli auth logout`
|
|
99
|
+
- `xcli compose`
|
|
100
|
+
- `xcli post`
|
|
101
|
+
- `xcli reply --to <tweet_id>`
|
|
102
|
+
- `xcli quote --to <tweet_id>`
|
|
103
|
+
- `xcli posts mine`
|
|
104
|
+
- `xcli posts get --id <tweet_id>`
|
|
105
|
+
- `xcli timeline --user <handle>`
|
|
106
|
+
|
|
107
|
+
## Safety model
|
|
108
|
+
|
|
109
|
+
- Posting commands send by default (with confirmation prompt).
|
|
110
|
+
- Use `--dry-run` to preview payload without posting.
|
|
111
|
+
- Non-interactive workflows can use `--yes`.
|
|
112
|
+
- Machine output is available with `--json`.
|
|
113
|
+
|
|
114
|
+
## Auth storage
|
|
115
|
+
|
|
116
|
+
Default token path uses platform config directories:
|
|
117
|
+
|
|
118
|
+
- macOS: `~/Library/Application Support/xcli/auth.json`
|
|
119
|
+
- Linux: `~/.config/xcli/auth.json`
|
|
120
|
+
- Windows: `%APPDATA%\\xcli\\auth.json`
|
|
121
|
+
|
|
122
|
+
Legacy compatibility fallback is supported for `~/.twitter/auth.json`.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install -e .[dev]
|
|
128
|
+
pytest
|
|
129
|
+
ruff check
|
|
130
|
+
mypy src
|
|
131
|
+
```
|
xcli_v2-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# xcli
|
|
2
|
+
|
|
3
|
+
`xcli` is a command line tool for interacting with the X API.
|
|
4
|
+
|
|
5
|
+
It is designed for local-first usage, safe publishing defaults, and open-source packaging.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install from PyPI:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install xcli-v2
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
1. Set OAuth app credentials:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export TWITTER_CLIENT_ID="..."
|
|
21
|
+
export TWITTER_CLIENT_SECRET="..."
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. Run login flow:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
xcli auth login
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Draft content:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
xcli compose "shipping small daily"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
4. Post from terminal text (default):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
xcli post "shipping small daily"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
5. Preview without posting:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
xcli post "shipping small daily" --dry-run
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
6. Post from file:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
xcli post --file draft.txt
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
7. Attach media (repeat `--media` up to 4 files):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
xcli post "launch day" --media image1.png --media image2.jpg
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Currently supported media types are image uploads accepted by the X media upload endpoint (jpeg, png, webp, bmp, tiff).
|
|
61
|
+
Media upload requires OAuth scope `media.write`; if you logged in before this was added,
|
|
62
|
+
run `xcli auth login` again to refresh token scopes.
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
- `xcli auth login`
|
|
67
|
+
- `xcli auth whoami`
|
|
68
|
+
- `xcli auth status`
|
|
69
|
+
- `xcli auth logout`
|
|
70
|
+
- `xcli compose`
|
|
71
|
+
- `xcli post`
|
|
72
|
+
- `xcli reply --to <tweet_id>`
|
|
73
|
+
- `xcli quote --to <tweet_id>`
|
|
74
|
+
- `xcli posts mine`
|
|
75
|
+
- `xcli posts get --id <tweet_id>`
|
|
76
|
+
- `xcli timeline --user <handle>`
|
|
77
|
+
|
|
78
|
+
## Safety model
|
|
79
|
+
|
|
80
|
+
- Posting commands send by default (with confirmation prompt).
|
|
81
|
+
- Use `--dry-run` to preview payload without posting.
|
|
82
|
+
- Non-interactive workflows can use `--yes`.
|
|
83
|
+
- Machine output is available with `--json`.
|
|
84
|
+
|
|
85
|
+
## Auth storage
|
|
86
|
+
|
|
87
|
+
Default token path uses platform config directories:
|
|
88
|
+
|
|
89
|
+
- macOS: `~/Library/Application Support/xcli/auth.json`
|
|
90
|
+
- Linux: `~/.config/xcli/auth.json`
|
|
91
|
+
- Windows: `%APPDATA%\\xcli\\auth.json`
|
|
92
|
+
|
|
93
|
+
Legacy compatibility fallback is supported for `~/.twitter/auth.json`.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install -e .[dev]
|
|
99
|
+
pytest
|
|
100
|
+
ruff check
|
|
101
|
+
mypy src
|
|
102
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xcli-v2"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Open-source CLI for composing and publishing X posts"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Twitter CLI Contributors" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["x", "twitter", "cli", "automation", "social"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Internet",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"platformdirs>=4.2.2",
|
|
30
|
+
"python-dotenv>=1.0.1",
|
|
31
|
+
"typer>=0.12.3",
|
|
32
|
+
"xdk>=0.8.1",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"mypy>=1.11.1",
|
|
38
|
+
"pytest>=8.3.2",
|
|
39
|
+
"ruff>=0.6.4",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
xcli = "xcli.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/xcli"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
pythonpath = ["src"]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py310"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
"src/xcli/cmd/*.py" = ["B008"]
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
python_version = "3.10"
|
|
64
|
+
strict = true
|
|
65
|
+
warn_unused_configs = true
|
|
66
|
+
warn_unused_ignores = true
|
|
67
|
+
|
|
68
|
+
[[tool.mypy.overrides]]
|
|
69
|
+
module = "xdk"
|
|
70
|
+
ignore_missing_imports = true
|
|
71
|
+
|
|
72
|
+
[[tool.mypy.overrides]]
|
|
73
|
+
module = "xdk.*"
|
|
74
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""CLI entrypoint for xcli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from xcli.cmd.auth import app as auth_app
|
|
8
|
+
from xcli.cmd.compose import compose_cmd
|
|
9
|
+
from xcli.cmd.posts import app as posts_app
|
|
10
|
+
from xcli.cmd.publish import post_cmd, quote_cmd, reply_cmd
|
|
11
|
+
from xcli.cmd.timeline import timeline_cmd
|
|
12
|
+
from xcli.core.errors import XcliError
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
no_args_is_help=True,
|
|
16
|
+
add_completion=False,
|
|
17
|
+
help="Command line tool for interacting with the X API.",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
app.add_typer(auth_app, name="auth")
|
|
21
|
+
app.add_typer(posts_app, name="posts")
|
|
22
|
+
app.command("compose")(compose_cmd)
|
|
23
|
+
app.command("post")(post_cmd)
|
|
24
|
+
app.command("reply")(reply_cmd)
|
|
25
|
+
app.command("quote")(quote_cmd)
|
|
26
|
+
app.command("timeline")(timeline_cmd)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
try:
|
|
31
|
+
app()
|
|
32
|
+
except XcliError as exc:
|
|
33
|
+
typer.secho(str(exc), fg=typer.colors.RED, err=True)
|
|
34
|
+
raise SystemExit(1) from exc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__": # pragma: no cover
|
|
38
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command modules for xcli."""
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Auth commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from xcli.core.config import load_settings
|
|
10
|
+
from xcli.core.errors import ApiError, AuthError
|
|
11
|
+
from xcli.core.output import emit
|
|
12
|
+
from xcli.core.session import make_authed_client
|
|
13
|
+
from xcli.core.token_store import TokenStore
|
|
14
|
+
from xcli.core.x_auth import run_login
|
|
15
|
+
from xcli.core.x_client import get_me, make_user_client
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(no_args_is_help=True, help="Authenticate xcli with the X API.")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("login")
|
|
21
|
+
def login(
|
|
22
|
+
no_browser: bool = typer.Option(False, help="Do not auto-open a browser."),
|
|
23
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
24
|
+
) -> None:
|
|
25
|
+
settings = load_settings()
|
|
26
|
+
token = run_login(settings, open_browser=not no_browser)
|
|
27
|
+
|
|
28
|
+
store = TokenStore()
|
|
29
|
+
store.save(token)
|
|
30
|
+
|
|
31
|
+
out: dict[str, object] = {
|
|
32
|
+
"message": "Login successful.",
|
|
33
|
+
"token_path": str(store.primary),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
access_token = token.get("access_token")
|
|
37
|
+
if isinstance(access_token, str) and access_token:
|
|
38
|
+
try:
|
|
39
|
+
client = make_user_client(access_token)
|
|
40
|
+
me = get_me(client)
|
|
41
|
+
out["username"] = me.get("username")
|
|
42
|
+
out["name"] = me.get("name")
|
|
43
|
+
out["id"] = me.get("id")
|
|
44
|
+
except ApiError as exc:
|
|
45
|
+
out["message"] = (
|
|
46
|
+
"Login successful, but profile lookup failed. "
|
|
47
|
+
"Run `xcli auth whoami` to retry."
|
|
48
|
+
)
|
|
49
|
+
out["warning"] = str(exc)
|
|
50
|
+
|
|
51
|
+
emit(out, json_output=json_output)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command("whoami")
|
|
55
|
+
def whoami(
|
|
56
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
57
|
+
) -> None:
|
|
58
|
+
client = make_authed_client()
|
|
59
|
+
me = get_me(client)
|
|
60
|
+
|
|
61
|
+
emit(
|
|
62
|
+
{
|
|
63
|
+
"message": "Authenticated user:",
|
|
64
|
+
"username": me.get("username"),
|
|
65
|
+
"name": me.get("name"),
|
|
66
|
+
"id": me.get("id"),
|
|
67
|
+
},
|
|
68
|
+
json_output=json_output,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command("status")
|
|
73
|
+
def status(
|
|
74
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
75
|
+
) -> None:
|
|
76
|
+
store = TokenStore()
|
|
77
|
+
token = store.load()
|
|
78
|
+
if not token:
|
|
79
|
+
emit(
|
|
80
|
+
{
|
|
81
|
+
"message": "Not authenticated.",
|
|
82
|
+
"logged_in": False,
|
|
83
|
+
"token_path": str(store.primary),
|
|
84
|
+
},
|
|
85
|
+
json_output=json_output,
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
out: dict[str, object] = {
|
|
90
|
+
"message": "Authentication status:",
|
|
91
|
+
"logged_in": bool(token.get("access_token")),
|
|
92
|
+
"token_path": str(store.primary),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
expires_at = token.get("expires_at")
|
|
96
|
+
if isinstance(expires_at, (int, float)):
|
|
97
|
+
out["expires_at"] = datetime.fromtimestamp(expires_at, tz=timezone.utc).isoformat()
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
client = make_authed_client()
|
|
101
|
+
me = get_me(client)
|
|
102
|
+
out["username"] = me.get("username")
|
|
103
|
+
out["name"] = me.get("name")
|
|
104
|
+
out["id"] = me.get("id")
|
|
105
|
+
except (AuthError, ApiError) as exc:
|
|
106
|
+
out["warning"] = str(exc)
|
|
107
|
+
|
|
108
|
+
emit(out, json_output=json_output)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("logout")
|
|
112
|
+
def logout(
|
|
113
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
114
|
+
) -> None:
|
|
115
|
+
store = TokenStore()
|
|
116
|
+
removed = store.clear()
|
|
117
|
+
|
|
118
|
+
out: dict[str, object] = {
|
|
119
|
+
"message": "Logged out." if removed else "No token files found. Already logged out.",
|
|
120
|
+
"count": len(removed),
|
|
121
|
+
"token_path": str(store.primary),
|
|
122
|
+
"removed": [str(path) for path in removed],
|
|
123
|
+
}
|
|
124
|
+
emit(out, json_output=json_output)
|