@mepuka/skygent 0.3.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/package.json +1 -1
- package/src/cli/compact-output.ts +52 -0
- package/src/cli/config.ts +30 -5
- package/src/cli/feed.ts +52 -15
- package/src/cli/filter-errors.ts +5 -9
- package/src/cli/filter.ts +5 -7
- package/src/cli/graph.ts +128 -37
- package/src/cli/interval.ts +4 -33
- package/src/cli/jetstream.ts +2 -0
- package/src/cli/option-schemas.ts +22 -0
- package/src/cli/output-format.ts +11 -0
- package/src/cli/pagination.ts +10 -11
- package/src/cli/parse-errors.ts +12 -0
- package/src/cli/parse.ts +1 -47
- package/src/cli/pipe-input.ts +18 -0
- package/src/cli/pipe.ts +16 -19
- package/src/cli/post.ts +57 -12
- package/src/cli/query-fields.ts +13 -3
- package/src/cli/query.ts +18 -39
- package/src/cli/search.ts +8 -25
- package/src/cli/shared-options.ts +11 -63
- package/src/cli/shared.ts +1 -1
- package/src/cli/store-errors.ts +5 -9
- package/src/cli/store.ts +6 -0
- package/src/cli/sync-factory.ts +13 -21
- package/src/cli/sync.ts +32 -51
- package/src/cli/thread-options.ts +8 -16
- package/src/cli/view-thread.ts +18 -15
- package/src/cli/watch.ts +12 -25
- package/src/domain/primitives.ts +20 -3
- package/src/services/bsky-client.ts +11 -5
- package/src/services/shared.ts +48 -1
- package/src/services/store-cleaner.ts +5 -2
- package/src/services/store-renamer.ts +3 -1
- package/src/services/sync-engine.ts +13 -4
package/src/cli/graph.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { AppConfigService } from "../services/app-config.js";
|
|
|
5
5
|
import { IdentityResolver } from "../services/identity-resolver.js";
|
|
6
6
|
import { ProfileResolver } from "../services/profile-resolver.js";
|
|
7
7
|
import type { ListItemView, ListView } from "../domain/bsky.js";
|
|
8
|
-
import {
|
|
8
|
+
import { AtUri } from "../domain/primitives.js";
|
|
9
|
+
import { actorArg, decodeActor } from "./shared-options.js";
|
|
9
10
|
import { CliInputError } from "./errors.js";
|
|
10
11
|
import { withExamples } from "./help.js";
|
|
11
12
|
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
@@ -14,6 +15,8 @@ import { renderProfileTable } from "./doc/table-renderers.js";
|
|
|
14
15
|
import { jsonNdjsonTableFormats } from "./output-format.js";
|
|
15
16
|
import { emitWithFormat } from "./output-render.js";
|
|
16
17
|
import { cursorOption as baseCursorOption, limitOption as baseLimitOption, parsePagination } from "./pagination.js";
|
|
18
|
+
import { CliPreferences } from "./preferences.js";
|
|
19
|
+
import { compactListItemView, compactListView, compactProfileView } from "./compact-output.js";
|
|
17
20
|
import {
|
|
18
21
|
buildRelationshipGraph,
|
|
19
22
|
relationshipEntries,
|
|
@@ -21,11 +24,8 @@ import {
|
|
|
21
24
|
type RelationshipNode
|
|
22
25
|
} from "../graph/relationships.js";
|
|
23
26
|
|
|
24
|
-
const actorArg = Args.text({ name: "actor" }).pipe(
|
|
25
|
-
Args.withDescription("Bluesky handle or DID")
|
|
26
|
-
);
|
|
27
|
-
|
|
28
27
|
const listUriArg = Args.text({ name: "uri" }).pipe(
|
|
28
|
+
Args.withSchema(AtUri),
|
|
29
29
|
Args.withDescription("Bluesky list URI (at://...)")
|
|
30
30
|
);
|
|
31
31
|
|
|
@@ -42,6 +42,17 @@ const formatOption = Options.choice("format", jsonNdjsonTableFormats).pipe(
|
|
|
42
42
|
Options.optional
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
+
const ensureSupportedFormat = (
|
|
46
|
+
format: Option.Option<typeof jsonNdjsonTableFormats[number]>,
|
|
47
|
+
configFormat: string
|
|
48
|
+
) =>
|
|
49
|
+
Option.isNone(format) && configFormat === "markdown"
|
|
50
|
+
? CliInputError.make({
|
|
51
|
+
message: 'Output format "markdown" is not supported for graph commands. Use --format json|ndjson|table.',
|
|
52
|
+
cause: { format: configFormat }
|
|
53
|
+
})
|
|
54
|
+
: Effect.void;
|
|
55
|
+
|
|
45
56
|
const purposeOption = Options.choice("purpose", ["modlist", "curatelist"]).pipe(
|
|
46
57
|
Options.withDescription("List purpose filter"),
|
|
47
58
|
Options.optional
|
|
@@ -121,22 +132,35 @@ const followersCommand = Command.make(
|
|
|
121
132
|
({ actor, limit, cursor, format }) =>
|
|
122
133
|
Effect.gen(function* () {
|
|
123
134
|
const appConfig = yield* AppConfigService;
|
|
135
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
136
|
+
const preferences = yield* CliPreferences;
|
|
124
137
|
const client = yield* BskyClient;
|
|
125
|
-
const
|
|
126
|
-
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
138
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
127
139
|
const options = {
|
|
128
140
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
129
141
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
130
142
|
};
|
|
131
|
-
const result = yield* client.getFollowers(
|
|
143
|
+
const result = yield* client.getFollowers(actor, options);
|
|
144
|
+
const subject = preferences.compact
|
|
145
|
+
? compactProfileView(result.subject)
|
|
146
|
+
: result.subject;
|
|
147
|
+
const followers = preferences.compact
|
|
148
|
+
? result.followers.map(compactProfileView)
|
|
149
|
+
: result.followers;
|
|
150
|
+
const payload = result.cursor
|
|
151
|
+
? { subject, followers, cursor: result.cursor }
|
|
152
|
+
: { subject, followers };
|
|
153
|
+
const followersStream = Stream.fromIterable(
|
|
154
|
+
followers as ReadonlyArray<unknown>
|
|
155
|
+
);
|
|
132
156
|
yield* emitWithFormat(
|
|
133
157
|
format,
|
|
134
158
|
appConfig.outputFormat,
|
|
135
159
|
jsonNdjsonTableFormats,
|
|
136
160
|
"json",
|
|
137
161
|
{
|
|
138
|
-
json: writeJson(
|
|
139
|
-
ndjson: writeJsonStream(
|
|
162
|
+
json: writeJson(payload),
|
|
163
|
+
ndjson: writeJsonStream(followersStream),
|
|
140
164
|
table: writeText(renderProfileTable(result.followers, result.cursor))
|
|
141
165
|
}
|
|
142
166
|
);
|
|
@@ -156,22 +180,35 @@ const followsCommand = Command.make(
|
|
|
156
180
|
({ actor, limit, cursor, format }) =>
|
|
157
181
|
Effect.gen(function* () {
|
|
158
182
|
const appConfig = yield* AppConfigService;
|
|
183
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
184
|
+
const preferences = yield* CliPreferences;
|
|
159
185
|
const client = yield* BskyClient;
|
|
160
|
-
const
|
|
161
|
-
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
186
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
162
187
|
const options = {
|
|
163
188
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
164
189
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
165
190
|
};
|
|
166
|
-
const result = yield* client.getFollows(
|
|
191
|
+
const result = yield* client.getFollows(actor, options);
|
|
192
|
+
const subject = preferences.compact
|
|
193
|
+
? compactProfileView(result.subject)
|
|
194
|
+
: result.subject;
|
|
195
|
+
const follows = preferences.compact
|
|
196
|
+
? result.follows.map(compactProfileView)
|
|
197
|
+
: result.follows;
|
|
198
|
+
const payload = result.cursor
|
|
199
|
+
? { subject, follows, cursor: result.cursor }
|
|
200
|
+
: { subject, follows };
|
|
201
|
+
const followsStream = Stream.fromIterable(
|
|
202
|
+
follows as ReadonlyArray<unknown>
|
|
203
|
+
);
|
|
167
204
|
yield* emitWithFormat(
|
|
168
205
|
format,
|
|
169
206
|
appConfig.outputFormat,
|
|
170
207
|
jsonNdjsonTableFormats,
|
|
171
208
|
"json",
|
|
172
209
|
{
|
|
173
|
-
json: writeJson(
|
|
174
|
-
ndjson: writeJsonStream(
|
|
210
|
+
json: writeJson(payload),
|
|
211
|
+
ndjson: writeJsonStream(followsStream),
|
|
175
212
|
table: writeText(renderProfileTable(result.follows, result.cursor))
|
|
176
213
|
}
|
|
177
214
|
);
|
|
@@ -191,22 +228,35 @@ const knownFollowersCommand = Command.make(
|
|
|
191
228
|
({ actor, limit, cursor, format }) =>
|
|
192
229
|
Effect.gen(function* () {
|
|
193
230
|
const appConfig = yield* AppConfigService;
|
|
231
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
232
|
+
const preferences = yield* CliPreferences;
|
|
194
233
|
const client = yield* BskyClient;
|
|
195
|
-
const
|
|
196
|
-
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
234
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
197
235
|
const options = {
|
|
198
236
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
199
237
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
200
238
|
};
|
|
201
|
-
const result = yield* client.getKnownFollowers(
|
|
239
|
+
const result = yield* client.getKnownFollowers(actor, options);
|
|
240
|
+
const subject = preferences.compact
|
|
241
|
+
? compactProfileView(result.subject)
|
|
242
|
+
: result.subject;
|
|
243
|
+
const followers = preferences.compact
|
|
244
|
+
? result.followers.map(compactProfileView)
|
|
245
|
+
: result.followers;
|
|
246
|
+
const payload = result.cursor
|
|
247
|
+
? { subject, followers, cursor: result.cursor }
|
|
248
|
+
: { subject, followers };
|
|
249
|
+
const followersStream = Stream.fromIterable(
|
|
250
|
+
followers as ReadonlyArray<unknown>
|
|
251
|
+
);
|
|
202
252
|
yield* emitWithFormat(
|
|
203
253
|
format,
|
|
204
254
|
appConfig.outputFormat,
|
|
205
255
|
jsonNdjsonTableFormats,
|
|
206
256
|
"json",
|
|
207
257
|
{
|
|
208
|
-
json: writeJson(
|
|
209
|
-
ndjson: writeJsonStream(
|
|
258
|
+
json: writeJson(payload),
|
|
259
|
+
ndjson: writeJsonStream(followersStream),
|
|
210
260
|
table: writeText(renderProfileTable(result.followers, result.cursor))
|
|
211
261
|
}
|
|
212
262
|
);
|
|
@@ -225,6 +275,7 @@ const relationshipsCommand = Command.make(
|
|
|
225
275
|
({ actor, others, format, raw }) =>
|
|
226
276
|
Effect.gen(function* () {
|
|
227
277
|
const appConfig = yield* AppConfigService;
|
|
278
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
228
279
|
const client = yield* BskyClient;
|
|
229
280
|
const identities = yield* IdentityResolver;
|
|
230
281
|
const profiles = yield* ProfileResolver;
|
|
@@ -236,7 +287,9 @@ const relationshipsCommand = Command.make(
|
|
|
236
287
|
? actorValue
|
|
237
288
|
: yield* identities.resolveDid(actorValue);
|
|
238
289
|
});
|
|
239
|
-
const resolvedActor =
|
|
290
|
+
const resolvedActor = actor.startsWith("did:")
|
|
291
|
+
? actor
|
|
292
|
+
: yield* identities.resolveDid(actor);
|
|
240
293
|
const parsedOthers = others
|
|
241
294
|
.split(",")
|
|
242
295
|
.map((item) => item.trim())
|
|
@@ -352,23 +405,31 @@ const listsCommand = Command.make(
|
|
|
352
405
|
({ actor, limit, cursor, purpose, format }) =>
|
|
353
406
|
Effect.gen(function* () {
|
|
354
407
|
const appConfig = yield* AppConfigService;
|
|
408
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
409
|
+
const preferences = yield* CliPreferences;
|
|
355
410
|
const client = yield* BskyClient;
|
|
356
|
-
const
|
|
357
|
-
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
411
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
358
412
|
const options = {
|
|
359
413
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
360
414
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
361
415
|
...(Option.isSome(purpose) ? { purposes: [purpose.value] } : {})
|
|
362
416
|
};
|
|
363
|
-
const result = yield* client.getLists(
|
|
417
|
+
const result = yield* client.getLists(actor, options);
|
|
418
|
+
const lists = preferences.compact
|
|
419
|
+
? result.lists.map(compactListView)
|
|
420
|
+
: result.lists;
|
|
421
|
+
const payload = result.cursor ? { lists, cursor: result.cursor } : { lists };
|
|
422
|
+
const listsStream = Stream.fromIterable(
|
|
423
|
+
lists as ReadonlyArray<ListView | ReturnType<typeof compactListView>>
|
|
424
|
+
);
|
|
364
425
|
yield* emitWithFormat(
|
|
365
426
|
format,
|
|
366
427
|
appConfig.outputFormat,
|
|
367
428
|
jsonNdjsonTableFormats,
|
|
368
429
|
"json",
|
|
369
430
|
{
|
|
370
|
-
json: writeJson(
|
|
371
|
-
ndjson: writeJsonStream(
|
|
431
|
+
json: writeJson(payload),
|
|
432
|
+
ndjson: writeJsonStream(listsStream),
|
|
372
433
|
table: writeText(renderListTable(result.lists, result.cursor))
|
|
373
434
|
}
|
|
374
435
|
);
|
|
@@ -388,13 +449,25 @@ const listCommand = Command.make(
|
|
|
388
449
|
({ uri, limit, cursor, format }) =>
|
|
389
450
|
Effect.gen(function* () {
|
|
390
451
|
const appConfig = yield* AppConfigService;
|
|
452
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
453
|
+
const preferences = yield* CliPreferences;
|
|
391
454
|
const client = yield* BskyClient;
|
|
392
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
455
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
393
456
|
const options = {
|
|
394
457
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
395
458
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
396
459
|
};
|
|
397
460
|
const result = yield* client.getList(uri, options);
|
|
461
|
+
const list = preferences.compact ? compactListView(result.list) : result.list;
|
|
462
|
+
const items = preferences.compact
|
|
463
|
+
? result.items.map(compactListItemView)
|
|
464
|
+
: result.items;
|
|
465
|
+
const payload = result.cursor
|
|
466
|
+
? { list, items, cursor: result.cursor }
|
|
467
|
+
: { list, items };
|
|
468
|
+
const itemsStream = Stream.fromIterable(
|
|
469
|
+
items as ReadonlyArray<ListItemView | ReturnType<typeof compactListItemView>>
|
|
470
|
+
);
|
|
398
471
|
const header = `${result.list.name} (${result.list.purpose}) by ${result.list.creator.handle}`;
|
|
399
472
|
const body = renderListItemsTable(result.items, result.cursor);
|
|
400
473
|
yield* emitWithFormat(
|
|
@@ -403,8 +476,8 @@ const listCommand = Command.make(
|
|
|
403
476
|
jsonNdjsonTableFormats,
|
|
404
477
|
"json",
|
|
405
478
|
{
|
|
406
|
-
json: writeJson(
|
|
407
|
-
ndjson: writeJsonStream(
|
|
479
|
+
json: writeJson(payload),
|
|
480
|
+
ndjson: writeJsonStream(itemsStream),
|
|
408
481
|
table: writeText(`${header}\n\n${body}`)
|
|
409
482
|
}
|
|
410
483
|
);
|
|
@@ -423,24 +496,33 @@ const blocksCommand = Command.make(
|
|
|
423
496
|
({ limit, cursor, format }) =>
|
|
424
497
|
Effect.gen(function* () {
|
|
425
498
|
const appConfig = yield* AppConfigService;
|
|
499
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
500
|
+
const preferences = yield* CliPreferences;
|
|
426
501
|
const client = yield* BskyClient;
|
|
427
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
502
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
428
503
|
const options = {
|
|
429
504
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
430
505
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
431
506
|
};
|
|
432
507
|
const result = yield* client.getBlocks(options);
|
|
508
|
+
const blocks = preferences.compact
|
|
509
|
+
? result.blocks.map(compactProfileView)
|
|
510
|
+
: result.blocks;
|
|
511
|
+
const payload = result.cursor ? { blocks, cursor: result.cursor } : { blocks };
|
|
512
|
+
const blocksStream = Stream.fromIterable(
|
|
513
|
+
blocks as ReadonlyArray<unknown>
|
|
514
|
+
);
|
|
433
515
|
yield* emitWithFormat(
|
|
434
516
|
format,
|
|
435
517
|
appConfig.outputFormat,
|
|
436
518
|
jsonNdjsonTableFormats,
|
|
437
519
|
"json",
|
|
438
520
|
{
|
|
439
|
-
json: writeJson(
|
|
521
|
+
json: writeJson(payload),
|
|
440
522
|
ndjson:
|
|
441
|
-
|
|
523
|
+
blocks.length === 0
|
|
442
524
|
? writeText("[]")
|
|
443
|
-
: writeJsonStream(
|
|
525
|
+
: writeJsonStream(blocksStream),
|
|
444
526
|
table: writeText(renderProfileTable(result.blocks, result.cursor))
|
|
445
527
|
}
|
|
446
528
|
);
|
|
@@ -459,24 +541,33 @@ const mutesCommand = Command.make(
|
|
|
459
541
|
({ limit, cursor, format }) =>
|
|
460
542
|
Effect.gen(function* () {
|
|
461
543
|
const appConfig = yield* AppConfigService;
|
|
544
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
545
|
+
const preferences = yield* CliPreferences;
|
|
462
546
|
const client = yield* BskyClient;
|
|
463
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
547
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
464
548
|
const options = {
|
|
465
549
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
466
550
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
467
551
|
};
|
|
468
552
|
const result = yield* client.getMutes(options);
|
|
553
|
+
const mutes = preferences.compact
|
|
554
|
+
? result.mutes.map(compactProfileView)
|
|
555
|
+
: result.mutes;
|
|
556
|
+
const payload = result.cursor ? { mutes, cursor: result.cursor } : { mutes };
|
|
557
|
+
const mutesStream = Stream.fromIterable(
|
|
558
|
+
mutes as ReadonlyArray<unknown>
|
|
559
|
+
);
|
|
469
560
|
yield* emitWithFormat(
|
|
470
561
|
format,
|
|
471
562
|
appConfig.outputFormat,
|
|
472
563
|
jsonNdjsonTableFormats,
|
|
473
564
|
"json",
|
|
474
565
|
{
|
|
475
|
-
json: writeJson(
|
|
566
|
+
json: writeJson(payload),
|
|
476
567
|
ndjson:
|
|
477
|
-
|
|
568
|
+
mutes.length === 0
|
|
478
569
|
? writeText("[]")
|
|
479
|
-
: writeJsonStream(
|
|
570
|
+
: writeJsonStream(mutesStream),
|
|
480
571
|
table: writeText(renderProfileTable(result.mutes, result.cursor))
|
|
481
572
|
}
|
|
482
573
|
);
|
package/src/cli/interval.ts
CHANGED
|
@@ -1,35 +1,6 @@
|
|
|
1
|
-
import { Duration,
|
|
2
|
-
import { CliInputError } from "./errors.js";
|
|
1
|
+
import { Duration, Option } from "effect";
|
|
3
2
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
try: () => Duration.decode(value as Duration.DurationInput),
|
|
7
|
-
catch: (cause) =>
|
|
8
|
-
CliInputError.make({
|
|
9
|
-
message: `Invalid duration: ${value}. Use formats like "30 seconds" or "500 millis".`,
|
|
10
|
-
cause
|
|
11
|
-
})
|
|
12
|
-
}).pipe(
|
|
13
|
-
Effect.flatMap((duration) =>
|
|
14
|
-
Duration.toMillis(duration) < 0
|
|
15
|
-
? Effect.fail(
|
|
16
|
-
CliInputError.make({
|
|
17
|
-
message: "Interval must be non-negative.",
|
|
18
|
-
cause: duration
|
|
19
|
-
})
|
|
20
|
-
)
|
|
21
|
-
: Effect.succeed(duration)
|
|
22
|
-
)
|
|
23
|
-
);
|
|
3
|
+
export const parseInterval = (interval: Option.Option<Duration.Duration>) =>
|
|
4
|
+
Option.getOrElse(interval, () => Duration.seconds(30));
|
|
24
5
|
|
|
25
|
-
export const
|
|
26
|
-
Option.match(interval, {
|
|
27
|
-
onSome: parseDurationText,
|
|
28
|
-
onNone: () => Effect.succeed(Duration.seconds(30))
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
export const parseOptionalDuration = (value: Option.Option<string>) =>
|
|
32
|
-
Option.match(value, {
|
|
33
|
-
onSome: (raw) => parseDurationText(raw).pipe(Effect.map(Option.some)),
|
|
34
|
-
onNone: () => Effect.succeed(Option.none())
|
|
35
|
-
});
|
|
6
|
+
export const parseOptionalDuration = (value: Option.Option<Duration.Duration>) => value;
|
package/src/cli/jetstream.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { DataSource } from "../domain/sync.js";
|
|
|
5
5
|
import type { StoreRef } from "../domain/store.js";
|
|
6
6
|
import { SyncCheckpointStore } from "../services/sync-checkpoint-store.js";
|
|
7
7
|
import { CliInputError } from "./errors.js";
|
|
8
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_COLLECTIONS = ["app.bsky.feed.post"];
|
|
10
11
|
|
|
@@ -31,6 +32,7 @@ export const jetstreamOptions = {
|
|
|
31
32
|
Options.withDescription("Enable compression if supported by runtime")
|
|
32
33
|
),
|
|
33
34
|
maxMessageSize: Options.integer("max-message-size").pipe(
|
|
35
|
+
Options.withSchema(PositiveInt),
|
|
34
36
|
Options.withDescription("Max message size in bytes"),
|
|
35
37
|
Options.optional
|
|
36
38
|
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Duration, Effect, ParseResult, Schema } from "effect";
|
|
2
|
+
import { parseDurationInput } from "./time.js";
|
|
3
|
+
|
|
4
|
+
export const PositiveInt = Schema.Int.pipe(Schema.greaterThan(0));
|
|
5
|
+
|
|
6
|
+
export const NonNegativeInt = Schema.NonNegativeInt;
|
|
7
|
+
|
|
8
|
+
export const boundedInt = (min: number, max: number) =>
|
|
9
|
+
Schema.Int.pipe(
|
|
10
|
+
Schema.greaterThanOrEqualTo(min),
|
|
11
|
+
Schema.lessThanOrEqualTo(max)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const DurationInput = Schema.transformOrFail(Schema.String, Schema.DurationFromSelf, {
|
|
15
|
+
strict: true,
|
|
16
|
+
decode: (raw, _options, ast) =>
|
|
17
|
+
parseDurationInput(raw).pipe(
|
|
18
|
+
Effect.mapError((error) => new ParseResult.Type(ast, raw, error.message))
|
|
19
|
+
),
|
|
20
|
+
encode: (duration) =>
|
|
21
|
+
Effect.succeed(`${Duration.toMillis(duration)} millis`)
|
|
22
|
+
}).pipe(Schema.greaterThanOrEqualToDuration(0));
|
package/src/cli/output-format.ts
CHANGED
|
@@ -13,6 +13,17 @@ export type TextJsonFormat = typeof textJsonFormats[number];
|
|
|
13
13
|
export const treeTableJsonFormats = ["tree", "table", "json"] as const;
|
|
14
14
|
export type TreeTableJsonFormat = typeof treeTableJsonFormats[number];
|
|
15
15
|
|
|
16
|
+
export const queryOutputFormats = [
|
|
17
|
+
"json",
|
|
18
|
+
"ndjson",
|
|
19
|
+
"markdown",
|
|
20
|
+
"table",
|
|
21
|
+
"compact",
|
|
22
|
+
"card",
|
|
23
|
+
"thread"
|
|
24
|
+
] as const;
|
|
25
|
+
export type QueryOutputFormat = typeof queryOutputFormats[number];
|
|
26
|
+
|
|
16
27
|
export const resolveOutputFormat = <T extends string>(
|
|
17
28
|
format: Option.Option<T>,
|
|
18
29
|
configFormat: OutputFormat,
|
package/src/cli/pagination.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Options } from "@effect/cli";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Option } from "effect";
|
|
3
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
4
4
|
|
|
5
|
-
export const limitOption = Options.integer("limit").pipe(
|
|
5
|
+
export const limitOption = Options.integer("limit").pipe(
|
|
6
|
+
Options.withSchema(PositiveInt),
|
|
7
|
+
Options.optional
|
|
8
|
+
);
|
|
6
9
|
export const cursorOption = Options.text("cursor").pipe(Options.optional);
|
|
7
10
|
|
|
8
11
|
export const parsePagination = (
|
|
9
12
|
limit: Option.Option<number>,
|
|
10
13
|
cursor: Option.Option<string>
|
|
11
|
-
) =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
limit: Option.getOrUndefined(parsedLimit),
|
|
16
|
-
cursor: Option.getOrUndefined(cursor)
|
|
17
|
-
};
|
|
18
|
-
});
|
|
14
|
+
) => ({
|
|
15
|
+
limit: Option.getOrUndefined(limit),
|
|
16
|
+
cursor: Option.getOrUndefined(cursor)
|
|
17
|
+
});
|
package/src/cli/parse-errors.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
type ParseIssue = { readonly _tag: string; readonly message?: string };
|
|
2
|
+
|
|
1
3
|
/** Safely parse JSON, returning `undefined` on failure. */
|
|
2
4
|
export const safeParseJson = (raw: string): unknown => {
|
|
3
5
|
try {
|
|
@@ -7,6 +9,16 @@ export const safeParseJson = (raw: string): unknown => {
|
|
|
7
9
|
}
|
|
8
10
|
};
|
|
9
11
|
|
|
12
|
+
export const jsonParseTip = "Tip: wrap JSON in single quotes to avoid shell escaping issues.";
|
|
13
|
+
|
|
14
|
+
export const findJsonParseIssue = (issues: ReadonlyArray<ParseIssue>) =>
|
|
15
|
+
issues.find(
|
|
16
|
+
(issue) =>
|
|
17
|
+
issue._tag === "Transformation" &&
|
|
18
|
+
typeof issue.message === "string" &&
|
|
19
|
+
issue.message.startsWith("JSON Parse error")
|
|
20
|
+
);
|
|
21
|
+
|
|
10
22
|
/** Format schema issues into an array of "path: message" strings. */
|
|
11
23
|
export const issueDetails = (
|
|
12
24
|
issues: ReadonlyArray<{ readonly path: ReadonlyArray<unknown>; readonly message: string }>
|
package/src/cli/parse.ts
CHANGED
|
@@ -1,52 +1,6 @@
|
|
|
1
1
|
import { Effect, ParseResult, Schema } from "effect";
|
|
2
2
|
import { CliJsonError } from "./errors.js";
|
|
3
|
-
|
|
4
|
-
type FormatParseErrorOptions = {
|
|
5
|
-
readonly label?: string;
|
|
6
|
-
readonly maxIssues?: number;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const formatPath = (path: ReadonlyArray<unknown>) =>
|
|
10
|
-
path.length > 0 ? path.map((entry) => String(entry)).join(".") : "value";
|
|
11
|
-
|
|
12
|
-
const formatParseError = (
|
|
13
|
-
error: ParseResult.ParseError,
|
|
14
|
-
options?: FormatParseErrorOptions
|
|
15
|
-
) => {
|
|
16
|
-
const issues = ParseResult.ArrayFormatter.formatErrorSync(error);
|
|
17
|
-
if (issues.length === 0) {
|
|
18
|
-
return ParseResult.TreeFormatter.formatErrorSync(error);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const jsonParseIssue = issues.find(
|
|
22
|
-
(issue) =>
|
|
23
|
-
issue._tag === "Transformation" &&
|
|
24
|
-
typeof issue.message === "string" &&
|
|
25
|
-
issue.message.startsWith("JSON Parse error")
|
|
26
|
-
);
|
|
27
|
-
if (jsonParseIssue) {
|
|
28
|
-
const header = options?.label
|
|
29
|
-
? `Invalid JSON input for ${options.label}.`
|
|
30
|
-
: "Invalid JSON input.";
|
|
31
|
-
return [
|
|
32
|
-
header,
|
|
33
|
-
jsonParseIssue.message,
|
|
34
|
-
"Tip: wrap JSON in single quotes to avoid shell escaping issues."
|
|
35
|
-
].join("\n");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const maxIssues = options?.maxIssues ?? 6;
|
|
39
|
-
const lines = issues.slice(0, maxIssues).map((issue) => {
|
|
40
|
-
const path = formatPath(issue.path);
|
|
41
|
-
return `${path}: ${issue.message}`;
|
|
42
|
-
});
|
|
43
|
-
if (issues.length > maxIssues) {
|
|
44
|
-
lines.push(`Additional issues: ${issues.length - maxIssues}`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const header = options?.label ? `Invalid ${options.label}.` : undefined;
|
|
48
|
-
return header ? [header, ...lines].join("\n") : lines.join("\n");
|
|
49
|
-
};
|
|
3
|
+
import { formatParseError } from "./shared.js";
|
|
50
4
|
|
|
51
5
|
type DecodeJsonOptions = {
|
|
52
6
|
readonly formatter?: (error: ParseResult.ParseError, raw: string) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { RawPost } from "../domain/raw.js";
|
|
3
|
+
import { Post } from "../domain/post.js";
|
|
4
|
+
import { StoreName } from "../domain/primitives.js";
|
|
5
|
+
|
|
6
|
+
export class StorePostInput extends Schema.Class<StorePostInput>("StorePostInput")({
|
|
7
|
+
store: StoreName,
|
|
8
|
+
post: Post
|
|
9
|
+
}) {}
|
|
10
|
+
|
|
11
|
+
export const PipeInput = Schema.Union(RawPost, Post, StorePostInput);
|
|
12
|
+
export type PipeInput = typeof PipeInput.Type;
|
|
13
|
+
|
|
14
|
+
export const isRawPostInput = (value: PipeInput): value is RawPost =>
|
|
15
|
+
typeof value === "object" && value !== null && "record" in value;
|
|
16
|
+
|
|
17
|
+
export const isStorePostInput = (value: PipeInput): value is StorePostInput =>
|
|
18
|
+
typeof value === "object" && value !== null && "post" in value;
|
package/src/cli/pipe.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Command, Options } from "@effect/cli";
|
|
2
2
|
import { Chunk, Effect, Option, Ref, Stream } from "effect";
|
|
3
3
|
import { ParseResult } from "effect";
|
|
4
|
-
import { RawPost } from "../domain/raw.js";
|
|
5
4
|
import type { Post } from "../domain/post.js";
|
|
6
5
|
import { FilterRuntime } from "../services/filter-runtime.js";
|
|
7
6
|
import { PostParser } from "../services/post-parser.js";
|
|
@@ -9,12 +8,14 @@ import { CliInput } from "./input.js";
|
|
|
9
8
|
import { CliInputError, CliJsonError } from "./errors.js";
|
|
10
9
|
import { parseFilterExpr } from "./filter-input.js";
|
|
11
10
|
import { decodeJson } from "./parse.js";
|
|
11
|
+
import { PipeInput, isRawPostInput, isStorePostInput } from "./pipe-input.js";
|
|
12
12
|
import { withExamples } from "./help.js";
|
|
13
13
|
import { filterOption, filterJsonOption } from "./shared-options.js";
|
|
14
14
|
import { formatSchemaError } from "./shared.js";
|
|
15
|
-
import { writeJsonStream
|
|
15
|
+
import { writeJsonStream } from "./output.js";
|
|
16
16
|
import { filterByFlags } from "../typeclass/chunk.js";
|
|
17
17
|
import { logErrorEvent, logWarn } from "./logging.js";
|
|
18
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
18
19
|
|
|
19
20
|
const onErrorOption = Options.choice("on-error", ["fail", "skip", "report"]).pipe(
|
|
20
21
|
Options.withDescription("Behavior on invalid input lines"),
|
|
@@ -22,6 +23,7 @@ const onErrorOption = Options.choice("on-error", ["fail", "skip", "report"]).pip
|
|
|
22
23
|
);
|
|
23
24
|
|
|
24
25
|
const batchSizeOption = Options.integer("batch-size").pipe(
|
|
26
|
+
Options.withSchema(PositiveInt),
|
|
25
27
|
Options.withDescription("Posts per filter batch (default: 50)"),
|
|
26
28
|
Options.optional
|
|
27
29
|
);
|
|
@@ -78,16 +80,8 @@ export const pipeCommand = Command.make(
|
|
|
78
80
|
const evaluateBatch = yield* runtime.evaluateBatch(expr);
|
|
79
81
|
|
|
80
82
|
const size = Option.getOrElse(batchSize, () => 50);
|
|
81
|
-
if (size <= 0) {
|
|
82
|
-
return yield* CliInputError.make({
|
|
83
|
-
message: "--batch-size must be a positive integer.",
|
|
84
|
-
cause: { batchSize: size }
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
83
|
|
|
88
84
|
const lineRef = yield* Ref.make(0);
|
|
89
|
-
const countRef = yield* Ref.make(0);
|
|
90
|
-
|
|
91
85
|
const parsed = input.lines.pipe(
|
|
92
86
|
Stream.map((line) => line.trim()),
|
|
93
87
|
Stream.filter((line) => line.length > 0),
|
|
@@ -97,8 +91,16 @@ export const pipeCommand = Command.make(
|
|
|
97
91
|
)
|
|
98
92
|
),
|
|
99
93
|
Stream.mapEffect(({ line, lineNumber }) =>
|
|
100
|
-
decodeJson(
|
|
101
|
-
Effect.flatMap((
|
|
94
|
+
decodeJson(PipeInput, line).pipe(
|
|
95
|
+
Effect.flatMap((inputPost) => {
|
|
96
|
+
if (isRawPostInput(inputPost)) {
|
|
97
|
+
return parser.parsePost(inputPost);
|
|
98
|
+
}
|
|
99
|
+
if (isStorePostInput(inputPost)) {
|
|
100
|
+
return Effect.succeed(inputPost.post);
|
|
101
|
+
}
|
|
102
|
+
return Effect.succeed(inputPost);
|
|
103
|
+
}),
|
|
102
104
|
Effect.map(Option.some),
|
|
103
105
|
Effect.catchAll((error) => {
|
|
104
106
|
if (onError === "fail") {
|
|
@@ -131,15 +133,10 @@ export const pipeCommand = Command.make(
|
|
|
131
133
|
Effect.map((flags) => filterByFlags(batch, flags))
|
|
132
134
|
)
|
|
133
135
|
),
|
|
134
|
-
Stream.mapConcat((chunk) => Chunk.toReadonlyArray(chunk))
|
|
135
|
-
Stream.tap(() => Ref.update(countRef, (count) => count + 1))
|
|
136
|
+
Stream.mapConcat((chunk) => Chunk.toReadonlyArray(chunk))
|
|
136
137
|
);
|
|
137
138
|
|
|
138
139
|
yield* writeJsonStream(filtered);
|
|
139
|
-
const count = yield* Ref.get(countRef);
|
|
140
|
-
if (count === 0) {
|
|
141
|
-
yield* writeText("[]");
|
|
142
|
-
}
|
|
143
140
|
})
|
|
144
141
|
).pipe(
|
|
145
142
|
Command.withDescription(
|
|
@@ -150,7 +147,7 @@ export const pipeCommand = Command.make(
|
|
|
150
147
|
"cat posts.ndjson | skygent pipe --filter-json '{\"_tag\":\"All\"}'"
|
|
151
148
|
],
|
|
152
149
|
[
|
|
153
|
-
"Note: stdin must be raw post NDJSON (
|
|
150
|
+
"Note: stdin must be raw post NDJSON or skygent post NDJSON (from query --format ndjson)."
|
|
154
151
|
]
|
|
155
152
|
)
|
|
156
153
|
)
|