@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/search.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
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 type { FeedGeneratorView, ProfileView } from "../domain/bsky.js";
|
|
10
9
|
import { StoreName } from "../domain/primitives.js";
|
|
11
10
|
import { storeOptions } from "./store.js";
|
|
12
11
|
import { withExamples } from "./help.js";
|
|
@@ -14,20 +13,21 @@ import { CliInputError } from "./errors.js";
|
|
|
14
13
|
import { decodeActor } from "./shared-options.js";
|
|
15
14
|
import { formatSchemaError } from "./shared.js";
|
|
16
15
|
import { writeJson, writeJsonStream, writeText } from "./output.js";
|
|
17
|
-
import { jsonNdjsonTableFormats
|
|
16
|
+
import { jsonNdjsonTableFormats } from "./output-format.js";
|
|
17
|
+
import { emitWithFormat } from "./output-render.js";
|
|
18
|
+
import { cursorOption as baseCursorOption, limitOption as baseLimitOption, parsePagination } from "./pagination.js";
|
|
19
|
+
import { parseLimit } from "./shared-options.js";
|
|
18
20
|
|
|
19
21
|
const queryArg = Args.text({ name: "query" }).pipe(
|
|
20
22
|
Args.withDescription("Search query string")
|
|
21
23
|
);
|
|
22
24
|
|
|
23
|
-
const limitOption =
|
|
24
|
-
Options.withDescription("Maximum number of results")
|
|
25
|
-
Options.optional
|
|
25
|
+
const limitOption = baseLimitOption.pipe(
|
|
26
|
+
Options.withDescription("Maximum number of results")
|
|
26
27
|
);
|
|
27
28
|
|
|
28
|
-
const cursorOption =
|
|
29
|
-
Options.withDescription("Pagination cursor")
|
|
30
|
-
Options.optional
|
|
29
|
+
const cursorOption = baseCursorOption.pipe(
|
|
30
|
+
Options.withDescription("Pagination cursor")
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
const typeaheadOption = Options.boolean("typeahead").pipe(
|
|
@@ -50,9 +50,8 @@ const networkOption = Options.boolean("network").pipe(
|
|
|
50
50
|
Options.withDescription("Search the Bluesky network instead of a local store")
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
-
const postCursorOption =
|
|
54
|
-
Options.withDescription("Pagination cursor (network) or offset (local)")
|
|
55
|
-
Options.optional
|
|
53
|
+
const postCursorOption = baseCursorOption.pipe(
|
|
54
|
+
Options.withDescription("Pagination cursor (network) or offset (local)")
|
|
56
55
|
);
|
|
57
56
|
|
|
58
57
|
const sortOption = Options.text("sort").pipe(
|
|
@@ -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 } = yield* 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 } = yield* 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,9 @@ 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
|
-
|
|
244
|
-
cause: { limit: limit.value }
|
|
245
|
-
});
|
|
246
|
-
}
|
|
222
|
+
const queryValue = yield* requireNonEmptyQuery(query);
|
|
223
|
+
const parsedLimit = yield* parseLimit(limit);
|
|
224
|
+
const limitValue = Option.getOrUndefined(parsedLimit);
|
|
247
225
|
if (network && Option.isSome(store)) {
|
|
248
226
|
return yield* CliInputError.make({
|
|
249
227
|
message: "--store cannot be used with --network.",
|
|
@@ -281,12 +259,6 @@ const postsCommand = Command.make(
|
|
|
281
259
|
});
|
|
282
260
|
}
|
|
283
261
|
|
|
284
|
-
const outputFormat = resolveOutputFormat(
|
|
285
|
-
format,
|
|
286
|
-
appConfig.outputFormat,
|
|
287
|
-
jsonNdjsonTableFormats,
|
|
288
|
-
"json"
|
|
289
|
-
);
|
|
290
262
|
const storeValue = Option.getOrElse(store, () => undefined);
|
|
291
263
|
|
|
292
264
|
if (network) {
|
|
@@ -331,13 +303,13 @@ const postsCommand = Command.make(
|
|
|
331
303
|
return String(decoded);
|
|
332
304
|
})
|
|
333
305
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
306
|
+
const parsedAuthor = yield* authorValue;
|
|
307
|
+
const parsedMentions = yield* mentionsValue;
|
|
308
|
+
const result = yield* client.searchPosts(queryValue, {
|
|
309
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
310
|
+
...(Option.isSome(cursorValue) ? { cursor: cursorValue.value } : {}),
|
|
311
|
+
...(sortValue ? { sort: sortValue } : {}),
|
|
312
|
+
...(Option.isSome(since) ? { since: since.value } : {}),
|
|
341
313
|
...(Option.isSome(until) ? { until: until.value } : {}),
|
|
342
314
|
...(parsedMentions ? { mentions: parsedMentions } : {}),
|
|
343
315
|
...(parsedAuthor ? { author: parsedAuthor } : {}),
|
|
@@ -359,21 +331,23 @@ const postsCommand = Command.make(
|
|
|
359
331
|
),
|
|
360
332
|
{ concurrency: "unbounded" }
|
|
361
333
|
);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
334
|
+
yield* emitWithFormat(
|
|
335
|
+
format,
|
|
336
|
+
appConfig.outputFormat,
|
|
337
|
+
jsonNdjsonTableFormats,
|
|
338
|
+
"json",
|
|
339
|
+
{
|
|
340
|
+
json: writeJson({
|
|
341
|
+
query: queryValue,
|
|
342
|
+
cursor: result.cursor,
|
|
343
|
+
hitsTotal: result.hitsTotal,
|
|
344
|
+
count: posts.length,
|
|
345
|
+
posts
|
|
346
|
+
}),
|
|
347
|
+
ndjson: writeJsonStream(Stream.fromIterable(posts)),
|
|
348
|
+
table: writeText(renderPostsTable(posts))
|
|
349
|
+
}
|
|
350
|
+
);
|
|
377
351
|
return;
|
|
378
352
|
}
|
|
379
353
|
|
|
@@ -419,28 +393,28 @@ const postsCommand = Command.make(
|
|
|
419
393
|
});
|
|
420
394
|
}
|
|
421
395
|
const input = {
|
|
422
|
-
query,
|
|
423
|
-
...(
|
|
396
|
+
query: queryValue,
|
|
397
|
+
...(limitValue !== undefined ? { limit: limitValue } : {}),
|
|
424
398
|
...(Option.isSome(cursorValue) ? { cursor: cursorValue.value } : {}),
|
|
425
399
|
...(localSort ? { sort: localSort } : {})
|
|
426
400
|
};
|
|
427
401
|
const result = yield* index.searchPosts(storeRef, input);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
402
|
+
yield* emitWithFormat(
|
|
403
|
+
format,
|
|
404
|
+
appConfig.outputFormat,
|
|
405
|
+
jsonNdjsonTableFormats,
|
|
406
|
+
"json",
|
|
407
|
+
{
|
|
408
|
+
json: writeJson({
|
|
409
|
+
query: queryValue,
|
|
410
|
+
cursor: result.cursor,
|
|
411
|
+
count: result.posts.length,
|
|
412
|
+
posts: result.posts
|
|
413
|
+
}),
|
|
414
|
+
ndjson: writeJsonStream(Stream.fromIterable(result.posts)),
|
|
415
|
+
table: writeText(renderPostsTable(result.posts))
|
|
416
|
+
}
|
|
417
|
+
);
|
|
444
418
|
})
|
|
445
419
|
).pipe(
|
|
446
420
|
Command.withDescription(
|
package/src/cli/shared.ts
CHANGED
|
@@ -1,20 +1 @@
|
|
|
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
|
-
});
|
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 } 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,
|
|
@@ -36,7 +44,7 @@ export const formatStoreConfigParseError = (
|
|
|
36
44
|
if (jsonParseIssue) {
|
|
37
45
|
return formatAgentError({
|
|
38
46
|
error: "StoreConfigJsonParseError",
|
|
39
|
-
message:
|
|
47
|
+
message: `Invalid JSON in --config-json. ${storeConfigDocHint}`,
|
|
40
48
|
received: raw,
|
|
41
49
|
details: [
|
|
42
50
|
jsonParseIssue.message,
|
|
@@ -49,7 +57,7 @@ export const formatStoreConfigParseError = (
|
|
|
49
57
|
if (issues.some((issue) => issue._tag === "Missing" && hasPath(issue, "filters"))) {
|
|
50
58
|
return formatAgentError({
|
|
51
59
|
error: "StoreConfigValidationError",
|
|
52
|
-
message:
|
|
60
|
+
message: `Store config requires a filters array. ${storeConfigDocHint}`,
|
|
53
61
|
received: receivedValue,
|
|
54
62
|
expected: storeConfigExample,
|
|
55
63
|
fix:
|
|
@@ -60,7 +68,7 @@ export const formatStoreConfigParseError = (
|
|
|
60
68
|
if (issues.some((issue) => hasPath(issue, "filters"))) {
|
|
61
69
|
return formatAgentError({
|
|
62
70
|
error: "StoreConfigValidationError",
|
|
63
|
-
message:
|
|
71
|
+
message: `Store config filters must include name, expr, and output fields. ${storeConfigDocHint}`,
|
|
64
72
|
received: receivedValue,
|
|
65
73
|
expected: storeConfigExample,
|
|
66
74
|
fix: "Each filter requires name, expr (filter JSON), and output (path/json/markdown).",
|
|
@@ -68,12 +76,15 @@ export const formatStoreConfigParseError = (
|
|
|
68
76
|
});
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
const details = issueDetails(issues);
|
|
80
|
+
const primaryIssue = details[0];
|
|
81
|
+
const issueHint = primaryIssue ? ` (${primaryIssue})` : "";
|
|
71
82
|
return formatAgentError({
|
|
72
83
|
error: "StoreConfigValidationError",
|
|
73
|
-
message:
|
|
84
|
+
message: `Store config failed validation${issueHint}. ${storeConfigDocHint}`,
|
|
74
85
|
received: receivedValue,
|
|
75
86
|
expected: storeConfigExample,
|
|
76
|
-
details
|
|
87
|
+
details,
|
|
77
88
|
fix:
|
|
78
89
|
"Check required fields (format, autoSync, filters). For ingestion filters, use --filter/--filter-json on sync/query."
|
|
79
90
|
});
|
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
|
}
|
package/src/cli/store.ts
CHANGED
|
@@ -14,12 +14,13 @@ import { StoreCleaner } from "../services/store-cleaner.js";
|
|
|
14
14
|
import { LineageStore } from "../services/lineage-store.js";
|
|
15
15
|
import { CliInputError } from "./errors.js";
|
|
16
16
|
import { OutputManager } from "../services/output-manager.js";
|
|
17
|
-
import { formatStoreConfigParseError } from "./store-errors.js";
|
|
17
|
+
import { formatStoreConfigHelp, formatStoreConfigParseError } from "./store-errors.js";
|
|
18
18
|
import { formatFilterExpr } from "../domain/filter-describe.js";
|
|
19
19
|
import { CliPreferences } from "./preferences.js";
|
|
20
20
|
import { StoreStats } from "../services/store-stats.js";
|
|
21
21
|
import { withExamples } from "./help.js";
|
|
22
22
|
import { resolveOutputFormat, treeTableJsonFormats } from "./output-format.js";
|
|
23
|
+
import { StoreRenamer } from "../services/store-renamer.js";
|
|
23
24
|
import {
|
|
24
25
|
buildStoreTreeData,
|
|
25
26
|
renderStoreTree,
|
|
@@ -33,6 +34,14 @@ const storeNameArg = Args.text({ name: "name" }).pipe(
|
|
|
33
34
|
Args.withSchema(StoreName),
|
|
34
35
|
Args.withDescription("Store name")
|
|
35
36
|
);
|
|
37
|
+
const storeRenameFromArg = Args.text({ name: "from" }).pipe(
|
|
38
|
+
Args.withSchema(StoreName),
|
|
39
|
+
Args.withDescription("Existing store name")
|
|
40
|
+
);
|
|
41
|
+
const storeRenameToArg = Args.text({ name: "to" }).pipe(
|
|
42
|
+
Args.withSchema(StoreName),
|
|
43
|
+
Args.withDescription("New store name")
|
|
44
|
+
);
|
|
36
45
|
const storeNameOption = Options.text("store").pipe(
|
|
37
46
|
Options.withSchema(StoreName),
|
|
38
47
|
Options.withDescription("Store name")
|
|
@@ -254,6 +263,27 @@ export const storeDelete = Command.make(
|
|
|
254
263
|
)
|
|
255
264
|
);
|
|
256
265
|
|
|
266
|
+
export const storeRename = Command.make(
|
|
267
|
+
"rename",
|
|
268
|
+
{ from: storeRenameFromArg, to: storeRenameToArg },
|
|
269
|
+
({ from, to }) =>
|
|
270
|
+
Effect.gen(function* () {
|
|
271
|
+
if (from === to) {
|
|
272
|
+
return yield* CliInputError.make({
|
|
273
|
+
message: "Old and new store names must be different.",
|
|
274
|
+
cause: { from, to }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const renamer = yield* StoreRenamer;
|
|
278
|
+
const result = yield* renamer.rename(from, to);
|
|
279
|
+
yield* writeJson(result);
|
|
280
|
+
})
|
|
281
|
+
).pipe(
|
|
282
|
+
Command.withDescription(
|
|
283
|
+
withExamples("Rename a store", ["skygent store rename old-name new-name"])
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
|
|
257
287
|
export const storeMaterialize = Command.make(
|
|
258
288
|
"materialize",
|
|
259
289
|
{ name: storeNameArg, filter: filterNameOption },
|
|
@@ -267,7 +297,9 @@ export const storeMaterialize = Command.make(
|
|
|
267
297
|
|
|
268
298
|
if (config.filters.length === 0) {
|
|
269
299
|
return yield* CliInputError.make({
|
|
270
|
-
message:
|
|
300
|
+
message: formatStoreConfigHelp(
|
|
301
|
+
`Store "${name}" has no configured filters to materialize. Add filters to the store config.`
|
|
302
|
+
),
|
|
271
303
|
cause: { store: name }
|
|
272
304
|
});
|
|
273
305
|
}
|
|
@@ -378,6 +410,7 @@ export const storeCommand = Command.make("store", {}).pipe(
|
|
|
378
410
|
storeCreate,
|
|
379
411
|
storeList,
|
|
380
412
|
storeShow,
|
|
413
|
+
storeRename,
|
|
381
414
|
storeDelete,
|
|
382
415
|
storeMaterialize,
|
|
383
416
|
storeStats,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Chunk, Effect, Option, Order, Stream } from "effect";
|
|
2
|
+
|
|
3
|
+
export const mergeOrderedStreams = <A, E, R>(
|
|
4
|
+
streams: ReadonlyArray<Stream.Stream<A, E, R>>,
|
|
5
|
+
order: Order.Order<A>
|
|
6
|
+
): Stream.Stream<A, E, R> => {
|
|
7
|
+
if (streams.length === 0) {
|
|
8
|
+
return Stream.empty;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return Stream.unwrapScoped(
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const pulls = yield* Effect.forEach(streams, (stream) => Stream.toPull(stream), {
|
|
14
|
+
discard: false
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const buffers: Array<ReadonlyArray<A>> = pulls.map(() => []);
|
|
18
|
+
const indices: number[] = pulls.map(() => 0);
|
|
19
|
+
const heads: Array<A | undefined> = pulls.map(() => undefined);
|
|
20
|
+
let active = pulls.length;
|
|
21
|
+
|
|
22
|
+
const pullChunk = (index: number) =>
|
|
23
|
+
pulls[index]!.pipe(
|
|
24
|
+
Effect.map(Option.some),
|
|
25
|
+
Effect.catchAll((cause) =>
|
|
26
|
+
Option.match(cause, {
|
|
27
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
28
|
+
onSome: (error) => Effect.fail(error)
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const nextValue = (index: number): Effect.Effect<Option.Option<A>, E, R> =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
while (true) {
|
|
36
|
+
const buffer = buffers[index] ?? [];
|
|
37
|
+
const position = indices[index] ?? 0;
|
|
38
|
+
if (position < buffer.length) {
|
|
39
|
+
const value = buffer[position]!;
|
|
40
|
+
indices[index] = position + 1;
|
|
41
|
+
return Option.some(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const nextChunkOption = yield* pullChunk(index);
|
|
45
|
+
if (Option.isNone(nextChunkOption)) {
|
|
46
|
+
return Option.none<A>();
|
|
47
|
+
}
|
|
48
|
+
const nextChunk = Chunk.toReadonlyArray(nextChunkOption.value);
|
|
49
|
+
if (nextChunk.length === 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
buffers[index] = nextChunk;
|
|
53
|
+
indices[index] = 1;
|
|
54
|
+
return Option.some(nextChunk[0]!);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < pulls.length; index += 1) {
|
|
59
|
+
const next = yield* nextValue(index);
|
|
60
|
+
if (Option.isNone(next)) {
|
|
61
|
+
active -= 1;
|
|
62
|
+
heads[index] = undefined;
|
|
63
|
+
} else {
|
|
64
|
+
heads[index] = next.value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const pull: Effect.Effect<Chunk.Chunk<A>, Option.Option<E>, R> =
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
if (active === 0) {
|
|
71
|
+
return yield* Effect.fail(Option.none<E>());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let selectedIndex = -1;
|
|
75
|
+
let selectedValue: A | undefined;
|
|
76
|
+
for (let index = 0; index < heads.length; index += 1) {
|
|
77
|
+
const value = heads[index];
|
|
78
|
+
if (value === undefined) continue;
|
|
79
|
+
if (selectedIndex < 0 || order(value, selectedValue as A) < 0) {
|
|
80
|
+
selectedIndex = index;
|
|
81
|
+
selectedValue = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (selectedIndex < 0 || selectedValue === undefined) {
|
|
86
|
+
return yield* Effect.fail(Option.none<E>());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const next = yield* nextValue(selectedIndex).pipe(
|
|
90
|
+
Effect.mapError(Option.some)
|
|
91
|
+
);
|
|
92
|
+
if (Option.isNone(next)) {
|
|
93
|
+
heads[selectedIndex] = undefined;
|
|
94
|
+
active -= 1;
|
|
95
|
+
} else {
|
|
96
|
+
heads[selectedIndex] = next.value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Chunk.of(selectedValue);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return Stream.fromPull(Effect.succeed(pull));
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
};
|