Skip to content

Commit 00556f4

Browse files
committed
fix: harden interceptor, extract shared utils, remove dead code, add missing tests
- Skip 401 retry for non-GET methods (body stream consumed) - Catch refresh failure and return original 401 response - Guard config YAML parse errors with fallback to defaults - Hide config command from help (dev-only) - Validate import mode/mapping, handle missing files - Make event/channel update accept partial fields - Fix export command references to use subcommand context - Handle --json flag in version and config commands - Remove CSV pagination comment (RFC 4180), add \r escape - Remove pagination from error payloads, guard null in unwrap - Use unwrap() in analysis commands instead of silent fallback - Extract 27 duplicated functions into shared/utils.ts - Remove dead auth barrel, 5 dead re-exports, 5 unused exports - Fix README: person update flags, channel get, event update - Add tests: import analyze, overview, event update, person activity Confidence: high Scope-risk: moderate
1 parent f0f6062 commit 00556f4

33 files changed

Lines changed: 695 additions & 2081 deletions

README.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ talkvalue auth list # list profiles
2020
talkvalue auth logout # remove profile
2121
```
2222

23-
For CI, set `TALKVALUE_TOKEN`:
23+
For CI/agents, set `TALKVALUE_TOKEN`:
2424

2525
```bash
2626
TALKVALUE_TOKEN="eyJ..." talkvalue path person list
@@ -29,14 +29,14 @@ TALKVALUE_TOKEN="eyJ..." talkvalue path person list
2929
## Commands
3030

3131
```bash
32-
talkvalue path overview # dashboard summary
32+
talkvalue path overview # dashboard
3333
talkvalue path overview stats # detailed stats
3434

3535
# People
3636
talkvalue path person list # list people
3737
talkvalue path person list --event-id 16 --sort joinedAt:desc
3838
talkvalue path person get <id>
39-
talkvalue path person update <id> --name ""
39+
talkvalue path person update <id> --first-name "" --last-name ""
4040
talkvalue path person delete <id> --confirm
4141
talkvalue path person merge <sourceId> <targetId> --confirm
4242
talkvalue path person merge-undo <mergeOperationId> --confirm
@@ -47,7 +47,7 @@ talkvalue path person export # CSV export
4747
talkvalue path event list
4848
talkvalue path event get <id>
4949
talkvalue path event create --name "" --start-at "" --time-zone ""
50-
talkvalue path event update <id> --name "" --start-at "" --time-zone ""
50+
talkvalue path event update <id> --name ""
5151
talkvalue path event delete <id> --confirm
5252
talkvalue path event person list <eventId>
5353
talkvalue path event person add <eventId> --email ""
@@ -56,7 +56,8 @@ talkvalue path event person export <eventId>
5656
# Channels
5757
talkvalue path channel list
5858
talkvalue path channel create --name ""
59-
talkvalue path channel update <id> --name ""
59+
talkvalue path channel get <id>
60+
talkvalue path channel update <id> --name "" --icon "" --color ""
6061
talkvalue path channel delete <id> --confirm
6162
talkvalue path channel people <channelId>
6263
talkvalue path channel add-person <channelId> --email ""
@@ -82,9 +83,6 @@ talkvalue path import create --file-key "…" --source-id <n> --mode UPDATE --ma
8283
talkvalue path import analyze --file ./data.csv
8384
talkvalue path import failed-export <id>
8485

85-
# Config
86-
talkvalue config list
87-
talkvalue config set <key> <value>
8886
talkvalue version
8987
```
9088

Lines changed: 154 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +1,181 @@
11
// This file is auto-generated by @hey-api/openapi-ts
22

3-
type Slot = 'body' | 'headers' | 'path' | 'query';
3+
type Slot = "body" | "headers" | "path" | "query";
44

