@mepuka/skygent 0.2.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/README.md +269 -31
- package/index.ts +18 -3
- package/package.json +1 -1
- package/src/cli/app.ts +4 -2
- package/src/cli/compact-output.ts +52 -0
- package/src/cli/config.ts +46 -4
- 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 +78 -61
- package/src/cli/filter-dsl.ts +146 -11
- package/src/cli/filter-errors.ts +13 -11
- package/src/cli/filter-help.ts +7 -0
- package/src/cli/filter-input.ts +3 -2
- package/src/cli/filter.ts +83 -5
- package/src/cli/graph.ts +297 -169
- package/src/cli/input.ts +45 -0
- package/src/cli/interval.ts +4 -33
- package/src/cli/jetstream.ts +2 -0
- package/src/cli/layers.ts +10 -0
- package/src/cli/logging.ts +8 -0
- package/src/cli/option-schemas.ts +22 -0
- package/src/cli/output-format.ts +11 -0
- package/src/cli/output-render.ts +14 -0
- package/src/cli/pagination.ts +17 -0
- package/src/cli/parse-errors.ts +30 -0
- package/src/cli/parse.ts +1 -47
- package/src/cli/pipe-input.ts +18 -0
- package/src/cli/pipe.ts +154 -0
- package/src/cli/post.ts +88 -66
- package/src/cli/query-fields.ts +13 -3
- package/src/cli/query.ts +354 -100
- package/src/cli/search.ts +93 -136
- package/src/cli/shared-options.ts +11 -63
- package/src/cli/shared.ts +1 -20
- package/src/cli/store-errors.ts +28 -21
- package/src/cli/store-tree.ts +6 -4
- package/src/cli/store.ts +41 -2
- package/src/cli/stream-merge.ts +105 -0
- package/src/cli/sync-factory.ts +24 -7
- package/src/cli/sync.ts +46 -67
- package/src/cli/thread-options.ts +25 -0
- package/src/cli/time.ts +171 -0
- package/src/cli/view-thread.ts +29 -32
- package/src/cli/watch.ts +55 -26
- package/src/domain/errors.ts +6 -1
- package/src/domain/format.ts +21 -0
- package/src/domain/order.ts +24 -0
- package/src/domain/primitives.ts +20 -3
- package/src/graph/relationships.ts +129 -0
- package/src/services/bsky-client.ts +11 -5
- package/src/services/jetstream-sync.ts +4 -4
- package/src/services/lineage-store.ts +15 -1
- package/src/services/shared.ts +48 -1
- package/src/services/store-cleaner.ts +5 -2
- package/src/services/store-commit.ts +60 -0
- package/src/services/store-manager.ts +69 -2
- package/src/services/store-renamer.ts +288 -0
- package/src/services/store-stats.ts +7 -5
- package/src/services/sync-engine.ts +149 -89
- package/src/services/sync-reporter.ts +3 -1
- package/src/services/sync-settings.ts +24 -0
package/src/cli/search.ts
CHANGED
|
@@ -1,33 +1,31 @@
|
|
|
1
1
|
import { Args, Command, Options } from "@effect/cli";
|
|
2
2
|
import { Effect, Option, Stream } from "effect";
|
|
3
|
-
import {
|
|
3
|
+
import { renderFeedTable, renderProfileTable } from "./doc/table-renderers.js";
|
|
4
4
|
import { BskyClient } from "../services/bsky-client.js";
|
|
5
5
|
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
|
|
10
|
-
import { StoreName } from "../domain/primitives.js";
|
|
9
|
+
import { ActorId, StoreName } from "../domain/primitives.js";
|
|
11
10
|
import { storeOptions } from "./store.js";
|
|
12
11
|
import { withExamples } from "./help.js";
|
|
13
12
|
import { CliInputError } from "./errors.js";
|
|
14
|
-
import { decodeActor } from "./shared-options.js";
|
|
15
13
|
import { formatSchemaError } from "./shared.js";
|
|
16
14
|
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
17
|
-
import { jsonNdjsonTableFormats
|
|
15
|
+
import { jsonNdjsonTableFormats } from "./output-format.js";
|
|
16
|
+
import { emitWithFormat } from "./output-render.js";
|
|
17
|
+
import { cursorOption as baseCursorOption, limitOption as baseLimitOption, parsePagination } from "./pagination.js";
|
|
18
18
|
|
|
19
19
|
const queryArg = Args.text({ name: "query" }).pipe(
|
|
20
20
|
Args.withDescription("Search query string")
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const limitOption =
|
|
24
|
-
Options.withDescription("Maximum number of results")
|
|
25
|
-
Options.optional
|
|
23
|
+
const limitOption = baseLimitOption.pipe(
|
|
24
|
+
Options.withDescription("Maximum number of results")
|
|
26
25
|
);
|
|
27
26
|
|
|
28
|
-
const cursorOption =
|
|
29
|
-
Options.withDescription("Pagination cursor")
|
|
30
|
-
Options.optional
|
|
27
|
+
const cursorOption = baseCursorOption.pipe(
|
|
28
|
+
Options.withDescription("Pagination cursor")
|
|
31
29
|
);
|
|
32
30
|
|
|
33
31
|
const typeaheadOption = Options.boolean("typeahead").pipe(
|
|
@@ -50,9 +48,8 @@ const networkOption = Options.boolean("network").pipe(
|
|
|
50
48
|
Options.withDescription("Search the Bluesky network instead of a local store")
|
|
51
49
|
);
|
|
52
50
|
|
|
53
|
-
const postCursorOption =
|
|
54
|
-
Options.withDescription("Pagination cursor (network) or offset (local)")
|
|
55
|
-
Options.optional
|
|
51
|
+
const postCursorOption = baseCursorOption.pipe(
|
|
52
|
+
Options.withDescription("Pagination cursor (network) or offset (local)")
|
|
56
53
|
);
|
|
57
54
|
|
|
58
55
|
const sortOption = Options.text("sort").pipe(
|
|
@@ -71,11 +68,13 @@ const untilOption = Options.text("until").pipe(
|
|
|
71
68
|
);
|
|
72
69
|
|
|
73
70
|
const mentionsOption = Options.text("mentions").pipe(
|
|
71
|
+
Options.withSchema(ActorId),
|
|
74
72
|
Options.withDescription("Filter network results by mention (handle or DID)"),
|
|
75
73
|
Options.optional
|
|
76
74
|
);
|
|
77
75
|
|
|
78
76
|
const authorOption = Options.text("author").pipe(
|
|
77
|
+
Options.withSchema(ActorId),
|
|
79
78
|
Options.withDescription("Filter network results by author (handle or DID)"),
|
|
80
79
|
Options.optional
|
|
81
80
|
);
|
|
@@ -100,35 +99,21 @@ const tagOption = Options.text("tag").pipe(
|
|
|
100
99
|
Options.optional
|
|
101
100
|
);
|
|
102
101
|
|
|
102
|
+
const requireNonEmptyQuery = (raw: string) =>
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const trimmed = raw.trim();
|
|
105
|
+
if (trimmed.length === 0) {
|
|
106
|
+
return yield* CliInputError.make({
|
|
107
|
+
message: "Search query must be non-empty.",
|
|
108
|
+
cause: { query: raw }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return trimmed;
|
|
112
|
+
});
|
|
103
113
|
|
|
104
|
-
type LocalSort = "relevance" | "newest" | "oldest";
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
actors: ReadonlyArray<ProfileView>,
|
|
108
|
-
cursor: string | undefined
|
|
109
|
-
) => {
|
|
110
|
-
const rows = actors.map((actor) => [
|
|
111
|
-
actor.handle,
|
|
112
|
-
actor.displayName ?? "",
|
|
113
|
-
actor.did
|
|
114
|
-
]);
|
|
115
|
-
const table = renderTableLegacy(["HANDLE", "DISPLAY NAME", "DID"], rows);
|
|
116
|
-
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
117
|
-
};
|
|
115
|
+
type LocalSort = "relevance" | "newest" | "oldest";
|
|
118
116
|
|
|
119
|
-
const renderFeedTable = (
|
|
120
|
-
feeds: ReadonlyArray<FeedGeneratorView>,
|
|
121
|
-
cursor: string | undefined
|
|
122
|
-
) => {
|
|
123
|
-
const rows = feeds.map((feed) => [
|
|
124
|
-
feed.displayName,
|
|
125
|
-
feed.creator.handle,
|
|
126
|
-
feed.uri,
|
|
127
|
-
typeof feed.likeCount === "number" ? String(feed.likeCount) : ""
|
|
128
|
-
]);
|
|
129
|
-
const table = renderTableLegacy(["NAME", "CREATOR", "URI", "LIKES"], rows);
|
|
130
|
-
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
131
|
-
};
|
|
132
117
|
|
|
133
118
|
const handlesCommand = Command.make(
|
|
134
119
|
"handles",
|
|
@@ -142,6 +127,7 @@ const handlesCommand = Command.make(
|
|
|
142
127
|
({ query, limit, cursor, typeahead, format }) =>
|
|
143
128
|
Effect.gen(function* () {
|
|
144
129
|
const appConfig = yield* AppConfigService;
|
|
130
|
+
const queryValue = yield* requireNonEmptyQuery(query);
|
|
145
131
|
if (typeahead && Option.isSome(cursor)) {
|
|
146
132
|
return yield* CliInputError.make({
|
|
147
133
|
message: "--cursor is not supported with --typeahead.",
|
|
@@ -149,27 +135,24 @@ const handlesCommand = Command.make(
|
|
|
149
135
|
});
|
|
150
136
|
}
|
|
151
137
|
const client = yield* BskyClient;
|
|
138
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
152
139
|
const options = {
|
|
153
|
-
...(
|
|
154
|
-
...(
|
|
140
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
141
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {}),
|
|
155
142
|
...(typeahead ? { typeahead: true } : {})
|
|
156
143
|
};
|
|
157
|
-
const result = yield* client.searchActors(
|
|
158
|
-
|
|
144
|
+
const result = yield* client.searchActors(queryValue, options);
|
|
145
|
+
yield* emitWithFormat(
|
|
159
146
|
format,
|
|
160
147
|
appConfig.outputFormat,
|
|
161
148
|
jsonNdjsonTableFormats,
|
|
162
|
-
"json"
|
|
149
|
+
"json",
|
|
150
|
+
{
|
|
151
|
+
json: writeJson(result),
|
|
152
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.actors)),
|
|
153
|
+
table: writeText(renderProfileTable(result.actors, result.cursor))
|
|
154
|
+
}
|
|
163
155
|
);
|
|
164
|
-
if (outputFormat === "ndjson") {
|
|
165
|
-
yield* writeJsonStream(Stream.fromIterable(result.actors));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (outputFormat === "table") {
|
|
169
|
-
yield* writeText(renderProfileTable(result.actors, result.cursor));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
yield* writeJson(result);
|
|
173
156
|
})
|
|
174
157
|
).pipe(
|
|
175
158
|
Command.withDescription(
|
|
@@ -186,27 +169,25 @@ const feedsCommand = Command.make(
|
|
|
186
169
|
({ query, limit, cursor, format }) =>
|
|
187
170
|
Effect.gen(function* () {
|
|
188
171
|
const appConfig = yield* AppConfigService;
|
|
172
|
+
const queryValue = yield* requireNonEmptyQuery(query);
|
|
189
173
|
const client = yield* BskyClient;
|
|
174
|
+
const { limit: limitValue, cursor: cursorValue } = parsePagination(limit, cursor);
|
|
190
175
|
const options = {
|
|
191
|
-
...(
|
|
192
|
-
...(
|
|
176
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
177
|
+
...(cursorValue !== undefined ? { cursor: cursorValue } : {})
|
|
193
178
|
};
|
|
194
|
-
const result = yield* client.searchFeedGenerators(
|
|
195
|
-
|
|
179
|
+
const result = yield* client.searchFeedGenerators(queryValue, options);
|
|
180
|
+
yield* emitWithFormat(
|
|
196
181
|
format,
|
|
197
182
|
appConfig.outputFormat,
|
|
198
183
|
jsonNdjsonTableFormats,
|
|
199
|
-
"json"
|
|
184
|
+
"json",
|
|
185
|
+
{
|
|
186
|
+
json: writeJson(result),
|
|
187
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.feeds)),
|
|
188
|
+
table: writeText(renderFeedTable(result.feeds, result.cursor))
|
|
189
|
+
}
|
|
200
190
|
);
|
|
201
|
-
if (outputFormat === "ndjson") {
|
|
202
|
-
yield* writeJsonStream(Stream.fromIterable(result.feeds));
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (outputFormat === "table") {
|
|
206
|
-
yield* writeText(renderFeedTable(result.feeds, result.cursor));
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
yield* writeJson(result);
|
|
210
191
|
})
|
|
211
192
|
).pipe(
|
|
212
193
|
Command.withDescription(
|
|
@@ -238,12 +219,8 @@ const postsCommand = Command.make(
|
|
|
238
219
|
({ query, store, network, limit, cursor, sort, since, until, mentions, author, lang, domain, url, tag, format }) =>
|
|
239
220
|
Effect.gen(function* () {
|
|
240
221
|
const appConfig = yield* AppConfigService;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
message: "--limit must be a positive integer.",
|
|
244
|
-
cause: { limit: limit.value }
|
|
245
|
-
});
|
|
246
|
-
}
|
|
222
|
+
const queryValue = yield* requireNonEmptyQuery(query);
|
|
223
|
+
const limitValue = Option.getOrUndefined(limit);
|
|
247
224
|
if (network && Option.isSome(store)) {
|
|
248
225
|
return yield* CliInputError.make({
|
|
249
226
|
message: "--store cannot be used with --network.",
|
|
@@ -281,12 +258,6 @@ const postsCommand = Command.make(
|
|
|
281
258
|
});
|
|
282
259
|
}
|
|
283
260
|
|
|
284
|
-
const outputFormat = resolveOutputFormat(
|
|
285
|
-
format,
|
|
286
|
-
appConfig.outputFormat,
|
|
287
|
-
jsonNdjsonTableFormats,
|
|
288
|
-
"json"
|
|
289
|
-
);
|
|
290
261
|
const storeValue = Option.getOrElse(store, () => undefined);
|
|
291
262
|
|
|
292
263
|
if (network) {
|
|
@@ -315,29 +286,13 @@ const postsCommand = Command.make(
|
|
|
315
286
|
.map((item) => item.trim())
|
|
316
287
|
.filter((item) => item.length > 0)
|
|
317
288
|
});
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
});
|
|
326
|
-
const mentionsValue = Option.match(mentions, {
|
|
327
|
-
onNone: () => Effect.void.pipe(Effect.as(undefined)),
|
|
328
|
-
onSome: (value) =>
|
|
329
|
-
Effect.gen(function* () {
|
|
330
|
-
const decoded = yield* decodeActor(value);
|
|
331
|
-
return String(decoded);
|
|
332
|
-
})
|
|
333
|
-
});
|
|
334
|
-
const parsedAuthor = yield* authorValue;
|
|
335
|
-
const parsedMentions = yield* mentionsValue;
|
|
336
|
-
const result = yield* client.searchPosts(query, {
|
|
337
|
-
...(Option.isSome(limit) ? { limit: limit.value } : {}),
|
|
338
|
-
...(Option.isSome(cursorValue) ? { cursor: cursorValue.value } : {}),
|
|
339
|
-
...(sortValue ? { sort: sortValue } : {}),
|
|
340
|
-
...(Option.isSome(since) ? { since: since.value } : {}),
|
|
289
|
+
const parsedAuthor = Option.getOrUndefined(author);
|
|
290
|
+
const parsedMentions = Option.getOrUndefined(mentions);
|
|
291
|
+
const result = yield* client.searchPosts(queryValue, {
|
|
292
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
293
|
+
...(Option.isSome(cursorValue) ? { cursor: cursorValue.value } : {}),
|
|
294
|
+
...(sortValue ? { sort: sortValue } : {}),
|
|
295
|
+
...(Option.isSome(since) ? { since: since.value } : {}),
|
|
341
296
|
...(Option.isSome(until) ? { until: until.value } : {}),
|
|
342
297
|
...(parsedMentions ? { mentions: parsedMentions } : {}),
|
|
343
298
|
...(parsedAuthor ? { author: parsedAuthor } : {}),
|
|
@@ -359,21 +314,23 @@ const postsCommand = Command.make(
|
|
|
359
314
|
),
|
|
360
315
|
{ concurrency: "unbounded" }
|
|
361
316
|
);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
317
|
+
yield* emitWithFormat(
|
|
318
|
+
format,
|
|
319
|
+
appConfig.outputFormat,
|
|
320
|
+
jsonNdjsonTableFormats,
|
|
321
|
+
"json",
|
|
322
|
+
{
|
|
323
|
+
json: writeJson({
|
|
324
|
+
query: queryValue,
|
|
325
|
+
cursor: result.cursor,
|
|
326
|
+
hitsTotal: result.hitsTotal,
|
|
327
|
+
count: posts.length,
|
|
328
|
+
posts
|
|
329
|
+
}),
|
|
330
|
+
ndjson: writeJsonStream(Stream.fromIterable(posts)),
|
|
331
|
+
table: writeText(renderPostsTable(posts))
|
|
332
|
+
}
|
|
333
|
+
);
|
|
377
334
|
return;
|
|
378
335
|
}
|
|
379
336
|
|
|
@@ -419,28 +376,28 @@ const postsCommand = Command.make(
|
|
|
419
376
|
});
|
|
420
377
|
}
|
|
421
378
|
const input = {
|
|
422
|
-
query,
|
|
423
|
-
...(
|
|
379
|
+
query: queryValue,
|
|
380
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
424
381
|
...(Option.isSome(cursorValue) ? { cursor: cursorValue.value } : {}),
|
|
425
382
|
...(localSort ? { sort: localSort } : {})
|
|
426
383
|
};
|
|
427
384
|
const result = yield* index.searchPosts(storeRef, input);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
385
|
+
yield* emitWithFormat(
|
|
386
|
+
format,
|
|
387
|
+
appConfig.outputFormat,
|
|
388
|
+
jsonNdjsonTableFormats,
|
|
389
|
+
"json",
|
|
390
|
+
{
|
|
391
|
+
json: writeJson({
|
|
392
|
+
query: queryValue,
|
|
393
|
+
cursor: result.cursor,
|
|
394
|
+
count: result.posts.length,
|
|
395
|
+
posts: result.posts
|
|
396
|
+
}),
|
|
397
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.posts)),
|
|
398
|
+
table: writeText(renderPostsTable(result.posts))
|
|
399
|
+
}
|
|
400
|
+
);
|
|
444
401
|
})
|
|
445
402
|
).pipe(
|
|
446
403
|
Command.withDescription(
|
|
@@ -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,20 +1 @@
|
|
|
1
|
-
export { formatSchemaError } from "../services/shared.js";
|
|
2
|
-
|
|
3
|
-
/** Safely parse JSON, returning `undefined` on failure. */
|
|
4
|
-
export const safeParseJson = (raw: string): unknown => {
|
|
5
|
-
try {
|
|
6
|
-
return JSON.parse(raw);
|
|
7
|
-
} catch {
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/** Format schema issues into an array of "path: message" strings. */
|
|
13
|
-
export const issueDetails = (
|
|
14
|
-
issues: ReadonlyArray<{ readonly path: ReadonlyArray<unknown>; readonly message: string }>
|
|
15
|
-
) =>
|
|
16
|
-
issues.map((issue) => {
|
|
17
|
-
const path =
|
|
18
|
-
issue.path.length > 0 ? issue.path.map((entry) => String(entry)).join(".") : "value";
|
|
19
|
-
return `${path}: ${issue.message}`;
|
|
20
|
-
});
|
|
1
|
+
export { formatParseError, formatSchemaError } from "../services/shared.js";
|
package/src/cli/store-errors.ts
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import { ParseResult } from "effect";
|
|
2
|
-
import { safeParseJson, issueDetails } from "./
|
|
2
|
+
import { safeParseJson, issueDetails, findJsonParseIssue, jsonParseTip } from "./parse-errors.js";
|
|
3
3
|
import { formatAgentError } from "./errors.js";
|
|
4
4
|
|
|
5
5
|
const storeConfigExample = {
|
|
6
6
|
format: { json: true, markdown: false },
|
|
7
7
|
autoSync: false,
|
|
8
|
-
filters: [
|
|
9
|
-
{
|
|
10
|
-
name: "tech",
|
|
11
|
-
expr: { _tag: "Hashtag", tag: "#tech" },
|
|
12
|
-
output: { path: "views/tech", json: true, markdown: true }
|
|
13
|
-
}
|
|
14
|
-
]
|
|
8
|
+
filters: []
|
|
15
9
|
};
|
|
16
10
|
|
|
11
|
+
const storeConfigExampleJson = JSON.stringify(storeConfigExample);
|
|
12
|
+
const storeConfigDocHint = "See docs/cli.md for a minimal StoreConfig JSON example.";
|
|
13
|
+
|
|
17
14
|
|
|
18
15
|
const hasPath = (issue: { readonly path: ReadonlyArray<unknown> }, key: string) =>
|
|
19
16
|
issue.path.length > 0 && issue.path[0] === key;
|
|
20
17
|
|
|
18
|
+
export const formatStoreConfigHelp = (
|
|
19
|
+
message: string,
|
|
20
|
+
error = "StoreConfigValidationError"
|
|
21
|
+
): string =>
|
|
22
|
+
formatAgentError({
|
|
23
|
+
error,
|
|
24
|
+
message: `${message} ${storeConfigDocHint}`,
|
|
25
|
+
expected: storeConfigExample,
|
|
26
|
+
fix: `Start with: --config-json '${storeConfigExampleJson}'`
|
|
27
|
+
});
|
|
28
|
+
|
|
21
29
|
|
|
22
30
|
export const formatStoreConfigParseError = (
|
|
23
31
|
error: ParseResult.ParseError,
|
|
@@ -27,20 +35,16 @@ export const formatStoreConfigParseError = (
|
|
|
27
35
|
const received = safeParseJson(raw);
|
|
28
36
|
const receivedValue = received === undefined ? raw : received;
|
|
29
37
|
|
|
30
|
-
const jsonParseIssue = issues
|
|
31
|
-
(issue) =>
|
|
32
|
-
issue._tag === "Transformation" &&
|
|
33
|
-
typeof issue.message === "string" &&
|
|
34
|
-
issue.message.startsWith("JSON Parse error")
|
|
35
|
-
);
|
|
38
|
+
const jsonParseIssue = findJsonParseIssue(issues);
|
|
36
39
|
if (jsonParseIssue) {
|
|
40
|
+
const jsonMessage = jsonParseIssue.message ?? "Invalid JSON input.";
|
|
37
41
|
return formatAgentError({
|
|
38
42
|
error: "StoreConfigJsonParseError",
|
|
39
|
-
message:
|
|
43
|
+
message: `Invalid JSON in --config-json. ${storeConfigDocHint}`,
|
|
40
44
|
received: raw,
|
|
41
45
|
details: [
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
jsonMessage,
|
|
47
|
+
jsonParseTip
|
|
44
48
|
],
|
|
45
49
|
expected: storeConfigExample
|
|
46
50
|
});
|
|
@@ -49,7 +53,7 @@ export const formatStoreConfigParseError = (
|
|
|
49
53
|
if (issues.some((issue) => issue._tag === "Missing" && hasPath(issue, "filters"))) {
|
|
50
54
|
return formatAgentError({
|
|
51
55
|
error: "StoreConfigValidationError",
|
|
52
|
-
message:
|
|
56
|
+
message: `Store config requires a filters array. ${storeConfigDocHint}`,
|
|
53
57
|
received: receivedValue,
|
|
54
58
|
expected: storeConfigExample,
|
|
55
59
|
fix:
|
|
@@ -60,7 +64,7 @@ export const formatStoreConfigParseError = (
|
|
|
60
64
|
if (issues.some((issue) => hasPath(issue, "filters"))) {
|
|
61
65
|
return formatAgentError({
|
|
62
66
|
error: "StoreConfigValidationError",
|
|
63
|
-
message:
|
|
67
|
+
message: `Store config filters must include name, expr, and output fields. ${storeConfigDocHint}`,
|
|
64
68
|
received: receivedValue,
|
|
65
69
|
expected: storeConfigExample,
|
|
66
70
|
fix: "Each filter requires name, expr (filter JSON), and output (path/json/markdown).",
|
|
@@ -68,12 +72,15 @@ export const formatStoreConfigParseError = (
|
|
|
68
72
|
});
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
const details = issueDetails(issues);
|
|
76
|
+
const primaryIssue = details[0];
|
|
77
|
+
const issueHint = primaryIssue ? ` (${primaryIssue})` : "";
|
|
71
78
|
return formatAgentError({
|
|
72
79
|
error: "StoreConfigValidationError",
|
|
73
|
-
message:
|
|
80
|
+
message: `Store config failed validation${issueHint}. ${storeConfigDocHint}`,
|
|
74
81
|
received: receivedValue,
|
|
75
82
|
expected: storeConfigExample,
|
|
76
|
-
details
|
|
83
|
+
details,
|
|
77
84
|
fix:
|
|
78
85
|
"Check required fields (format, autoSync, filters). For ingestion filters, use --filter/--filter-json on sync/query."
|
|
79
86
|
});
|
package/src/cli/store-tree.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Doc from "@effect/printer/Doc";
|
|
2
|
-
import { Chunk, Context, Effect, Option } from "effect";
|
|
2
|
+
import { Chunk, Context, Effect, Option, Order } from "effect";
|
|
3
3
|
import { StoreIndex } from "../services/store-index.js";
|
|
4
4
|
import { StoreManager } from "../services/store-manager.js";
|
|
5
5
|
import { LineageStore } from "../services/lineage-store.js";
|
|
@@ -9,6 +9,7 @@ import { StoreEventLog } from "../services/store-event-log.js";
|
|
|
9
9
|
import { DataSource } from "../domain/sync.js";
|
|
10
10
|
import type { FilterExpr } from "../domain/filter.js";
|
|
11
11
|
import { formatFilterExpr } from "../domain/filter-describe.js";
|
|
12
|
+
import { updatedAtOrder } from "../domain/order.js";
|
|
12
13
|
import type { StoreName } from "../domain/primitives.js";
|
|
13
14
|
import type { StoreRef } from "../domain/store.js";
|
|
14
15
|
import type { StoreLineage } from "../domain/derivation.js";
|
|
@@ -123,9 +124,10 @@ const resolveSyncInfo = (
|
|
|
123
124
|
if (candidates.length === 0) {
|
|
124
125
|
return { syncStatus: "unknown" as const };
|
|
125
126
|
}
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
const checkpointOrder = updatedAtOrder<(typeof candidates)[number]>();
|
|
128
|
+
const latest = candidates.reduce((acc, candidate) =>
|
|
129
|
+
Order.max(checkpointOrder)(acc, candidate)
|
|
130
|
+
);
|
|
129
131
|
if (!latest) {
|
|
130
132
|
return { syncStatus: "unknown" as const };
|
|
131
133
|
}
|