geekbot-cli 0.2.2 → 0.2.3
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/package.json +1 -1
- package/src/auth/resolver.ts +8 -2
- package/src/cli/commands/report.ts +2 -2
- package/src/handlers/standup-handlers.ts +34 -8
- package/src/schemas/report.ts +6 -6
- package/src/schemas/standup.ts +1 -1
- package/src/utils/input-parsers.ts +3 -3
- package/src/utils/receipt.ts +13 -1
- package/src/utils/validation.ts +3 -2
package/package.json
CHANGED
package/src/auth/resolver.ts
CHANGED
|
@@ -13,13 +13,19 @@ export async function resolveCredential(
|
|
|
13
13
|
): Promise<CredentialResult> {
|
|
14
14
|
// Priority 1: --api-key flag
|
|
15
15
|
if (options.apiKeyFlag) {
|
|
16
|
-
|
|
16
|
+
const trimmed = options.apiKeyFlag.trim();
|
|
17
|
+
if (trimmed) {
|
|
18
|
+
return { apiKey: trimmed, source: "flag" };
|
|
19
|
+
}
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
// Priority 2: GEEKBOT_API_KEY env var
|
|
20
23
|
const envKey = process.env.GEEKBOT_API_KEY;
|
|
21
24
|
if (envKey) {
|
|
22
|
-
|
|
25
|
+
const trimmed = envKey.trim();
|
|
26
|
+
if (trimmed) {
|
|
27
|
+
return { apiKey: trimmed, source: "env" };
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
// Priority 3: OS keychain
|
|
@@ -11,8 +11,8 @@ export function createReportCommand(): Command {
|
|
|
11
11
|
.description("List reports with optional filters")
|
|
12
12
|
.option("--standup-id <id>", "Filter by standup ID")
|
|
13
13
|
.option("--user-id <id>", "Filter by user ID")
|
|
14
|
-
.option("--before <date>", "Reports before date (
|
|
15
|
-
.option("--after <date>", "Reports after date (
|
|
14
|
+
.option("--before <date>", "Reports before date (YYYY-MM-DD or unix timestamp)")
|
|
15
|
+
.option("--after <date>", "Reports after date (YYYY-MM-DD or unix timestamp)")
|
|
16
16
|
.option("--limit <n>", "Max number of reports to return")
|
|
17
17
|
.addHelpText(
|
|
18
18
|
"after",
|
|
@@ -99,6 +99,18 @@ async function enrichNotFound(
|
|
|
99
99
|
await fn(client);
|
|
100
100
|
} catch (error) {
|
|
101
101
|
if (error instanceof CliError && error.code === "not_found") {
|
|
102
|
+
// Don't enrich user-not-member errors with standup-ID suggestions
|
|
103
|
+
const isUserNotMember = /user is not member/i.test(error.message);
|
|
104
|
+
if (isUserNotMember) {
|
|
105
|
+
throw new CliError(
|
|
106
|
+
error.message,
|
|
107
|
+
error.code,
|
|
108
|
+
error.exitCode,
|
|
109
|
+
error.retryable,
|
|
110
|
+
"The specified user is not a member of this standup. Check members with: geekbot standup get <id>",
|
|
111
|
+
error.context,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
102
114
|
const suggestion = await buildNotFoundSuggestion(client, resourceType);
|
|
103
115
|
if (suggestion) {
|
|
104
116
|
throw new CliError(
|
|
@@ -117,11 +129,14 @@ async function enrichNotFound(
|
|
|
117
129
|
|
|
118
130
|
// ── Handlers ──────────────────────────────────────────────────────────
|
|
119
131
|
|
|
120
|
-
/** Brief standup projection —
|
|
132
|
+
/** Brief standup projection — essential fields for discovery */
|
|
121
133
|
export interface StandupBrief {
|
|
122
134
|
id: number;
|
|
123
135
|
name: string;
|
|
124
136
|
channel: string;
|
|
137
|
+
time: string;
|
|
138
|
+
timezone: string;
|
|
139
|
+
days: string[];
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
/**
|
|
@@ -173,6 +188,9 @@ export async function handleStandupList(
|
|
|
173
188
|
id: s.id,
|
|
174
189
|
name: s.name,
|
|
175
190
|
channel: s.channel,
|
|
191
|
+
time: s.time,
|
|
192
|
+
timezone: s.timezone,
|
|
193
|
+
days: s.days,
|
|
176
194
|
}));
|
|
177
195
|
writeOutput(successList(brief));
|
|
178
196
|
return;
|
|
@@ -352,9 +370,12 @@ export async function handleStandupReplace(
|
|
|
352
370
|
const previousStandup = StandupSchema.parse(prevRaw);
|
|
353
371
|
|
|
354
372
|
// Build full body — PUT requires complete representation
|
|
355
|
-
|
|
373
|
+
// Carry forward from previous standup when flags are omitted
|
|
374
|
+
const time = options.time ?? previousStandup.time.slice(0, 5);
|
|
356
375
|
validateTimeFormat(time);
|
|
357
|
-
const days =
|
|
376
|
+
const days = options.days
|
|
377
|
+
? validateDayAbbreviations(options.days.split(","))
|
|
378
|
+
: previousStandup.days;
|
|
358
379
|
|
|
359
380
|
const body: Record<string, unknown> = {
|
|
360
381
|
name: options.name,
|
|
@@ -363,9 +384,7 @@ export async function handleStandupReplace(
|
|
|
363
384
|
days,
|
|
364
385
|
};
|
|
365
386
|
|
|
366
|
-
|
|
367
|
-
body.timezone = options.timezone;
|
|
368
|
-
}
|
|
387
|
+
body.timezone = options.timezone ?? previousStandup.timezone;
|
|
369
388
|
|
|
370
389
|
// questions: use provided or carry forward from existing standup
|
|
371
390
|
if (options.questions !== undefined) {
|
|
@@ -378,11 +397,14 @@ export async function handleStandupReplace(
|
|
|
378
397
|
body.users = validateSlackIdList(options.users, "user ID");
|
|
379
398
|
body.sync_channel_members = false;
|
|
380
399
|
} else {
|
|
381
|
-
body.
|
|
400
|
+
body.users = previousStandup.users.map((u) => u.id);
|
|
401
|
+
body.sync_channel_members = previousStandup.sync_channel_members ?? false;
|
|
382
402
|
}
|
|
383
403
|
|
|
384
404
|
if (options.waitTime !== undefined) {
|
|
385
405
|
body.wait_time = validateWaitTime(options.waitTime);
|
|
406
|
+
} else {
|
|
407
|
+
body.wait_time = previousStandup.wait_time;
|
|
386
408
|
}
|
|
387
409
|
|
|
388
410
|
const raw = await client.put<unknown>(`/v1/standups/${numericId}`, body);
|
|
@@ -522,10 +544,14 @@ function buildReplaceUndoCommand(id: number, prev: Standup): string {
|
|
|
522
544
|
parts.push(`--days ${shellEscape(prev.days.join(","))}`);
|
|
523
545
|
}
|
|
524
546
|
|
|
525
|
-
if (prev.wait_time
|
|
547
|
+
if (prev.wait_time !== 0) {
|
|
526
548
|
parts.push(`--wait-time ${prev.wait_time}`);
|
|
527
549
|
}
|
|
528
550
|
|
|
551
|
+
if (prev.users.length > 0) {
|
|
552
|
+
parts.push(`--users ${prev.users.map((u) => u.id).join(",")}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
529
555
|
if (prev.questions.length > 0) {
|
|
530
556
|
parts.push(`--questions ${shellEscape(JSON.stringify(prev.questions.map((q) => q.text)))}`);
|
|
531
557
|
}
|
package/src/schemas/report.ts
CHANGED
|
@@ -6,7 +6,7 @@ const ReportAnswerSchema = z.object({
|
|
|
6
6
|
id: z.number(),
|
|
7
7
|
question: z.string().optional().default(""),
|
|
8
8
|
question_id: z.number().optional(),
|
|
9
|
-
answer: z.string(),
|
|
9
|
+
answer: z.string().nullable(),
|
|
10
10
|
answer_type: z.string().optional().default("text"),
|
|
11
11
|
images: z
|
|
12
12
|
.array(
|
|
@@ -34,7 +34,7 @@ const TimelineReportRawSchema = z.object({
|
|
|
34
34
|
slack_ts: z.string().nullable().optional(),
|
|
35
35
|
channel: z.string().optional().default(""),
|
|
36
36
|
questions: z.array(ReportAnswerSchema),
|
|
37
|
-
member: CompactUserSchema,
|
|
37
|
+
member: CompactUserSchema.optional(),
|
|
38
38
|
is_anonymous: z.boolean().optional().default(false),
|
|
39
39
|
broadcast_thread: z.boolean().optional().default(false),
|
|
40
40
|
is_confidential: z.boolean().optional().default(false),
|
|
@@ -49,11 +49,11 @@ const TimelineReportRawSchema = z.object({
|
|
|
49
49
|
const SubmittedReportRawSchema = z.object({
|
|
50
50
|
id: z.number(),
|
|
51
51
|
standup_id: z.number(),
|
|
52
|
-
timestamp: z.number(),
|
|
52
|
+
timestamp: z.number().nullable(),
|
|
53
53
|
slack_ts: z.string().nullable().optional(),
|
|
54
54
|
started_at: z.number().optional(),
|
|
55
55
|
done_at: z.number().optional(),
|
|
56
|
-
broadcasted_at: z.
|
|
56
|
+
broadcasted_at: z.string().nullable().optional(),
|
|
57
57
|
channel: z.string().optional().default(""),
|
|
58
58
|
member: z
|
|
59
59
|
.object({
|
|
@@ -89,7 +89,7 @@ function normalizeTimelineReport(raw: z.output<typeof TimelineReportRawSchema>):
|
|
|
89
89
|
standup_id: raw.standup_id,
|
|
90
90
|
created_at: new Date(raw.timestamp * 1000).toISOString(),
|
|
91
91
|
questions: raw.questions,
|
|
92
|
-
member: raw.member, // Already normalized by CompactUserSchema (profileImg -> profile_img)
|
|
92
|
+
member: raw.member ?? null, // Already normalized by CompactUserSchema (profileImg -> profile_img)
|
|
93
93
|
is_anonymous: raw.is_anonymous,
|
|
94
94
|
standup_name: raw.standup_name,
|
|
95
95
|
};
|
|
@@ -103,7 +103,7 @@ export const SubmittedReportSchema = SubmittedReportRawSchema.transform(
|
|
|
103
103
|
(raw): Report => ({
|
|
104
104
|
id: raw.id,
|
|
105
105
|
standup_id: raw.standup_id,
|
|
106
|
-
created_at: new Date(raw.timestamp * 1000).toISOString(),
|
|
106
|
+
created_at: new Date((raw.timestamp ?? raw.done_at ?? Date.now() / 1000) * 1000).toISOString(),
|
|
107
107
|
questions: raw.answers, // POST uses "answers" key, normalize to "questions"
|
|
108
108
|
member: raw.member
|
|
109
109
|
? {
|
package/src/schemas/standup.ts
CHANGED
|
@@ -50,7 +50,7 @@ const StandupRawSchema = z.object({
|
|
|
50
50
|
|
|
51
51
|
/** Normalize a raw standup: convert wait_time from seconds to minutes */
|
|
52
52
|
function normalizeStandup(raw: z.output<typeof StandupRawSchema>) {
|
|
53
|
-
return { ...raw, wait_time: raw.wait_time / 60 }; // NORM: seconds -> minutes
|
|
53
|
+
return { ...raw, wait_time: raw.wait_time === -1 ? -1 : raw.wait_time / 60 }; // NORM: seconds -> minutes; -1 is "exact time" sentinel
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Normalized standup -- wait_time converted from seconds to minutes */
|
|
@@ -197,7 +197,7 @@ export function parseDateFilter(raw: string, label: string): string {
|
|
|
197
197
|
"validation_error",
|
|
198
198
|
ExitCode.VALIDATION,
|
|
199
199
|
false,
|
|
200
|
-
"Accepted formats:
|
|
200
|
+
"Accepted formats: YYYY-MM-DD (2024-01-15) or unix timestamp (1705276800)",
|
|
201
201
|
);
|
|
202
202
|
}
|
|
203
203
|
|
|
@@ -208,7 +208,7 @@ export function parseDateFilter(raw: string, label: string): string {
|
|
|
208
208
|
"validation_error",
|
|
209
209
|
ExitCode.VALIDATION,
|
|
210
210
|
false,
|
|
211
|
-
"Accepted formats:
|
|
211
|
+
"Accepted formats: YYYY-MM-DD (2024-01-15) or unix timestamp (1705276800)",
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -226,7 +226,7 @@ export function parseDateFilter(raw: string, label: string): string {
|
|
|
226
226
|
"validation_error",
|
|
227
227
|
ExitCode.VALIDATION,
|
|
228
228
|
false,
|
|
229
|
-
"Accepted formats:
|
|
229
|
+
"Accepted formats: YYYY-MM-DD (2024-01-15) or unix timestamp (1705276800)",
|
|
230
230
|
);
|
|
231
231
|
}
|
|
232
232
|
|
package/src/utils/receipt.ts
CHANGED
|
@@ -41,10 +41,14 @@ export function buildDeleteUndoCommand(standup: Standup): string {
|
|
|
41
41
|
parts.push(`--days ${shellEscape(standup.days.join(","))}`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
if (standup.wait_time
|
|
44
|
+
if (standup.wait_time !== 0) {
|
|
45
45
|
parts.push(`--wait-time ${standup.wait_time}`);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
if (standup.users.length > 0) {
|
|
49
|
+
parts.push(`--users ${standup.users.map((u) => u.id).join(",")}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
48
52
|
if (standup.questions.length > 0) {
|
|
49
53
|
parts.push(`--questions ${shellEscape(JSON.stringify(standup.questions.map((q) => q.text)))}`);
|
|
50
54
|
}
|
|
@@ -60,6 +64,8 @@ const FIELD_TO_FLAG: Record<string, string> = {
|
|
|
60
64
|
timezone: "--timezone",
|
|
61
65
|
days: "--days",
|
|
62
66
|
wait_time: "--wait-time",
|
|
67
|
+
users: "--users",
|
|
68
|
+
questions: "--questions",
|
|
63
69
|
};
|
|
64
70
|
|
|
65
71
|
/**
|
|
@@ -83,6 +89,12 @@ export function buildUpdateUndoCommand(
|
|
|
83
89
|
parts.push(`${flag} ${shellEscape(prevValue.slice(0, 5))}`);
|
|
84
90
|
} else if (key === "days" && Array.isArray(prevValue)) {
|
|
85
91
|
parts.push(`${flag} ${shellEscape(prevValue.join(","))}`);
|
|
92
|
+
} else if (key === "users" && Array.isArray(prevValue)) {
|
|
93
|
+
parts.push(`${flag} ${prevValue.map((u: { id: string }) => u.id).join(",")}`);
|
|
94
|
+
} else if (key === "questions" && Array.isArray(prevValue)) {
|
|
95
|
+
parts.push(
|
|
96
|
+
`${flag} ${shellEscape(JSON.stringify(prevValue.map((q: { text: string }) => q.text)))}`,
|
|
97
|
+
);
|
|
86
98
|
} else if (typeof prevValue === "number") {
|
|
87
99
|
parts.push(`${flag} ${prevValue}`);
|
|
88
100
|
} else if (typeof prevValue === "string") {
|
package/src/utils/validation.ts
CHANGED
|
@@ -70,13 +70,14 @@ export function validateSlackIdList(value: string, label: string): string[] {
|
|
|
70
70
|
*/
|
|
71
71
|
export function validateWaitTime(value: string): number {
|
|
72
72
|
const num = Number(value);
|
|
73
|
+
if (num === -1) return num; // -1 is "exact time" sentinel
|
|
73
74
|
if (!Number.isSafeInteger(num) || num < 0) {
|
|
74
75
|
throw new CliError(
|
|
75
|
-
`Invalid wait time: "${value}". Must be a non-negative integer.`,
|
|
76
|
+
`Invalid wait time: "${value}". Must be a non-negative integer or -1 for exact time.`,
|
|
76
77
|
"validation_error",
|
|
77
78
|
ExitCode.VALIDATION,
|
|
78
79
|
false,
|
|
79
|
-
`Provide a numeric value in minutes, e.g.: --wait-time 15`,
|
|
80
|
+
`Provide a numeric value in minutes, e.g.: --wait-time 15 (or -1 for exact time)`,
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
83
|
return num;
|