55
export type Field =
6-
| {
7-
in: Exclude<Slot, 'body'>;
8-
/**
9-
* Field name. This is the name we want the user to see and use.
10-
*/
11-
key: string;
12-
/**
13-
* Field mapped name. This is the name we want to use in the request.
14-
* If omitted, we use the same value as `key`.
15-
*/
16-
map?: string;
17-
}
18-
| {
19-
in: Extract<Slot, 'body'>;
20-
/**
21-
* Key isn't required for bodies.
22-
*/
23-
key?: string;
24-
map?: string;
25-
}
26-
| {
27-
/**
28-
* Field name. This is the name we want the user to see and use.
29-
*/
30-
key: string;
31-
/**
32-
* Field mapped name. This is the name we want to use in the request.
33-
* If `in` is omitted, `map` aliases `key` to the transport layer.
34-
*/
35-
map: Slot;
36-
};
6+
| {
7+
in: Exclude<Slot, "body">;
8+
/**
9+
* Field name. This is the name we want the user to see and use.
10+
*/
11+
key: string;
12+
/**
13+
* Field mapped name. This is the name we want to use in the request.
14+
* If omitted, we use the same value as `key`.
15+
*/
16+
map?: string;
17+
}
18+
| {
19+
in: Extract<Slot, "body">;
20+
/**
21+
* Key isn't required for bodies.
22+
*/
23+
key?: string;
24+
map?: string;
25+
}
26+
| {
27+
/**
28+
* Field name. This is the name we want the user to see and use.
29+
*/
30+
key: string;
31+
/**
32+
* Field mapped name. This is the name we want to use in the request.
33+
* If `in` is omitted, `map` aliases `key` to the transport layer.
34+
*/
35+
map: Slot;
36+
};
3737

3838
export interface Fields {
39-
allowExtra?: Partial<Record<Slot, boolean>>;
40-
args?: ReadonlyArray<Field>;
39+
allowExtra?: Partial<Record<Slot, boolean>>;
40+
args?: ReadonlyArray<Field>;
4141
}
4242

4343
export type FieldsConfig = ReadonlyArray<Field | Fields>;
4444

4545
const extraPrefixesMap: Record<string, Slot> = {
46-
$body_: 'body',
47-
$headers_: 'headers',
48-
$path_: 'path',
49-
$query_: 'query',
46+
$body_: "body",
47+
$headers_: "headers",
48+
$path_: "path",
49+
$query_: "query",
5050
};
5151
const extraPrefixes = Object.entries(extraPrefixesMap);
5252

5353
type KeyMap = Map<
54-
string,
55-
| {
56-
in: Slot;
57-
map?: string;
58-
}
59-
| {
60-
in?: never;
61-
map: Slot;
62-
}
54+
string,
55+
| {
56+
in: Slot;
57+
map?: string;
58+
}
59+
| {
60+
in?: never;
61+
map: Slot;
62+
}
6363
>;
6464

6565
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
66-
if (!map) {
67-
map = new Map();
68-
}
69-
70-
for (const config of fields) {
71-
if ('in' in config) {
72-
if (config.key) {
73-
map.set(config.key, {
74-
in: config.in,
75-
map: config.map,
76-
});
77-
}
78-
} else if ('key' in config) {
79-
map.set(config.key, {
80-
map: config.map,
81-
});
82-
} else if (config.args) {
83-
buildKeyMap(config.args, map);
84-
}
85-
}
86-
87-
return map;
66+
if (!map) {
67+
map = new Map();
68+
}
69+
70+
for (const config of fields) {
71+
if ("in" in config) {
72+
if (config.key) {
73+
map.set(config.key, {
74+
in: config.in,
75+
map: config.map,
76+
});
77+
}
78+
} else if ("key" in config) {
79+
map.set(config.key, {
80+
map: config.map,
81+
});
82+
} else if (config.args) {
83+
buildKeyMap(config.args, map);
84+
}
85+
}
86+
87+
return map;
8888
};
8989

9090
interface Params {
91-
body: unknown;
92-
headers: Record<string, unknown>;
93-
path: Record<string, unknown>;
94-
query: Record<string, unknown>;
91+
body: unknown;
92+
headers: Record<string, unknown>;
93+
path: Record<string, unknown>;
94+
query: Record<string, unknown>;
9595
}
9696

