@mepuka/skygent 0.2.0 → 0.3.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.
- package/README.md +269 -31
- package/index.ts +18 -3
- package/package.json +1 -1
- package/src/cli/app.ts +4 -2
- package/src/cli/config.ts +20 -3
- package/src/cli/doc/table-renderers.ts +29 -0
- package/src/cli/doc/thread.ts +2 -4
- package/src/cli/exit-codes.ts +2 -0
- package/src/cli/feed.ts +35 -55
- package/src/cli/filter-dsl.ts +146 -11
- package/src/cli/filter-errors.ts +9 -3
- package/src/cli/filter-help.ts +7 -0
- package/src/cli/filter-input.ts +3 -2
- package/src/cli/filter.ts +84 -4
- package/src/cli/graph.ts +193 -156
- package/src/cli/input.ts +45 -0
- package/src/cli/layers.ts +10 -0
- package/src/cli/logging.ts +8 -0
- package/src/cli/output-render.ts +14 -0
- package/src/cli/pagination.ts +18 -0
- package/src/cli/parse-errors.ts +18 -0
- package/src/cli/pipe.ts +157 -0
- package/src/cli/post.ts +43 -66
- package/src/cli/query.ts +349 -74
- package/src/cli/search.ts +92 -118
- package/src/cli/shared.ts +0 -19
- package/src/cli/store-errors.ts +24 -13
- package/src/cli/store-tree.ts +6 -4
- package/src/cli/store.ts +35 -2
- package/src/cli/stream-merge.ts +105 -0
- package/src/cli/sync-factory.ts +28 -3
- package/src/cli/sync.ts +16 -18
- package/src/cli/thread-options.ts +33 -0
- package/src/cli/time.ts +171 -0
- package/src/cli/view-thread.ts +12 -18
- package/src/cli/watch.ts +61 -19
- package/src/domain/errors.ts +6 -1
- package/src/domain/format.ts +21 -0
- package/src/domain/order.ts +24 -0
- package/src/graph/relationships.ts +129 -0
- package/src/services/jetstream-sync.ts +4 -4
- package/src/services/lineage-store.ts +15 -1
- package/src/services/store-commit.ts +60 -0
- package/src/services/store-manager.ts +69 -2
- package/src/services/store-renamer.ts +286 -0
- package/src/services/store-stats.ts +7 -5
- package/src/services/sync-engine.ts +136 -85
- package/src/services/sync-reporter.ts +3 -1
- package/src/services/sync-settings.ts +24 -0
package/src/cli/graph.ts
CHANGED
|
@@ -3,13 +3,23 @@ import { Effect, Option, Stream } from "effect";
|
|
|
3
3
|
import { BskyClient } from "../services/bsky-client.js";
|
|
4
4
|
import { AppConfigService } from "../services/app-config.js";
|
|
5
5
|
import { IdentityResolver } from "../services/identity-resolver.js";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { ProfileResolver } from "../services/profile-resolver.js";
|
|
7
|
+
import type { ListItemView, ListView } from "../domain/bsky.js";
|
|
8
|
+
import { decodeActor } from "./shared-options.js";
|
|
8
9
|
import { CliInputError } from "./errors.js";
|
|
9
10
|
import { withExamples } from "./help.js";
|
|
10
11
|
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
11
12
|
import { renderTableLegacy } from "./doc/table.js";
|
|
12
|
-
import {
|
|
13
|
+
import { renderProfileTable } from "./doc/table-renderers.js";
|
|
14
|
+
import { jsonNdjsonTableFormats } from "./output-format.js";
|
|
15
|
+
import { emitWithFormat } from "./output-render.js";
|
|
16
|
+
import { cursorOption as baseCursorOption, limitOption as baseLimitOption, parsePagination } from "./pagination.js";
|
|
17
|
+
import {
|
|
18
|
+
buildRelationshipGraph,
|
|
19
|
+
relationshipEntries,
|
|
20
|
+
type RelationshipEntry,
|
|
21
|
+
type RelationshipNode
|
|
22
|
+
} from "../graph/relationships.js";
|
|
13
23
|
|
|
14
24
|
const actorArg = Args.text({ name: "actor" }).pipe(
|
|
15
25
|
Args.withDescription("Bluesky handle or DID")
|
|
@@ -19,14 +29,12 @@ const listUriArg = Args.text({ name: "uri" }).pipe(
|
|
|
19
29
|
Args.withDescription("Bluesky list URI (at://...)")
|
|
20
30
|
);
|
|
21
31
|
|
|
22
|
-
const limitOption =
|
|
23
|
-
Options.withDescription("Maximum number of results")
|
|
24
|
-
Options.optional
|
|
32
|
+
const limitOption = baseLimitOption.pipe(
|
|
33
|
+
Options.withDescription("Maximum number of results")
|
|
25
34
|
);
|
|
26
35
|
|
|
27
|
-
const cursorOption =
|
|
28
|
-
Options.withDescription("Pagination cursor")
|
|
29
|
-
Options.optional
|
|
36
|
+
const cursorOption = baseCursorOption.pipe(
|
|
37
|
+
Options.withDescription("Pagination cursor")
|
|
30
38
|
);
|
|
31
39
|
|
|
32
40
|
const formatOption = Options.choice("format", jsonNdjsonTableFormats).pipe(
|
|
@@ -42,19 +50,9 @@ const purposeOption = Options.choice("purpose", ["modlist", "curatelist"]).pipe(
|
|
|
42
50
|
const othersOption = Options.text("others").pipe(
|
|
43
51
|
Options.withDescription("Comma-separated list of actors to compare" )
|
|
44
52
|
);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cursor: string | undefined
|
|
49
|
-
) => {
|
|
50
|
-
const rows = actors.map((actor) => [
|
|
51
|
-
actor.handle,
|
|
52
|
-
actor.displayName ?? "",
|
|
53
|
-
actor.did
|
|
54
|
-
]);
|
|
55
|
-
const table = renderTableLegacy(["HANDLE", "DISPLAY NAME", "DID"], rows);
|
|
56
|
-
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
57
|
-
};
|
|
53
|
+
const rawOption = Options.boolean("raw").pipe(
|
|
54
|
+
Options.withDescription("Output raw relationship data (no wrapper fields)")
|
|
55
|
+
);
|
|
58
56
|
|
|
59
57
|
const renderListTable = (
|
|
60
58
|
lists: ReadonlyArray<ListView>,
|
|
@@ -71,35 +69,33 @@ const renderListTable = (
|
|
|
71
69
|
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
72
70
|
};
|
|
73
71
|
|
|
74
|
-
const renderRelationshipsTable = (
|
|
75
|
-
const rows =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
did: string;
|
|
81
|
-
following?: string;
|
|
82
|
-
followedBy?: string;
|
|
83
|
-
blocking?: string;
|
|
84
|
-
blockedBy?: string;
|
|
85
|
-
blockingByList?: string;
|
|
86
|
-
blockedByList?: string;
|
|
87
|
-
};
|
|
72
|
+
const renderRelationshipsTable = (entries: ReadonlyArray<RelationshipEntry>) => {
|
|
73
|
+
const rows = entries.map((entry) => {
|
|
74
|
+
const otherInputs = entry.other.inputs.join(", ");
|
|
75
|
+
const handle = entry.other.handle ?? "";
|
|
76
|
+
const did = entry.other.did ?? (entry.other.notFound ? "not-found" : "");
|
|
77
|
+
const rel = entry.relationship;
|
|
88
78
|
return [
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
otherInputs,
|
|
80
|
+
handle,
|
|
81
|
+
did,
|
|
82
|
+
rel.following ? "yes" : "",
|
|
83
|
+
rel.followedBy ? "yes" : "",
|
|
84
|
+
rel.mutual ? "yes" : "",
|
|
85
|
+
rel.blocking ? "yes" : "",
|
|
86
|
+
rel.blockedBy ? "yes" : "",
|
|
87
|
+
rel.blockingByList ? "yes" : "",
|
|
88
|
+
rel.blockedByList ? "yes" : ""
|
|
96
89
|
];
|
|
97
90
|
});
|
|
98
91
|
return renderTableLegacy(
|
|
99
92
|
[
|
|
93
|
+
"INPUTS",
|
|
94
|
+
"HANDLE",
|
|
100
95
|
"DID",
|
|
101
96
|
"FOLLOWING",
|
|
102
97
|
"FOLLOWED BY",
|
|
98
|
+
"MUTUAL",
|
|
103
99
|
"BLOCKING",
|
|
104
100
|
"BLOCKED BY",
|
|
105
101
|
"BLOCK BY LIST",
|
|
@@ -127,27 +123,23 @@ const followersCommand = Command.make(
|
|
|
127
123
|
const appConfig = yield* AppConfigService;
|
|
128
124
|
const client = yield* BskyClient;
|
|
129
125
|
const resolvedActor = yield* decodeActor(actor);
|
|
130
|
-
const
|
|
126
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
131
127
|
const options = {
|
|
132
|
-
...(
|
|
133
|
-
...(
|
|
128
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
129
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
134
130
|
};
|
|
135
131
|
const result = yield* client.getFollowers(resolvedActor, options);
|
|
136
|
-
|
|
132
|
+
yield* emitWithFormat(
|
|
137
133
|
format,
|
|
138
134
|
appConfig.outputFormat,
|
|
139
135
|
jsonNdjsonTableFormats,
|
|
140
|
-
"json"
|
|
136
|
+
"json",
|
|
137
|
+
{
|
|
138
|
+
json: writeJson(result),
|
|
139
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.followers)),
|
|
140
|
+
table: writeText(renderProfileTable(result.followers, result.cursor))
|
|
141
|
+
}
|
|
141
142
|
);
|
|
142
|
-
if (outputFormat === "ndjson") {
|
|
143
|
-
yield* writeJsonStream(Stream.fromIterable(result.followers));
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (outputFormat === "table") {
|
|
147
|
-
yield* writeText(renderProfileTable(result.followers, result.cursor));
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
yield* writeJson(result);
|
|
151
143
|
})
|
|
152
144
|
).pipe(
|
|
153
145
|
Command.withDescription(
|
|
@@ -166,27 +158,23 @@ const followsCommand = Command.make(
|
|
|
166
158
|
const appConfig = yield* AppConfigService;
|
|
167
159
|
const client = yield* BskyClient;
|
|
168
160
|
const resolvedActor = yield* decodeActor(actor);
|
|
169
|
-
const
|
|
161
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
170
162
|
const options = {
|
|
171
|
-
...(
|
|
172
|
-
...(
|
|
163
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
164
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
173
165
|
};
|
|
174
166
|
const result = yield* client.getFollows(resolvedActor, options);
|
|
175
|
-
|
|
167
|
+
yield* emitWithFormat(
|
|
176
168
|
format,
|
|
177
169
|
appConfig.outputFormat,
|
|
178
170
|
jsonNdjsonTableFormats,
|
|
179
|
-
"json"
|
|
171
|
+
"json",
|
|
172
|
+
{
|
|
173
|
+
json: writeJson(result),
|
|
174
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.follows)),
|
|
175
|
+
table: writeText(renderProfileTable(result.follows, result.cursor))
|
|
176
|
+
}
|
|
180
177
|
);
|
|
181
|
-
if (outputFormat === "ndjson") {
|
|
182
|
-
yield* writeJsonStream(Stream.fromIterable(result.follows));
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (outputFormat === "table") {
|
|
186
|
-
yield* writeText(renderProfileTable(result.follows, result.cursor));
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
yield* writeJson(result);
|
|
190
178
|
})
|
|
191
179
|
).pipe(
|
|
192
180
|
Command.withDescription(
|
|
@@ -205,27 +193,23 @@ const knownFollowersCommand = Command.make(
|
|
|
205
193
|
const appConfig = yield* AppConfigService;
|
|
206
194
|
const client = yield* BskyClient;
|
|
207
195
|
const resolvedActor = yield* decodeActor(actor);
|
|
208
|
-
const
|
|
196
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
209
197
|
const options = {
|
|
210
|
-
...(
|
|
211
|
-
...(
|
|
198
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
199
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
212
200
|
};
|
|
213
201
|
const result = yield* client.getKnownFollowers(resolvedActor, options);
|
|
214
|
-
|
|
202
|
+
yield* emitWithFormat(
|
|
215
203
|
format,
|
|
216
204
|
appConfig.outputFormat,
|
|
217
205
|
jsonNdjsonTableFormats,
|
|
218
|
-
"json"
|
|
206
|
+
"json",
|
|
207
|
+
{
|
|
208
|
+
json: writeJson(result),
|
|
209
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.followers)),
|
|
210
|
+
table: writeText(renderProfileTable(result.followers, result.cursor))
|
|
211
|
+
}
|
|
219
212
|
);
|
|
220
|
-
if (outputFormat === "ndjson") {
|
|
221
|
-
yield* writeJsonStream(Stream.fromIterable(result.followers));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (outputFormat === "table") {
|
|
225
|
-
yield* writeText(renderProfileTable(result.followers, result.cursor));
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
yield* writeJson(result);
|
|
229
213
|
})
|
|
230
214
|
).pipe(
|
|
231
215
|
Command.withDescription(
|
|
@@ -237,12 +221,13 @@ const knownFollowersCommand = Command.make(
|
|
|
237
221
|
|
|
238
222
|
const relationshipsCommand = Command.make(
|
|
239
223
|
"relationships",
|
|
240
|
-
{ actor: actorArg, others: othersOption, format: formatOption },
|
|
241
|
-
({ actor, others, format }) =>
|
|
224
|
+
{ actor: actorArg, others: othersOption, format: formatOption, raw: rawOption },
|
|
225
|
+
({ actor, others, format, raw }) =>
|
|
242
226
|
Effect.gen(function* () {
|
|
243
227
|
const appConfig = yield* AppConfigService;
|
|
244
228
|
const client = yield* BskyClient;
|
|
245
229
|
const identities = yield* IdentityResolver;
|
|
230
|
+
const profiles = yield* ProfileResolver;
|
|
246
231
|
const resolveDid = (value: string) =>
|
|
247
232
|
Effect.gen(function* () {
|
|
248
233
|
const decoded = yield* decodeActor(value);
|
|
@@ -274,22 +259,84 @@ const relationshipsCommand = Command.make(
|
|
|
274
259
|
(value) => resolveDid(value),
|
|
275
260
|
{ concurrency: "unbounded" }
|
|
276
261
|
);
|
|
262
|
+
const didToInputs = new Map<string, Array<string>>();
|
|
263
|
+
resolvedOthers.forEach((did, index) => {
|
|
264
|
+
const input = uniqueOthers[index];
|
|
265
|
+
if (input) {
|
|
266
|
+
const existing = didToInputs.get(did);
|
|
267
|
+
if (existing) {
|
|
268
|
+
existing.push(input);
|
|
269
|
+
} else {
|
|
270
|
+
didToInputs.set(did, [input]);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
277
274
|
const result = yield* client.getRelationships(resolvedActor, resolvedOthers);
|
|
278
|
-
const
|
|
275
|
+
const handleFromInputs = (inputs: ReadonlyArray<string>) =>
|
|
276
|
+
inputs.find((input) => !input.startsWith("did:"));
|
|
277
|
+
const inputsForActor = [actor];
|
|
278
|
+
const didsNeedingHandle = [resolvedActor, ...resolvedOthers].filter((did) => {
|
|
279
|
+
const inputs = did === resolvedActor ? inputsForActor : didToInputs.get(did) ?? [];
|
|
280
|
+
return handleFromInputs(inputs) === undefined;
|
|
281
|
+
});
|
|
282
|
+
const handles = yield* Effect.forEach(
|
|
283
|
+
didsNeedingHandle,
|
|
284
|
+
(did) =>
|
|
285
|
+
profiles.handleForDid(did).pipe(
|
|
286
|
+
Effect.either,
|
|
287
|
+
Effect.map((result) => [did, result] as const)
|
|
288
|
+
),
|
|
289
|
+
{ concurrency: "unbounded" }
|
|
290
|
+
);
|
|
291
|
+
const handleMap = new Map<string, string>();
|
|
292
|
+
for (const [did, result] of handles) {
|
|
293
|
+
if (result._tag === "Right") {
|
|
294
|
+
handleMap.set(did, String(result.right));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const buildNode = (
|
|
298
|
+
did: string,
|
|
299
|
+
inputs: ReadonlyArray<string>,
|
|
300
|
+
handle?: string
|
|
301
|
+
): RelationshipNode => ({
|
|
302
|
+
did,
|
|
303
|
+
inputs,
|
|
304
|
+
...(handle ? { handle } : {})
|
|
305
|
+
});
|
|
306
|
+
const actorHandle = handleFromInputs(inputsForActor) ?? handleMap.get(resolvedActor);
|
|
307
|
+
const nodesByKey = new Map<string, RelationshipNode>();
|
|
308
|
+
nodesByKey.set(resolvedActor, buildNode(resolvedActor, inputsForActor, actorHandle));
|
|
309
|
+
for (const [did, inputs] of didToInputs.entries()) {
|
|
310
|
+
const handle = handleFromInputs(inputs) ?? handleMap.get(did);
|
|
311
|
+
nodesByKey.set(did, buildNode(did, inputs, handle));
|
|
312
|
+
}
|
|
313
|
+
const graphResult = buildRelationshipGraph(
|
|
314
|
+
resolvedActor,
|
|
315
|
+
nodesByKey,
|
|
316
|
+
result.relationships
|
|
317
|
+
);
|
|
318
|
+
const entries = relationshipEntries(graphResult.graph);
|
|
319
|
+
yield* emitWithFormat(
|
|
279
320
|
format,
|
|
280
321
|
appConfig.outputFormat,
|
|
281
322
|
jsonNdjsonTableFormats,
|
|
282
|
-
"json"
|
|
323
|
+
"json",
|
|
324
|
+
{
|
|
325
|
+
json: raw
|
|
326
|
+
? writeJson(result)
|
|
327
|
+
: writeJson({
|
|
328
|
+
actor: nodesByKey.get(resolvedActor) ?? {
|
|
329
|
+
did: resolvedActor,
|
|
330
|
+
inputs: [actor]
|
|
331
|
+
},
|
|
332
|
+
relationships: entries
|
|
333
|
+
}),
|
|
334
|
+
ndjson: raw
|
|
335
|
+
? writeJsonStream(Stream.fromIterable(result.relationships))
|
|
336
|
+
: writeJsonStream(Stream.fromIterable(entries)),
|
|
337
|
+
table: writeText(renderRelationshipsTable(entries))
|
|
338
|
+
}
|
|
283
339
|
);
|
|
284
|
-
if (outputFormat === "ndjson") {
|
|
285
|
-
yield* writeJsonStream(Stream.fromIterable(result.relationships));
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (outputFormat === "table") {
|
|
289
|
-
yield* writeText(renderRelationshipsTable(result.relationships));
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
yield* writeJson(result);
|
|
293
340
|
})
|
|
294
341
|
).pipe(
|
|
295
342
|
Command.withDescription(
|
|
@@ -307,28 +354,24 @@ const listsCommand = Command.make(
|
|
|
307
354
|
const appConfig = yield* AppConfigService;
|
|
308
355
|
const client = yield* BskyClient;
|
|
309
356
|
const resolvedActor = yield* decodeActor(actor);
|
|
310
|
-
const
|
|
357
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
311
358
|
const options = {
|
|
312
|
-
...(
|
|
313
|
-
...(
|
|
359
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
360
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
314
361
|
...(Option.isSome(purpose) ? { purposes: [purpose.value] } : {})
|
|
315
362
|
};
|
|
316
363
|
const result = yield* client.getLists(resolvedActor, options);
|
|
317
|
-
|
|
364
|
+
yield* emitWithFormat(
|
|
318
365
|
format,
|
|
319
366
|
appConfig.outputFormat,
|
|
320
367
|
jsonNdjsonTableFormats,
|
|
321
|
-
"json"
|
|
368
|
+
"json",
|
|
369
|
+
{
|
|
370
|
+
json: writeJson(result),
|
|
371
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.lists)),
|
|
372
|
+
table: writeText(renderListTable(result.lists, result.cursor))
|
|
373
|
+
}
|
|
322
374
|
);
|
|
323
|
-
if (outputFormat === "ndjson") {
|
|
324
|
-
yield* writeJsonStream(Stream.fromIterable(result.lists));
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (outputFormat === "table") {
|
|
328
|
-
yield* writeText(renderListTable(result.lists, result.cursor));
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
yield* writeJson(result);
|
|
332
375
|
})
|
|
333
376
|
).pipe(
|
|
334
377
|
Command.withDescription(
|
|
@@ -346,29 +389,25 @@ const listCommand = Command.make(
|
|
|
346
389
|
Effect.gen(function* () {
|
|
347
390
|
const appConfig = yield* AppConfigService;
|
|
348
391
|
const client = yield* BskyClient;
|
|
349
|
-
const
|
|
392
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
350
393
|
const options = {
|
|
351
|
-
...(
|
|
352
|
-
...(
|
|
394
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
395
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
353
396
|
};
|
|
354
397
|
const result = yield* client.getList(uri, options);
|
|
355
|
-
const
|
|
398
|
+
const header = `${result.list.name} (${result.list.purpose}) by ${result.list.creator.handle}`;
|
|
399
|
+
const body = renderListItemsTable(result.items, result.cursor);
|
|
400
|
+
yield* emitWithFormat(
|
|
356
401
|
format,
|
|
357
402
|
appConfig.outputFormat,
|
|
358
403
|
jsonNdjsonTableFormats,
|
|
359
|
-
"json"
|
|
404
|
+
"json",
|
|
405
|
+
{
|
|
406
|
+
json: writeJson(result),
|
|
407
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.items)),
|
|
408
|
+
table: writeText(`${header}\n\n${body}`)
|
|
409
|
+
}
|
|
360
410
|
);
|
|
361
|
-
if (outputFormat === "ndjson") {
|
|
362
|
-
yield* writeJsonStream(Stream.fromIterable(result.items));
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
if (outputFormat === "table") {
|
|
366
|
-
const header = `${result.list.name} (${result.list.purpose}) by ${result.list.creator.handle}`;
|
|
367
|
-
const body = renderListItemsTable(result.items, result.cursor);
|
|
368
|
-
yield* writeText(`${header}\n\n${body}`);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
yield* writeJson(result);
|
|
372
411
|
})
|
|
373
412
|
).pipe(
|
|
374
413
|
Command.withDescription(
|
|
@@ -385,27 +424,26 @@ const blocksCommand = Command.make(
|
|
|
385
424
|
Effect.gen(function* () {
|
|
386
425
|
const appConfig = yield* AppConfigService;
|
|
387
426
|
const client = yield* BskyClient;
|
|
388
|
-
const
|
|
427
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
389
428
|
const options = {
|
|
390
|
-
...(
|
|
391
|
-
...(
|
|
429
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
430
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
392
431
|
};
|
|
393
432
|
const result = yield* client.getBlocks(options);
|
|
394
|
-
|
|
433
|
+
yield* emitWithFormat(
|
|
395
434
|
format,
|
|
396
435
|
appConfig.outputFormat,
|
|
397
436
|
jsonNdjsonTableFormats,
|
|
398
|
-
"json"
|
|
437
|
+
"json",
|
|
438
|
+
{
|
|
439
|
+
json: writeJson(result),
|
|
440
|
+
ndjson:
|
|
441
|
+
result.blocks.length === 0
|
|
442
|
+
? writeText("[]")
|
|
443
|
+
: writeJsonStream(Stream.fromIterable(result.blocks)),
|
|
444
|
+
table: writeText(renderProfileTable(result.blocks, result.cursor))
|
|
445
|
+
}
|
|
399
446
|
);
|
|
400
|
-
if (outputFormat === "ndjson") {
|
|
401
|
-
yield* writeJsonStream(Stream.fromIterable(result.blocks));
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
if (outputFormat === "table") {
|
|
405
|
-
yield* writeText(renderProfileTable(result.blocks, result.cursor));
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
yield* writeJson(result);
|
|
409
447
|
})
|
|
410
448
|
).pipe(
|
|
411
449
|
Command.withDescription(
|
|
@@ -422,27 +460,26 @@ const mutesCommand = Command.make(
|
|
|
422
460
|
Effect.gen(function* () {
|
|
423
461
|
const appConfig = yield* AppConfigService;
|
|
424
462
|
const client = yield* BskyClient;
|
|
425
|
-
const
|
|
463
|
+
const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
|
|
426
464
|
const options = {
|
|
427
|
-
...(
|
|
428
|
-
...(
|
|
465
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
466
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
429
467
|
};
|
|
430
468
|
const result = yield* client.getMutes(options);
|
|
431
|
-
|
|
469
|
+
yield* emitWithFormat(
|
|
432
470
|
format,
|
|
433
471
|
appConfig.outputFormat,
|
|
434
472
|
jsonNdjsonTableFormats,
|
|
435
|
-
"json"
|
|
473
|
+
"json",
|
|
474
|
+
{
|
|
475
|
+
json: writeJson(result),
|
|
476
|
+
ndjson:
|
|
477
|
+
result.mutes.length === 0
|
|
478
|
+
? writeText("[]")
|
|
479
|
+
: writeJsonStream(Stream.fromIterable(result.mutes)),
|
|
480
|
+
table: writeText(renderProfileTable(result.mutes, result.cursor))
|
|
481
|
+
}
|
|
436
482
|
);
|
|
437
|
-
if (outputFormat === "ndjson") {
|
|
438
|
-
yield* writeJsonStream(Stream.fromIterable(result.mutes));
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
if (outputFormat === "table") {
|
|
442
|
-
yield* writeText(renderProfileTable(result.mutes, result.cursor));
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
yield* writeJson(result);
|
|
446
483
|
})
|
|
447
484
|
).pipe(
|
|
448
485
|
Command.withDescription(
|
package/src/cli/input.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SystemError, type PlatformError } from "@effect/platform/Error";
|
|
2
|
+
import { Context, Effect, Layer, Stream } from "effect";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
|
|
5
|
+
export interface CliInputService {
|
|
6
|
+
readonly lines: Stream.Stream<string, PlatformError>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const makeLines = () =>
|
|
10
|
+
Stream.unwrapScoped(
|
|
11
|
+
Effect.acquireRelease(
|
|
12
|
+
Effect.sync(() =>
|
|
13
|
+
createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
crlfDelay: Infinity
|
|
16
|
+
})
|
|
17
|
+
),
|
|
18
|
+
(rl) => Effect.sync(() => rl.close())
|
|
19
|
+
).pipe(
|
|
20
|
+
Effect.map((rl) =>
|
|
21
|
+
Stream.fromAsyncIterable(
|
|
22
|
+
rl,
|
|
23
|
+
(cause) =>
|
|
24
|
+
new SystemError({
|
|
25
|
+
module: "Stream",
|
|
26
|
+
method: "stdin",
|
|
27
|
+
reason: "Unknown",
|
|
28
|
+
cause
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export class CliInput extends Context.Tag("@skygent/CliInput")<
|
|
36
|
+
CliInput,
|
|
37
|
+
CliInputService
|
|
38
|
+
>() {
|
|
39
|
+
static readonly layer = Layer.succeed(
|
|
40
|
+
CliInput,
|
|
41
|
+
CliInput.of({
|
|
42
|
+
lines: makeLines()
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
}
|
package/src/cli/layers.ts
CHANGED
|
@@ -18,10 +18,12 @@ import { SyncCheckpointStore } from "../services/sync-checkpoint-store.js";
|
|
|
18
18
|
import { SyncReporter } from "../services/sync-reporter.js";
|
|
19
19
|
import { SyncSettings } from "../services/sync-settings.js";
|
|
20
20
|
import { StoreCleaner } from "../services/store-cleaner.js";
|
|
21
|
+
import { StoreRenamer } from "../services/store-renamer.js";
|
|
21
22
|
import { LinkValidator } from "../services/link-validator.js";
|
|
22
23
|
import { TrendingTopics } from "../services/trending-topics.js";
|
|
23
24
|
import { ResourceMonitor } from "../services/resource-monitor.js";
|
|
24
25
|
import { CliOutput } from "./output.js";
|
|
26
|
+
import { CliInput } from "./input.js";
|
|
25
27
|
import { DerivationEngine } from "../services/derivation-engine.js";
|
|
26
28
|
import { DerivationValidator } from "../services/derivation-validator.js";
|
|
27
29
|
import { DerivationSettings } from "../services/derivation-settings.js";
|
|
@@ -114,6 +116,12 @@ const viewCheckpointLayer = ViewCheckpointStore.layer.pipe(
|
|
|
114
116
|
const lineageLayer = LineageStore.layer.pipe(
|
|
115
117
|
Layer.provideMerge(storageLayer)
|
|
116
118
|
);
|
|
119
|
+
const storeRenamerLayer = StoreRenamer.layer.pipe(
|
|
120
|
+
Layer.provideMerge(appConfigLayer),
|
|
121
|
+
Layer.provideMerge(managerLayer),
|
|
122
|
+
Layer.provideMerge(storeDbLayer),
|
|
123
|
+
Layer.provideMerge(lineageLayer)
|
|
124
|
+
);
|
|
117
125
|
const compilerLayer = FilterCompiler.layer;
|
|
118
126
|
const postParserLayer = PostParser.layer;
|
|
119
127
|
const derivationEngineLayer = DerivationEngine.layer.pipe(
|
|
@@ -157,6 +165,7 @@ export const CliLive = Layer.mergeAll(
|
|
|
157
165
|
appConfigLayer,
|
|
158
166
|
filterSettingsLayer,
|
|
159
167
|
credentialLayer,
|
|
168
|
+
CliInput.layer,
|
|
160
169
|
CliOutput.layer,
|
|
161
170
|
resourceMonitorLayer,
|
|
162
171
|
managerLayer,
|
|
@@ -164,6 +173,7 @@ export const CliLive = Layer.mergeAll(
|
|
|
164
173
|
indexLayer,
|
|
165
174
|
eventLogLayer,
|
|
166
175
|
cleanerLayer,
|
|
176
|
+
storeRenamerLayer,
|
|
167
177
|
syncLayer,
|
|
168
178
|
checkpointLayer,
|
|
169
179
|
viewCheckpointLayer,
|
package/src/cli/logging.ts
CHANGED
|
@@ -132,5 +132,13 @@ export const makeSyncReporter = (
|
|
|
132
132
|
{ discard: true }
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
|
+
}).pipe(Effect.orElseSucceed(() => undefined)),
|
|
136
|
+
warn: (message, data) =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
const format = yield* resolveLogFormat;
|
|
139
|
+
yield* logEventWith(output, format, "WARN", {
|
|
140
|
+
message,
|
|
141
|
+
...data
|
|
142
|
+
});
|
|
135
143
|
}).pipe(Effect.orElseSucceed(() => undefined))
|
|
136
144
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import type { OutputFormat } from "../domain/config.js";
|
|
3
|
+
import { resolveOutputFormat } from "./output-format.js";
|
|
4
|
+
|
|
5
|
+
export const emitWithFormat = <T extends string, E, R>(
|
|
6
|
+
format: Option.Option<T>,
|
|
7
|
+
configFormat: OutputFormat,
|
|
8
|
+
supported: readonly T[],
|
|
9
|
+
fallback: T,
|
|
10
|
+
handlers: { readonly [K in T]: Effect.Effect<unknown, E, R> }
|
|
11
|
+
): Effect.Effect<unknown, E, R> => {
|
|
12
|
+
const resolved = resolveOutputFormat(format, configFormat, supported, fallback);
|
|
13
|
+
return handlers[resolved];
|
|
14
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Options } from "@effect/cli";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { parseLimit } from "./shared-options.js";
|
|
4
|
+
|
|
5
|
+
export const limitOption = Options.integer("limit").pipe(Options.optional);
|
|
6
|
+
export const cursorOption = Options.text("cursor").pipe(Options.optional);
|
|
7
|
+
|
|
8
|
+
export const parsePagination = (
|
|
9
|
+
limit: Option.Option<number>,
|
|
10
|
+
cursor: Option.Option<string>
|
|
11
|
+
) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const parsedLimit = yield* parseLimit(limit);
|
|
14
|
+
return {
|
|
15
|
+
limit: Option.getOrUndefined(parsedLimit),
|
|
16
|
+
cursor: Option.getOrUndefined(cursor)
|
|
17
|
+
};
|
|
18
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Safely parse JSON, returning `undefined` on failure. */
|
|
2
|
+
export const safeParseJson = (raw: string): unknown => {
|
|
3
|
+
try {
|
|
4
|
+
return JSON.parse(raw);
|
|
5
|
+
} catch {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Format schema issues into an array of "path: message" strings. */
|
|
11
|
+
export const issueDetails = (
|
|
12
|
+
issues: ReadonlyArray<{ readonly path: ReadonlyArray<unknown>; readonly message: string }>
|
|
13
|
+
) =>
|
|
14
|
+
issues.map((issue) => {
|
|
15
|
+
const path =
|
|
16
|
+
issue.path.length > 0 ? issue.path.map((entry) => String(entry)).join(".") : "value";
|
|
17
|
+
return `${path}: ${issue.message}`;
|
|
18
|
+
});
|