geekbot-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 +517 -0
- package/package.json +50 -0
- package/scripts/postinstall.mjs +27 -0
- package/skills/geekbot/SKILL.md +281 -0
- package/skills/geekbot/check-cli.sh +36 -0
- package/skills/geekbot/cli-commands.md +382 -0
- package/skills/geekbot/error-recovery.md +95 -0
- package/skills/geekbot/manager-workflows.md +408 -0
- package/skills/geekbot/reporter-workflows.md +275 -0
- package/skills/geekbot/standup-templates.json +244 -0
- package/src/auth/keychain.ts +38 -0
- package/src/auth/resolver.ts +44 -0
- package/src/cli/commands/auth.ts +56 -0
- package/src/cli/commands/me.ts +34 -0
- package/src/cli/commands/poll.ts +91 -0
- package/src/cli/commands/report.ts +66 -0
- package/src/cli/commands/standup.ts +234 -0
- package/src/cli/commands/team.ts +40 -0
- package/src/cli/globals.ts +31 -0
- package/src/cli/index.ts +94 -0
- package/src/errors/cli-error.ts +16 -0
- package/src/errors/error-handler.ts +63 -0
- package/src/errors/exit-codes.ts +14 -0
- package/src/errors/not-found-helper.ts +86 -0
- package/src/handlers/auth-handlers.ts +152 -0
- package/src/handlers/me-handlers.ts +27 -0
- package/src/handlers/poll-handlers.ts +187 -0
- package/src/handlers/report-handlers.ts +87 -0
- package/src/handlers/standup-handlers.ts +534 -0
- package/src/handlers/team-handlers.ts +38 -0
- package/src/http/authenticated-client.ts +17 -0
- package/src/http/client.ts +138 -0
- package/src/http/errors.ts +134 -0
- package/src/output/envelope.ts +50 -0
- package/src/output/formatter.ts +12 -0
- package/src/schemas/common.ts +13 -0
- package/src/schemas/poll.ts +89 -0
- package/src/schemas/report.ts +124 -0
- package/src/schemas/standup.ts +64 -0
- package/src/schemas/team.ts +11 -0
- package/src/schemas/user.ts +70 -0
- package/src/types.ts +30 -0
- package/src/utils/constants.ts +24 -0
- package/src/utils/input-parsers.ts +234 -0
- package/src/utils/receipt.ts +94 -0
- package/src/utils/validation.ts +128 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { handleError } from "../../errors/error-handler.ts";
|
|
3
|
+
import {
|
|
4
|
+
handlePollCreate,
|
|
5
|
+
handlePollGet,
|
|
6
|
+
handlePollList,
|
|
7
|
+
handlePollVotes,
|
|
8
|
+
} from "../../handlers/poll-handlers.ts";
|
|
9
|
+
import { getGlobalOptions } from "../globals.ts";
|
|
10
|
+
|
|
11
|
+
export function createPollCommand(): Command {
|
|
12
|
+
const poll = new Command("poll").description("Manage polls (Slack teams only)");
|
|
13
|
+
|
|
14
|
+
poll
|
|
15
|
+
.command("list")
|
|
16
|
+
.description("List all polls")
|
|
17
|
+
.addHelpText("after", "\nExamples:\n geekbot poll list")
|
|
18
|
+
.action(async function (this: Command) {
|
|
19
|
+
const globalOpts = getGlobalOptions(this);
|
|
20
|
+
try {
|
|
21
|
+
await handlePollList(globalOpts);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
handleError(error, globalOpts.debug);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
poll
|
|
28
|
+
.command("get")
|
|
29
|
+
.description("Get a poll by ID")
|
|
30
|
+
.argument("<id>", "Poll ID (numeric)")
|
|
31
|
+
.addHelpText("after", "\nExamples:\n geekbot poll get 456")
|
|
32
|
+
.action(async function (this: Command, id: string) {
|
|
33
|
+
const globalOpts = getGlobalOptions(this);
|
|
34
|
+
try {
|
|
35
|
+
await handlePollGet(id, globalOpts);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
handleError(error, globalOpts.debug);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
poll
|
|
42
|
+
.command("create")
|
|
43
|
+
.description("Create a new poll")
|
|
44
|
+
.requiredOption("--name <name>", "Poll name")
|
|
45
|
+
.requiredOption("--channel <channel>", "Slack channel")
|
|
46
|
+
.requiredOption("--question <text>", "Poll question text")
|
|
47
|
+
.requiredOption("--choices <json>", "Choices as JSON array of strings")
|
|
48
|
+
.addHelpText(
|
|
49
|
+
"after",
|
|
50
|
+
'\nExamples:\n geekbot poll create --name "Lunch" --channel "#team" --question "Where?" --choices \'["Pizza", "Sushi"]\'',
|
|
51
|
+
)
|
|
52
|
+
.action(async function (this: Command) {
|
|
53
|
+
const globalOpts = getGlobalOptions(this);
|
|
54
|
+
try {
|
|
55
|
+
const opts = this.opts();
|
|
56
|
+
await handlePollCreate(
|
|
57
|
+
{
|
|
58
|
+
name: opts.name,
|
|
59
|
+
channel: opts.channel,
|
|
60
|
+
question: opts.question,
|
|
61
|
+
choices: opts.choices,
|
|
62
|
+
},
|
|
63
|
+
globalOpts,
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
handleError(error, globalOpts.debug);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
poll
|
|
71
|
+
.command("votes")
|
|
72
|
+
.description("Get voting results for a poll")
|
|
73
|
+
.argument("<id>", "Poll ID (numeric)")
|
|
74
|
+
.option("--after <date>", "Votes after date")
|
|
75
|
+
.option("--before <date>", "Votes before date")
|
|
76
|
+
.addHelpText(
|
|
77
|
+
"after",
|
|
78
|
+
"\nExamples:\n geekbot poll votes 456\n geekbot poll votes 456 --after 2024-01-01",
|
|
79
|
+
)
|
|
80
|
+
.action(async function (this: Command, id: string) {
|
|
81
|
+
const globalOpts = getGlobalOptions(this);
|
|
82
|
+
try {
|
|
83
|
+
const opts = this.opts();
|
|
84
|
+
await handlePollVotes(id, { after: opts.after, before: opts.before }, globalOpts);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
handleError(error, globalOpts.debug);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return poll;
|
|
91
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { handleError } from "../../errors/error-handler.ts";
|
|
3
|
+
import { handleReportCreate, handleReportList } from "../../handlers/report-handlers.ts";
|
|
4
|
+
import { getGlobalOptions } from "../globals.ts";
|
|
5
|
+
|
|
6
|
+
export function createReportCommand(): Command {
|
|
7
|
+
const report = new Command("report").description("Manage reports");
|
|
8
|
+
|
|
9
|
+
report
|
|
10
|
+
.command("list")
|
|
11
|
+
.description("List reports with optional filters")
|
|
12
|
+
.option("--standup-id <id>", "Filter by standup ID")
|
|
13
|
+
.option("--user-id <id>", "Filter by user ID")
|
|
14
|
+
.option("--before <date>", "Reports before date (ISO 8601 or unix timestamp)")
|
|
15
|
+
.option("--after <date>", "Reports after date (ISO 8601 or unix timestamp)")
|
|
16
|
+
.option("--limit <n>", "Max number of reports to return")
|
|
17
|
+
.addHelpText(
|
|
18
|
+
"after",
|
|
19
|
+
"\nExamples:\n geekbot report list --standup-id 123\n geekbot report list --standup-id 123 --limit 10\n geekbot report list --after 2024-01-01",
|
|
20
|
+
)
|
|
21
|
+
.action(async function (this: Command) {
|
|
22
|
+
const globalOpts = getGlobalOptions(this);
|
|
23
|
+
try {
|
|
24
|
+
const opts = this.opts();
|
|
25
|
+
await handleReportList(
|
|
26
|
+
{
|
|
27
|
+
standupId: opts.standupId,
|
|
28
|
+
userId: opts.userId,
|
|
29
|
+
before: opts.before,
|
|
30
|
+
after: opts.after,
|
|
31
|
+
limit: opts.limit,
|
|
32
|
+
},
|
|
33
|
+
globalOpts,
|
|
34
|
+
);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
handleError(error, globalOpts.debug);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
report
|
|
41
|
+
.command("create")
|
|
42
|
+
.description("Submit a report for a standup")
|
|
43
|
+
.requiredOption("--standup-id <id>", "Standup ID to report on")
|
|
44
|
+
.requiredOption("--answers <json>", 'Answers as JSON object: {"question_id": "answer", ...}')
|
|
45
|
+
.addHelpText(
|
|
46
|
+
"after",
|
|
47
|
+
'\nExamples:\n geekbot report create --standup-id 123 --answers \'{"101": "Done feature X", "102": "Working on Y"}\'',
|
|
48
|
+
)
|
|
49
|
+
.action(async function (this: Command) {
|
|
50
|
+
const globalOpts = getGlobalOptions(this);
|
|
51
|
+
try {
|
|
52
|
+
const opts = this.opts();
|
|
53
|
+
await handleReportCreate(
|
|
54
|
+
{
|
|
55
|
+
standupId: opts.standupId,
|
|
56
|
+
answers: opts.answers,
|
|
57
|
+
},
|
|
58
|
+
globalOpts,
|
|
59
|
+
);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
handleError(error, globalOpts.debug);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return report;
|
|
66
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { handleError } from "../../errors/error-handler.ts";
|
|
3
|
+
import {
|
|
4
|
+
handleStandupCreate,
|
|
5
|
+
handleStandupDelete,
|
|
6
|
+
handleStandupDuplicate,
|
|
7
|
+
handleStandupGet,
|
|
8
|
+
handleStandupList,
|
|
9
|
+
handleStandupReplace,
|
|
10
|
+
handleStandupStart,
|
|
11
|
+
handleStandupUpdate,
|
|
12
|
+
} from "../../handlers/standup-handlers.ts";
|
|
13
|
+
import { getGlobalOptions } from "../globals.ts";
|
|
14
|
+
|
|
15
|
+
export function createStandupCommand(): Command {
|
|
16
|
+
const standup = new Command("standup").description("Manage standups");
|
|
17
|
+
|
|
18
|
+
standup
|
|
19
|
+
.command("list")
|
|
20
|
+
.description("List standups you participate in")
|
|
21
|
+
.option("--admin", "Include all team standups (admin only)")
|
|
22
|
+
.option("--brief", "Show only id, name, channel, time, timezone, and days")
|
|
23
|
+
.option("--name <name>", "Filter by name (case-insensitive substring match)")
|
|
24
|
+
.option("--channel <channel>", "Filter by channel (case-insensitive substring match)")
|
|
25
|
+
.option("--mine", "Show only standups you are a member of")
|
|
26
|
+
.option("--member <id>", "Filter by member user ID")
|
|
27
|
+
.option("--limit <n>", "Max number of standups to return")
|
|
28
|
+
.addHelpText(
|
|
29
|
+
"after",
|
|
30
|
+
'\nExamples:\n geekbot standup list\n geekbot standup list --admin\n geekbot standup list --brief\n geekbot standup list --brief --limit 10\n geekbot standup list --name "daily"\n geekbot standup list --channel "#status"\n geekbot standup list --mine --brief\n geekbot standup list --member "UHNM44125" --brief',
|
|
31
|
+
)
|
|
32
|
+
.action(async function (this: Command) {
|
|
33
|
+
const globalOpts = getGlobalOptions(this);
|
|
34
|
+
try {
|
|
35
|
+
const opts = this.opts();
|
|
36
|
+
await handleStandupList(
|
|
37
|
+
{
|
|
38
|
+
admin: opts.admin,
|
|
39
|
+
brief: opts.brief,
|
|
40
|
+
name: opts.name,
|
|
41
|
+
channel: opts.channel,
|
|
42
|
+
mine: opts.mine,
|
|
43
|
+
member: opts.member,
|
|
44
|
+
limit: opts.limit,
|
|
45
|
+
},
|
|
46
|
+
globalOpts,
|
|
47
|
+
);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
handleError(error, globalOpts.debug);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
standup
|
|
54
|
+
.command("get")
|
|
55
|
+
.description("Get a standup by ID")
|
|
56
|
+
.argument("<id>", "Standup ID (numeric)")
|
|
57
|
+
.addHelpText("after", "\nExamples:\n geekbot standup get 123")
|
|
58
|
+
.action(async function (this: Command, id: string) {
|
|
59
|
+
const globalOpts = getGlobalOptions(this);
|
|
60
|
+
try {
|
|
61
|
+
await handleStandupGet(id, globalOpts);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
handleError(error, globalOpts.debug);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
standup
|
|
68
|
+
.command("create")
|
|
69
|
+
.description("Create a new standup")
|
|
70
|
+
.requiredOption("--name <name>", "Standup name")
|
|
71
|
+
.requiredOption("--channel <channel>", "Slack channel name")
|
|
72
|
+
.option("--time <time>", "Time in HH:MM 24-hour format (default: 10:00)")
|
|
73
|
+
.option("--timezone <tz>", "IANA timezone")
|
|
74
|
+
.option("--days <days>", "Comma-separated days (default: Mon-Fri)")
|
|
75
|
+
.requiredOption("--questions <json>", "Questions as JSON array")
|
|
76
|
+
.option("--users <ids>", "Comma-separated user IDs")
|
|
77
|
+
.option("--wait-time <minutes>", "Minutes between users")
|
|
78
|
+
.addHelpText(
|
|
79
|
+
"after",
|
|
80
|
+
'\nExamples:\n geekbot standup create --name "Daily" --channel "#engineering"\n geekbot standup create --name "Weekly" --channel "#team" --days "Mon" --time "09:00"',
|
|
81
|
+
)
|
|
82
|
+
.action(async function (this: Command) {
|
|
83
|
+
const globalOpts = getGlobalOptions(this);
|
|
84
|
+
try {
|
|
85
|
+
const opts = this.opts();
|
|
86
|
+
await handleStandupCreate(
|
|
87
|
+
{
|
|
88
|
+
name: opts.name,
|
|
89
|
+
channel: opts.channel,
|
|
90
|
+
time: opts.time,
|
|
91
|
+
timezone: opts.timezone,
|
|
92
|
+
days: opts.days,
|
|
93
|
+
questions: opts.questions,
|
|
94
|
+
users: opts.users,
|
|
95
|
+
waitTime: opts.waitTime,
|
|
96
|
+
},
|
|
97
|
+
globalOpts,
|
|
98
|
+
);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
handleError(error, globalOpts.debug);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
standup
|
|
105
|
+
.command("update")
|
|
106
|
+
.description("Partially update a standup (PATCH)")
|
|
107
|
+
.argument("<id>", "Standup ID (numeric)")
|
|
108
|
+
.option("--name <name>", "New standup name")
|
|
109
|
+
.option("--channel <channel>", "New channel")
|
|
110
|
+
.option("--time <time>", "New time (HH:MM)")
|
|
111
|
+
.option("--timezone <tz>", "New timezone")
|
|
112
|
+
.option("--days <days>", "New days (comma-separated)")
|
|
113
|
+
.option("--questions <json>", "Questions as JSON array")
|
|
114
|
+
.option("--users <ids>", "Comma-separated user IDs")
|
|
115
|
+
.option("--wait-time <minutes>", "New wait time in minutes")
|
|
116
|
+
.addHelpText(
|
|
117
|
+
"after",
|
|
118
|
+
'\nExamples:\n geekbot standup update 123 --name "Updated Daily"\n geekbot standup update 123 --time "14:00" --days "Mon,Wed,Fri"\n geekbot standup update 123 --questions \'["What did you do?","Any blockers?"]\'',
|
|
119
|
+
)
|
|
120
|
+
.action(async function (this: Command, id: string) {
|
|
121
|
+
const globalOpts = getGlobalOptions(this);
|
|
122
|
+
try {
|
|
123
|
+
const opts = this.opts();
|
|
124
|
+
await handleStandupUpdate(
|
|
125
|
+
id,
|
|
126
|
+
{
|
|
127
|
+
name: opts.name,
|
|
128
|
+
channel: opts.channel,
|
|
129
|
+
time: opts.time,
|
|
130
|
+
timezone: opts.timezone,
|
|
131
|
+
days: opts.days,
|
|
132
|
+
questions: opts.questions,
|
|
133
|
+
users: opts.users,
|
|
134
|
+
waitTime: opts.waitTime,
|
|
135
|
+
},
|
|
136
|
+
globalOpts,
|
|
137
|
+
);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
handleError(error, globalOpts.debug);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
standup
|
|
144
|
+
.command("replace")
|
|
145
|
+
.description("Fully replace a standup (PUT)")
|
|
146
|
+
.argument("<id>", "Standup ID (numeric)")
|
|
147
|
+
.requiredOption("--name <name>", "Standup name")
|
|
148
|
+
.requiredOption("--channel <channel>", "Slack channel")
|
|
149
|
+
.option("--time <time>", "Time (HH:MM)", "10:00")
|
|
150
|
+
.option("--timezone <tz>", "Timezone", "UTC")
|
|
151
|
+
.option("--days <days>", "Days (comma-separated)", "Mon,Tue,Wed,Thu,Fri")
|
|
152
|
+
.option("--questions <json>", "Questions as JSON array")
|
|
153
|
+
.option("--users <ids>", "User IDs (comma-separated)")
|
|
154
|
+
.option("--wait-time <minutes>", "Wait time in minutes")
|
|
155
|
+
.addHelpText(
|
|
156
|
+
"after",
|
|
157
|
+
'\nExamples:\n geekbot standup replace 123 --name "New Daily" --channel "#general"',
|
|
158
|
+
)
|
|
159
|
+
.action(async function (this: Command, id: string) {
|
|
160
|
+
const globalOpts = getGlobalOptions(this);
|
|
161
|
+
try {
|
|
162
|
+
const opts = this.opts();
|
|
163
|
+
await handleStandupReplace(
|
|
164
|
+
id,
|
|
165
|
+
{
|
|
166
|
+
name: opts.name,
|
|
167
|
+
channel: opts.channel,
|
|
168
|
+
time: opts.time,
|
|
169
|
+
timezone: opts.timezone,
|
|
170
|
+
days: opts.days,
|
|
171
|
+
questions: opts.questions,
|
|
172
|
+
users: opts.users,
|
|
173
|
+
waitTime: opts.waitTime,
|
|
174
|
+
},
|
|
175
|
+
globalOpts,
|
|
176
|
+
);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
handleError(error, globalOpts.debug);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
standup
|
|
183
|
+
.command("delete")
|
|
184
|
+
.description("Delete a standup")
|
|
185
|
+
.argument("<id>", "Standup ID (numeric)")
|
|
186
|
+
.option("--yes", "Confirm deletion (required)")
|
|
187
|
+
.addHelpText("after", "\nExamples:\n geekbot standup delete 123 --yes")
|
|
188
|
+
.action(async function (this: Command, id: string) {
|
|
189
|
+
const globalOpts = getGlobalOptions(this);
|
|
190
|
+
try {
|
|
191
|
+
const opts = this.opts();
|
|
192
|
+
await handleStandupDelete(id, { yes: opts.yes }, globalOpts);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
handleError(error, globalOpts.debug);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
standup
|
|
199
|
+
.command("duplicate")
|
|
200
|
+
.description("Duplicate an existing standup")
|
|
201
|
+
.argument("<id>", "Standup ID to duplicate (numeric)")
|
|
202
|
+
.requiredOption("--name <name>", "Name for the new standup")
|
|
203
|
+
.addHelpText("after", '\nExamples:\n geekbot standup duplicate 123 --name "Copy of Daily"')
|
|
204
|
+
.action(async function (this: Command, id: string) {
|
|
205
|
+
const globalOpts = getGlobalOptions(this);
|
|
206
|
+
try {
|
|
207
|
+
const opts = this.opts();
|
|
208
|
+
await handleStandupDuplicate(id, { name: opts.name }, globalOpts);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
handleError(error, globalOpts.debug);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
standup
|
|
215
|
+
.command("start")
|
|
216
|
+
.description("Trigger a standup immediately")
|
|
217
|
+
.argument("<id>", "Standup ID (numeric)")
|
|
218
|
+
.option("--users <ids>", "Comma-separated user IDs (omit for all)")
|
|
219
|
+
.addHelpText(
|
|
220
|
+
"after",
|
|
221
|
+
'\nExamples:\n geekbot standup start 123\n geekbot standup start 123 --users "U123,U456"',
|
|
222
|
+
)
|
|
223
|
+
.action(async function (this: Command, id: string) {
|
|
224
|
+
const globalOpts = getGlobalOptions(this);
|
|
225
|
+
try {
|
|
226
|
+
const opts = this.opts();
|
|
227
|
+
await handleStandupStart(id, { users: opts.users }, globalOpts);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
handleError(error, globalOpts.debug);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return standup;
|
|
234
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { handleError } from "../../errors/error-handler.ts";
|
|
3
|
+
import { handleTeamList, handleTeamSearch } from "../../handlers/team-handlers.ts";
|
|
4
|
+
import { getGlobalOptions } from "../globals.ts";
|
|
5
|
+
|
|
6
|
+
export function createTeamCommand(): Command {
|
|
7
|
+
const team = new Command("team").description("View team information");
|
|
8
|
+
|
|
9
|
+
team
|
|
10
|
+
.command("list")
|
|
11
|
+
.description("List all teams with members")
|
|
12
|
+
.addHelpText("after", "\nExamples:\n geekbot team list")
|
|
13
|
+
.action(async function (this: Command) {
|
|
14
|
+
const globalOpts = getGlobalOptions(this);
|
|
15
|
+
try {
|
|
16
|
+
await handleTeamList(globalOpts);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
handleError(error, globalOpts.debug);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
team
|
|
23
|
+
.command("search")
|
|
24
|
+
.description("Search team members by name, username, or email")
|
|
25
|
+
.argument("<query>", "Search term (case-insensitive substring match)")
|
|
26
|
+
.addHelpText(
|
|
27
|
+
"after",
|
|
28
|
+
'\nExamples:\n geekbot team search jenny\n geekbot team search "smith"\n geekbot team search @example.com',
|
|
29
|
+
)
|
|
30
|
+
.action(async function (this: Command, query: string) {
|
|
31
|
+
const globalOpts = getGlobalOptions(this);
|
|
32
|
+
try {
|
|
33
|
+
await handleTeamSearch(query, globalOpts);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
handleError(error, globalOpts.debug);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return team;
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Command, Option } from "commander";
|
|
2
|
+
|
|
3
|
+
export interface GlobalOptions {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
output: "json";
|
|
6
|
+
debug: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Add global options to the root program.
|
|
11
|
+
* These are inherited by all subcommands via optsWithGlobals().
|
|
12
|
+
*/
|
|
13
|
+
export function addGlobalOptions(program: Command): void {
|
|
14
|
+
program
|
|
15
|
+
.option("--api-key <key>", "Geekbot API key (overrides GEEKBOT_API_KEY env var)")
|
|
16
|
+
.addOption(new Option("--output <format>", "Output format").choices(["json"]).default("json"))
|
|
17
|
+
.option("--debug", "Show debug output on stderr", false);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract global options from a command's optsWithGlobals().
|
|
22
|
+
* Call this from any action handler.
|
|
23
|
+
*/
|
|
24
|
+
export function getGlobalOptions(cmd: Command): GlobalOptions {
|
|
25
|
+
const opts = cmd.optsWithGlobals();
|
|
26
|
+
return {
|
|
27
|
+
apiKey: opts.apiKey,
|
|
28
|
+
output: "json",
|
|
29
|
+
debug: opts.debug === true,
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command, CommanderError } from "commander";
|
|
3
|
+
import { CliError } from "../errors/cli-error.ts";
|
|
4
|
+
import { handleError } from "../errors/error-handler.ts";
|
|
5
|
+
import { ExitCode } from "../errors/exit-codes.ts";
|
|
6
|
+
import { APP_NAME, APP_VERSION } from "../utils/constants.ts";
|
|
7
|
+
import { createAuthCommand } from "./commands/auth.ts";
|
|
8
|
+
import { createMeCommand } from "./commands/me.ts";
|
|
9
|
+
import { createPollCommand } from "./commands/poll.ts";
|
|
10
|
+
import { createReportCommand } from "./commands/report.ts";
|
|
11
|
+
import { createStandupCommand } from "./commands/standup.ts";
|
|
12
|
+
import { createTeamCommand } from "./commands/team.ts";
|
|
13
|
+
import { addGlobalOptions } from "./globals.ts";
|
|
14
|
+
|
|
15
|
+
// Recursively apply exitOverride to all subcommands so Commander throws
|
|
16
|
+
// CommanderError instead of calling process.exit() on usage errors.
|
|
17
|
+
function applyExitOverride(cmd: Command): void {
|
|
18
|
+
for (const sub of cmd.commands) {
|
|
19
|
+
sub.exitOverride();
|
|
20
|
+
applyExitOverride(sub);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create and configure the CLI program with all commands, options,
|
|
26
|
+
* exitOverride, and configureOutput wiring. Returns the fully configured
|
|
27
|
+
* Commander program instance without calling parseAsync.
|
|
28
|
+
*/
|
|
29
|
+
export function createProgram(): Command {
|
|
30
|
+
const program = new Command()
|
|
31
|
+
.name(APP_NAME)
|
|
32
|
+
.version(APP_VERSION, "-v, --version")
|
|
33
|
+
.description("Geekbot CLI -- manage standups, reports, and polls for AI agents and humans")
|
|
34
|
+
.exitOverride();
|
|
35
|
+
|
|
36
|
+
// Route Commander.js error output to stderr (Pitfall 4)
|
|
37
|
+
program.configureOutput({
|
|
38
|
+
writeOut: (str) => process.stderr.write(str),
|
|
39
|
+
writeErr: (str) => process.stderr.write(str),
|
|
40
|
+
outputError: (str, write) => write(str),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Add global flags
|
|
44
|
+
addGlobalOptions(program);
|
|
45
|
+
|
|
46
|
+
// Register resource subcommands (noun-verb pattern: CLI-01)
|
|
47
|
+
program.addCommand(createStandupCommand());
|
|
48
|
+
program.addCommand(createReportCommand());
|
|
49
|
+
program.addCommand(createPollCommand());
|
|
50
|
+
program.addCommand(createAuthCommand());
|
|
51
|
+
program.addCommand(createMeCommand());
|
|
52
|
+
program.addCommand(createTeamCommand());
|
|
53
|
+
|
|
54
|
+
applyExitOverride(program);
|
|
55
|
+
|
|
56
|
+
return program;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse argv and handle errors from Commander and action handlers.
|
|
61
|
+
* Exported for testing; the entrypoint calls this automatically.
|
|
62
|
+
*/
|
|
63
|
+
export async function main(program: Command, argv: string[] = process.argv): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
await program.parseAsync(argv);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const opts = program.opts();
|
|
68
|
+
if (error instanceof CommanderError) {
|
|
69
|
+
// Map Commander usage errors (missing args, unknown options, etc.)
|
|
70
|
+
// to CliError with ExitCode.USAGE so they go through the JSON envelope.
|
|
71
|
+
// Preserve Commander's exitCode 0 for --help/--version (not an error).
|
|
72
|
+
if (error.exitCode === 0) {
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
const usageError = new CliError(
|
|
76
|
+
error.message,
|
|
77
|
+
"usage_error",
|
|
78
|
+
ExitCode.USAGE,
|
|
79
|
+
false,
|
|
80
|
+
"Run with --help for usage information",
|
|
81
|
+
);
|
|
82
|
+
handleError(usageError, opts.debug === true);
|
|
83
|
+
} else {
|
|
84
|
+
handleError(error, opts.debug === true);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Run when executed as the CLI entrypoint (not when imported for testing).
|
|
90
|
+
// import.meta.main is true in Bun when this file is the entrypoint.
|
|
91
|
+
if (import.meta.main) {
|
|
92
|
+
const program = createProgram();
|
|
93
|
+
main(program);
|
|
94
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExitCodeValue } from "./exit-codes.ts";
|
|
2
|
+
|
|
3
|
+
export class CliError extends Error {
|
|
4
|
+
public override readonly name = "CliError";
|
|
5
|
+
|
|
6
|
+
constructor(
|
|
7
|
+
message: string,
|
|
8
|
+
public readonly code: string,
|
|
9
|
+
public readonly exitCode: ExitCodeValue,
|
|
10
|
+
public readonly retryable: boolean = false,
|
|
11
|
+
public readonly suggestion?: string,
|
|
12
|
+
public readonly context?: Record<string, unknown>,
|
|
13
|
+
) {
|
|
14
|
+
super(message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
import { failure } from "../output/envelope.ts";
|
|
3
|
+
import { writeOutput } from "../output/formatter.ts";
|
|
4
|
+
import { CliError } from "./cli-error.ts";
|
|
5
|
+
import { ExitCode } from "./exit-codes.ts";
|
|
6
|
+
|
|
7
|
+
function formatZodMessage(error: ZodError): string {
|
|
8
|
+
return error.issues
|
|
9
|
+
.map((issue) => {
|
|
10
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
11
|
+
return `${path}: ${issue.message}`;
|
|
12
|
+
})
|
|
13
|
+
.join("; ");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function handleError(error: unknown, debug: boolean = false): never {
|
|
17
|
+
if (error instanceof ZodError) {
|
|
18
|
+
writeOutput(
|
|
19
|
+
failure({
|
|
20
|
+
code: "schema_validation_error",
|
|
21
|
+
message: `Unexpected API response: ${formatZodMessage(error)}`,
|
|
22
|
+
retryable: false,
|
|
23
|
+
suggestion:
|
|
24
|
+
"The API returned data in an unexpected format. The API may have changed or there may be a version mismatch.",
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (debug) {
|
|
29
|
+
process.stderr.write(`[debug] ZodError issues: ${JSON.stringify(error.issues)}\n`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
process.exit(ExitCode.API_ERROR);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (error instanceof CliError) {
|
|
36
|
+
writeOutput(
|
|
37
|
+
failure({
|
|
38
|
+
code: error.code,
|
|
39
|
+
message: error.message,
|
|
40
|
+
retryable: error.retryable,
|
|
41
|
+
suggestion: error.suggestion ?? null,
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (debug && error.context) {
|
|
46
|
+
process.stderr.write(`[debug] Error context: ${JSON.stringify(error.context)}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process.exit(error.exitCode);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Unknown/unexpected error
|
|
53
|
+
writeOutput(
|
|
54
|
+
failure({
|
|
55
|
+
code: "internal_error",
|
|
56
|
+
message: error instanceof Error ? error.message : String(error),
|
|
57
|
+
retryable: false,
|
|
58
|
+
suggestion: "This is an unexpected error. Please report it.",
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
process.exit(ExitCode.GENERAL);
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const ExitCode = {
|
|
2
|
+
SUCCESS: 0,
|
|
3
|
+
GENERAL: 1,
|
|
4
|
+
USAGE: 2,
|
|
5
|
+
NOT_FOUND: 3,
|
|
6
|
+
AUTH: 4,
|
|
7
|
+
FORBIDDEN: 5,
|
|
8
|
+
VALIDATION: 6,
|
|
9
|
+
NETWORK: 7,
|
|
10
|
+
CONFLICT: 8,
|
|
11
|
+
API_ERROR: 9,
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode];
|