9797
const stripEmptySlots = (params: Params) => {
98-
for (const [slot, value] of Object.entries(params)) {
99-
if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
100-
delete params[slot as Slot];
101-
}
102-
}
98+
for (const [slot, value] of Object.entries(params)) {
99+
if (
100+
value &&
101+
typeof value === "object" &&
102+
!Array.isArray(value) &&
103+
!Object.keys(value).length
104+
) {
105+
delete params[slot as Slot];
106+
}
107+
}
103108
};
104109

105-
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
106-
const params: Params = {
107-
body: {},
108-
headers: {},
109-
path: {},
110-
query: {},
111-
};
112-
113-
const map = buildKeyMap(fields);
114-
115-
let config: FieldsConfig[number] | undefined;
116-
117-
for (const [index, arg] of args.entries()) {
118-
if (fields[index]) {
119-
config = fields[index];
120-
}
121-
122-
if (!config) {
123-
continue;
124-
}
125-
126-
if ('in' in config) {
127-
if (config.key) {
128-
const field = map.get(config.key)!;
129-
const name = field.map || config.key;
130-
if (field.in) {
131-
(params[field.in] as Record<string, unknown>)[name] = arg;
132-
}
133-
} else {
134-
params.body = arg;
135-
}
136-
} else {
137-
for (const [key, value] of Object.entries(arg ?? {})) {
138-
const field = map.get(key);
139-
140-
if (field) {
141-
if (field.in) {
142-
const name = field.map || key;
143-
(params[field.in] as Record<string, unknown>)[name] = value;
144-
} else {
145-
params[field.map] = value;
146-
}
147-
} else {
148-
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix));
149-
150-
if (extra) {
151-
const [prefix, slot] = extra;
152-
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value;
153-
} else if ('allowExtra' in config && config.allowExtra) {
154-
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
155-
if (allowed) {
156-
(params[slot as Slot] as Record<string, unknown>)[key] = value;
157-
break;
158-
}
159-
}
160-
}
161-
}
162-
}
163-
}
164-
}
165-
166-
stripEmptySlots(params);
167-
168-
return params;
110+
export const buildClientParams = (
111+
args: ReadonlyArray<unknown>,
112+
fields: FieldsConfig,
113+
) => {
114+
const params: Params = {
115+
body: {},
116+
headers: {},
117+
path: {},
118+
query: {},
119+
};
120+
121+
const map = buildKeyMap(fields);
122+
123+
let config: FieldsConfig[number] | undefined;
124+
125+
for (const [index, arg] of args.entries()) {
126+
if (fields[index]) {
127+
config = fields[index];
128+
}
129+
130+
if (!config) {
131+
continue;
132+
}
133+
134+
if ("in" in config) {
135+
if (config.key) {
136+
const field = map.get(config.key)!;
137+
const name = field.map || config.key;
138+
if (field.in) {
139+
(params[field.in] as Record<string, unknown>)[name] = arg;
140+
}
141+
} else {
142+
params.body = arg;
143+
}
144+
} else {
145+
for (const [key, value] of Object.entries(arg ?? {})) {
146+
const field = map.get(key);
147+
148+
if (field) {
149+
if (field.in) {
150+
const name = field.map || key;
151+
(params[field.in] as Record<string, unknown>)[name] = value;
152+
} else {
153+
params[field.map] = value;
154+
}
155+
} else {
156+
const extra = extraPrefixes.find(([prefix]) =>
157+
key.startsWith(prefix),
158+
);
159+
160+
if (extra) {
161+
const [prefix, slot] = extra;
162+
(params[slot] as Record<string, unknown>)[
163+
key.slice(prefix.length)
164+
] = value;
165+
} else if ("allowExtra" in config && config.allowExtra) {
166+
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
167+
if (allowed) {
168+
(params[slot as Slot] as Record<string, unknown>)[key] = value;
169+
break;
170+
}
171+
}
172+
}
173+
}
174+
}
175+
}
176+
}
177+
178+
stripEmptySlots(params);
179+
180+
return params;
169181
};

0 commit comments

Comments
 (0)