@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.
Files changed (49) hide show
  1. package/README.md +269 -31
  2. package/index.ts +18 -3
  3. package/package.json +1 -1
  4. package/src/cli/app.ts +4 -2
  5. package/src/cli/config.ts +20 -3
  6. package/src/cli/doc/table-renderers.ts +29 -0
  7. package/src/cli/doc/thread.ts +2 -4
  8. package/src/cli/exit-codes.ts +2 -0
  9. package/src/cli/feed.ts +35 -55
  10. package/src/cli/filter-dsl.ts +146 -11
  11. package/src/cli/filter-errors.ts +9 -3
  12. package/src/cli/filter-help.ts +7 -0
  13. package/src/cli/filter-input.ts +3 -2
  14. package/src/cli/filter.ts +84 -4
  15. package/src/cli/graph.ts +193 -156
  16. package/src/cli/input.ts +45 -0
  17. package/src/cli/layers.ts +10 -0
  18. package/src/cli/logging.ts +8 -0
  19. package/src/cli/output-render.ts +14 -0
  20. package/src/cli/pagination.ts +18 -0
  21. package/src/cli/parse-errors.ts +18 -0
  22. package/src/cli/pipe.ts +157 -0
  23. package/src/cli/post.ts +43 -66
  24. package/src/cli/query.ts +349 -74
  25. package/src/cli/search.ts +92 -118
  26. package/src/cli/shared.ts +0 -19
  27. package/src/cli/store-errors.ts +24 -13
  28. package/src/cli/store-tree.ts +6 -4
  29. package/src/cli/store.ts +35 -2
  30. package/src/cli/stream-merge.ts +105 -0
  31. package/src/cli/sync-factory.ts +28 -3
  32. package/src/cli/sync.ts +16 -18
  33. package/src/cli/thread-options.ts +33 -0
  34. package/src/cli/time.ts +171 -0
  35. package/src/cli/view-thread.ts +12 -18
  36. package/src/cli/watch.ts +61 -19
  37. package/src/domain/errors.ts +6 -1
  38. package/src/domain/format.ts +21 -0
  39. package/src/domain/order.ts +24 -0
  40. package/src/graph/relationships.ts +129 -0
  41. package/src/services/jetstream-sync.ts +4 -4
  42. package/src/services/lineage-store.ts +15 -1
  43. package/src/services/store-commit.ts +60 -0
  44. package/src/services/store-manager.ts +69 -2
  45. package/src/services/store-renamer.ts +286 -0
  46. package/src/services/store-stats.ts +7 -5
  47. package/src/services/sync-engine.ts +136 -85
  48. package/src/services/sync-reporter.ts +3 -1
  49. 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 type { ListItemView, ListView, ProfileView, RelationshipView } from "../domain/bsky.js";
7
- import { decodeActor, parseLimit } from "./shared-options.js";
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 { jsonNdjsonTableFormats, resolveOutputFormat } from "./output-format.js";
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 = Options.integer("limit").pipe(
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 = Options.text("cursor").pipe(
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
- const renderProfileTable = (
47
- actors: ReadonlyArray<ProfileView>,
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 = (relationships: ReadonlyArray<RelationshipView>) => {
75
- const rows = relationships.map((rel) => {
76
- if ("notFound" in rel && rel.notFound) {
77
- return [String(rel.actor), "not-found", "", "", "", "", ""];
78
- }
79
- const relationship = rel as RelationshipView & {
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
- relationship.did,
90
- relationship.following ? "yes" : "",
91
- relationship.followedBy ? "yes" : "",
92
- relationship.blocking ? "yes" : "",
93
- relationship.blockedBy ? "yes" : "",
94
- relationship.blockingByList ? "yes" : "",
95
- relationship.blockedByList ? "yes" : ""
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 parsedLimit = yield* parseLimit(limit);
126
+ const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
131
127
  const options = {
132
- ...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
133
- ...(Option.isSome(cursor) ? { cursor: cursor.value } : {})
128
+ ...(limitValue !== undefined ? { limit: limitValue } : {}),
129
+ ...(cursorValue !== undefined ? { cursor: cursorValue } : {})
134
130
  };
135
131
  const result = yield* client.getFollowers(resolvedActor, options);
136
- const outputFormat = resolveOutputFormat(
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 parsedLimit = yield* parseLimit(limit);
161
+ const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
170
162
  const options = {
171
- ...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
172
- ...(Option.isSome(cursor) ? { cursor: cursor.value } : {})
163
+ ...(limitValue !== undefined ? { limit: limitValue } : {}),
164
+ ...(cursorValue !== undefined ? { cursor: cursorValue } : {})
173
165
  };
174
166
  const result = yield* client.getFollows(resolvedActor, options);
175
- const outputFormat = resolveOutputFormat(
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 parsedLimit = yield* parseLimit(limit);
196
+ const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
209
197
  const options = {
210
- ...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
211
- ...(Option.isSome(cursor) ? { cursor: cursor.value } : {})
198
+ ...(limitValue !== undefined ? { limit: limitValue } : {}),
199
+ ...(cursorValue !== undefined ? { cursor: cursorValue } : {})
212
200
  };
213
201
  const result = yield* client.getKnownFollowers(resolvedActor, options);
214
- const outputFormat = resolveOutputFormat(
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 outputFormat = resolveOutputFormat(
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 parsedLimit = yield* parseLimit(limit);
357
+ const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
311
358
  const options = {
312
- ...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
313
- ...(Option.isSome(cursor) ? { cursor: cursor.value } : {}),
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
- const outputFormat = resolveOutputFormat(
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 parsedLimit = yield* parseLimit(limit);
392
+ const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
350
393
  const options = {
351
- ...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
352
- ...(Option.isSome(cursor) ? { cursor: cursor.value } : {})
394
+ ...(limitValue !== undefined ? { limit: limitValue } : {}),
395
+ ...(cursorValue !== undefined ? { cursor: cursorValue } : {})
353
396
  };
354
397
  const result = yield* client.getList(uri, options);
355
- const outputFormat = resolveOutputFormat(
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 parsedLimit = yield* parseLimit(limit);
427
+ const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
389
428
  const options = {
390
- ...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
391
- ...(Option.isSome(cursor) ? { cursor: cursor.value } : {})
429
+ ...(limitValue !== undefined ? { limit: limitValue } : {}),
430
+ ...(cursorValue !== undefined ? { cursor: cursorValue } : {})
392
431
  };
393
432
  const result = yield* client.getBlocks(options);
394
- const outputFormat = resolveOutputFormat(
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 parsedLimit = yield* parseLimit(limit);
463
+ const { limit: limitValue, cursor: cursorValue } = yield* parsePagination(limit, cursor);
426
464
  const options = {
427
- ...(Option.isSome(parsedLimit) ? { limit: parsedLimit.value } : {}),
428
- ...(Option.isSome(cursor) ? { cursor: cursor.value } : {})
465
+ ...(limitValue !== undefined ? { limit: limitValue } : {}),
466
+ ...(cursorValue !== undefined ? { cursor: cursorValue } : {})
429
467
  };
430
468
  const result = yield* client.getMutes(options);
431
- const outputFormat = resolveOutputFormat(
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(
@@ -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,
@@ -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
+ });