@eliya-oss/agent-slack 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.
@@ -0,0 +1,579 @@
1
+ import { flagBoolean, flagString, requireFlag, splitCsv } from "../cli/args.js";
2
+ import { describeAllCommands, describeCommandGroup, findCommandMetadata, renderCompletion, renderHumanHelp } from "../cli/metadata.js";
3
+ import { NotAuthenticated, ResourceNotFound, UnsupportedMethod, UsageError } from "../domain/errors.js";
4
+ import { ProfileName, Scope } from "../domain/ids.js";
5
+ import { pagingFrom, successEnvelope, toNdjson } from "../output/envelope.js";
6
+ import { projectFields } from "../output/projection.js";
7
+ import { getProfile, parseTokenType, requireYes, tokenFor } from "./auth.js";
8
+ import { parseJsonPayload, requirePositional } from "./payload.js";
9
+ import { callSlack } from "./slack-call.js";
10
+ export const dispatch = async (parsed, services) => {
11
+ const pos = parsed.positionals;
12
+ const [first, second, third] = pos;
13
+ if (flagBoolean(parsed, "help") && !flagBoolean(parsed, "json")) {
14
+ return {
15
+ method: "help",
16
+ profile: null,
17
+ stdoutValue: null,
18
+ rawStdout: renderHumanHelp(pos)
19
+ };
20
+ }
21
+ if (pos.length === 0 || first === "describe") {
22
+ return {
23
+ method: "describe",
24
+ profile: null,
25
+ stdoutValue: describeAllCommands()
26
+ };
27
+ }
28
+ if (first === "completion") {
29
+ const shell = requirePositional(pos, 1, "SHELL");
30
+ const completion = renderCompletion(shell);
31
+ if (completion === "") {
32
+ throw new UsageError("Unsupported completion shell", { shell });
33
+ }
34
+ return {
35
+ method: "completion",
36
+ profile: null,
37
+ stdoutValue: null,
38
+ rawStdout: completion
39
+ };
40
+ }
41
+ if (flagBoolean(parsed, "help") && flagBoolean(parsed, "json")) {
42
+ return describePath(pos, null);
43
+ }
44
+ if (second === "describe") {
45
+ return describePath(first === undefined ? [] : [first], null);
46
+ }
47
+ if (first === "api") {
48
+ if (second === "methods" && third === "list") {
49
+ const family = flagString(parsed, "family");
50
+ return { method: "api.methods.list", profile: null, stdoutValue: services.methodCatalog.listMethods(family) };
51
+ }
52
+ if (second === "method" && third === "describe") {
53
+ const method = requirePositional(pos, 3, "METHOD");
54
+ const described = services.methodCatalog.describeMethod(method);
55
+ if (described === null) {
56
+ throw new UnsupportedMethod(`No bundled metadata for ${method}`, { method });
57
+ }
58
+ return { method: "api.method.describe", profile: null, stdoutValue: described };
59
+ }
60
+ if (second === "call") {
61
+ return apiCall(parsed, services);
62
+ }
63
+ }
64
+ if (first === "auth") {
65
+ return authCommand(parsed, services);
66
+ }
67
+ if (first === "team") {
68
+ return teamCommand(parsed, services);
69
+ }
70
+ if (first === "enterprise" && second === "get") {
71
+ return methodCall(parsed, services, { method: "team.info", payload: {} });
72
+ }
73
+ if (first === "user") {
74
+ return userCommand(parsed, services);
75
+ }
76
+ if (first === "usergroups") {
77
+ return usergroupsCommand(parsed, services);
78
+ }
79
+ if (first === "conversation") {
80
+ return conversationCommand(parsed, services);
81
+ }
82
+ if (first === "thread" && second === "get") {
83
+ return methodCall(parsed, services, {
84
+ method: "conversations.replies",
85
+ payload: {
86
+ channel: requireFlag(parsed, "channel"),
87
+ ts: requireFlag(parsed, "ts"),
88
+ limit: numberFlag(parsed, "limit")
89
+ }
90
+ });
91
+ }
92
+ if (first === "message") {
93
+ if (second === "get") {
94
+ const ts = requireFlag(parsed, "ts");
95
+ return methodCall(parsed, services, {
96
+ method: "conversations.history",
97
+ payload: {
98
+ channel: requireFlag(parsed, "channel"),
99
+ latest: ts,
100
+ oldest: ts,
101
+ inclusive: true,
102
+ limit: 1
103
+ }
104
+ });
105
+ }
106
+ if (second === "permalink") {
107
+ return methodCall(parsed, services, {
108
+ method: "chat.getPermalink",
109
+ payload: { channel: requireFlag(parsed, "channel"), message_ts: requireFlag(parsed, "ts") }
110
+ });
111
+ }
112
+ }
113
+ if (first === "search") {
114
+ return searchCommand(parsed, services);
115
+ }
116
+ if (first === "file") {
117
+ return fileCommand(parsed, services);
118
+ }
119
+ if (first === "reaction" && second === "get") {
120
+ return methodCall(parsed, services, {
121
+ method: "reactions.get",
122
+ payload: { channel: requireFlag(parsed, "channel"), timestamp: requireFlag(parsed, "ts") }
123
+ });
124
+ }
125
+ if (first === "pin" && second === "list") {
126
+ return methodCall(parsed, services, {
127
+ method: "pins.list",
128
+ payload: { channel: requirePositional(pos, 2, "CHANNEL_ID") }
129
+ });
130
+ }
131
+ if (first === "bookmark" && second === "list") {
132
+ return methodCall(parsed, services, {
133
+ method: "bookmarks.list",
134
+ payload: { channel_id: requirePositional(pos, 2, "CHANNEL_ID") }
135
+ });
136
+ }
137
+ if (first === "emoji" && second === "list") {
138
+ return methodCall(parsed, services, { method: "emoji.list", payload: {} });
139
+ }
140
+ if (first === "dnd" && second === "status") {
141
+ return methodCall(parsed, services, {
142
+ method: "dnd.info",
143
+ payload: pos[2] === undefined ? {} : { user: pos[2] }
144
+ });
145
+ }
146
+ return describePath(pos, new UsageError(`Unknown command: ${pos.join(" ")}`, { command: pos.join(" ") }));
147
+ };
148
+ const describePath = (path, error) => {
149
+ const metadata = findCommandMetadata(path);
150
+ const value = metadata ?? describeCommandGroup(path[0] ?? "slk");
151
+ if (error !== null && metadata === null) {
152
+ throw error;
153
+ }
154
+ return { method: "describe", profile: null, stdoutValue: value };
155
+ };
156
+ const authCommand = async (parsed, services) => {
157
+ const pos = parsed.positionals;
158
+ const profileName = flagString(parsed, "profile", "default") ?? "default";
159
+ const second = pos[1];
160
+ if (second === "profiles" && pos[2] === "list") {
161
+ return {
162
+ method: "auth.profiles.list",
163
+ profile: null,
164
+ stdoutValue: sanitizeProfiles(await services.tokenStore.listProfiles())
165
+ };
166
+ }
167
+ if (second === "logout") {
168
+ requireYes({ yes: flagBoolean(parsed, "yes"), message: "Refusing to delete auth profile without --yes" });
169
+ return {
170
+ method: "auth.logout",
171
+ profile: null,
172
+ stdoutValue: { deleted: await services.tokenStore.deleteProfile(profileName), profile: profileName }
173
+ };
174
+ }
175
+ if (second === "status" || second === "scopes") {
176
+ const profile = await getProfile(services, profileName);
177
+ return {
178
+ method: `auth.${second}`,
179
+ profile,
180
+ stdoutValue: second === "scopes" ? { scopes: profile.scopes } : sanitizeProfile(profile)
181
+ };
182
+ }
183
+ if (second === "test") {
184
+ return methodCall(parsed, services, { method: "auth.test", payload: {} });
185
+ }
186
+ if (second === "login") {
187
+ const token = flagString(parsed, "token") ?? process.env.SLK_TOKEN ?? process.env.SLACK_BOT_TOKEN;
188
+ if (token !== undefined && !flagBoolean(parsed, "oauth")) {
189
+ const profile = {
190
+ name: ProfileName.make(profileName),
191
+ tokenType: "bot",
192
+ botToken: token,
193
+ scopes: splitCsv(flagString(parsed, "scopes")).map(Scope.make)
194
+ };
195
+ await services.tokenStore.setProfile(profile);
196
+ return { method: "auth.login", profile, stdoutValue: sanitizeProfile(profile) };
197
+ }
198
+ const clientId = flagString(parsed, "client-id") ?? process.env.SLK_CLIENT_ID;
199
+ const clientSecret = flagString(parsed, "client-secret") ?? process.env.SLK_CLIENT_SECRET;
200
+ if (clientId === undefined || clientSecret === undefined) {
201
+ throw new NotAuthenticated("OAuth login needs --client-id/--client-secret or SLK_CLIENT_ID/SLK_CLIENT_SECRET");
202
+ }
203
+ const scopes = splitCsv(flagString(parsed, "scopes"));
204
+ const profile = await services.oauthFlow.login({
205
+ profileName,
206
+ clientId,
207
+ clientSecret,
208
+ scopes: scopes.length === 0 ? defaultOAuthScopes : scopes,
209
+ userScopes: splitCsv(flagString(parsed, "user-scopes")),
210
+ redirectUri: flagString(parsed, "redirect-uri"),
211
+ authUrlOut: flagString(parsed, "auth-url-out"),
212
+ timeoutMs: numberFlag(parsed, "timeout-ms")
213
+ });
214
+ await services.tokenStore.setProfile(profile);
215
+ return { method: "auth.login", profile, stdoutValue: sanitizeProfile(profile) };
216
+ }
217
+ throw new UsageError("Unknown auth command", { command: pos.join(" ") });
218
+ };
219
+ const defaultOAuthScopes = [
220
+ "channels:read",
221
+ "channels:history",
222
+ "groups:read",
223
+ "groups:history",
224
+ "im:read",
225
+ "im:history",
226
+ "mpim:read",
227
+ "mpim:history",
228
+ "users:read",
229
+ "files:read",
230
+ "reactions:read",
231
+ "pins:read",
232
+ "bookmarks:read",
233
+ "team:read"
234
+ ];
235
+ const apiCall = async (parsed, services) => {
236
+ const method = requirePositional(parsed.positionals, 2, "METHOD");
237
+ const payload = await parseJsonPayload({
238
+ inline: flagString(parsed, "payload"),
239
+ positional: parsed.positionals[3]
240
+ });
241
+ return methodCall(parsed, services, { method, payload });
242
+ };
243
+ const teamCommand = async (parsed, services) => {
244
+ const second = parsed.positionals[1];
245
+ if (second === "get") {
246
+ return methodCall(parsed, services, { method: "team.info", payload: {} });
247
+ }
248
+ if (second === "profile" && parsed.positionals[2] === "get") {
249
+ return methodCall(parsed, services, { method: "team.profile.get", payload: {} });
250
+ }
251
+ throw new UsageError("Unknown team command", { command: parsed.positionals.join(" ") });
252
+ };
253
+ const userCommand = async (parsed, services) => {
254
+ const second = parsed.positionals[1];
255
+ if (second === "list") {
256
+ return methodCall(parsed, services, { method: "users.list", payload: { limit: numberFlag(parsed, "limit") } });
257
+ }
258
+ if (second === "get") {
259
+ return methodCall(parsed, services, { method: "users.info", payload: { user: requirePositional(parsed.positionals, 2, "USER_ID") } });
260
+ }
261
+ if (second === "lookup") {
262
+ return methodCall(parsed, services, { method: "users.lookupByEmail", payload: { email: requireFlag(parsed, "email") } });
263
+ }
264
+ if (second === "presence" && parsed.positionals[2] === "get") {
265
+ return methodCall(parsed, services, { method: "users.getPresence", payload: { user: requirePositional(parsed.positionals, 3, "USER_ID") } });
266
+ }
267
+ throw new UsageError("Unknown user command", { command: parsed.positionals.join(" ") });
268
+ };
269
+ const usergroupsCommand = async (parsed, services) => {
270
+ const second = parsed.positionals[1];
271
+ if (second === "list") {
272
+ return methodCall(parsed, services, { method: "usergroups.list", payload: {} });
273
+ }
274
+ if (second === "users" && parsed.positionals[2] === "list") {
275
+ return methodCall(parsed, services, {
276
+ method: "usergroups.users.list",
277
+ payload: { usergroup: requirePositional(parsed.positionals, 3, "USERGROUP_ID") }
278
+ });
279
+ }
280
+ throw new UsageError("Unknown usergroups command", { command: parsed.positionals.join(" ") });
281
+ };
282
+ const conversationCommand = async (parsed, services) => {
283
+ const second = parsed.positionals[1];
284
+ if (second === "list") {
285
+ return methodCall(parsed, services, {
286
+ method: "conversations.list",
287
+ payload: {
288
+ types: flagString(parsed, "types", "public_channel,private_channel,mpim,im"),
289
+ exclude_archived: true,
290
+ limit: numberFlag(parsed, "limit")
291
+ }
292
+ });
293
+ }
294
+ if (second === "get") {
295
+ return methodCall(parsed, services, {
296
+ method: "conversations.info",
297
+ payload: { channel: requirePositional(parsed.positionals, 2, "CHANNEL_ID") }
298
+ });
299
+ }
300
+ if (second === "members") {
301
+ return methodCall(parsed, services, {
302
+ method: "conversations.members",
303
+ payload: { channel: requirePositional(parsed.positionals, 2, "CHANNEL_ID"), limit: numberFlag(parsed, "limit") }
304
+ });
305
+ }
306
+ if (second === "context") {
307
+ return conversationContext(parsed, services);
308
+ }
309
+ if (second === "history") {
310
+ const channel = requirePositional(parsed.positionals, 2, "CHANNEL_ID");
311
+ return methodCall(parsed, services, {
312
+ method: "conversations.history",
313
+ payload: {
314
+ channel,
315
+ oldest: flagString(parsed, "oldest") ?? sinceToOldest(flagString(parsed, "since")),
316
+ latest: flagString(parsed, "latest"),
317
+ inclusive: flagBoolean(parsed, "inclusive"),
318
+ limit: numberFlag(parsed, "limit")
319
+ }
320
+ });
321
+ }
322
+ throw new UsageError("Unknown conversation command", { command: parsed.positionals.join(" ") });
323
+ };
324
+ const conversationContext = async (parsed, services) => {
325
+ const channel = requirePositional(parsed.positionals, 2, "CHANNEL_ID");
326
+ const profileName = flagString(parsed, "profile", "default") ?? "default";
327
+ const tokenType = parseTokenType(flagString(parsed, "token"));
328
+ const profile = await getProfile(services, profileName);
329
+ const token = tokenFor(profile, tokenType);
330
+ const include = new Set(splitCsv(flagString(parsed, "include")));
331
+ const history = await callSlack(services, {
332
+ method: "conversations.history",
333
+ payload: cleanObject({
334
+ channel,
335
+ oldest: flagString(parsed, "oldest") ?? sinceToOldest(flagString(parsed, "since")),
336
+ latest: flagString(parsed, "latest"),
337
+ inclusive: flagBoolean(parsed, "inclusive"),
338
+ limit: numberFlag(parsed, "limit")
339
+ }),
340
+ token,
341
+ profile,
342
+ all: flagBoolean(parsed, "all"),
343
+ allowWrite: false,
344
+ yes: false
345
+ });
346
+ const messages = extractArray(history.response.messages);
347
+ const context = {
348
+ channel,
349
+ messages
350
+ };
351
+ if (include.has("users")) {
352
+ const users = {};
353
+ for (const userId of uniqueStrings(messages.map((message) => stringField(extractLooseRecord(message), "user")))) {
354
+ const user = await callSlack(services, {
355
+ method: "users.info",
356
+ payload: { user: userId },
357
+ token,
358
+ profile,
359
+ all: false,
360
+ allowWrite: false,
361
+ yes: false
362
+ });
363
+ users[userId] = user.response.user ?? user.response;
364
+ }
365
+ context.users = users;
366
+ }
367
+ if (include.has("permalinks")) {
368
+ const permalinks = {};
369
+ for (const ts of uniqueStrings(messages.map((message) => stringField(extractLooseRecord(message), "ts")))) {
370
+ const permalink = await callSlack(services, {
371
+ method: "chat.getPermalink",
372
+ payload: { channel, message_ts: ts },
373
+ token,
374
+ profile,
375
+ all: false,
376
+ allowWrite: false,
377
+ yes: false
378
+ });
379
+ const value = stringField(permalink.response, "permalink");
380
+ if (value !== undefined) {
381
+ permalinks[ts] = value;
382
+ }
383
+ }
384
+ context.permalinks = permalinks;
385
+ }
386
+ if (include.has("threads")) {
387
+ const threads = {};
388
+ for (const message of messages) {
389
+ const record = extractLooseRecord(message);
390
+ const ts = stringField(record, "thread_ts") ?? stringField(record, "ts");
391
+ const hasReplies = typeof record.reply_count === "number" && record.reply_count > 0;
392
+ if (ts !== undefined && hasReplies) {
393
+ const thread = await callSlack(services, {
394
+ method: "conversations.replies",
395
+ payload: { channel, ts },
396
+ token,
397
+ profile,
398
+ all: false,
399
+ allowWrite: false,
400
+ yes: false
401
+ });
402
+ threads[ts] = thread.response.messages ?? thread.response;
403
+ }
404
+ }
405
+ context.threads = threads;
406
+ }
407
+ return {
408
+ method: "conversation.context",
409
+ profile,
410
+ response: history.response,
411
+ stdoutValue: context,
412
+ items: messages
413
+ };
414
+ };
415
+ const searchCommand = async (parsed, services) => {
416
+ const second = parsed.positionals[1];
417
+ const query = requireFlag(parsed, "query");
418
+ if (second === "context") {
419
+ return methodCall(parsed, services, {
420
+ method: "assistant.search.context",
421
+ payload: { query, content_types: splitCsv(flagString(parsed, "content-types")) }
422
+ });
423
+ }
424
+ if (second === "messages") {
425
+ return methodCall(parsed, services, { method: "search.messages", payload: { query } });
426
+ }
427
+ if (second === "files") {
428
+ return methodCall(parsed, services, { method: "search.files", payload: { query } });
429
+ }
430
+ throw new UsageError("Unknown search command", { command: parsed.positionals.join(" ") });
431
+ };
432
+ const fileCommand = async (parsed, services) => {
433
+ const second = parsed.positionals[1];
434
+ if (second === "list") {
435
+ return methodCall(parsed, services, {
436
+ method: "files.list",
437
+ payload: {
438
+ channel: flagString(parsed, "channel"),
439
+ user: flagString(parsed, "user"),
440
+ count: numberFlag(parsed, "limit")
441
+ }
442
+ });
443
+ }
444
+ if (second === "get") {
445
+ return methodCall(parsed, services, { method: "files.info", payload: { file: requirePositional(parsed.positionals, 2, "FILE_ID") } });
446
+ }
447
+ if (second === "download") {
448
+ return fileDownload(parsed, services);
449
+ }
450
+ throw new UsageError("Unknown file command", { command: parsed.positionals.join(" ") });
451
+ };
452
+ const fileDownload = async (parsed, services) => {
453
+ const profileName = flagString(parsed, "profile", "default") ?? "default";
454
+ const tokenType = parseTokenType(flagString(parsed, "token"));
455
+ const profile = await getProfile(services, profileName);
456
+ const token = tokenFor(profile, tokenType);
457
+ const fileId = requirePositional(parsed.positionals, 2, "FILE_ID");
458
+ const outPath = requireFlag(parsed, "out");
459
+ const result = await callSlack(services, {
460
+ method: "files.info",
461
+ payload: { file: fileId },
462
+ token,
463
+ profile,
464
+ all: false,
465
+ allowWrite: false,
466
+ yes: false
467
+ });
468
+ const file = extractRecord(result.response.file);
469
+ const url = stringField(file, "url_private_download") ?? stringField(file, "url_private");
470
+ if (url === undefined) {
471
+ throw new ResourceNotFound("Slack file metadata did not include a private download URL", { file: fileId });
472
+ }
473
+ const download = await services.fileDownloader.download({ url, token, outPath });
474
+ return {
475
+ method: "file.download",
476
+ profile,
477
+ response: result.response,
478
+ stdoutValue: { file, download }
479
+ };
480
+ };
481
+ const methodCall = async (parsed, services, input) => {
482
+ const profileName = flagString(parsed, "profile", "default") ?? "default";
483
+ const tokenType = parseTokenType(flagString(parsed, "token"));
484
+ const profile = await getProfile(services, profileName);
485
+ const token = tokenFor(profile, tokenType);
486
+ const cleanPayload = Object.fromEntries(Object.entries(input.payload).filter(([, value]) => value !== undefined && value !== ""));
487
+ const result = await callSlack(services, {
488
+ method: input.method,
489
+ payload: cleanPayload,
490
+ token,
491
+ profile,
492
+ all: flagBoolean(parsed, "all"),
493
+ allowWrite: flagBoolean(parsed, "allow-write"),
494
+ yes: flagBoolean(parsed, "yes")
495
+ });
496
+ if (flagBoolean(parsed, "raw")) {
497
+ return {
498
+ method: input.method,
499
+ profile,
500
+ stdoutValue: result.response,
501
+ rawStdout: `${JSON.stringify(result.response, null, 2)}\n`,
502
+ response: result.response,
503
+ items: result.items
504
+ };
505
+ }
506
+ return {
507
+ method: input.method,
508
+ profile,
509
+ stdoutValue: result.response,
510
+ response: result.response,
511
+ items: result.items
512
+ };
513
+ };
514
+ export const renderDispatchResult = (parsed, result) => {
515
+ if (result.rawStdout !== undefined) {
516
+ return result.rawStdout;
517
+ }
518
+ if (flagString(parsed, "format") === "ndjson") {
519
+ return toNdjson(result.items ?? []);
520
+ }
521
+ const data = projectFields(result.stdoutValue, flagString(parsed, "fields"));
522
+ const envelope = successEnvelope({
523
+ method: result.method,
524
+ profile: result.profile,
525
+ data,
526
+ ...(result.response === undefined ? {} : { paging: pagingFrom(result.response) }),
527
+ ...(result.warnings === undefined ? {} : { warnings: result.warnings })
528
+ });
529
+ return `${JSON.stringify(envelope, null, 2)}\n`;
530
+ };
531
+ const numberFlag = (parsed, name) => {
532
+ const value = flagString(parsed, name);
533
+ if (value === undefined) {
534
+ return undefined;
535
+ }
536
+ const parsedNumber = Number(value);
537
+ if (!Number.isInteger(parsedNumber) || parsedNumber < 1) {
538
+ throw new UsageError(`--${name} must be a positive integer`, { flag: name, value });
539
+ }
540
+ return parsedNumber;
541
+ };
542
+ const sinceToOldest = (since) => {
543
+ if (since === undefined) {
544
+ return undefined;
545
+ }
546
+ const match = since.match(/^(\d+)([hd])$/);
547
+ if (match === null) {
548
+ return undefined;
549
+ }
550
+ const amount = Number(match[1]);
551
+ const unit = match[2];
552
+ const seconds = unit === "h" ? amount * 60 * 60 : amount * 24 * 60 * 60;
553
+ return String(Math.floor(Date.now() / 1000) - seconds);
554
+ };
555
+ const cleanObject = (input) => Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== ""));
556
+ const sanitizeProfiles = (profiles) => profiles.map(sanitizeProfile);
557
+ const sanitizeProfile = (profile) => ({
558
+ name: profile.name,
559
+ teamId: profile.teamId ?? null,
560
+ enterpriseId: profile.enterpriseId ?? null,
561
+ userId: profile.userId ?? null,
562
+ botId: profile.botId ?? null,
563
+ tokenType: profile.tokenType,
564
+ scopes: profile.scopes,
565
+ hasBotToken: profile.botToken !== undefined,
566
+ hasUserToken: profile.userToken !== undefined,
567
+ hasAdminToken: profile.adminToken !== undefined,
568
+ hasAppToken: profile.appToken !== undefined
569
+ });
570
+ const extractRecord = (value) => {
571
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
572
+ return value;
573
+ }
574
+ throw new ResourceNotFound("Slack response did not include a file object");
575
+ };
576
+ const stringField = (record, name) => typeof record[name] === "string" && record[name].length > 0 ? record[name] : undefined;
577
+ const extractArray = (value) => Array.isArray(value) ? value : [];
578
+ const extractLooseRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
579
+ const uniqueStrings = (values) => [...new Set(values.filter((value) => value !== undefined))];
@@ -0,0 +1,22 @@
1
+ import { parseArgs } from "../cli/args.js";
2
+ import { errorEnvelope } from "../output/envelope.js";
3
+ import { dispatch, renderDispatchResult } from "./commands.js";
4
+ export const executeCli = async (argv, services) => {
5
+ try {
6
+ const parsed = parseArgs(argv);
7
+ const result = await dispatch(parsed, services);
8
+ return {
9
+ exitCode: 0,
10
+ stdout: renderDispatchResult(parsed, result),
11
+ stderr: ""
12
+ };
13
+ }
14
+ catch (error) {
15
+ const { envelope, exitCode } = errorEnvelope(error);
16
+ return {
17
+ exitCode,
18
+ stdout: "",
19
+ stderr: `${JSON.stringify(envelope, null, 2)}\n`
20
+ };
21
+ }
22
+ };
@@ -0,0 +1,40 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { InvalidPayload, UsageError } from "../domain/errors.js";
3
+ export const readStdin = async () => {
4
+ const chunks = [];
5
+ for await (const chunk of process.stdin) {
6
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
7
+ }
8
+ return Buffer.concat(chunks).toString("utf8");
9
+ };
10
+ export const parseJsonPayload = async (input) => {
11
+ const source = input.inline ?? input.positional;
12
+ if (source === undefined) {
13
+ return {};
14
+ }
15
+ const raw = source === "-"
16
+ ? await readStdin()
17
+ : source.startsWith("@")
18
+ ? await readFile(source.slice(1), "utf8")
19
+ : source;
20
+ try {
21
+ const parsed = JSON.parse(raw);
22
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
23
+ throw new InvalidPayload("Payload JSON must be an object");
24
+ }
25
+ return parsed;
26
+ }
27
+ catch (error) {
28
+ if (error instanceof InvalidPayload) {
29
+ throw error;
30
+ }
31
+ throw new InvalidPayload("Invalid JSON payload", { source: source === "-" ? "stdin" : source });
32
+ }
33
+ };
34
+ export const requirePositional = (positionals, index, label) => {
35
+ const value = positionals[index];
36
+ if (value === undefined) {
37
+ throw new UsageError(`Missing ${label}`, { argument: label });
38
+ }
39
+ return value;
40
+ };
@@ -0,0 +1 @@
1
+ export {};