@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/store.ts
CHANGED
|
@@ -14,12 +14,14 @@ 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";
|
|
24
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
23
25
|
import {
|
|
24
26
|
buildStoreTreeData,
|
|
25
27
|
renderStoreTree,
|
|
@@ -33,6 +35,14 @@ const storeNameArg = Args.text({ name: "name" }).pipe(
|
|
|
33
35
|
Args.withSchema(StoreName),
|
|
34
36
|
Args.withDescription("Store name")
|
|
35
37
|
);
|
|
38
|
+
const storeRenameFromArg = Args.text({ name: "from" }).pipe(
|
|
39
|
+
Args.withSchema(StoreName),
|
|
40
|
+
Args.withDescription("Existing store name")
|
|
41
|
+
);
|
|
42
|
+
const storeRenameToArg = Args.text({ name: "to" }).pipe(
|
|
43
|
+
Args.withSchema(StoreName),
|
|
44
|
+
Args.withDescription("New store name")
|
|
45
|
+
);
|
|
36
46
|
const storeNameOption = Options.text("store").pipe(
|
|
37
47
|
Options.withSchema(StoreName),
|
|
38
48
|
Options.withDescription("Store name")
|
|
@@ -53,6 +63,7 @@ const treeAnsiOption = Options.boolean("ansi").pipe(
|
|
|
53
63
|
Options.withDescription("Enable ANSI color output for tree format")
|
|
54
64
|
);
|
|
55
65
|
const treeWidthOption = Options.integer("width").pipe(
|
|
66
|
+
Options.withSchema(PositiveInt),
|
|
56
67
|
Options.withDescription("Line width for tree rendering (enables wrapping)"),
|
|
57
68
|
Options.optional
|
|
58
69
|
);
|
|
@@ -239,6 +250,10 @@ export const storeDelete = Command.make(
|
|
|
239
250
|
const cleaner = yield* StoreCleaner;
|
|
240
251
|
const result = yield* cleaner.deleteStore(name);
|
|
241
252
|
if (!result.deleted) {
|
|
253
|
+
if (result.reason === "missing") {
|
|
254
|
+
yield* writeJson(result);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
242
257
|
return yield* CliInputError.make({
|
|
243
258
|
message: `Store "${name}" was not deleted.`,
|
|
244
259
|
cause: result
|
|
@@ -254,6 +269,27 @@ export const storeDelete = Command.make(
|
|
|
254
269
|
)
|
|
255
270
|
);
|
|
256
271
|
|
|
272
|
+
export const storeRename = Command.make(
|
|
273
|
+
"rename",
|
|
274
|
+
{ from: storeRenameFromArg, to: storeRenameToArg },
|
|
275
|
+
({ from, to }) =>
|
|
276
|
+
Effect.gen(function* () {
|
|
277
|
+
if (from === to) {
|
|
278
|
+
return yield* CliInputError.make({
|
|
279
|
+
message: "Old and new store names must be different.",
|
|
280
|
+
cause: { from, to }
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const renamer = yield* StoreRenamer;
|
|
284
|
+
const result = yield* renamer.rename(from, to);
|
|
285
|
+
yield* writeJson(result);
|
|
286
|
+
})
|
|
287
|
+
).pipe(
|
|
288
|
+
Command.withDescription(
|
|
289
|
+
withExamples("Rename a store", ["skygent store rename old-name new-name"])
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
|
|
257
293
|
export const storeMaterialize = Command.make(
|
|
258
294
|
"materialize",
|
|
259
295
|
{ name: storeNameArg, filter: filterNameOption },
|
|
@@ -267,7 +303,9 @@ export const storeMaterialize = Command.make(
|
|
|
267
303
|
|
|
268
304
|
if (config.filters.length === 0) {
|
|
269
305
|
return yield* CliInputError.make({
|
|
270
|
-
message:
|
|
306
|
+
message: formatStoreConfigHelp(
|
|
307
|
+
`Store "${name}" has no configured filters to materialize. Add filters to the store config.`
|
|
308
|
+
),
|
|
271
309
|
cause: { store: name }
|
|
272
310
|
});
|
|
273
311
|
}
|
|
@@ -378,6 +416,7 @@ export const storeCommand = Command.make("store", {}).pipe(
|
|
|
378
416
|
storeCreate,
|
|
379
417
|
storeList,
|
|
380
418
|
storeShow,
|
|
419
|
+
storeRename,
|
|
381
420
|
storeDelete,
|
|
382
421
|
storeMaterialize,
|
|
383
422
|
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
|
+
};
|
package/src/cli/sync-factory.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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";
|
|
5
5
|
import { OutputManager } from "../services/output-manager.js";
|
|
6
6
|
import { ResourceMonitor } from "../services/resource-monitor.js";
|
|
7
|
+
import { StoreIndex } from "../services/store-index.js";
|
|
7
8
|
import { parseFilterExpr } from "./filter-input.js";
|
|
8
9
|
import { CliOutput, writeJson, writeJsonStream } from "./output.js";
|
|
9
10
|
import { storeOptions } from "./store.js";
|
|
10
11
|
import { logInfo, logWarn, makeSyncReporter } from "./logging.js";
|
|
11
|
-
import { parseInterval } from "./interval.js";
|
|
12
|
+
import { parseInterval, parseOptionalDuration } from "./interval.js";
|
|
12
13
|
import type { StoreName } from "../domain/primitives.js";
|
|
13
14
|
|
|
14
15
|
/** Common options shared by sync and watch API-based commands */
|
|
@@ -18,6 +19,7 @@ export interface CommonCommandInput {
|
|
|
18
19
|
readonly filterJson: Option.Option<string>;
|
|
19
20
|
readonly quiet: boolean;
|
|
20
21
|
readonly refresh: boolean;
|
|
22
|
+
readonly limit?: Option.Option<number>;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/** Build the command body for a one-shot sync command (timeline, feed, notifications). */
|
|
@@ -32,6 +34,7 @@ export const makeSyncCommandBody = (
|
|
|
32
34
|
const monitor = yield* ResourceMonitor;
|
|
33
35
|
const output = yield* CliOutput;
|
|
34
36
|
const outputManager = yield* OutputManager;
|
|
37
|
+
const index = yield* StoreIndex;
|
|
35
38
|
const storeRef = yield* storeOptions.loadStoreRef(input.store);
|
|
36
39
|
const storeConfig = yield* storeOptions.loadStoreConfig(input.store);
|
|
37
40
|
const expr = yield* parseFilterExpr(input.filter, input.filterJson);
|
|
@@ -44,8 +47,12 @@ export const makeSyncCommandBody = (
|
|
|
44
47
|
store: storeRef.name
|
|
45
48
|
});
|
|
46
49
|
}
|
|
50
|
+
const limitValue = Option.getOrUndefined(input.limit ?? Option.none());
|
|
47
51
|
const result = yield* sync
|
|
48
|
-
.sync(makeDataSource(), storeRef, expr, {
|
|
52
|
+
.sync(makeDataSource(), storeRef, expr, {
|
|
53
|
+
policy,
|
|
54
|
+
...(limitValue !== undefined ? { limit: limitValue } : {})
|
|
55
|
+
})
|
|
49
56
|
.pipe(
|
|
50
57
|
Effect.provideService(SyncReporter, makeSyncReporter(input.quiet, monitor, output))
|
|
51
58
|
);
|
|
@@ -56,13 +63,16 @@ export const makeSyncCommandBody = (
|
|
|
56
63
|
filters: materialized.filters.map((spec) => spec.name)
|
|
57
64
|
});
|
|
58
65
|
}
|
|
66
|
+
const totalPosts = yield* index.count(storeRef);
|
|
59
67
|
yield* logInfo("Sync complete", { source: sourceName, store: storeRef.name, ...extraLogFields });
|
|
60
|
-
yield* writeJson(result as SyncResult);
|
|
68
|
+
yield* writeJson({ ...(result as SyncResult), totalPosts });
|
|
61
69
|
});
|
|
62
70
|
|
|
63
71
|
/** Common options for watch API-based commands */
|
|
64
72
|
export interface WatchCommandInput extends CommonCommandInput {
|
|
65
|
-
readonly interval: Option.Option<
|
|
73
|
+
readonly interval: Option.Option<Duration.Duration>;
|
|
74
|
+
readonly maxCycles: Option.Option<number>;
|
|
75
|
+
readonly until: Option.Option<Duration.Duration>;
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
/** Build the command body for a watch command (timeline, feed, notifications). */
|
|
@@ -81,7 +91,8 @@ export const makeWatchCommandBody = (
|
|
|
81
91
|
const expr = yield* parseFilterExpr(input.filter, input.filterJson);
|
|
82
92
|
const basePolicy = storeConfig.syncPolicy ?? "dedupe";
|
|
83
93
|
const policy = input.refresh ? "refresh" : basePolicy;
|
|
84
|
-
const parsedInterval =
|
|
94
|
+
const parsedInterval = parseInterval(input.interval);
|
|
95
|
+
const parsedUntil = parseOptionalDuration(input.until);
|
|
85
96
|
yield* logInfo("Starting watch", { source: sourceName, store: storeRef.name, ...extraLogFields });
|
|
86
97
|
if (policy === "refresh") {
|
|
87
98
|
yield* logWarn("Refresh mode updates existing posts and may grow the event log.", {
|
|
@@ -89,7 +100,7 @@ export const makeWatchCommandBody = (
|
|
|
89
100
|
store: storeRef.name
|
|
90
101
|
});
|
|
91
102
|
}
|
|
92
|
-
|
|
103
|
+
let stream = sync
|
|
93
104
|
.watch(
|
|
94
105
|
WatchConfig.make({
|
|
95
106
|
source: makeDataSource(),
|
|
@@ -103,5 +114,11 @@ export const makeWatchCommandBody = (
|
|
|
103
114
|
Stream.map((event) => event.result),
|
|
104
115
|
Stream.provideService(SyncReporter, makeSyncReporter(input.quiet, monitor, output))
|
|
105
116
|
);
|
|
117
|
+
if (Option.isSome(input.maxCycles)) {
|
|
118
|
+
stream = stream.pipe(Stream.take(input.maxCycles.value));
|
|
119
|
+
}
|
|
120
|
+
if (Option.isSome(parsedUntil)) {
|
|
121
|
+
stream = stream.pipe(Stream.interruptWhen(Effect.sleep(parsedUntil.value)));
|
|
122
|
+
}
|
|
106
123
|
yield* writeJsonStream(stream);
|
|
107
124
|
});
|
package/src/cli/sync.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Options } from "@effect/cli";
|
|
2
|
-
import {
|
|
2
|
+
import { Effect, Layer, Option } from "effect";
|
|
3
3
|
import { Jetstream } from "effect-jetstream";
|
|
4
4
|
import { filterExprSignature } from "../domain/filter.js";
|
|
5
5
|
import { DataSource, SyncResult } from "../domain/sync.js";
|
|
@@ -9,6 +9,7 @@ import { logInfo, makeSyncReporter } from "./logging.js";
|
|
|
9
9
|
import { SyncReporter } from "../services/sync-reporter.js";
|
|
10
10
|
import { ResourceMonitor } from "../services/resource-monitor.js";
|
|
11
11
|
import { OutputManager } from "../services/output-manager.js";
|
|
12
|
+
import { StoreIndex } from "../services/store-index.js";
|
|
12
13
|
import { CliOutput, writeJson } from "./output.js";
|
|
13
14
|
import { parseFilterExpr } from "./filter-input.js";
|
|
14
15
|
import { withExamples } from "./help.js";
|
|
@@ -27,61 +28,44 @@ import {
|
|
|
27
28
|
postFilterJsonOption,
|
|
28
29
|
authorFilterOption,
|
|
29
30
|
includePinsOption,
|
|
30
|
-
decodeActor,
|
|
31
31
|
quietOption,
|
|
32
32
|
refreshOption,
|
|
33
33
|
strictOption,
|
|
34
|
-
maxErrorsOption
|
|
35
|
-
parseMaxErrors,
|
|
36
|
-
parseLimit,
|
|
37
|
-
parseBoundedIntOption
|
|
34
|
+
maxErrorsOption
|
|
38
35
|
} from "./shared-options.js";
|
|
36
|
+
import {
|
|
37
|
+
depthOption as threadDepthOption,
|
|
38
|
+
parentHeightOption as threadParentHeightOption,
|
|
39
|
+
parseThreadDepth
|
|
40
|
+
} from "./thread-options.js";
|
|
41
|
+
import { DurationInput, PositiveInt } from "./option-schemas.js";
|
|
39
42
|
|
|
40
|
-
const
|
|
43
|
+
const syncLimitOption = Options.integer("limit").pipe(
|
|
44
|
+
Options.withSchema(PositiveInt),
|
|
45
|
+
Options.withDescription("Maximum number of posts to sync"),
|
|
46
|
+
Options.optional
|
|
47
|
+
);
|
|
48
|
+
const jetstreamLimitOption = Options.integer("limit").pipe(
|
|
49
|
+
Options.withSchema(PositiveInt),
|
|
41
50
|
Options.withDescription("Maximum number of Jetstream events to process"),
|
|
42
51
|
Options.optional
|
|
43
52
|
);
|
|
44
53
|
const durationOption = Options.text("duration").pipe(
|
|
54
|
+
Options.withSchema(DurationInput),
|
|
45
55
|
Options.withDescription("Stop after a duration (e.g. \"2 minutes\")"),
|
|
46
56
|
Options.optional
|
|
47
57
|
);
|
|
48
|
-
const depthOption =
|
|
49
|
-
|
|
50
|
-
Options.optional
|
|
58
|
+
const depthOption = threadDepthOption(
|
|
59
|
+
"Thread reply depth to include (0-1000, default 6)"
|
|
51
60
|
);
|
|
52
|
-
const parentHeightOption =
|
|
53
|
-
|
|
54
|
-
Options.optional
|
|
61
|
+
const parentHeightOption = threadParentHeightOption(
|
|
62
|
+
"Thread parent height to include (0-1000, default 80)"
|
|
55
63
|
);
|
|
56
64
|
|
|
57
|
-
const parseDuration = (value: Option.Option<string>) =>
|
|
58
|
-
Option.match(value, {
|
|
59
|
-
onNone: () => Effect.succeed(Option.none()),
|
|
60
|
-
onSome: (raw) =>
|
|
61
|
-
Effect.try({
|
|
62
|
-
try: () => Duration.decode(raw as Duration.DurationInput),
|
|
63
|
-
catch: (cause) =>
|
|
64
|
-
CliInputError.make({
|
|
65
|
-
message: `Invalid duration: ${raw}. Use formats like \"2 minutes\".`,
|
|
66
|
-
cause
|
|
67
|
-
})
|
|
68
|
-
}).pipe(
|
|
69
|
-
Effect.flatMap((duration) =>
|
|
70
|
-
Duration.toMillis(duration) < 0
|
|
71
|
-
? Effect.fail(
|
|
72
|
-
CliInputError.make({
|
|
73
|
-
message: "Duration must be non-negative.",
|
|
74
|
-
cause: duration
|
|
75
|
-
})
|
|
76
|
-
)
|
|
77
|
-
: Effect.succeed(Option.some(duration))
|
|
78
|
-
)
|
|
79
|
-
)
|
|
80
|
-
});
|
|
81
65
|
|
|
82
66
|
const timelineCommand = Command.make(
|
|
83
67
|
"timeline",
|
|
84
|
-
{ store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption },
|
|
68
|
+
{ store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption, limit: syncLimitOption },
|
|
85
69
|
makeSyncCommandBody("timeline", () => DataSource.timeline())
|
|
86
70
|
).pipe(
|
|
87
71
|
Command.withDescription(
|
|
@@ -98,7 +82,7 @@ const timelineCommand = Command.make(
|
|
|
98
82
|
|
|
99
83
|
const feedCommand = Command.make(
|
|
100
84
|
"feed",
|
|
101
|
-
{ uri: feedUriArg, store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption },
|
|
85
|
+
{ uri: feedUriArg, store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption, limit: syncLimitOption },
|
|
102
86
|
({ uri, ...rest }) => makeSyncCommandBody("feed", () => DataSource.feed(uri), { uri })(rest)
|
|
103
87
|
).pipe(
|
|
104
88
|
Command.withDescription(
|
|
@@ -114,7 +98,7 @@ const feedCommand = Command.make(
|
|
|
114
98
|
|
|
115
99
|
const listCommand = Command.make(
|
|
116
100
|
"list",
|
|
117
|
-
{ uri: listUriArg, store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption },
|
|
101
|
+
{ uri: listUriArg, store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption, limit: syncLimitOption },
|
|
118
102
|
({ uri, ...rest }) => makeSyncCommandBody("list", () => DataSource.list(uri), { uri })(rest)
|
|
119
103
|
).pipe(
|
|
120
104
|
Command.withDescription(
|
|
@@ -130,7 +114,7 @@ const listCommand = Command.make(
|
|
|
130
114
|
|
|
131
115
|
const notificationsCommand = Command.make(
|
|
132
116
|
"notifications",
|
|
133
|
-
{ store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption },
|
|
117
|
+
{ store: storeNameOption, filter: filterOption, filterJson: filterJsonOption, quiet: quietOption, refresh: refreshOption, limit: syncLimitOption },
|
|
134
118
|
makeSyncCommandBody("notifications", () => DataSource.notifications())
|
|
135
119
|
).pipe(
|
|
136
120
|
Command.withDescription(
|
|
@@ -152,18 +136,18 @@ const authorCommand = Command.make(
|
|
|
152
136
|
postFilter: postFilterOption,
|
|
153
137
|
postFilterJson: postFilterJsonOption,
|
|
154
138
|
quiet: quietOption,
|
|
155
|
-
refresh: refreshOption
|
|
139
|
+
refresh: refreshOption,
|
|
140
|
+
limit: syncLimitOption
|
|
156
141
|
},
|
|
157
|
-
({ actor, filter, includePins, postFilter, postFilterJson, store, quiet, refresh }) =>
|
|
142
|
+
({ actor, filter, includePins, postFilter, postFilterJson, store, quiet, refresh, limit }) =>
|
|
158
143
|
Effect.gen(function* () {
|
|
159
|
-
const resolvedActor = yield* decodeActor(actor);
|
|
160
144
|
const apiFilter = Option.getOrUndefined(filter);
|
|
161
|
-
const source = DataSource.author(
|
|
145
|
+
const source = DataSource.author(actor, {
|
|
162
146
|
...(apiFilter !== undefined ? { filter: apiFilter } : {}),
|
|
163
147
|
...(includePins ? { includePins: true } : {})
|
|
164
148
|
});
|
|
165
149
|
const run = makeSyncCommandBody("author", () => source, {
|
|
166
|
-
actor
|
|
150
|
+
actor,
|
|
167
151
|
...(apiFilter !== undefined ? { filter: apiFilter } : {}),
|
|
168
152
|
...(includePins ? { includePins: true } : {})
|
|
169
153
|
});
|
|
@@ -172,7 +156,8 @@ const authorCommand = Command.make(
|
|
|
172
156
|
filter: postFilter,
|
|
173
157
|
filterJson: postFilterJson,
|
|
174
158
|
quiet,
|
|
175
|
-
refresh
|
|
159
|
+
refresh,
|
|
160
|
+
limit
|
|
176
161
|
});
|
|
177
162
|
})
|
|
178
163
|
).pipe(
|
|
@@ -198,19 +183,13 @@ const threadCommand = Command.make(
|
|
|
198
183
|
filter: filterOption,
|
|
199
184
|
filterJson: filterJsonOption,
|
|
200
185
|
quiet: quietOption,
|
|
201
|
-
refresh: refreshOption
|
|
186
|
+
refresh: refreshOption,
|
|
187
|
+
limit: syncLimitOption
|
|
202
188
|
},
|
|
203
|
-
({ uri, depth, parentHeight, filter, filterJson, store, quiet, refresh }) =>
|
|
189
|
+
({ uri, depth, parentHeight, filter, filterJson, store, quiet, refresh, limit }) =>
|
|
204
190
|
Effect.gen(function* () {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
parentHeight,
|
|
208
|
-
"parent-height",
|
|
209
|
-
0,
|
|
210
|
-
1000
|
|
211
|
-
);
|
|
212
|
-
const depthValue = Option.getOrUndefined(parsedDepth);
|
|
213
|
-
const parentHeightValue = Option.getOrUndefined(parsedParentHeight);
|
|
191
|
+
const { depth: depthValue, parentHeight: parentHeightValue } =
|
|
192
|
+
parseThreadDepth(depth, parentHeight);
|
|
214
193
|
const source = DataSource.thread(uri, {
|
|
215
194
|
...(depthValue !== undefined ? { depth: depthValue } : {}),
|
|
216
195
|
...(parentHeightValue !== undefined ? { parentHeight: parentHeightValue } : {})
|
|
@@ -220,7 +199,7 @@ const threadCommand = Command.make(
|
|
|
220
199
|
...(depthValue !== undefined ? { depth: depthValue } : {}),
|
|
221
200
|
...(parentHeightValue !== undefined ? { parentHeight: parentHeightValue } : {})
|
|
222
201
|
});
|
|
223
|
-
return yield* run({ store, filter, filterJson, quiet, refresh });
|
|
202
|
+
return yield* run({ store, filter, filterJson, quiet, refresh, limit });
|
|
224
203
|
})
|
|
225
204
|
).pipe(
|
|
226
205
|
Command.withDescription(
|
|
@@ -248,7 +227,7 @@ const jetstreamCommand = Command.make(
|
|
|
248
227
|
cursor: jetstreamOptions.cursor,
|
|
249
228
|
compress: jetstreamOptions.compress,
|
|
250
229
|
maxMessageSize: jetstreamOptions.maxMessageSize,
|
|
251
|
-
limit:
|
|
230
|
+
limit: jetstreamLimitOption,
|
|
252
231
|
duration: durationOption,
|
|
253
232
|
strict: strictOption,
|
|
254
233
|
maxErrors: maxErrorsOption
|
|
@@ -273,6 +252,7 @@ const jetstreamCommand = Command.make(
|
|
|
273
252
|
const monitor = yield* ResourceMonitor;
|
|
274
253
|
const output = yield* CliOutput;
|
|
275
254
|
const outputManager = yield* OutputManager;
|
|
255
|
+
const index = yield* StoreIndex;
|
|
276
256
|
const storeRef = yield* storeOptions.loadStoreRef(store);
|
|
277
257
|
const expr = yield* parseFilterExpr(filter, filterJson);
|
|
278
258
|
const filterHash = filterExprSignature(expr);
|
|
@@ -288,10 +268,8 @@ const jetstreamCommand = Command.make(
|
|
|
288
268
|
storeRef,
|
|
289
269
|
filterHash
|
|
290
270
|
);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
const parsedMaxErrors = yield* parseMaxErrors(maxErrors);
|
|
294
|
-
if (Option.isNone(parsedLimit) && Option.isNone(parsedDuration)) {
|
|
271
|
+
const parsedDuration = duration;
|
|
272
|
+
if (Option.isNone(limit) && Option.isNone(parsedDuration)) {
|
|
295
273
|
return yield* CliInputError.make({
|
|
296
274
|
message:
|
|
297
275
|
"Jetstream sync requires --limit or --duration. Use watch jetstream for continuous streaming.",
|
|
@@ -307,9 +285,9 @@ const jetstreamCommand = Command.make(
|
|
|
307
285
|
});
|
|
308
286
|
const result = yield* Effect.gen(function* () {
|
|
309
287
|
const engine = yield* JetstreamSyncEngine;
|
|
310
|
-
const limitValue = Option.getOrUndefined(
|
|
288
|
+
const limitValue = Option.getOrUndefined(limit);
|
|
311
289
|
const durationValue = Option.getOrUndefined(parsedDuration);
|
|
312
|
-
const maxErrorsValue = Option.getOrUndefined(
|
|
290
|
+
const maxErrorsValue = Option.getOrUndefined(maxErrors);
|
|
313
291
|
return yield* engine.sync({
|
|
314
292
|
source: selection.source,
|
|
315
293
|
store: storeRef,
|
|
@@ -332,8 +310,9 @@ const jetstreamCommand = Command.make(
|
|
|
332
310
|
filters: materialized.filters.map((spec) => spec.name)
|
|
333
311
|
});
|
|
334
312
|
}
|
|
313
|
+
const totalPosts = yield* index.count(storeRef);
|
|
335
314
|
yield* logInfo("Sync complete", { source: "jetstream", store: storeRef.name });
|
|
336
|
-
yield* writeJson(result as SyncResult);
|
|
315
|
+
yield* writeJson({ ...(result as SyncResult), totalPosts });
|
|
337
316
|
})
|
|
338
317
|
).pipe(
|
|
339
318
|
Command.withDescription(
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Options } from "@effect/cli";
|
|
2
|
+
import { Option } from "effect";
|
|
3
|
+
import { boundedInt } from "./option-schemas.js";
|
|
4
|
+
|
|
5
|
+
export const depthOption = (description: string) =>
|
|
6
|
+
Options.integer("depth").pipe(
|
|
7
|
+
Options.withSchema(boundedInt(0, 1000)),
|
|
8
|
+
Options.withDescription(description),
|
|
9
|
+
Options.optional
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export const parentHeightOption = (description: string) =>
|
|
13
|
+
Options.integer("parent-height").pipe(
|
|
14
|
+
Options.withSchema(boundedInt(0, 1000)),
|
|
15
|
+
Options.withDescription(description),
|
|
16
|
+
Options.optional
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export const parseThreadDepth = (
|
|
20
|
+
depth: Option.Option<number>,
|
|
21
|
+
parentHeight: Option.Option<number>
|
|
22
|
+
) => ({
|
|
23
|
+
depth: Option.getOrUndefined(depth),
|
|
24
|
+
parentHeight: Option.getOrUndefined(parentHeight)
|
|
25
|
+
});
|