basecamp-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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +317 -0
- data/bin/basecamp +6 -0
- data/lib/basecamp/cli.rb +95 -0
- data/lib/basecamp/client.rb +88 -0
- data/lib/basecamp/commands/auth.rb +117 -0
- data/lib/basecamp/commands/boards.rb +42 -0
- data/lib/basecamp/commands/card.rb +58 -0
- data/lib/basecamp/commands/cards.rb +48 -0
- data/lib/basecamp/commands/init.rb +37 -0
- data/lib/basecamp/commands/move.rb +30 -0
- data/lib/basecamp/commands/projects.rb +31 -0
- data/lib/basecamp/config.rb +75 -0
- data/lib/basecamp/version.rb +5 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6ad45dc6324b786c253b79d9c76d4751e32d90a62382e90091b160a14c6a5e96
|
|
4
|
+
data.tar.gz: 3532827c6e377a67c344d9a1d4b284344c7604e304a2b65bafe4430294ca0429
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1ba797440b833e8c8c1b62aad1cc4d674d784f420bc4a079d228c5ba18117f5c4cc3dbea4d3bce5e5ad8ac2270a0b73ca23a164ff409d11940e2ba68a2e5d81e
|
|
7
|
+
data.tar.gz: da8dfe2cbbe8774d2058b8eef6fe242c8b907b278f370f80449182a31f0f73a31f425ac16550b56ab47900744aa3fad6c3005696b0532a0a8e76a09c0431200b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Basecamp CLI
|
|
2
|
+
|
|
3
|
+
A simple command-line interface for Basecamp. List projects, browse card tables (Kanban boards), view cards, and move cards between columns.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **List projects** - See all projects in your Basecamp account
|
|
8
|
+
- **Browse boards** - View card tables and their columns within a project
|
|
9
|
+
- **List cards** - See all cards in a board, optionally filtered by column
|
|
10
|
+
- **View card details** - See full card info including description and comments
|
|
11
|
+
- **Move cards** - Move cards between columns
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### From RubyGems
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install basecamp-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### From source
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/robzolkos/ruby-basecamp-cli.git
|
|
25
|
+
cd ruby-basecamp-cli
|
|
26
|
+
bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then either:
|
|
30
|
+
- Run directly: `./bin/basecamp`
|
|
31
|
+
- Add to PATH: `export PATH="$PATH:/path/to/basecamp-cli/bin"`
|
|
32
|
+
- Install as gem: `rake install`
|
|
33
|
+
|
|
34
|
+
### Requirements
|
|
35
|
+
|
|
36
|
+
- Ruby 3.0+
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
### 1. Register your application with Basecamp
|
|
41
|
+
|
|
42
|
+
Go to [launchpad.37signals.com/integrations](https://launchpad.37signals.com/integrations) and register a new application.
|
|
43
|
+
|
|
44
|
+
You'll need to provide:
|
|
45
|
+
- **Name**: e.g., "Basecamp CLI"
|
|
46
|
+
- **Redirect URI**: `http://localhost:3002/callback`
|
|
47
|
+
|
|
48
|
+
After registering, you'll receive:
|
|
49
|
+
- **Client ID**
|
|
50
|
+
- **Client Secret**
|
|
51
|
+
|
|
52
|
+
You'll also need your **Account ID**, which is the number in your Basecamp URL:
|
|
53
|
+
```
|
|
54
|
+
https://3.basecamp.com/YOUR_ACCOUNT_ID/...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Configure the CLI
|
|
58
|
+
|
|
59
|
+
Run the init command and enter your credentials:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
./bin/basecamp init
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This saves your configuration to `~/.basecamp.json`.
|
|
66
|
+
|
|
67
|
+
### 3. Authenticate
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
./bin/basecamp auth
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This opens your browser for OAuth authorization. After approving, the token is saved to `~/.basecamp_token.json`.
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
### `basecamp projects`
|
|
78
|
+
|
|
79
|
+
List all projects in your account.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
$ basecamp projects
|
|
83
|
+
Projects
|
|
84
|
+
============================================================
|
|
85
|
+
[*] 12345678 Website Redesign
|
|
86
|
+
Main company website overhaul
|
|
87
|
+
[*] 23456789 Mobile App
|
|
88
|
+
iOS and Android development
|
|
89
|
+
[ ] 34567890 Old Project
|
|
90
|
+
Archived project
|
|
91
|
+
|
|
92
|
+
[*] = active
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Output:**
|
|
96
|
+
- `[*]` indicates active projects, `[ ]` indicates inactive
|
|
97
|
+
- Project ID and name on each line
|
|
98
|
+
- Description (if set) indented below
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### `basecamp boards <project_id>`
|
|
103
|
+
|
|
104
|
+
List card tables (Kanban boards) in a project, with column summary.
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
$ basecamp boards 12345678
|
|
108
|
+
Card Tables in: Website Redesign
|
|
109
|
+
============================================================
|
|
110
|
+
87654321 Development Tasks
|
|
111
|
+
|
|
112
|
+
Columns:
|
|
113
|
+
- Backlog (12 cards)
|
|
114
|
+
- In Progress (3 cards)
|
|
115
|
+
- Review (2 cards)
|
|
116
|
+
- Done (45 cards)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Output:**
|
|
120
|
+
- Board ID and title
|
|
121
|
+
- List of columns with card counts
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### `basecamp cards <project_id> <board_id> [--column <name>]`
|
|
126
|
+
|
|
127
|
+
List cards in a board. Use `--column` to filter by column name (partial match).
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
$ basecamp cards 12345678 87654321
|
|
131
|
+
Cards: Development Tasks
|
|
132
|
+
======================================================================
|
|
133
|
+
|
|
134
|
+
Backlog (12)
|
|
135
|
+
----------------------------------------
|
|
136
|
+
11111111 Fix login validation
|
|
137
|
+
by Jane Smith
|
|
138
|
+
22222222 Add password reset flow
|
|
139
|
+
by John Doe
|
|
140
|
+
33333333 Update API documentation
|
|
141
|
+
by Jane Smith
|
|
142
|
+
|
|
143
|
+
In Progress (3)
|
|
144
|
+
----------------------------------------
|
|
145
|
+
44444444 Implement dark mode
|
|
146
|
+
by John Doe
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
With column filter:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
$ basecamp cards 12345678 87654321 --column "Progress"
|
|
153
|
+
Cards: Development Tasks
|
|
154
|
+
======================================================================
|
|
155
|
+
|
|
156
|
+
In Progress (3)
|
|
157
|
+
----------------------------------------
|
|
158
|
+
44444444 Implement dark mode
|
|
159
|
+
by John Doe
|
|
160
|
+
55555555 Refactor authentication
|
|
161
|
+
by Jane Smith
|
|
162
|
+
66666666 Add unit tests
|
|
163
|
+
by John Doe
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Output:**
|
|
167
|
+
- Cards grouped by column
|
|
168
|
+
- Card ID and title
|
|
169
|
+
- Creator name
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### `basecamp card <project_id> <card_id> [--comments]`
|
|
174
|
+
|
|
175
|
+
View full details of a single card. Use `--comments` to include comments.
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
$ basecamp card 12345678 44444444 --comments
|
|
179
|
+
Card: Implement dark mode
|
|
180
|
+
======================================================================
|
|
181
|
+
|
|
182
|
+
ID: 44444444
|
|
183
|
+
Creator: John Doe
|
|
184
|
+
Created: 2025-01-15T09:30:00.000Z
|
|
185
|
+
Updated: 2025-01-20T14:22:00.000Z
|
|
186
|
+
URL: https://3.basecamp.com/12345678/buckets/.../cards/44444444
|
|
187
|
+
Assigned: Jane Smith, Bob Wilson
|
|
188
|
+
|
|
189
|
+
Description:
|
|
190
|
+
----------------------------------------
|
|
191
|
+
Add dark mode support to the application. Should respect system
|
|
192
|
+
preferences and allow manual toggle. See design specs in Figma.
|
|
193
|
+
|
|
194
|
+
Comments (2):
|
|
195
|
+
----------------------------------------
|
|
196
|
+
|
|
197
|
+
Jane Smith (2025-01-16T10:00:00.000Z):
|
|
198
|
+
I've started on the color palette. Will share preview tomorrow.
|
|
199
|
+
|
|
200
|
+
John Doe (2025-01-17T09:15:00.000Z):
|
|
201
|
+
Looks great! Let's also add a toggle in the settings menu.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Output:**
|
|
205
|
+
- Card metadata (ID, creator, timestamps, URL, assignees)
|
|
206
|
+
- Full description text (HTML stripped)
|
|
207
|
+
- Comments with author and timestamp (when `--comments` flag used)
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### `basecamp move <project_id> <board_id> <card_id> --to <column>`
|
|
212
|
+
|
|
213
|
+
Move a card to a different column.
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
$ basecamp move 12345678 87654321 44444444 --to "Review"
|
|
217
|
+
Card 44444444 moved to 'Review'
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Output:**
|
|
221
|
+
- Confirmation message with card ID and target column
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Configuration Files
|
|
226
|
+
|
|
227
|
+
| File | Purpose |
|
|
228
|
+
|------|---------|
|
|
229
|
+
| `~/.basecamp.json` | Client credentials and account ID |
|
|
230
|
+
| `~/.basecamp_token.json` | OAuth access token (auto-generated) |
|
|
231
|
+
|
|
232
|
+
### `~/.basecamp.json` format
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"client_id": "your_client_id",
|
|
237
|
+
"client_secret": "your_client_secret",
|
|
238
|
+
"account_id": "12345678",
|
|
239
|
+
"redirect_uri": "http://localhost:3002/callback"
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## API Coverage
|
|
244
|
+
|
|
245
|
+
Progress towards full [Basecamp API](https://github.com/basecamp/bc3-api) implementation.
|
|
246
|
+
|
|
247
|
+
### Projects & Structure
|
|
248
|
+
- [x] Projects - list
|
|
249
|
+
- [ ] Projects - create, update, delete
|
|
250
|
+
- [ ] Basecamps (workspaces)
|
|
251
|
+
- [ ] Templates
|
|
252
|
+
|
|
253
|
+
### Card Tables (Kanban)
|
|
254
|
+
- [x] Card tables - list, get
|
|
255
|
+
- [x] Card table cards - list, get, move
|
|
256
|
+
- [ ] Card table cards - create, update, delete
|
|
257
|
+
- [ ] Card table columns - list, create, update, delete
|
|
258
|
+
- [ ] Card table steps
|
|
259
|
+
|
|
260
|
+
### To-dos
|
|
261
|
+
- [ ] Todosets
|
|
262
|
+
- [ ] Todolists
|
|
263
|
+
- [ ] Todolist groups
|
|
264
|
+
- [ ] Todos - list, create, update, complete, delete
|
|
265
|
+
|
|
266
|
+
### Communication
|
|
267
|
+
- [ ] Message boards
|
|
268
|
+
- [ ] Messages
|
|
269
|
+
- [ ] Message types
|
|
270
|
+
- [x] Comments - list (on cards)
|
|
271
|
+
- [ ] Comments - create, update, delete
|
|
272
|
+
- [ ] Campfires (chat)
|
|
273
|
+
- [ ] Chatbots
|
|
274
|
+
|
|
275
|
+
### Documents & Files
|
|
276
|
+
- [ ] Vaults (folders)
|
|
277
|
+
- [ ] Documents
|
|
278
|
+
- [ ] Uploads
|
|
279
|
+
- [ ] Attachments
|
|
280
|
+
|
|
281
|
+
### Schedule
|
|
282
|
+
- [ ] Schedules
|
|
283
|
+
- [ ] Schedule entries
|
|
284
|
+
|
|
285
|
+
### Check-ins
|
|
286
|
+
- [ ] Questionnaires
|
|
287
|
+
- [ ] Questions
|
|
288
|
+
- [ ] Question answers
|
|
289
|
+
|
|
290
|
+
### Email
|
|
291
|
+
- [ ] Inboxes
|
|
292
|
+
- [ ] Inbox replies
|
|
293
|
+
- [ ] Forwards
|
|
294
|
+
|
|
295
|
+
### Client Portal
|
|
296
|
+
- [ ] Client visibility
|
|
297
|
+
- [ ] Client approvals
|
|
298
|
+
- [ ] Client correspondences
|
|
299
|
+
- [ ] Client replies
|
|
300
|
+
|
|
301
|
+
### People & Permissions
|
|
302
|
+
- [ ] People - list, get
|
|
303
|
+
- [ ] Subscriptions
|
|
304
|
+
|
|
305
|
+
### Other
|
|
306
|
+
- [ ] Events
|
|
307
|
+
- [ ] Recordings
|
|
308
|
+
- [ ] Reports
|
|
309
|
+
- [ ] Search
|
|
310
|
+
- [ ] Timeline
|
|
311
|
+
- [ ] Lineup markers
|
|
312
|
+
- [ ] Rich text
|
|
313
|
+
- [ ] Webhooks
|
|
314
|
+
|
|
315
|
+
## License
|
|
316
|
+
|
|
317
|
+
MIT
|
data/bin/basecamp
ADDED
data/lib/basecamp/cli.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'version'
|
|
5
|
+
require_relative 'config'
|
|
6
|
+
require_relative 'client'
|
|
7
|
+
require_relative 'commands/init'
|
|
8
|
+
require_relative 'commands/auth'
|
|
9
|
+
require_relative 'commands/projects'
|
|
10
|
+
require_relative 'commands/boards'
|
|
11
|
+
require_relative 'commands/cards'
|
|
12
|
+
require_relative 'commands/card'
|
|
13
|
+
require_relative 'commands/move'
|
|
14
|
+
|
|
15
|
+
module Basecamp
|
|
16
|
+
class CLI
|
|
17
|
+
HELP = <<~HELP
|
|
18
|
+
Usage: basecamp <command> [options]
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
init Configure the CLI (client_id, secret, account)
|
|
22
|
+
auth Authenticate with Basecamp (OAuth)
|
|
23
|
+
projects List all projects
|
|
24
|
+
boards <project_id> List card tables in a project
|
|
25
|
+
cards <project_id> <board_id> List cards (--column <name> to filter)
|
|
26
|
+
card <project_id> <card_id> Show card details (--comments for comments)
|
|
27
|
+
move <project_id> <board_id> <card_id> --to <column> Move a card
|
|
28
|
+
version Show version
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
basecamp projects
|
|
32
|
+
basecamp boards 12345678
|
|
33
|
+
basecamp cards 12345678 87654321 --column "Doing"
|
|
34
|
+
basecamp card 12345678 11111111 --comments
|
|
35
|
+
basecamp move 12345678 87654321 11111111 --to "Done"
|
|
36
|
+
HELP
|
|
37
|
+
|
|
38
|
+
def run(args)
|
|
39
|
+
if args.empty? || args.first == 'help' || args.first == '--help' || args.first == '-h'
|
|
40
|
+
puts HELP
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
command = args.shift
|
|
45
|
+
|
|
46
|
+
case command
|
|
47
|
+
when 'version', '--version', '-v'
|
|
48
|
+
puts "basecamp-cli #{VERSION}"
|
|
49
|
+
return
|
|
50
|
+
when 'init'
|
|
51
|
+
Commands::Init.new.run
|
|
52
|
+
when 'auth'
|
|
53
|
+
Commands::Auth.new.run
|
|
54
|
+
when 'projects'
|
|
55
|
+
Commands::Projects.new.run
|
|
56
|
+
when 'boards'
|
|
57
|
+
project_id = args.shift or raise "Usage: basecamp boards <project_id>"
|
|
58
|
+
Commands::Boards.new.run(project_id)
|
|
59
|
+
when 'cards'
|
|
60
|
+
project_id = args.shift or raise "Usage: basecamp cards <project_id> <board_id>"
|
|
61
|
+
board_id = args.shift or raise "Usage: basecamp cards <project_id> <board_id>"
|
|
62
|
+
column = extract_option(args, '--column')
|
|
63
|
+
Commands::Cards.new.run(project_id, board_id, column: column)
|
|
64
|
+
when 'card'
|
|
65
|
+
project_id = args.shift or raise "Usage: basecamp card <project_id> <card_id>"
|
|
66
|
+
card_id = args.shift or raise "Usage: basecamp card <project_id> <card_id>"
|
|
67
|
+
comments = args.include?('--comments')
|
|
68
|
+
Commands::Card.new.run(project_id, card_id, comments: comments)
|
|
69
|
+
when 'move'
|
|
70
|
+
project_id = args.shift or raise "Usage: basecamp move <project_id> <board_id> <card_id> --to <column>"
|
|
71
|
+
board_id = args.shift or raise "Usage: basecamp move <project_id> <board_id> <card_id> --to <column>"
|
|
72
|
+
card_id = args.shift or raise "Usage: basecamp move <project_id> <board_id> <card_id> --to <column>"
|
|
73
|
+
to = extract_option(args, '--to') or raise "Usage: basecamp move ... --to <column>"
|
|
74
|
+
Commands::Move.new.run(project_id, board_id, card_id, to: to)
|
|
75
|
+
else
|
|
76
|
+
puts "Unknown command: #{command}"
|
|
77
|
+
puts HELP
|
|
78
|
+
exit 1
|
|
79
|
+
end
|
|
80
|
+
rescue => e
|
|
81
|
+
$stderr.puts "Error: #{e.message}"
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def extract_option(args, flag)
|
|
88
|
+
idx = args.index(flag)
|
|
89
|
+
return nil unless idx
|
|
90
|
+
|
|
91
|
+
args.delete_at(idx)
|
|
92
|
+
args.delete_at(idx)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
|
|
8
|
+
module Basecamp
|
|
9
|
+
class Client
|
|
10
|
+
USER_AGENT = 'Basecamp CLI (https://github.com/rzolkos/basecamp-cli)'
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@token = Config.token
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(path)
|
|
17
|
+
url = path.start_with?('http') ? path : "#{Config.api_base_url}#{path}"
|
|
18
|
+
request(:get, url)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def post(path, data = {})
|
|
22
|
+
url = path.start_with?('http') ? path : "#{Config.api_base_url}#{path}"
|
|
23
|
+
request(:post, url, data)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def put(path, data = {})
|
|
27
|
+
url = path.start_with?('http') ? path : "#{Config.api_base_url}#{path}"
|
|
28
|
+
request(:put, url, data)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Fetch all pages of a paginated endpoint
|
|
32
|
+
def get_all(path)
|
|
33
|
+
results = []
|
|
34
|
+
url = path.start_with?('http') ? path : "#{Config.api_base_url}#{path}"
|
|
35
|
+
|
|
36
|
+
loop do
|
|
37
|
+
response = request_raw(:get, url)
|
|
38
|
+
results.concat(JSON.parse(response.body))
|
|
39
|
+
|
|
40
|
+
# Check for next page
|
|
41
|
+
link_header = response['Link']
|
|
42
|
+
break unless link_header
|
|
43
|
+
|
|
44
|
+
next_link = link_header.split(',').find { |l| l.include?('rel="next"') }
|
|
45
|
+
break unless next_link
|
|
46
|
+
|
|
47
|
+
url = next_link.match(/<([^>]+)>/)[1]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
results
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def request(method, url, data = nil)
|
|
56
|
+
response = request_raw(method, url, data)
|
|
57
|
+
return nil if response.body.nil? || response.body.empty?
|
|
58
|
+
JSON.parse(response.body)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def request_raw(method, url, data = nil)
|
|
62
|
+
uri = URI(url)
|
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
64
|
+
http.use_ssl = true
|
|
65
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
66
|
+
|
|
67
|
+
request = case method
|
|
68
|
+
when :get then Net::HTTP::Get.new(uri.request_uri)
|
|
69
|
+
when :post then Net::HTTP::Post.new(uri.request_uri)
|
|
70
|
+
when :put then Net::HTTP::Put.new(uri.request_uri)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
74
|
+
request['User-Agent'] = USER_AGENT
|
|
75
|
+
request['Content-Type'] = 'application/json' if data
|
|
76
|
+
|
|
77
|
+
request.body = data.to_json if data
|
|
78
|
+
|
|
79
|
+
response = http.request(request)
|
|
80
|
+
|
|
81
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
82
|
+
raise "API error: #{response.code} #{response.message}\n#{response.body}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
response
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'webrick'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'openssl'
|
|
8
|
+
|
|
9
|
+
module Basecamp
|
|
10
|
+
module Commands
|
|
11
|
+
class Auth
|
|
12
|
+
def run
|
|
13
|
+
puts "Basecamp OAuth Authentication"
|
|
14
|
+
puts "=" * 40
|
|
15
|
+
|
|
16
|
+
@auth_code = nil
|
|
17
|
+
start_server
|
|
18
|
+
open_authorization_url
|
|
19
|
+
wait_for_callback
|
|
20
|
+
exchange_code_for_token
|
|
21
|
+
|
|
22
|
+
puts "\nAuthentication successful!"
|
|
23
|
+
puts "Token saved to: #{Config::TOKEN_FILE}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def start_server
|
|
29
|
+
port = URI(Config.redirect_uri).port || 3002
|
|
30
|
+
puts "\nStarting callback server on port #{port}..."
|
|
31
|
+
|
|
32
|
+
@server = WEBrick::HTTPServer.new(
|
|
33
|
+
Port: port,
|
|
34
|
+
Logger: WEBrick::Log.new('/dev/null'),
|
|
35
|
+
AccessLog: []
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@server.mount_proc '/callback' do |req, res|
|
|
39
|
+
@auth_code = req.query['code']
|
|
40
|
+
|
|
41
|
+
res.status = 200
|
|
42
|
+
res.content_type = 'text/html'
|
|
43
|
+
res.body = if @auth_code
|
|
44
|
+
'<html><body style="font-family:sans-serif;text-align:center;padding:50px;">' \
|
|
45
|
+
'<h1>Authentication Successful!</h1><p>You can close this window.</p></body></html>'
|
|
46
|
+
else
|
|
47
|
+
'<html><body style="font-family:sans-serif;text-align:center;padding:50px;">' \
|
|
48
|
+
'<h1>Authentication Failed</h1><p>No authorization code received.</p></body></html>'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Thread.new { @server.start }
|
|
53
|
+
sleep 0.5
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def open_authorization_url
|
|
57
|
+
params = URI.encode_www_form(
|
|
58
|
+
type: 'web_server',
|
|
59
|
+
client_id: Config.client_id,
|
|
60
|
+
redirect_uri: Config.redirect_uri
|
|
61
|
+
)
|
|
62
|
+
auth_url = "#{Config::AUTHORIZATION_URL}?#{params}"
|
|
63
|
+
|
|
64
|
+
puts "\nOpening browser for authorization..."
|
|
65
|
+
puts "URL: #{auth_url}"
|
|
66
|
+
puts "\nIf browser doesn't open, copy the URL above."
|
|
67
|
+
|
|
68
|
+
system("xdg-open '#{auth_url}' 2>/dev/null || open '#{auth_url}' 2>/dev/null || true")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def wait_for_callback
|
|
72
|
+
puts "\nWaiting for authorization..."
|
|
73
|
+
|
|
74
|
+
timeout = 120
|
|
75
|
+
start = Time.now
|
|
76
|
+
|
|
77
|
+
until @auth_code
|
|
78
|
+
sleep 0.5
|
|
79
|
+
if Time.now - start > timeout
|
|
80
|
+
@server.shutdown
|
|
81
|
+
raise 'Timeout waiting for authorization'
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@server.shutdown
|
|
86
|
+
puts "Authorization code received"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def exchange_code_for_token
|
|
90
|
+
puts "\nExchanging code for token..."
|
|
91
|
+
|
|
92
|
+
uri = URI(Config::TOKEN_URL)
|
|
93
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
94
|
+
http.use_ssl = true
|
|
95
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
96
|
+
|
|
97
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
98
|
+
request.set_form_data(
|
|
99
|
+
type: 'web_server',
|
|
100
|
+
client_id: Config.client_id,
|
|
101
|
+
client_secret: Config.client_secret,
|
|
102
|
+
redirect_uri: Config.redirect_uri,
|
|
103
|
+
code: @auth_code
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
response = http.request(request)
|
|
107
|
+
|
|
108
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
109
|
+
raise "Token exchange failed: #{response.code} #{response.message}\n#{response.body}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
token_data = JSON.parse(response.body, symbolize_names: true)
|
|
113
|
+
Config.save_token(token_data)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Commands
|
|
5
|
+
class Boards
|
|
6
|
+
def run(project_id)
|
|
7
|
+
client = Client.new
|
|
8
|
+
|
|
9
|
+
# Get project details to find the card table dock
|
|
10
|
+
project = client.get("/projects/#{project_id}.json")
|
|
11
|
+
|
|
12
|
+
puts "Card Tables in: #{project['name']}"
|
|
13
|
+
puts "=" * 60
|
|
14
|
+
|
|
15
|
+
# Find card tables in the dock
|
|
16
|
+
dock = project['dock'] || []
|
|
17
|
+
card_table_dock = dock.find { |d| d['name'] == 'kanban_board' }
|
|
18
|
+
|
|
19
|
+
unless card_table_dock
|
|
20
|
+
puts "No card table found in this project."
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get the card table
|
|
25
|
+
card_table_url = card_table_dock['url']
|
|
26
|
+
card_table = client.get(card_table_url)
|
|
27
|
+
|
|
28
|
+
puts "#{card_table['id']} #{card_table['title']}"
|
|
29
|
+
|
|
30
|
+
# Show columns summary
|
|
31
|
+
lists = card_table['lists'] || []
|
|
32
|
+
if lists.any?
|
|
33
|
+
puts ""
|
|
34
|
+
puts "Columns:"
|
|
35
|
+
lists.each do |list|
|
|
36
|
+
puts " - #{list['title']} (#{list['cards_count']} cards)"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Commands
|
|
5
|
+
class Card
|
|
6
|
+
def run(project_id, card_id, comments: false)
|
|
7
|
+
client = Client.new
|
|
8
|
+
|
|
9
|
+
card = client.get("/buckets/#{project_id}/card_tables/cards/#{card_id}.json")
|
|
10
|
+
|
|
11
|
+
puts "Card: #{card['title']}"
|
|
12
|
+
puts "=" * 70
|
|
13
|
+
puts ""
|
|
14
|
+
puts "ID: #{card['id']}"
|
|
15
|
+
puts "Creator: #{card.dig('creator', 'name')}"
|
|
16
|
+
puts "Created: #{card['created_at']}"
|
|
17
|
+
puts "Updated: #{card['updated_at']}"
|
|
18
|
+
puts "URL: #{card['app_url']}"
|
|
19
|
+
|
|
20
|
+
if card['assignees'] && !card['assignees'].empty?
|
|
21
|
+
names = card['assignees'].map { |a| a['name'] }.join(', ')
|
|
22
|
+
puts "Assigned: #{names}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
puts ""
|
|
26
|
+
puts "Description:"
|
|
27
|
+
puts "-" * 40
|
|
28
|
+
description = strip_html(card['content'] || card['description'] || 'No description')
|
|
29
|
+
puts description
|
|
30
|
+
puts ""
|
|
31
|
+
|
|
32
|
+
if comments && card['comments_count']&.positive?
|
|
33
|
+
puts "Comments (#{card['comments_count']}):"
|
|
34
|
+
puts "-" * 40
|
|
35
|
+
|
|
36
|
+
all_comments = client.get_all(card['comments_url'])
|
|
37
|
+
|
|
38
|
+
all_comments.each do |comment|
|
|
39
|
+
author = comment.dig('creator', 'name') || 'Unknown'
|
|
40
|
+
content = strip_html(comment['content'] || '')
|
|
41
|
+
created = comment['created_at']
|
|
42
|
+
|
|
43
|
+
puts ""
|
|
44
|
+
puts "#{author} (#{created}):"
|
|
45
|
+
puts content
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def strip_html(html)
|
|
53
|
+
return '' unless html
|
|
54
|
+
html.gsub(/<[^>]*>/, ' ').gsub(/\s+/, ' ').strip
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Commands
|
|
5
|
+
class Cards
|
|
6
|
+
def run(project_id, board_id, column: nil)
|
|
7
|
+
client = Client.new
|
|
8
|
+
|
|
9
|
+
# Get the card table
|
|
10
|
+
card_table = client.get("/buckets/#{project_id}/card_tables/#{board_id}.json")
|
|
11
|
+
|
|
12
|
+
puts "Cards: #{card_table['title']}"
|
|
13
|
+
puts "=" * 70
|
|
14
|
+
|
|
15
|
+
lists = card_table['lists'] || []
|
|
16
|
+
|
|
17
|
+
if lists.empty?
|
|
18
|
+
puts "No columns found."
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
lists.each do |list|
|
|
23
|
+
column_name = list['title']
|
|
24
|
+
|
|
25
|
+
# Filter by column if specified
|
|
26
|
+
if column
|
|
27
|
+
next unless column_name.downcase.include?(column.downcase)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
next if list['cards_count'].zero?
|
|
31
|
+
|
|
32
|
+
puts ""
|
|
33
|
+
puts "#{column_name} (#{list['cards_count']})"
|
|
34
|
+
puts "-" * 40
|
|
35
|
+
|
|
36
|
+
# Fetch cards from this column
|
|
37
|
+
cards = client.get(list['cards_url'])
|
|
38
|
+
|
|
39
|
+
cards.each do |card|
|
|
40
|
+
creator = card.dig('creator', 'name') || 'Unknown'
|
|
41
|
+
puts " #{card['id']} #{card['title']}"
|
|
42
|
+
puts " by #{creator}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Commands
|
|
5
|
+
class Init
|
|
6
|
+
def run(client_id: nil, client_secret: nil, account_id: nil, redirect_uri: nil)
|
|
7
|
+
puts "Basecamp CLI Configuration"
|
|
8
|
+
puts "=" * 40
|
|
9
|
+
|
|
10
|
+
config = {}
|
|
11
|
+
|
|
12
|
+
config[:client_id] = client_id || prompt("Client ID")
|
|
13
|
+
config[:client_secret] = client_secret || prompt("Client Secret")
|
|
14
|
+
config[:account_id] = account_id || prompt("Account ID")
|
|
15
|
+
config[:redirect_uri] = redirect_uri || prompt("Redirect URI", default: "http://localhost:3002/callback")
|
|
16
|
+
|
|
17
|
+
Config.save(config)
|
|
18
|
+
|
|
19
|
+
puts "\nConfiguration saved to: #{Config::CONFIG_FILE}"
|
|
20
|
+
puts "Run 'basecamp auth' to authenticate."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def prompt(label, default: nil)
|
|
26
|
+
if default
|
|
27
|
+
print "#{label} [#{default}]: "
|
|
28
|
+
else
|
|
29
|
+
print "#{label}: "
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
value = $stdin.gets.chomp
|
|
33
|
+
value.empty? ? default : value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Commands
|
|
5
|
+
class Move
|
|
6
|
+
def run(project_id, board_id, card_id, to:)
|
|
7
|
+
client = Client.new
|
|
8
|
+
|
|
9
|
+
# Get the card table to find the target column
|
|
10
|
+
card_table = client.get("/buckets/#{project_id}/card_tables/#{board_id}.json")
|
|
11
|
+
|
|
12
|
+
lists = card_table['lists'] || []
|
|
13
|
+
target_column = lists.find { |l| l['title'].downcase == to.downcase }
|
|
14
|
+
|
|
15
|
+
unless target_column
|
|
16
|
+
puts "Column '#{to}' not found."
|
|
17
|
+
puts "Available columns: #{lists.map { |l| l['title'] }.join(', ')}"
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Move the card
|
|
22
|
+
client.post("/buckets/#{project_id}/card_tables/cards/#{card_id}/moves.json", {
|
|
23
|
+
column_id: target_column['id']
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
puts "Card #{card_id} moved to '#{target_column['title']}'"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Commands
|
|
5
|
+
class Projects
|
|
6
|
+
def run
|
|
7
|
+
client = Client.new
|
|
8
|
+
projects = client.get('/projects.json')
|
|
9
|
+
|
|
10
|
+
if projects.empty?
|
|
11
|
+
puts "No projects found."
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
puts "Projects"
|
|
16
|
+
puts "=" * 60
|
|
17
|
+
|
|
18
|
+
projects.each do |project|
|
|
19
|
+
status = project['status']
|
|
20
|
+
status_icon = status == 'active' ? '*' : ' '
|
|
21
|
+
|
|
22
|
+
puts "[#{status_icon}] #{project['id']} #{project['name']}"
|
|
23
|
+
puts " #{project['description']}" if project['description'] && !project['description'].empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
puts ""
|
|
27
|
+
puts "[*] = active"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
class Config
|
|
5
|
+
CONFIG_FILE = File.expand_path('~/.basecamp.json')
|
|
6
|
+
TOKEN_FILE = File.expand_path('~/.basecamp_token.json')
|
|
7
|
+
|
|
8
|
+
# OAuth endpoints
|
|
9
|
+
AUTHORIZATION_URL = 'https://launchpad.37signals.com/authorization/new'
|
|
10
|
+
TOKEN_URL = 'https://launchpad.37signals.com/authorization/token'
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def load
|
|
14
|
+
return @config if @config
|
|
15
|
+
|
|
16
|
+
unless File.exist?(CONFIG_FILE)
|
|
17
|
+
raise "Config file not found: #{CONFIG_FILE}\nRun 'basecamp init' to create one."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@config = JSON.parse(File.read(CONFIG_FILE), symbolize_names: true)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def save(config)
|
|
24
|
+
File.write(CONFIG_FILE, JSON.pretty_generate(config))
|
|
25
|
+
@config = config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def client_id
|
|
29
|
+
load[:client_id]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def client_secret
|
|
33
|
+
load[:client_secret]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def account_id
|
|
37
|
+
load[:account_id]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def redirect_uri
|
|
41
|
+
load[:redirect_uri] || 'http://localhost:3002/callback'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def api_base_url
|
|
45
|
+
"https://3.basecampapi.com/#{account_id}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def token
|
|
49
|
+
return @token if @token
|
|
50
|
+
|
|
51
|
+
unless File.exist?(TOKEN_FILE)
|
|
52
|
+
raise "Not authenticated. Run 'basecamp auth' first."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
token_data = JSON.parse(File.read(TOKEN_FILE), symbolize_names: true)
|
|
56
|
+
|
|
57
|
+
if token_data[:expires_at] && Time.now.to_i > token_data[:expires_at]
|
|
58
|
+
raise "Token expired. Run 'basecamp auth' to refresh."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@token = token_data[:access_token]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def save_token(token_data)
|
|
65
|
+
token_data[:expires_at] = Time.now.to_i + token_data[:expires_in] if token_data[:expires_in]
|
|
66
|
+
File.write(TOKEN_FILE, JSON.pretty_generate(token_data))
|
|
67
|
+
@token = token_data[:access_token]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def clear_token_cache
|
|
71
|
+
@token = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: basecamp-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rob Zolkos
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: webrick
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.8'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.8'
|
|
27
|
+
description: A simple CLI for Basecamp. List projects, browse card tables, view cards,
|
|
28
|
+
and move cards between columns.
|
|
29
|
+
email:
|
|
30
|
+
- rob@zolkos.com
|
|
31
|
+
executables:
|
|
32
|
+
- basecamp
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- LICENSE
|
|
37
|
+
- README.md
|
|
38
|
+
- bin/basecamp
|
|
39
|
+
- lib/basecamp/cli.rb
|
|
40
|
+
- lib/basecamp/client.rb
|
|
41
|
+
- lib/basecamp/commands/auth.rb
|
|
42
|
+
- lib/basecamp/commands/boards.rb
|
|
43
|
+
- lib/basecamp/commands/card.rb
|
|
44
|
+
- lib/basecamp/commands/cards.rb
|
|
45
|
+
- lib/basecamp/commands/init.rb
|
|
46
|
+
- lib/basecamp/commands/move.rb
|
|
47
|
+
- lib/basecamp/commands/projects.rb
|
|
48
|
+
- lib/basecamp/config.rb
|
|
49
|
+
- lib/basecamp/version.rb
|
|
50
|
+
homepage: https://github.com/robzolkos/ruby-basecamp-cli
|
|
51
|
+
licenses:
|
|
52
|
+
- MIT
|
|
53
|
+
metadata:
|
|
54
|
+
homepage_uri: https://github.com/robzolkos/ruby-basecamp-cli
|
|
55
|
+
source_code_uri: https://github.com/robzolkos/ruby-basecamp-cli
|
|
56
|
+
post_install_message:
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 3.0.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.5.22
|
|
72
|
+
signing_key:
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: Command-line interface for Basecamp
|
|
75
|
+
test_files: []
|