@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/post.ts
CHANGED
|
@@ -8,12 +8,16 @@ import { renderPostsTable } from "../domain/format.js";
|
|
|
8
8
|
import { AppConfigService } from "../services/app-config.js";
|
|
9
9
|
import { withExamples } from "./help.js";
|
|
10
10
|
import { postUriArg } from "./shared-options.js";
|
|
11
|
+
import { PostCid } from "../domain/primitives.js";
|
|
11
12
|
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
12
13
|
import { renderTableLegacy } from "./doc/table.js";
|
|
13
14
|
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 { CliInputError } from "./errors.js";
|
|
19
|
+
import { CliPreferences } from "./preferences.js";
|
|
20
|
+
import { compactPost, compactPostLike, compactProfileView } from "./compact-output.js";
|
|
17
21
|
|
|
18
22
|
const limitOption = baseLimitOption.pipe(
|
|
19
23
|
Options.withDescription("Maximum number of results")
|
|
@@ -28,7 +32,19 @@ const formatOption = Options.choice("format", jsonNdjsonTableFormats).pipe(
|
|
|
28
32
|
Options.optional
|
|
29
33
|
);
|
|
30
34
|
|
|
35
|
+
const ensureSupportedFormat = (
|
|
36
|
+
format: Option.Option<typeof jsonNdjsonTableFormats[number]>,
|
|
37
|
+
configFormat: string
|
|
38
|
+
) =>
|
|
39
|
+
Option.isNone(format) && configFormat === "markdown"
|
|
40
|
+
? CliInputError.make({
|
|
41
|
+
message: 'Output format "markdown" is not supported for post commands. Use --format json|ndjson|table.',
|
|
42
|
+
cause: { format: configFormat }
|
|
43
|
+
})
|
|
44
|
+
: Effect.void;
|
|
45
|
+
|
|
31
46
|
const cidOption = Options.text("cid").pipe(
|
|
47
|
+
Options.withSchema(PostCid),
|
|
32
48
|
Options.withDescription("Filter engagement by specific record CID"),
|
|
33
49
|
Options.optional
|
|
34
50
|
);
|
|
@@ -60,21 +76,30 @@ const likesCommand = Command.make(
|
|
|
60
76
|
({ uri, cid, limit, cursor, format }) =>
|
|
61
77
|
Effect.gen(function* () {
|
|
62
78
|
const appConfig = yield* AppConfigService;
|
|
79
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
80
|
+
const preferences = yield* CliPreferences;
|
|
63
81
|
const client = yield* BskyClient;
|
|
64
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
82
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
65
83
|
const result = yield* client.getLikes(uri, {
|
|
66
84
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
67
85
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
68
86
|
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
69
87
|
});
|
|
88
|
+
const likes = preferences.compact
|
|
89
|
+
? result.likes.map(compactPostLike)
|
|
90
|
+
: result.likes;
|
|
91
|
+
const payload = result.cursor ? { likes, cursor: result.cursor } : { likes };
|
|
92
|
+
const likesStream = Stream.fromIterable(
|
|
93
|
+
likes as ReadonlyArray<unknown>
|
|
94
|
+
);
|
|
70
95
|
yield* emitWithFormat(
|
|
71
96
|
format,
|
|
72
97
|
appConfig.outputFormat,
|
|
73
98
|
jsonNdjsonTableFormats,
|
|
74
99
|
"json",
|
|
75
100
|
{
|
|
76
|
-
json: writeJson(
|
|
77
|
-
ndjson: writeJsonStream(
|
|
101
|
+
json: writeJson(payload),
|
|
102
|
+
ndjson: writeJsonStream(likesStream),
|
|
78
103
|
table: writeText(renderLikesTable(result.likes, result.cursor))
|
|
79
104
|
}
|
|
80
105
|
);
|
|
@@ -94,21 +119,32 @@ const repostedByCommand = Command.make(
|
|
|
94
119
|
({ uri, cid, limit, cursor, format }) =>
|
|
95
120
|
Effect.gen(function* () {
|
|
96
121
|
const appConfig = yield* AppConfigService;
|
|
122
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
123
|
+
const preferences = yield* CliPreferences;
|
|
97
124
|
const client = yield* BskyClient;
|
|
98
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
125
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
99
126
|
const result = yield* client.getRepostedBy(uri, {
|
|
100
127
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
101
128
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
102
129
|
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
103
130
|
});
|
|
131
|
+
const repostedBy = preferences.compact
|
|
132
|
+
? result.repostedBy.map(compactProfileView)
|
|
133
|
+
: result.repostedBy;
|
|
134
|
+
const payload = result.cursor
|
|
135
|
+
? { repostedBy, cursor: result.cursor }
|
|
136
|
+
: { repostedBy };
|
|
137
|
+
const repostedStream = Stream.fromIterable(
|
|
138
|
+
repostedBy as ReadonlyArray<unknown>
|
|
139
|
+
);
|
|
104
140
|
yield* emitWithFormat(
|
|
105
141
|
format,
|
|
106
142
|
appConfig.outputFormat,
|
|
107
143
|
jsonNdjsonTableFormats,
|
|
108
144
|
"json",
|
|
109
145
|
{
|
|
110
|
-
json: writeJson(
|
|
111
|
-
ndjson: writeJsonStream(
|
|
146
|
+
json: writeJson(payload),
|
|
147
|
+
ndjson: writeJsonStream(repostedStream),
|
|
112
148
|
table: writeText(renderProfileTable(result.repostedBy, result.cursor))
|
|
113
149
|
}
|
|
114
150
|
);
|
|
@@ -127,26 +163,35 @@ const quotesCommand = Command.make(
|
|
|
127
163
|
({ uri, cid, limit, cursor, format }) =>
|
|
128
164
|
Effect.gen(function* () {
|
|
129
165
|
const appConfig = yield* AppConfigService;
|
|
166
|
+
yield* ensureSupportedFormat(format, appConfig.outputFormat);
|
|
167
|
+
const preferences = yield* CliPreferences;
|
|
130
168
|
const client = yield* BskyClient;
|
|
131
169
|
const parser = yield* PostParser;
|
|
132
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
170
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
133
171
|
const result = yield* client.getQuotes(uri, {
|
|
134
172
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
135
173
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
136
174
|
...(Option.isSome(cid) ? { cid: cid.value } : {})
|
|
137
175
|
});
|
|
138
176
|
const posts = yield* parseRawPosts(parser, result.posts);
|
|
177
|
+
const compactPosts = preferences.compact
|
|
178
|
+
? posts.map(compactPost)
|
|
179
|
+
: posts;
|
|
180
|
+
const payload = {
|
|
181
|
+
...result,
|
|
182
|
+
posts: compactPosts
|
|
183
|
+
};
|
|
184
|
+
const postStream = Stream.fromIterable(
|
|
185
|
+
compactPosts as ReadonlyArray<unknown>
|
|
186
|
+
);
|
|
139
187
|
yield* emitWithFormat(
|
|
140
188
|
format,
|
|
141
189
|
appConfig.outputFormat,
|
|
142
190
|
jsonNdjsonTableFormats,
|
|
143
191
|
"json",
|
|
144
192
|
{
|
|
145
|
-
json: writeJson(
|
|
146
|
-
|
|
147
|
-
posts
|
|
148
|
-
}),
|
|
149
|
-
ndjson: writeJsonStream(Stream.fromIterable(posts)),
|
|
193
|
+
json: writeJson(payload),
|
|
194
|
+
ndjson: writeJsonStream(postStream),
|
|
150
195
|
table: writeText(renderPostsTable(posts))
|
|
151
196
|
}
|
|
152
197
|
);
|
package/src/cli/query-fields.ts
CHANGED
|
@@ -7,6 +7,11 @@ type FieldSelector = {
|
|
|
7
7
|
readonly raw: string;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export type FieldSelectorsResolution = {
|
|
11
|
+
readonly selectors: Option.Option<ReadonlyArray<FieldSelector>>;
|
|
12
|
+
readonly source: "implicit" | "explicit";
|
|
13
|
+
};
|
|
14
|
+
|
|
10
15
|
const fieldPresets: Record<string, ReadonlyArray<string>> = {
|
|
11
16
|
minimal: ["uri", "author", "text", "createdAt"],
|
|
12
17
|
social: ["uri", "author", "text", "metrics", "hashtags"],
|
|
@@ -162,11 +167,16 @@ export const parseFieldSelectors = (
|
|
|
162
167
|
export const resolveFieldSelectors = (
|
|
163
168
|
fields: Option.Option<string>,
|
|
164
169
|
compact: boolean
|
|
165
|
-
): Effect.Effect<
|
|
170
|
+
): Effect.Effect<FieldSelectorsResolution, CliInputError> =>
|
|
166
171
|
Option.match(fields, {
|
|
167
172
|
onNone: () =>
|
|
168
|
-
compact ? parseFieldSelectors("@minimal") : Effect.succeed(Option.none())
|
|
169
|
-
|
|
173
|
+
(compact ? parseFieldSelectors("@minimal") : Effect.succeed(Option.none())).pipe(
|
|
174
|
+
Effect.map((selectors) => ({ selectors, source: "implicit" as const }))
|
|
175
|
+
),
|
|
176
|
+
onSome: (raw) =>
|
|
177
|
+
parseFieldSelectors(raw).pipe(
|
|
178
|
+
Effect.map((selectors) => ({ selectors, source: "explicit" as const }))
|
|
179
|
+
)
|
|
170
180
|
});
|
|
171
181
|
|
|
172
182
|
const getPathValue = (source: unknown, path: ReadonlyArray<string>): unknown => {
|
package/src/cli/query.ts
CHANGED
|
@@ -34,6 +34,8 @@ import { StoreNotFound } from "../domain/errors.js";
|
|
|
34
34
|
import { StorePostOrder } from "../domain/order.js";
|
|
35
35
|
import { formatSchemaError } from "./shared.js";
|
|
36
36
|
import { mergeOrderedStreams } from "./stream-merge.js";
|
|
37
|
+
import { queryOutputFormats, resolveOutputFormat } from "./output-format.js";
|
|
38
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
37
39
|
|
|
38
40
|
const storeNamesArg = Args.text({ name: "store" }).pipe(
|
|
39
41
|
Args.repeated,
|
|
@@ -56,10 +58,12 @@ const untilOption = Options.text("until").pipe(
|
|
|
56
58
|
Options.optional
|
|
57
59
|
);
|
|
58
60
|
const limitOption = Options.integer("limit").pipe(
|
|
61
|
+
Options.withSchema(PositiveInt),
|
|
59
62
|
Options.withDescription("Maximum number of posts to return"),
|
|
60
63
|
Options.optional
|
|
61
64
|
);
|
|
62
65
|
const scanLimitOption = Options.integer("scan-limit").pipe(
|
|
66
|
+
Options.withSchema(PositiveInt),
|
|
63
67
|
Options.withDescription("Maximum rows to scan before filtering (advanced)"),
|
|
64
68
|
Options.optional
|
|
65
69
|
);
|
|
@@ -70,15 +74,7 @@ const sortOption = Options.choice("sort", ["asc", "desc"]).pipe(
|
|
|
70
74
|
const newestFirstOption = Options.boolean("newest-first").pipe(
|
|
71
75
|
Options.withDescription("Sort newest posts first (alias for --sort desc)")
|
|
72
76
|
);
|
|
73
|
-
const formatOption = Options.choice("format",
|
|
74
|
-
"json",
|
|
75
|
-
"ndjson",
|
|
76
|
-
"markdown",
|
|
77
|
-
"table",
|
|
78
|
-
"compact",
|
|
79
|
-
"card",
|
|
80
|
-
"thread"
|
|
81
|
-
]).pipe(
|
|
77
|
+
const formatOption = Options.choice("format", queryOutputFormats).pipe(
|
|
82
78
|
Options.optional,
|
|
83
79
|
Options.withDescription("Output format (default: config output format)")
|
|
84
80
|
);
|
|
@@ -89,6 +85,7 @@ const ansiOption = Options.boolean("ansi").pipe(
|
|
|
89
85
|
Options.withDescription("Enable ANSI colors in output")
|
|
90
86
|
);
|
|
91
87
|
const widthOption = Options.integer("width").pipe(
|
|
88
|
+
Options.withSchema(PositiveInt),
|
|
92
89
|
Options.withDescription("Line width for terminal output"),
|
|
93
90
|
Options.optional
|
|
94
91
|
);
|
|
@@ -284,8 +281,11 @@ export const queryCommand = Command.make(
|
|
|
284
281
|
const parsedRange = yield* parseRangeOptions(range, since, until);
|
|
285
282
|
const parsedFilter = yield* parseOptionalFilterExpr(filter, filterJson);
|
|
286
283
|
const expr = Option.getOrElse(parsedFilter, () => all());
|
|
287
|
-
const outputFormat =
|
|
288
|
-
|
|
284
|
+
const outputFormat = resolveOutputFormat(
|
|
285
|
+
format,
|
|
286
|
+
appConfig.outputFormat,
|
|
287
|
+
queryOutputFormats,
|
|
288
|
+
"json"
|
|
289
289
|
);
|
|
290
290
|
if (multiStore && outputFormat === "thread") {
|
|
291
291
|
return yield* CliInputError.make({
|
|
@@ -294,19 +294,20 @@ export const queryCommand = Command.make(
|
|
|
294
294
|
});
|
|
295
295
|
}
|
|
296
296
|
const compact = preferences.compact;
|
|
297
|
-
const selectorsOption
|
|
297
|
+
const { selectors: selectorsOption, source: selectorsSource } =
|
|
298
|
+
yield* resolveFieldSelectors(fields, compact);
|
|
298
299
|
const project = (post: Post) =>
|
|
299
300
|
Option.match(selectorsOption, {
|
|
300
301
|
onNone: () => post,
|
|
301
302
|
onSome: (selectors) => projectFields(post, selectors)
|
|
302
303
|
});
|
|
303
|
-
if (
|
|
304
|
+
if (selectorsSource === "explicit" && outputFormat !== "json" && outputFormat !== "ndjson") {
|
|
304
305
|
return yield* CliInputError.make({
|
|
305
306
|
message: "--fields is only supported with json or ndjson output.",
|
|
306
307
|
cause: { format: outputFormat }
|
|
307
308
|
});
|
|
308
309
|
}
|
|
309
|
-
if (count &&
|
|
310
|
+
if (count && selectorsSource === "explicit") {
|
|
310
311
|
return yield* CliInputError.make({
|
|
311
312
|
message: "--count cannot be combined with --fields.",
|
|
312
313
|
cause: { count, fields }
|
|
@@ -315,18 +316,6 @@ export const queryCommand = Command.make(
|
|
|
315
316
|
|
|
316
317
|
const w = Option.getOrUndefined(width);
|
|
317
318
|
|
|
318
|
-
if (Option.isSome(limit) && limit.value <= 0) {
|
|
319
|
-
return yield* CliInputError.make({
|
|
320
|
-
message: "--limit must be a positive integer.",
|
|
321
|
-
cause: { limit: limit.value }
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
if (Option.isSome(scanLimit) && scanLimit.value <= 0) {
|
|
325
|
-
return yield* CliInputError.make({
|
|
326
|
-
message: "--scan-limit must be a positive integer.",
|
|
327
|
-
cause: { scanLimit: scanLimit.value }
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
319
|
const sortValue = Option.getOrUndefined(sort);
|
|
331
320
|
const order =
|
|
332
321
|
newestFirst
|
|
@@ -366,13 +355,12 @@ export const queryCommand = Command.make(
|
|
|
366
355
|
}
|
|
367
356
|
|
|
368
357
|
if (
|
|
369
|
-
hasFilter &&
|
|
370
358
|
Option.isNone(limit) &&
|
|
371
|
-
(outputFormat === "thread" || outputFormat === "table")
|
|
359
|
+
(outputFormat === "thread" || outputFormat === "table" || outputFormat === "markdown")
|
|
372
360
|
) {
|
|
373
361
|
yield* output
|
|
374
362
|
.writeStderr(
|
|
375
|
-
"Warning: thread
|
|
363
|
+
"Warning: table/markdown/thread output collects all matched posts in memory. Consider adding --limit."
|
|
376
364
|
)
|
|
377
365
|
.pipe(Effect.catchAll(() => Effect.void));
|
|
378
366
|
}
|
|
@@ -538,16 +526,7 @@ export const queryCommand = Command.make(
|
|
|
538
526
|
}
|
|
539
527
|
|
|
540
528
|
if (outputFormat === "ndjson") {
|
|
541
|
-
|
|
542
|
-
const counted = stream.pipe(
|
|
543
|
-
Stream.map(toOutput),
|
|
544
|
-
Stream.tap(() => Ref.update(countRef, (count) => count + 1))
|
|
545
|
-
);
|
|
546
|
-
yield* writeJsonStream(counted);
|
|
547
|
-
const count = yield* Ref.get(countRef);
|
|
548
|
-
if (count === 0) {
|
|
549
|
-
yield* writeText("[]");
|
|
550
|
-
}
|
|
529
|
+
yield* writeJsonStream(stream.pipe(Stream.map(toOutput)));
|
|
551
530
|
yield* warnIfScanLimitReached();
|
|
552
531
|
return;
|
|
553
532
|
}
|
package/src/cli/search.ts
CHANGED
|
@@ -6,17 +6,15 @@ import { PostParser } from "../services/post-parser.js";
|
|
|
6
6
|
import { StoreIndex } from "../services/store-index.js";
|
|
7
7
|
import { renderPostsTable } from "../domain/format.js";
|
|
8
8
|
import { AppConfigService } from "../services/app-config.js";
|
|
9
|
-
import { StoreName } from "../domain/primitives.js";
|
|
9
|
+
import { ActorId, StoreName } from "../domain/primitives.js";
|
|
10
10
|
import { storeOptions } from "./store.js";
|
|
11
11
|
import { withExamples } from "./help.js";
|
|
12
12
|
import { CliInputError } from "./errors.js";
|
|
13
|
-
import { decodeActor } from "./shared-options.js";
|
|
14
13
|
import { formatSchemaError } from "./shared.js";
|
|
15
14
|
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
16
15
|
import { jsonNdjsonTableFormats } from "./output-format.js";
|
|
17
16
|
import { emitWithFormat } from "./output-render.js";
|
|
18
17
|
import { cursorOption as baseCursorOption, limitOption as baseLimitOption, parsePagination } from "./pagination.js";
|
|
19
|
-
import { parseLimit } from "./shared-options.js";
|
|
20
18
|
|
|
21
19
|
const queryArg = Args.text({ name: "query" }).pipe(
|
|
22
20
|
Args.withDescription("Search query string")
|
|
@@ -70,11 +68,13 @@ const untilOption = Options.text("until").pipe(
|
|
|
70
68
|
);
|
|
71
69
|
|
|
72
70
|
const mentionsOption = Options.text("mentions").pipe(
|
|
71
|
+
Options.withSchema(ActorId),
|
|
73
72
|
Options.withDescription("Filter network results by mention (handle or DID)"),
|
|
74
73
|
Options.optional
|
|
75
74
|
);
|
|
76
75
|
|
|
77
76
|
const authorOption = Options.text("author").pipe(
|
|
77
|
+
Options.withSchema(ActorId),
|
|
78
78
|
Options.withDescription("Filter network results by author (handle or DID)"),
|
|
79
79
|
Options.optional
|
|
80
80
|
);
|
|
@@ -135,7 +135,7 @@ const handlesCommand = Command.make(
|
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
137
|
const client = yield* BskyClient;
|
|
138
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
138
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
139
139
|
const options = {
|
|
140
140
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
141
141
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
@@ -171,7 +171,7 @@ const feedsCommand = Command.make(
|
|
|
171
171
|
const appConfig = yield* AppConfigService;
|
|
172
172
|
const queryValue = yield* requireNonEmptyQuery(query);
|
|
173
173
|
const client = yield* BskyClient;
|
|
174
|
-
const { limit: limitValue, cursor: cursorValue } =
|
|
174
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
175
175
|
const options = {
|
|
176
176
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
177
177
|
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
@@ -220,8 +220,7 @@ const postsCommand = Command.make(
|
|
|
220
220
|
Effect.gen(function* () {
|
|
221
221
|
const appConfig = yield* AppConfigService;
|
|
222
222
|
const queryValue = yield* requireNonEmptyQuery(query);
|
|
223
|
-
const
|
|
224
|
-
const limitValue = Option.getOrUndefined(parsedLimit);
|
|
223
|
+
const limitValue = Option.getOrUndefined(limit);
|
|
225
224
|
if (network && Option.isSome(store)) {
|
|
226
225
|
return yield* CliInputError.make({
|
|
227
226
|
message: "--store cannot be used with --network.",
|
|
@@ -287,24 +286,8 @@ const postsCommand = Command.make(
|
|
|
287
286
|
.map((item) => item.trim())
|
|
288
287
|
.filter((item) => item.length > 0)
|
|
289
288
|
});
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
onSome: (value) =>
|
|
293
|
-
Effect.gen(function* () {
|
|
294
|
-
const decoded = yield* decodeActor(value);
|
|
295
|
-
return String(decoded);
|
|
296
|
-
})
|
|
297
|
-
});
|
|
298
|
-
const mentionsValue = Option.match(mentions, {
|
|
299
|
-
onNone: () => Effect.void.pipe(Effect.as(undefined)),
|
|
300
|
-
onSome: (value) =>
|
|
301
|
-
Effect.gen(function* () {
|
|
302
|
-
const decoded = yield* decodeActor(value);
|
|
303
|
-
return String(decoded);
|
|
304
|
-
})
|
|
305
|
-
});
|
|
306
|
-
const parsedAuthor = yield* authorValue;
|
|
307
|
-
const parsedMentions = yield* mentionsValue;
|
|
289
|
+
const parsedAuthor = Option.getOrUndefined(author);
|
|
290
|
+
const parsedMentions = Option.getOrUndefined(mentions);
|
|
308
291
|
const result = yield* client.searchPosts(queryValue, {
|
|
309
292
|
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
310
293
|
...(Option.isSome(cursorValue) ? { cursor: cursorValue.value } : {}),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Args, Options } from "@effect/cli";
|
|
2
|
-
import { Effect,
|
|
3
|
-
import {
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import { ActorId, AtUri, PostUri, StoreName } from "../domain/primitives.js";
|
|
4
4
|
import { filterDslDescription, filterJsonDescription } from "./filter-help.js";
|
|
5
5
|
import { CliInputError } from "./errors.js";
|
|
6
6
|
import { formatSchemaError } from "./shared.js";
|
|
7
|
+
import { NonNegativeInt } from "./option-schemas.js";
|
|
7
8
|
|
|
8
9
|
/** --store option with StoreName schema validation */
|
|
9
10
|
export const storeNameOption = Options.text("store").pipe(
|
|
@@ -52,27 +53,32 @@ export const strictOption = Options.boolean("strict").pipe(
|
|
|
52
53
|
|
|
53
54
|
/** --max-errors option (optional) */
|
|
54
55
|
export const maxErrorsOption = Options.integer("max-errors").pipe(
|
|
56
|
+
Options.withSchema(NonNegativeInt),
|
|
55
57
|
Options.withDescription("Stop after exceeding N errors (default: unlimited)"),
|
|
56
58
|
Options.optional
|
|
57
59
|
);
|
|
58
60
|
|
|
59
61
|
/** Positional arg for feed URI */
|
|
60
62
|
export const feedUriArg = Args.text({ name: "uri" }).pipe(
|
|
63
|
+
Args.withSchema(AtUri),
|
|
61
64
|
Args.withDescription("Bluesky feed URI (at://...)")
|
|
62
65
|
);
|
|
63
66
|
|
|
64
67
|
/** Positional arg for list URI */
|
|
65
68
|
export const listUriArg = Args.text({ name: "uri" }).pipe(
|
|
69
|
+
Args.withSchema(AtUri),
|
|
66
70
|
Args.withDescription("Bluesky list URI (at://...)")
|
|
67
71
|
);
|
|
68
72
|
|
|
69
73
|
/** Positional arg for author handle or DID */
|
|
70
74
|
export const actorArg = Args.text({ name: "actor" }).pipe(
|
|
75
|
+
Args.withSchema(ActorId),
|
|
71
76
|
Args.withDescription("Bluesky handle or DID")
|
|
72
77
|
);
|
|
73
78
|
|
|
74
79
|
/** Positional arg for post URI */
|
|
75
80
|
export const postUriArg = Args.text({ name: "uri" }).pipe(
|
|
81
|
+
Args.withSchema(PostUri),
|
|
76
82
|
Args.withDescription("Bluesky post URI (at://...)")
|
|
77
83
|
);
|
|
78
84
|
|
|
@@ -100,70 +106,12 @@ export const includePinsOption = Options.boolean("include-pins").pipe(
|
|
|
100
106
|
);
|
|
101
107
|
|
|
102
108
|
/** Validate --max-errors value is non-negative */
|
|
103
|
-
export const
|
|
104
|
-
|
|
105
|
-
onNone: () => Effect.succeed(Option.none()),
|
|
106
|
-
onSome: (value) =>
|
|
107
|
-
value < 0
|
|
108
|
-
? Effect.fail(
|
|
109
|
-
CliInputError.make({
|
|
110
|
-
message: "max-errors must be a non-negative integer.",
|
|
111
|
-
cause: value
|
|
112
|
-
})
|
|
113
|
-
)
|
|
114
|
-
: Effect.succeed(Option.some(value))
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
export const decodeActor = (actor: string) => {
|
|
118
|
-
if (actor.startsWith("did:")) {
|
|
119
|
-
return Schema.decodeUnknown(Did)(actor).pipe(
|
|
120
|
-
Effect.mapError((error) =>
|
|
121
|
-
CliInputError.make({
|
|
122
|
-
message: `Invalid DID: ${formatSchemaError(error)}`,
|
|
123
|
-
cause: { actor }
|
|
124
|
-
})
|
|
125
|
-
)
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
return Schema.decodeUnknown(Handle)(actor).pipe(
|
|
109
|
+
export const decodeActor = (actor: string) =>
|
|
110
|
+
Schema.decodeUnknown(ActorId)(actor).pipe(
|
|
129
111
|
Effect.mapError((error) =>
|
|
130
112
|
CliInputError.make({
|
|
131
|
-
message: `Invalid
|
|
113
|
+
message: `Invalid actor: ${formatSchemaError(error)}`,
|
|
132
114
|
cause: { actor }
|
|
133
115
|
})
|
|
134
116
|
)
|
|
135
117
|
);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export const parseLimit = (limit: Option.Option<number>) =>
|
|
139
|
-
Option.match(limit, {
|
|
140
|
-
onNone: () => Effect.succeed(Option.none()),
|
|
141
|
-
onSome: (value) =>
|
|
142
|
-
value <= 0
|
|
143
|
-
? Effect.fail(
|
|
144
|
-
CliInputError.make({
|
|
145
|
-
message: "--limit must be a positive integer.",
|
|
146
|
-
cause: value
|
|
147
|
-
})
|
|
148
|
-
)
|
|
149
|
-
: Effect.succeed(Option.some(value))
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
export const parseBoundedIntOption = (
|
|
153
|
-
value: Option.Option<number>,
|
|
154
|
-
name: string,
|
|
155
|
-
min: number,
|
|
156
|
-
max: number
|
|
157
|
-
) =>
|
|
158
|
-
Option.match(value, {
|
|
159
|
-
onNone: () => Effect.succeed(Option.none()),
|
|
160
|
-
onSome: (raw) =>
|
|
161
|
-
raw < min || raw > max
|
|
162
|
-
? Effect.fail(
|
|
163
|
-
CliInputError.make({
|
|
164
|
-
message: `${name} must be between ${min} and ${max}.`,
|
|
165
|
-
cause: raw
|
|
166
|
-
})
|
|
167
|
-
)
|
|
168
|
-
: Effect.succeed(Option.some(raw))
|
|
169
|
-
});
|
package/src/cli/shared.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { formatSchemaError } from "../services/shared.js";
|
|
1
|
+
export { formatParseError, formatSchemaError } from "../services/shared.js";
|
package/src/cli/store-errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ParseResult } from "effect";
|
|
2
|
-
import { safeParseJson, issueDetails } from "./parse-errors.js";
|
|
2
|
+
import { safeParseJson, issueDetails, findJsonParseIssue, jsonParseTip } from "./parse-errors.js";
|
|
3
3
|
import { formatAgentError } from "./errors.js";
|
|
4
4
|
|
|
5
5
|
const storeConfigExample = {
|
|
@@ -35,20 +35,16 @@ export const formatStoreConfigParseError = (
|
|
|
35
35
|
const received = safeParseJson(raw);
|
|
36
36
|
const receivedValue = received === undefined ? raw : received;
|
|
37
37
|
|
|
38
|
-
const jsonParseIssue = issues
|
|
39
|
-
(issue) =>
|
|
40
|
-
issue._tag === "Transformation" &&
|
|
41
|
-
typeof issue.message === "string" &&
|
|
42
|
-
issue.message.startsWith("JSON Parse error")
|
|
43
|
-
);
|
|
38
|
+
const jsonParseIssue = findJsonParseIssue(issues);
|
|
44
39
|
if (jsonParseIssue) {
|
|
40
|
+
const jsonMessage = jsonParseIssue.message ?? "Invalid JSON input.";
|
|
45
41
|
return formatAgentError({
|
|
46
42
|
error: "StoreConfigJsonParseError",
|
|
47
43
|
message: `Invalid JSON in --config-json. ${storeConfigDocHint}`,
|
|
48
44
|
received: raw,
|
|
49
45
|
details: [
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
jsonMessage,
|
|
47
|
+
jsonParseTip
|
|
52
48
|
],
|
|
53
49
|
expected: storeConfigExample
|
|
54
50
|
});
|
package/src/cli/store.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { StoreStats } from "../services/store-stats.js";
|
|
|
21
21
|
import { withExamples } from "./help.js";
|
|
22
22
|
import { resolveOutputFormat, treeTableJsonFormats } from "./output-format.js";
|
|
23
23
|
import { StoreRenamer } from "../services/store-renamer.js";
|
|
24
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
24
25
|
import {
|
|
25
26
|
buildStoreTreeData,
|
|
26
27
|
renderStoreTree,
|
|
@@ -62,6 +63,7 @@ const treeAnsiOption = Options.boolean("ansi").pipe(
|
|
|
62
63
|
Options.withDescription("Enable ANSI color output for tree format")
|
|
63
64
|
);
|
|
64
65
|
const treeWidthOption = Options.integer("width").pipe(
|
|
66
|
+
Options.withSchema(PositiveInt),
|
|
65
67
|
Options.withDescription("Line width for tree rendering (enables wrapping)"),
|
|
66
68
|
Options.optional
|
|
67
69
|
);
|
|
@@ -248,6 +250,10 @@ export const storeDelete = Command.make(
|
|
|
248
250
|
const cleaner = yield* StoreCleaner;
|
|
249
251
|
const result = yield* cleaner.deleteStore(name);
|
|
250
252
|
if (!result.deleted) {
|
|
253
|
+
if (result.reason === "missing") {
|
|
254
|
+
yield* writeJson(result);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
251
257
|
return yield* CliInputError.make({
|
|
252
258
|
message: `Store "${name}" was not deleted.`,
|
|
253
259
|
cause: result
|
package/src/cli/sync-factory.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Option, Stream } from "effect";
|
|
1
|
+
import { Duration, Effect, Option, Stream } from "effect";
|
|
2
2
|
import { DataSource, SyncResult, WatchConfig } from "../domain/sync.js";
|
|
3
3
|
import { SyncEngine } from "../services/sync-engine.js";
|
|
4
4
|
import { SyncReporter } from "../services/sync-reporter.js";
|
|
@@ -10,7 +10,6 @@ import { CliOutput, writeJson, writeJsonStream } from "./output.js";
|
|
|
10
10
|
import { storeOptions } from "./store.js";
|
|
11
11
|
import { logInfo, logWarn, makeSyncReporter } from "./logging.js";
|
|
12
12
|
import { parseInterval, parseOptionalDuration } from "./interval.js";
|
|
13
|
-
import { CliInputError } from "./errors.js";
|
|
14
13
|
import type { StoreName } from "../domain/primitives.js";
|
|
15
14
|
|
|
16
15
|
/** Common options shared by sync and watch API-based commands */
|
|
@@ -20,6 +19,7 @@ export interface CommonCommandInput {
|
|
|
20
19
|
readonly filterJson: Option.Option<string>;
|
|
21
20
|
readonly quiet: boolean;
|
|
22
21
|
readonly refresh: boolean;
|
|
22
|
+
readonly limit?: Option.Option<number>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Build the command body for a one-shot sync command (timeline, feed, notifications). */
|
|
@@ -47,8 +47,12 @@ export const makeSyncCommandBody = (
|
|
|
47
47
|
store: storeRef.name
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
|
+
const limitValue = Option.getOrUndefined(input.limit ?? Option.none());
|
|
50
51
|
const result = yield* sync
|
|
51
|
-
.sync(makeDataSource(), storeRef, expr, {
|
|
52
|
+
.sync(makeDataSource(), storeRef, expr, {
|
|
53
|
+
policy,
|
|
54
|
+
...(limitValue !== undefined ? { limit: limitValue } : {})
|
|
55
|
+
})
|
|
52
56
|
.pipe(
|
|
53
57
|
Effect.provideService(SyncReporter, makeSyncReporter(input.quiet, monitor, output))
|
|
54
58
|
);
|
|
@@ -66,9 +70,9 @@ export const makeSyncCommandBody = (
|
|
|
66
70
|
|
|
67
71
|
/** Common options for watch API-based commands */
|
|
68
72
|
export interface WatchCommandInput extends CommonCommandInput {
|
|
69
|
-
readonly interval: Option.Option<
|
|
73
|
+
readonly interval: Option.Option<Duration.Duration>;
|
|
70
74
|
readonly maxCycles: Option.Option<number>;
|
|
71
|
-
readonly until: Option.Option<
|
|
75
|
+
readonly until: Option.Option<Duration.Duration>;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/** Build the command body for a watch command (timeline, feed, notifications). */
|
|
@@ -87,20 +91,8 @@ export const makeWatchCommandBody = (
|
|
|
87
91
|
const expr = yield* parseFilterExpr(input.filter, input.filterJson);
|
|
88
92
|
const basePolicy = storeConfig.syncPolicy ?? "dedupe";
|
|
89
93
|
const policy = input.refresh ? "refresh" : basePolicy;
|
|
90
|
-
const parsedInterval =
|
|
91
|
-
const parsedUntil =
|
|
92
|
-
const parsedMaxCycles = yield* Option.match(input.maxCycles, {
|
|
93
|
-
onNone: () => Effect.succeed(Option.none<number>()),
|
|
94
|
-
onSome: (value) =>
|
|
95
|
-
value <= 0
|
|
96
|
-
? Effect.fail(
|
|
97
|
-
CliInputError.make({
|
|
98
|
-
message: "--max-cycles must be a positive integer.",
|
|
99
|
-
cause: { maxCycles: value }
|
|
100
|
-
})
|
|
101
|
-
)
|
|
102
|
-
: Effect.succeed(Option.some(value))
|
|
103
|
-
});
|
|
94
|
+
const parsedInterval = parseInterval(input.interval);
|
|
95
|
+
const parsedUntil = parseOptionalDuration(input.until);
|
|
104
96
|
yield* logInfo("Starting watch", { source: sourceName, store: storeRef.name, ...extraLogFields });
|
|
105
97
|
if (policy === "refresh") {
|
|
106
98
|
yield* logWarn("Refresh mode updates existing posts and may grow the event log.", {
|
|
@@ -122,8 +114,8 @@ export const makeWatchCommandBody = (
|
|
|
122
114
|
Stream.map((event) => event.result),
|
|
123
115
|
Stream.provideService(SyncReporter, makeSyncReporter(input.quiet, monitor, output))
|
|
124
116
|
);
|
|
125
|
-
if (Option.isSome(
|
|
126
|
-
stream = stream.pipe(Stream.take(
|
|
117
|
+
if (Option.isSome(input.maxCycles)) {
|
|
118
|
+
stream = stream.pipe(Stream.take(input.maxCycles.value));
|
|
127
119
|
}
|
|
128
120
|
if (Option.isSome(parsedUntil)) {
|
|
129
121
|
stream = stream.pipe(Stream.interruptWhen(Effect.sleep(parsedUntil.value)));
|