@clankmates/cli 0.2.0 → 0.3.1
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/README.md +72 -216
- package/package.json +1 -1
- package/skills/codex/clankmates/SKILL.md +40 -11
- package/src/cli.ts +26 -3
- package/src/commands/auth.ts +241 -26
- package/src/commands/channel.ts +302 -52
- package/src/commands/doctor.ts +121 -0
- package/src/commands/post.ts +124 -17
- package/src/commands/user.ts +52 -0
- package/src/lib/args.ts +1 -0
- package/src/lib/client.ts +318 -37
- package/src/types/api.ts +89 -3
package/src/commands/auth.ts
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { resolveOutputMode } from "../lib/context";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
booleanFlag,
|
|
4
|
+
requiredPositional,
|
|
5
|
+
requiredStringFlag,
|
|
6
|
+
stringFlag,
|
|
7
|
+
type ParsedArgs,
|
|
8
|
+
} from "../lib/args";
|
|
3
9
|
import {
|
|
4
10
|
loadConfig,
|
|
5
11
|
resolveBaseUrl,
|
|
6
12
|
resolveProfile,
|
|
7
13
|
resolveProfileName,
|
|
8
|
-
updateProfile
|
|
14
|
+
updateProfile,
|
|
9
15
|
} from "../lib/config";
|
|
10
16
|
import { ClankmatesClient } from "../lib/client";
|
|
11
17
|
import { CliError } from "../lib/errors";
|
|
12
|
-
import { printValue, type Io } from "../lib/output";
|
|
18
|
+
import { printJson, printValue, type Io } from "../lib/output";
|
|
13
19
|
import { getConfigPath } from "../lib/paths";
|
|
14
|
-
import {
|
|
15
|
-
|
|
20
|
+
import {
|
|
21
|
+
resolveMasterToken,
|
|
22
|
+
resolveOwnerReadToken,
|
|
23
|
+
resolveReadOnlyToken,
|
|
24
|
+
} from "../lib/tokens";
|
|
25
|
+
import type {
|
|
26
|
+
AccessKeyAttributes,
|
|
27
|
+
AccessKeyIssueResponse,
|
|
28
|
+
AccessKeyScope,
|
|
29
|
+
ProfileConfig,
|
|
30
|
+
WhoamiResponse,
|
|
31
|
+
} from "../types/api";
|
|
16
32
|
|
|
17
33
|
export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
18
34
|
const subcommand = args.positionals[0];
|
|
@@ -31,11 +47,13 @@ export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
31
47
|
const resolvedBaseUrl = resolveBaseUrl(baseUrl, profile.baseUrl);
|
|
32
48
|
const validationProfile = {
|
|
33
49
|
...profile,
|
|
34
|
-
baseUrl: resolvedBaseUrl
|
|
50
|
+
baseUrl: resolvedBaseUrl,
|
|
35
51
|
};
|
|
36
52
|
|
|
37
53
|
if (Boolean(masterToken) === Boolean(readOnlyToken)) {
|
|
38
|
-
throw new CliError(
|
|
54
|
+
throw new CliError(
|
|
55
|
+
"Provide exactly one of `--master-token` or `--read-only-token`.",
|
|
56
|
+
);
|
|
39
57
|
}
|
|
40
58
|
|
|
41
59
|
const client = new ClankmatesClient(validationProfile);
|
|
@@ -62,54 +80,88 @@ export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
62
80
|
storedProfile.baseUrl = baseUrl;
|
|
63
81
|
}
|
|
64
82
|
},
|
|
65
|
-
configPath
|
|
83
|
+
configPath,
|
|
66
84
|
);
|
|
67
85
|
|
|
68
86
|
printValue(io, outputMode, {
|
|
69
87
|
authenticated: true,
|
|
70
88
|
profile: profileName,
|
|
71
89
|
baseUrl: resolvedBaseUrl,
|
|
72
|
-
tokenKind: masterToken ? "master" : "read_only"
|
|
90
|
+
tokenKind: masterToken ? "master" : "read_only",
|
|
73
91
|
});
|
|
74
92
|
return;
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
case "whoami": {
|
|
78
|
-
const { profileName, profile } = resolveProfile(
|
|
96
|
+
const { profileName, profile } = resolveProfile(
|
|
97
|
+
config,
|
|
98
|
+
stringFlag(args.flags, "profile"),
|
|
99
|
+
);
|
|
79
100
|
const outputMode = resolveOutputMode(profile, args.flags);
|
|
101
|
+
const explicitChannelToken = stringFlag(args.flags, "channelToken");
|
|
80
102
|
const resolvedOwnerReadToken = resolveOwnerReadToken(profile);
|
|
103
|
+
const resolvedToken = explicitChannelToken
|
|
104
|
+
? {
|
|
105
|
+
token: explicitChannelToken,
|
|
106
|
+
source: "flag",
|
|
107
|
+
tokenKind: "channel" as const,
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
token: resolvedOwnerReadToken.token,
|
|
111
|
+
source: resolvedOwnerReadToken.source,
|
|
112
|
+
tokenKind: "owner_read" as const,
|
|
113
|
+
};
|
|
81
114
|
|
|
82
|
-
if (!
|
|
83
|
-
throw new CliError(
|
|
115
|
+
if (!resolvedToken.token) {
|
|
116
|
+
throw new CliError(
|
|
117
|
+
"No token configured for `auth whoami`. Provide `--channel-token`, set `CLANKMATES_READ_ONLY_TOKEN`, `CLANKMATES_MASTER_TOKEN`, or log in for the selected profile.",
|
|
118
|
+
);
|
|
84
119
|
}
|
|
85
120
|
|
|
86
|
-
const whoami = await new ClankmatesClient(profile).whoami(
|
|
87
|
-
|
|
121
|
+
const whoami = await new ClankmatesClient(profile).whoami(
|
|
122
|
+
resolvedToken.token,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
printValue(
|
|
126
|
+
io,
|
|
127
|
+
outputMode,
|
|
128
|
+
formatWhoamiOutput(profileName, profile.baseUrl, resolvedToken, whoami),
|
|
129
|
+
);
|
|
88
130
|
return;
|
|
89
131
|
}
|
|
90
132
|
|
|
91
133
|
case "logout": {
|
|
92
|
-
const { profileName } = resolveProfile(
|
|
134
|
+
const { profileName } = resolveProfile(
|
|
135
|
+
config,
|
|
136
|
+
stringFlag(args.flags, "profile"),
|
|
137
|
+
);
|
|
93
138
|
await updateProfile(
|
|
94
139
|
profileName,
|
|
95
140
|
(profile) => {
|
|
96
141
|
profile.masterToken = undefined;
|
|
97
142
|
profile.readOnlyToken = undefined;
|
|
98
143
|
},
|
|
99
|
-
configPath
|
|
144
|
+
configPath,
|
|
100
145
|
);
|
|
101
146
|
io.stdout(`Cleared owner tokens for profile ${profileName}`);
|
|
102
147
|
return;
|
|
103
148
|
}
|
|
104
149
|
|
|
105
150
|
case "token": {
|
|
106
|
-
const tokenCommand = requiredPositional(
|
|
151
|
+
const tokenCommand = requiredPositional(
|
|
152
|
+
args.positionals,
|
|
153
|
+
1,
|
|
154
|
+
"Missing auth token subcommand",
|
|
155
|
+
);
|
|
107
156
|
|
|
108
157
|
if (tokenCommand !== "inspect") {
|
|
109
158
|
throw new CliError("Unknown auth token subcommand", 2);
|
|
110
159
|
}
|
|
111
160
|
|
|
112
|
-
const { profileName, profile } = resolveProfile(
|
|
161
|
+
const { profileName, profile } = resolveProfile(
|
|
162
|
+
config,
|
|
163
|
+
stringFlag(args.flags, "profile"),
|
|
164
|
+
);
|
|
113
165
|
const outputMode = resolveOutputMode(profile, args.flags);
|
|
114
166
|
const resolvedMasterToken = resolveMasterToken(profile);
|
|
115
167
|
const resolvedReadOnlyToken = resolveReadOnlyToken(profile);
|
|
@@ -124,50 +176,213 @@ export async function runAuthCommand(args: ParsedArgs, io: Io): Promise<void> {
|
|
|
124
176
|
readOnlyTokenSource: resolvedReadOnlyToken.source,
|
|
125
177
|
ownerReadTokenAvailable: Boolean(resolvedOwnerReadToken.token),
|
|
126
178
|
ownerReadTokenSource: resolvedOwnerReadToken.source,
|
|
127
|
-
storedChannelTokens: Object.keys(profile.channelTokens).length
|
|
179
|
+
storedChannelTokens: Object.keys(profile.channelTokens).length,
|
|
128
180
|
});
|
|
129
181
|
return;
|
|
130
182
|
}
|
|
131
183
|
|
|
184
|
+
case "key":
|
|
185
|
+
case "access-key": {
|
|
186
|
+
await runAccessKeyCommand(args, config, io);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
132
190
|
default:
|
|
133
191
|
throw new CliError("Unknown auth subcommand", 2);
|
|
134
192
|
}
|
|
135
193
|
}
|
|
136
194
|
|
|
195
|
+
async function runAccessKeyCommand(
|
|
196
|
+
args: ParsedArgs,
|
|
197
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
198
|
+
io: Io,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const keyCommand = requiredPositional(
|
|
201
|
+
args.positionals,
|
|
202
|
+
1,
|
|
203
|
+
"Missing auth key subcommand",
|
|
204
|
+
);
|
|
205
|
+
const { profileName, profile } = resolveProfile(
|
|
206
|
+
config,
|
|
207
|
+
stringFlag(args.flags, "profile"),
|
|
208
|
+
);
|
|
209
|
+
const outputMode = resolveOutputMode(profile, args.flags);
|
|
210
|
+
const client = new ClankmatesClient(profile);
|
|
211
|
+
const configPath = getConfigPath();
|
|
212
|
+
|
|
213
|
+
switch (keyCommand) {
|
|
214
|
+
case "list": {
|
|
215
|
+
const scope = parseAccessKeyScope(stringFlag(args.flags, "scope"), false);
|
|
216
|
+
const response = await client.listAccessKeys(scope);
|
|
217
|
+
|
|
218
|
+
if (outputMode === "json") {
|
|
219
|
+
printJson(io, { items: response.items });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
printValue(
|
|
224
|
+
io,
|
|
225
|
+
outputMode,
|
|
226
|
+
response.items.map((item) => formatAccessKeyRow(item)),
|
|
227
|
+
);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
case "issue": {
|
|
232
|
+
const scope = requireAccessKeyScope(
|
|
233
|
+
requiredStringFlag(args.flags, "scope"),
|
|
234
|
+
);
|
|
235
|
+
const response = await client.issueAccessKey({
|
|
236
|
+
scope,
|
|
237
|
+
name: requiredStringFlag(args.flags, "name"),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (booleanFlag(args.flags, "tokenOnly")) {
|
|
241
|
+
io.stdout(response.token);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
printValue(io, outputMode, response);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case "revoke": {
|
|
250
|
+
const response = await client.revokeAccessKey(
|
|
251
|
+
requiredPositional(args.positionals, 2, "Missing access key id"),
|
|
252
|
+
);
|
|
253
|
+
await pruneInvalidStoredOwnerTokens(
|
|
254
|
+
profileName,
|
|
255
|
+
profile,
|
|
256
|
+
client,
|
|
257
|
+
configPath,
|
|
258
|
+
);
|
|
259
|
+
printValue(io, outputMode, response);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
default:
|
|
264
|
+
throw new CliError("Unknown auth key subcommand", 2);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
137
268
|
function defaultProfileConfig(): ProfileConfig {
|
|
138
269
|
return {
|
|
139
270
|
baseUrl: resolveBaseUrl(),
|
|
140
271
|
output: "table",
|
|
141
|
-
channelTokens: {}
|
|
272
|
+
channelTokens: {},
|
|
142
273
|
};
|
|
143
274
|
}
|
|
144
275
|
|
|
145
276
|
function formatWhoamiOutput(
|
|
146
277
|
profileName: string,
|
|
147
278
|
baseUrl: string,
|
|
148
|
-
|
|
149
|
-
whoami: WhoamiResponse
|
|
279
|
+
resolvedToken: { source: string; tokenKind: "owner_read" | "channel" },
|
|
280
|
+
whoami: WhoamiResponse,
|
|
150
281
|
) {
|
|
151
282
|
const base = {
|
|
152
283
|
authenticated: whoami.authenticated,
|
|
153
284
|
profile: profileName,
|
|
154
285
|
baseUrl,
|
|
155
|
-
|
|
286
|
+
tokenKind: resolvedToken.tokenKind,
|
|
287
|
+
tokenSource: resolvedToken.source,
|
|
288
|
+
ownerTokenSource:
|
|
289
|
+
resolvedToken.tokenKind === "owner_read" ? resolvedToken.source : undefined,
|
|
156
290
|
actorType: whoami.actor.type,
|
|
157
|
-
actorId: whoami.actor.id
|
|
291
|
+
actorId: whoami.actor.id,
|
|
158
292
|
};
|
|
159
293
|
|
|
160
294
|
if (whoami.actor.type === "user") {
|
|
161
295
|
return {
|
|
162
296
|
...base,
|
|
163
297
|
email: whoami.actor.email,
|
|
164
|
-
actorScope: whoami.actor.scope ?? "master"
|
|
298
|
+
actorScope: whoami.actor.scope ?? "master",
|
|
299
|
+
authenticatedVia: whoami.actor.authenticated_via ?? "",
|
|
300
|
+
publicHandle: whoami.actor.public_handle ?? "",
|
|
301
|
+
publicProfilePath: whoami.actor.public_profile_path ?? "",
|
|
302
|
+
publicProfileUrl: whoami.actor.public_profile_url ?? "",
|
|
165
303
|
};
|
|
166
304
|
}
|
|
167
305
|
|
|
168
306
|
return {
|
|
169
307
|
...base,
|
|
170
308
|
name: whoami.actor.name,
|
|
171
|
-
visibility: whoami.actor.visibility
|
|
309
|
+
visibility: whoami.actor.visibility,
|
|
310
|
+
publiclyListed: whoami.actor.publicly_listed ?? false,
|
|
311
|
+
postingPausedUntil: whoami.actor.posting_paused_until ?? "",
|
|
312
|
+
ownerId: whoami.actor.owner_id ?? "",
|
|
313
|
+
ownerPublicHandle: whoami.actor.owner_public_handle ?? "",
|
|
314
|
+
publicPath: whoami.actor.public_path ?? "",
|
|
315
|
+
publicUrl: whoami.actor.public_url ?? "",
|
|
172
316
|
};
|
|
173
317
|
}
|
|
318
|
+
|
|
319
|
+
function parseAccessKeyScope(
|
|
320
|
+
scope: string | undefined,
|
|
321
|
+
required: boolean,
|
|
322
|
+
): AccessKeyScope | undefined {
|
|
323
|
+
if (scope === undefined) {
|
|
324
|
+
if (required) {
|
|
325
|
+
throw new CliError("Missing `--scope`", 2);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (scope !== "master" && scope !== "read_only") {
|
|
332
|
+
throw new CliError("`--scope` must be either `master` or `read_only`.", 2);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return scope;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function requireAccessKeyScope(scope: string): AccessKeyScope {
|
|
339
|
+
const parsed = parseAccessKeyScope(scope, true);
|
|
340
|
+
|
|
341
|
+
if (!parsed) {
|
|
342
|
+
throw new CliError("Missing `--scope`", 2);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return parsed;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function formatAccessKeyRow(item: { id: string; attributes: AccessKeyAttributes }) {
|
|
349
|
+
return {
|
|
350
|
+
id: item.id,
|
|
351
|
+
scope: item.attributes.scope,
|
|
352
|
+
name: item.attributes.name ?? "",
|
|
353
|
+
expiresAt: item.attributes.expires_at,
|
|
354
|
+
issuedAt: item.attributes.inserted_at ?? "",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function pruneInvalidStoredOwnerTokens(
|
|
359
|
+
profileName: string,
|
|
360
|
+
profile: ProfileConfig,
|
|
361
|
+
client: ClankmatesClient,
|
|
362
|
+
configPath: string,
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
const invalidMasterToken = profile.masterToken
|
|
365
|
+
? !(await client.canAuthenticate(profile.masterToken))
|
|
366
|
+
: false;
|
|
367
|
+
const invalidReadOnlyToken = profile.readOnlyToken
|
|
368
|
+
? !(await client.canAuthenticate(profile.readOnlyToken))
|
|
369
|
+
: false;
|
|
370
|
+
|
|
371
|
+
if (!invalidMasterToken && !invalidReadOnlyToken) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
await updateProfile(
|
|
376
|
+
profileName,
|
|
377
|
+
(storedProfile) => {
|
|
378
|
+
if (invalidMasterToken) {
|
|
379
|
+
storedProfile.masterToken = undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (invalidReadOnlyToken) {
|
|
383
|
+
storedProfile.readOnlyToken = undefined;
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
configPath,
|
|
387
|
+
);
|
|
388
|
+
}
|