@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/time.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Duration, Effect, Schema } from "effect";
|
|
2
|
+
import { Timestamp } from "../domain/primitives.js";
|
|
3
|
+
import { CliInputError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
export type TimeParseError = (message: string, cause?: unknown) => CliInputError;
|
|
6
|
+
|
|
7
|
+
type TimeParseOptions = {
|
|
8
|
+
readonly label?: string;
|
|
9
|
+
readonly onError?: TimeParseError;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const defaultError: TimeParseError = (message, cause) =>
|
|
13
|
+
CliInputError.make({ message, cause });
|
|
14
|
+
|
|
15
|
+
const compactDurationPattern = /^(-?\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/i;
|
|
16
|
+
const compactDurationUnits: Record<string, number> = {
|
|
17
|
+
ms: 1,
|
|
18
|
+
s: 1000,
|
|
19
|
+
m: 60_000,
|
|
20
|
+
h: 3_600_000,
|
|
21
|
+
d: 86_400_000,
|
|
22
|
+
w: 604_800_000
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const parseCompactDurationMillis = (raw: string): number | undefined => {
|
|
26
|
+
const match = raw.match(compactDurationPattern);
|
|
27
|
+
if (!match) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const value = Number.parseFloat(match[1] ?? "");
|
|
31
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
32
|
+
const multiplier = compactDurationUnits[unit];
|
|
33
|
+
if (!Number.isFinite(value) || multiplier === undefined) {
|
|
34
|
+
return Number.NaN;
|
|
35
|
+
}
|
|
36
|
+
return value * multiplier;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const toUtcStartOfDay = (date: Date) =>
|
|
40
|
+
new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
41
|
+
|
|
42
|
+
const parseDateOnly = (raw: string): Date | undefined => {
|
|
43
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw);
|
|
44
|
+
if (!match) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const year = Number.parseInt(match[1] ?? "", 10);
|
|
48
|
+
const month = Number.parseInt(match[2] ?? "", 10);
|
|
49
|
+
const day = Number.parseInt(match[3] ?? "", 10);
|
|
50
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
54
|
+
if (
|
|
55
|
+
date.getUTCFullYear() !== year ||
|
|
56
|
+
date.getUTCMonth() !== month - 1 ||
|
|
57
|
+
date.getUTCDate() !== day
|
|
58
|
+
) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return date;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const looksLikeDate = (raw: string) => /^\d{4}-\d{2}-\d{2}/.test(raw);
|
|
65
|
+
|
|
66
|
+
const hasExplicitTimezone = (raw: string) => /([zZ]|[+-]\d{2}:\d{2})$/.test(raw);
|
|
67
|
+
|
|
68
|
+
const makeErrorFactory = (options?: TimeParseOptions): TimeParseError => {
|
|
69
|
+
const base = options?.onError ?? defaultError;
|
|
70
|
+
const label = options?.label;
|
|
71
|
+
if (!label) {
|
|
72
|
+
return base;
|
|
73
|
+
}
|
|
74
|
+
return (message, cause) => base(`${label}: ${message}`, cause);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const parseDurationInput = (
|
|
78
|
+
raw: string,
|
|
79
|
+
options?: TimeParseOptions
|
|
80
|
+
): Effect.Effect<Duration.Duration, CliInputError> =>
|
|
81
|
+
Effect.suspend(() => {
|
|
82
|
+
const onError = makeErrorFactory(options);
|
|
83
|
+
const trimmed = raw.trim();
|
|
84
|
+
if (trimmed.length === 0) {
|
|
85
|
+
return Effect.fail(onError("Duration cannot be empty."));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const compactMillis = parseCompactDurationMillis(trimmed);
|
|
89
|
+
if (compactMillis !== undefined) {
|
|
90
|
+
if (!Number.isFinite(compactMillis)) {
|
|
91
|
+
return Effect.fail(onError("Duration must be a finite number."));
|
|
92
|
+
}
|
|
93
|
+
if (compactMillis < 0) {
|
|
94
|
+
return Effect.fail(onError("Duration must be non-negative."));
|
|
95
|
+
}
|
|
96
|
+
return Effect.succeed(Duration.millis(compactMillis));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Effect.try({
|
|
100
|
+
try: () => Duration.decode(trimmed as Duration.DurationInput),
|
|
101
|
+
catch: (cause) =>
|
|
102
|
+
onError(
|
|
103
|
+
`Invalid duration "${raw}". Use formats like "30 seconds", "500 millis", or "1.5h".`,
|
|
104
|
+
cause
|
|
105
|
+
)
|
|
106
|
+
}).pipe(
|
|
107
|
+
Effect.flatMap((duration) => {
|
|
108
|
+
if (!Duration.isFinite(duration)) {
|
|
109
|
+
return Effect.fail(onError("Duration must be finite."));
|
|
110
|
+
}
|
|
111
|
+
if (Duration.toMillis(duration) < 0) {
|
|
112
|
+
return Effect.fail(onError("Duration must be non-negative."));
|
|
113
|
+
}
|
|
114
|
+
return Effect.succeed(duration);
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const parseTimeInput = (
|
|
120
|
+
raw: string,
|
|
121
|
+
now: Date,
|
|
122
|
+
options?: TimeParseOptions
|
|
123
|
+
): Effect.Effect<Date, CliInputError> =>
|
|
124
|
+
Effect.suspend(() => {
|
|
125
|
+
const onError = makeErrorFactory(options);
|
|
126
|
+
const trimmed = raw.trim();
|
|
127
|
+
if (trimmed.length === 0) {
|
|
128
|
+
return Effect.fail(onError("Time value cannot be empty."));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const lower = trimmed.toLowerCase();
|
|
132
|
+
if (lower === "now") {
|
|
133
|
+
return Effect.succeed(new Date(now.getTime()));
|
|
134
|
+
}
|
|
135
|
+
if (lower === "today") {
|
|
136
|
+
return Effect.succeed(toUtcStartOfDay(now));
|
|
137
|
+
}
|
|
138
|
+
if (lower === "yesterday") {
|
|
139
|
+
return Effect.succeed(new Date(toUtcStartOfDay(now).getTime() - 86_400_000));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const dateOnly = parseDateOnly(trimmed);
|
|
143
|
+
if (dateOnly) {
|
|
144
|
+
return Effect.succeed(dateOnly);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (looksLikeDate(trimmed)) {
|
|
148
|
+
if (/[Tt]/.test(trimmed) && !hasExplicitTimezone(trimmed)) {
|
|
149
|
+
return Effect.fail(
|
|
150
|
+
onError(
|
|
151
|
+
"Timestamp must include a timezone (e.g. 2026-01-01T00:00:00Z)."
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return Schema.decodeUnknown(Timestamp)(trimmed).pipe(
|
|
156
|
+
Effect.mapError((cause) =>
|
|
157
|
+
onError(
|
|
158
|
+
`Invalid timestamp "${raw}". Expected ISO 8601 with timezone (e.g. 2026-01-01T00:00:00Z).`,
|
|
159
|
+
cause
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return parseDurationInput(trimmed, options).pipe(
|
|
166
|
+
Effect.map((duration) => {
|
|
167
|
+
const millis = Duration.toMillis(duration);
|
|
168
|
+
return new Date(now.getTime() - millis);
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
});
|
package/src/cli/view-thread.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
-
import { Chunk, Console, Effect, Option,
|
|
2
|
+
import { Chunk, Console, Effect, Option, Stream } from "effect";
|
|
3
3
|
import { PostUri, StoreName } from "../domain/primitives.js";
|
|
4
4
|
import type { Post } from "../domain/post.js";
|
|
5
5
|
import { all } from "../domain/filter.js";
|
|
@@ -11,15 +11,20 @@ import { StoreIndex } from "../services/store-index.js";
|
|
|
11
11
|
import { SyncEngine } from "../services/sync-engine.js";
|
|
12
12
|
import { renderThread } from "./doc/thread.js";
|
|
13
13
|
import { renderPlain, renderAnsi } from "./doc/render.js";
|
|
14
|
-
import { writeJson, writeText } from "./output.js";
|
|
14
|
+
import { CliOutput, writeJson, writeText } from "./output.js";
|
|
15
15
|
import { storeOptions } from "./store.js";
|
|
16
16
|
import { withExamples } from "./help.js";
|
|
17
17
|
import { CliInputError } from "./errors.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
depthOption as threadDepthOption,
|
|
20
|
+
parentHeightOption as threadParentHeightOption,
|
|
21
|
+
parseThreadDepth
|
|
22
|
+
} from "./thread-options.js";
|
|
20
23
|
import { textJsonFormats } from "./output-format.js";
|
|
24
|
+
import { PositiveInt } from "./option-schemas.js";
|
|
21
25
|
|
|
22
26
|
const uriArg = Args.text({ name: "uri" }).pipe(
|
|
27
|
+
Args.withSchema(PostUri),
|
|
23
28
|
Args.withDescription("AT-URI of any post in the thread")
|
|
24
29
|
);
|
|
25
30
|
|
|
@@ -38,6 +43,7 @@ const ansiOption = Options.boolean("ansi").pipe(
|
|
|
38
43
|
);
|
|
39
44
|
|
|
40
45
|
const widthOption = Options.integer("width").pipe(
|
|
46
|
+
Options.withSchema(PositiveInt),
|
|
41
47
|
Options.withDescription("Line width for terminal output"),
|
|
42
48
|
Options.optional
|
|
43
49
|
);
|
|
@@ -47,14 +53,9 @@ const formatOption = Options.choice("format", textJsonFormats).pipe(
|
|
|
47
53
|
Options.optional
|
|
48
54
|
);
|
|
49
55
|
|
|
50
|
-
const depthOption =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const parentHeightOption = Options.integer("parent-height").pipe(
|
|
56
|
-
Options.withDescription("Parent height (API only, default: 80)"),
|
|
57
|
-
Options.optional
|
|
56
|
+
const depthOption = threadDepthOption("Reply depth (API only, default: 6)");
|
|
57
|
+
const parentHeightOption = threadParentHeightOption(
|
|
58
|
+
"Parent height (API only, default: 80)"
|
|
58
59
|
);
|
|
59
60
|
|
|
60
61
|
export const threadCommand = Command.make(
|
|
@@ -71,32 +72,28 @@ export const threadCommand = Command.make(
|
|
|
71
72
|
},
|
|
72
73
|
({ uri, store, compact, ansi, width, format, depth, parentHeight }) =>
|
|
73
74
|
Effect.gen(function* () {
|
|
75
|
+
const output = yield* CliOutput;
|
|
74
76
|
const outputFormat = Option.getOrElse(format, () => "text" as const);
|
|
75
77
|
const w = Option.getOrUndefined(width);
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
0,
|
|
81
|
-
1000
|
|
82
|
-
);
|
|
83
|
-
const d = Option.getOrElse(parsedDepth, () => 6);
|
|
84
|
-
const ph = Option.getOrElse(parsedParentHeight, () => 80);
|
|
78
|
+
const { depth: depthValue, parentHeight: parentHeightValue } =
|
|
79
|
+
parseThreadDepth(depth, parentHeight);
|
|
80
|
+
const d = depthValue ?? 6;
|
|
81
|
+
const ph = parentHeightValue ?? 80;
|
|
85
82
|
|
|
86
83
|
let posts: ReadonlyArray<Post>;
|
|
87
84
|
|
|
88
85
|
if (Option.isSome(store)) {
|
|
89
86
|
const index = yield* StoreIndex;
|
|
90
87
|
const storeRef = yield* storeOptions.loadStoreRef(store.value);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const hasTarget = yield* index.hasUri(storeRef,
|
|
88
|
+
const totalPosts = yield* index.count(storeRef);
|
|
89
|
+
if (totalPosts > 20000) {
|
|
90
|
+
yield* output
|
|
91
|
+
.writeStderr(
|
|
92
|
+
`ℹ️ Store ${storeRef.name} has ${totalPosts} posts. Thread rendering will load all posts into memory.`
|
|
93
|
+
)
|
|
94
|
+
.pipe(Effect.catchAll(() => Effect.void));
|
|
95
|
+
}
|
|
96
|
+
const hasTarget = yield* index.hasUri(storeRef, uri);
|
|
100
97
|
if (!hasTarget) {
|
|
101
98
|
const engine = yield* SyncEngine;
|
|
102
99
|
const source = DataSource.thread(uri, { depth: d, parentHeight: ph });
|
|
@@ -106,7 +103,7 @@ export const threadCommand = Command.make(
|
|
|
106
103
|
const stream = index.query(storeRef, query);
|
|
107
104
|
const collected = yield* Stream.runCollect(stream);
|
|
108
105
|
const allPosts = Chunk.toReadonlyArray(collected);
|
|
109
|
-
const threadPosts = selectThreadPosts(allPosts, String(
|
|
106
|
+
const threadPosts = selectThreadPosts(allPosts, String(uri));
|
|
110
107
|
if (threadPosts.length === 0) {
|
|
111
108
|
return yield* CliInputError.make({
|
|
112
109
|
message: `Thread not found for ${uri}.`,
|
|
@@ -114,7 +111,7 @@ export const threadCommand = Command.make(
|
|
|
114
111
|
});
|
|
115
112
|
}
|
|
116
113
|
// B1: Hint when only root post exists in store
|
|
117
|
-
if (threadPosts.length === 1 && threadPosts[0]?.uri ===
|
|
114
|
+
if (threadPosts.length === 1 && threadPosts[0]?.uri === uri) {
|
|
118
115
|
yield* Console.log("\nℹ️ Only root post found in store. Use --no-store to fetch full thread from API.\n");
|
|
119
116
|
}
|
|
120
117
|
posts = threadPosts;
|
package/src/cli/watch.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { ResourceMonitor } from "../services/resource-monitor.js";
|
|
|
13
13
|
import { withExamples } from "./help.js";
|
|
14
14
|
import { buildJetstreamSelection, jetstreamOptions } from "./jetstream.js";
|
|
15
15
|
import { makeWatchCommandBody } from "./sync-factory.js";
|
|
16
|
+
import { parseOptionalDuration } from "./interval.js";
|
|
16
17
|
import {
|
|
17
18
|
feedUriArg,
|
|
18
19
|
listUriArg,
|
|
@@ -25,29 +26,41 @@ import {
|
|
|
25
26
|
postFilterJsonOption,
|
|
26
27
|
authorFilterOption,
|
|
27
28
|
includePinsOption,
|
|
28
|
-
decodeActor,
|
|
29
29
|
quietOption,
|
|
30
30
|
refreshOption,
|
|
31
31
|
strictOption,
|
|
32
|
-
maxErrorsOption
|
|
33
|
-
parseMaxErrors,
|
|
34
|
-
parseBoundedIntOption
|
|
32
|
+
maxErrorsOption
|
|
35
33
|
} from "./shared-options.js";
|
|
34
|
+
import {
|
|
35
|
+
depthOption as threadDepthOption,
|
|
36
|
+
parentHeightOption as threadParentHeightOption,
|
|
37
|
+
parseThreadDepth
|
|
38
|
+
} from "./thread-options.js";
|
|
39
|
+
import { DurationInput, PositiveInt } from "./option-schemas.js";
|
|
36
40
|
|
|
37
41
|
const intervalOption = Options.text("interval").pipe(
|
|
42
|
+
Options.withSchema(DurationInput),
|
|
38
43
|
Options.withDescription(
|
|
39
44
|
"Polling interval (e.g. \"30 seconds\", \"500 millis\") (default: 30 seconds)"
|
|
40
45
|
),
|
|
41
46
|
Options.optional
|
|
42
47
|
);
|
|
43
|
-
const
|
|
44
|
-
Options.
|
|
48
|
+
const maxCyclesOption = Options.integer("max-cycles").pipe(
|
|
49
|
+
Options.withSchema(PositiveInt),
|
|
50
|
+
Options.withDescription("Stop after N watch cycles"),
|
|
45
51
|
Options.optional
|
|
46
52
|
);
|
|
47
|
-
const
|
|
48
|
-
Options.
|
|
53
|
+
const untilOption = Options.text("until").pipe(
|
|
54
|
+
Options.withSchema(DurationInput),
|
|
55
|
+
Options.withDescription("Stop after a duration (e.g. \"10 minutes\")"),
|
|
49
56
|
Options.optional
|
|
50
57
|
);
|
|
58
|
+
const depthOption = threadDepthOption(
|
|
59
|
+
"Thread reply depth to include (0-1000, default 6)"
|
|
60
|
+
);
|
|
61
|
+
const parentHeightOption = threadParentHeightOption(
|
|
62
|
+
"Thread parent height to include (0-1000, default 80)"
|
|
63
|
+
);
|
|
51
64
|
|
|
52
65
|
const timelineCommand = Command.make(
|
|
53
66
|
"timeline",
|
|
@@ -56,6 +69,8 @@ const timelineCommand = Command.make(
|
|
|
56
69
|
filter: filterOption,
|
|
57
70
|
filterJson: filterJsonOption,
|
|
58
71
|
interval: intervalOption,
|
|
72
|
+
maxCycles: maxCyclesOption,
|
|
73
|
+
until: untilOption,
|
|
59
74
|
quiet: quietOption,
|
|
60
75
|
refresh: refreshOption
|
|
61
76
|
},
|
|
@@ -81,6 +96,8 @@ const feedCommand = Command.make(
|
|
|
81
96
|
filter: filterOption,
|
|
82
97
|
filterJson: filterJsonOption,
|
|
83
98
|
interval: intervalOption,
|
|
99
|
+
maxCycles: maxCyclesOption,
|
|
100
|
+
until: untilOption,
|
|
84
101
|
quiet: quietOption,
|
|
85
102
|
refresh: refreshOption
|
|
86
103
|
},
|
|
@@ -105,6 +122,8 @@ const listCommand = Command.make(
|
|
|
105
122
|
filter: filterOption,
|
|
106
123
|
filterJson: filterJsonOption,
|
|
107
124
|
interval: intervalOption,
|
|
125
|
+
maxCycles: maxCyclesOption,
|
|
126
|
+
until: untilOption,
|
|
108
127
|
quiet: quietOption,
|
|
109
128
|
refresh: refreshOption
|
|
110
129
|
},
|
|
@@ -128,6 +147,8 @@ const notificationsCommand = Command.make(
|
|
|
128
147
|
filter: filterOption,
|
|
129
148
|
filterJson: filterJsonOption,
|
|
130
149
|
interval: intervalOption,
|
|
150
|
+
maxCycles: maxCyclesOption,
|
|
151
|
+
until: untilOption,
|
|
131
152
|
quiet: quietOption,
|
|
132
153
|
refresh: refreshOption
|
|
133
154
|
},
|
|
@@ -152,19 +173,20 @@ const authorCommand = Command.make(
|
|
|
152
173
|
postFilter: postFilterOption,
|
|
153
174
|
postFilterJson: postFilterJsonOption,
|
|
154
175
|
interval: intervalOption,
|
|
176
|
+
maxCycles: maxCyclesOption,
|
|
177
|
+
until: untilOption,
|
|
155
178
|
quiet: quietOption,
|
|
156
179
|
refresh: refreshOption
|
|
157
180
|
},
|
|
158
|
-
({ actor, filter, includePins, postFilter, postFilterJson, interval, store, quiet, refresh }) =>
|
|
181
|
+
({ actor, filter, includePins, postFilter, postFilterJson, interval, maxCycles, until, store, quiet, refresh }) =>
|
|
159
182
|
Effect.gen(function* () {
|
|
160
|
-
const resolvedActor = yield* decodeActor(actor);
|
|
161
183
|
const apiFilter = Option.getOrUndefined(filter);
|
|
162
|
-
const source = DataSource.author(
|
|
184
|
+
const source = DataSource.author(actor, {
|
|
163
185
|
...(apiFilter !== undefined ? { filter: apiFilter } : {}),
|
|
164
186
|
...(includePins ? { includePins: true } : {})
|
|
165
187
|
});
|
|
166
188
|
const run = makeWatchCommandBody("author", () => source, {
|
|
167
|
-
actor
|
|
189
|
+
actor,
|
|
168
190
|
...(apiFilter !== undefined ? { filter: apiFilter } : {}),
|
|
169
191
|
...(includePins ? { includePins: true } : {})
|
|
170
192
|
});
|
|
@@ -173,6 +195,8 @@ const authorCommand = Command.make(
|
|
|
173
195
|
filter: postFilter,
|
|
174
196
|
filterJson: postFilterJson,
|
|
175
197
|
interval,
|
|
198
|
+
maxCycles,
|
|
199
|
+
until,
|
|
176
200
|
quiet,
|
|
177
201
|
refresh
|
|
178
202
|
});
|
|
@@ -200,20 +224,15 @@ const threadCommand = Command.make(
|
|
|
200
224
|
filter: filterOption,
|
|
201
225
|
filterJson: filterJsonOption,
|
|
202
226
|
interval: intervalOption,
|
|
227
|
+
maxCycles: maxCyclesOption,
|
|
228
|
+
until: untilOption,
|
|
203
229
|
quiet: quietOption,
|
|
204
230
|
refresh: refreshOption
|
|
205
231
|
},
|
|
206
|
-
({ uri, depth, parentHeight, filter, filterJson, interval, store, quiet, refresh }) =>
|
|
232
|
+
({ uri, depth, parentHeight, filter, filterJson, interval, maxCycles, until, store, quiet, refresh }) =>
|
|
207
233
|
Effect.gen(function* () {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
parentHeight,
|
|
211
|
-
"parent-height",
|
|
212
|
-
0,
|
|
213
|
-
1000
|
|
214
|
-
);
|
|
215
|
-
const depthValue = Option.getOrUndefined(parsedDepth);
|
|
216
|
-
const parentHeightValue = Option.getOrUndefined(parsedParentHeight);
|
|
234
|
+
const { depth: depthValue, parentHeight: parentHeightValue } =
|
|
235
|
+
parseThreadDepth(depth, parentHeight);
|
|
217
236
|
const source = DataSource.thread(uri, {
|
|
218
237
|
...(depthValue !== undefined ? { depth: depthValue } : {}),
|
|
219
238
|
...(parentHeightValue !== undefined ? { parentHeight: parentHeightValue } : {})
|
|
@@ -223,7 +242,7 @@ const threadCommand = Command.make(
|
|
|
223
242
|
...(depthValue !== undefined ? { depth: depthValue } : {}),
|
|
224
243
|
...(parentHeightValue !== undefined ? { parentHeight: parentHeightValue } : {})
|
|
225
244
|
});
|
|
226
|
-
return yield* run({ store, filter, filterJson, interval, quiet, refresh });
|
|
245
|
+
return yield* run({ store, filter, filterJson, interval, maxCycles, until, quiet, refresh });
|
|
227
246
|
})
|
|
228
247
|
).pipe(
|
|
229
248
|
Command.withDescription(
|
|
@@ -251,6 +270,8 @@ const jetstreamCommand = Command.make(
|
|
|
251
270
|
cursor: jetstreamOptions.cursor,
|
|
252
271
|
compress: jetstreamOptions.compress,
|
|
253
272
|
maxMessageSize: jetstreamOptions.maxMessageSize,
|
|
273
|
+
maxCycles: maxCyclesOption,
|
|
274
|
+
until: untilOption,
|
|
254
275
|
strict: strictOption,
|
|
255
276
|
maxErrors: maxErrorsOption
|
|
256
277
|
},
|
|
@@ -265,6 +286,8 @@ const jetstreamCommand = Command.make(
|
|
|
265
286
|
cursor,
|
|
266
287
|
compress,
|
|
267
288
|
maxMessageSize,
|
|
289
|
+
maxCycles,
|
|
290
|
+
until,
|
|
268
291
|
strict,
|
|
269
292
|
maxErrors
|
|
270
293
|
}) =>
|
|
@@ -286,14 +309,14 @@ const jetstreamCommand = Command.make(
|
|
|
286
309
|
storeRef,
|
|
287
310
|
filterHash
|
|
288
311
|
);
|
|
289
|
-
const
|
|
312
|
+
const parsedUntil = parseOptionalDuration(until);
|
|
290
313
|
const engineLayer = JetstreamSyncEngine.layer.pipe(
|
|
291
314
|
Layer.provideMerge(Jetstream.live(selection.config))
|
|
292
315
|
);
|
|
293
316
|
yield* logInfo("Starting watch", { source: "jetstream", store: storeRef.name });
|
|
294
317
|
yield* Effect.gen(function* () {
|
|
295
318
|
const engine = yield* JetstreamSyncEngine;
|
|
296
|
-
const maxErrorsValue = Option.getOrUndefined(
|
|
319
|
+
const maxErrorsValue = Option.getOrUndefined(maxErrors);
|
|
297
320
|
const stream = engine.watch({
|
|
298
321
|
source: selection.source,
|
|
299
322
|
store: storeRef,
|
|
@@ -310,7 +333,13 @@ const jetstreamCommand = Command.make(
|
|
|
310
333
|
makeSyncReporter(quiet, monitor, output)
|
|
311
334
|
)
|
|
312
335
|
);
|
|
313
|
-
|
|
336
|
+
const limited = Option.isSome(maxCycles)
|
|
337
|
+
? outputStream.pipe(Stream.take(maxCycles.value))
|
|
338
|
+
: outputStream;
|
|
339
|
+
const timed = Option.isSome(parsedUntil)
|
|
340
|
+
? limited.pipe(Stream.interruptWhen(Effect.sleep(parsedUntil.value)))
|
|
341
|
+
: limited;
|
|
342
|
+
return yield* writeJsonStream(timed);
|
|
314
343
|
}).pipe(Effect.provide(engineLayer));
|
|
315
344
|
})
|
|
316
345
|
).pipe(
|
package/src/domain/errors.ts
CHANGED
|
@@ -42,6 +42,11 @@ export class StoreNotFound extends Schema.TaggedError<StoreNotFound>()(
|
|
|
42
42
|
{ name: StoreName }
|
|
43
43
|
) {}
|
|
44
44
|
|
|
45
|
+
export class StoreAlreadyExists extends Schema.TaggedError<StoreAlreadyExists>()(
|
|
46
|
+
"StoreAlreadyExists",
|
|
47
|
+
{ name: StoreName }
|
|
48
|
+
) {}
|
|
49
|
+
|
|
45
50
|
export class StoreIoError extends Schema.TaggedError<StoreIoError>()(
|
|
46
51
|
"StoreIoError",
|
|
47
52
|
{ path: StorePath, cause: Schema.Unknown }
|
|
@@ -68,4 +73,4 @@ export class FilterLibraryError extends Schema.TaggedError<FilterLibraryError>()
|
|
|
68
73
|
}
|
|
69
74
|
) {}
|
|
70
75
|
|
|
71
|
-
export type StoreError = StoreNotFound | StoreIoError | StoreIndexError;
|
|
76
|
+
export type StoreError = StoreNotFound | StoreAlreadyExists | StoreIoError | StoreIndexError;
|
package/src/domain/format.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Post } from "./post.js";
|
|
|
2
2
|
import { displayWidth, padEndDisplay } from "./text-width.js";
|
|
3
3
|
|
|
4
4
|
const headers = ["Created At", "Author", "Text", "URI"];
|
|
5
|
+
const headersWithStore = ["Store", ...headers];
|
|
5
6
|
const textLimit = 80;
|
|
6
7
|
|
|
7
8
|
export const normalizeWhitespace = (text: string) =>
|
|
@@ -41,6 +42,16 @@ const postToMarkdownRow = (post: Post) => [
|
|
|
41
42
|
post.uri.replace(/\|/g, "\\|")
|
|
42
43
|
];
|
|
43
44
|
|
|
45
|
+
const storePostToRow = (entry: { readonly store: string; readonly post: Post }) => [
|
|
46
|
+
entry.store,
|
|
47
|
+
...postToRow(entry.post)
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const storePostToMarkdownRow = (entry: { readonly store: string; readonly post: Post }) => [
|
|
51
|
+
entry.store,
|
|
52
|
+
...postToMarkdownRow(entry.post)
|
|
53
|
+
];
|
|
54
|
+
|
|
44
55
|
const renderTable = (
|
|
45
56
|
head: ReadonlyArray<string>,
|
|
46
57
|
rows: ReadonlyArray<ReadonlyArray<string>>
|
|
@@ -89,3 +100,13 @@ export const renderPostsTable = (posts: ReadonlyArray<Post>) =>
|
|
|
89
100
|
|
|
90
101
|
export const renderPostsMarkdown = (posts: ReadonlyArray<Post>) =>
|
|
91
102
|
renderMarkdownTable(headers, posts.map(postToMarkdownRow));
|
|
103
|
+
|
|
104
|
+
export const renderStorePostsTable = (
|
|
105
|
+
entries: ReadonlyArray<{ readonly store: string; readonly post: Post }>
|
|
106
|
+
) =>
|
|
107
|
+
renderTable(headersWithStore, entries.map(storePostToRow));
|
|
108
|
+
|
|
109
|
+
export const renderStorePostsMarkdown = (
|
|
110
|
+
entries: ReadonlyArray<{ readonly store: string; readonly post: Post }>
|
|
111
|
+
) =>
|
|
112
|
+
renderMarkdownTable(headersWithStore, entries.map(storePostToMarkdownRow));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Order } from "effect";
|
|
2
|
+
import type { Post } from "./post.js";
|
|
3
|
+
import type { StoreRef } from "./store.js";
|
|
4
|
+
|
|
5
|
+
export const LocaleStringOrder = Order.make<string>((left, right) => {
|
|
6
|
+
const result = left.localeCompare(right);
|
|
7
|
+
if (result < 0) return -1;
|
|
8
|
+
if (result > 0) return 1;
|
|
9
|
+
return 0;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const PostOrder = Order.mapInput(
|
|
13
|
+
Order.tuple(Order.Date, LocaleStringOrder),
|
|
14
|
+
(post: Post) => [post.createdAt, post.uri] as const
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const StorePostOrder = Order.mapInput(
|
|
18
|
+
Order.tuple(Order.Date, LocaleStringOrder, LocaleStringOrder),
|
|
19
|
+
(entry: { readonly post: Post; readonly store: StoreRef }) =>
|
|
20
|
+
[entry.post.createdAt, entry.post.uri, entry.store.name] as const
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const updatedAtOrder = <A extends { readonly updatedAt: Date }>() =>
|
|
24
|
+
Order.mapInput(Order.Date, (value: A) => value.updatedAt);
|
package/src/domain/primitives.ts
CHANGED
|
@@ -16,18 +16,35 @@ export const Hashtag = Schema.String.pipe(
|
|
|
16
16
|
);
|
|
17
17
|
export type Hashtag = typeof Hashtag.Type;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const atUriPattern = /^at:\/\/\S+$/;
|
|
20
|
+
|
|
21
|
+
export const AtUri = Schema.String.pipe(
|
|
22
|
+
Schema.pattern(atUriPattern),
|
|
23
|
+
Schema.brand("AtUri")
|
|
24
|
+
);
|
|
20
25
|
export type AtUri = typeof AtUri.Type;
|
|
21
26
|
|
|
22
|
-
export const PostUri = Schema.String.pipe(
|
|
27
|
+
export const PostUri = Schema.String.pipe(
|
|
28
|
+
Schema.pattern(atUriPattern),
|
|
29
|
+
Schema.brand("PostUri")
|
|
30
|
+
);
|
|
23
31
|
export type PostUri = typeof PostUri.Type;
|
|
24
32
|
|
|
25
33
|
export const PostCid = Schema.String.pipe(Schema.brand("PostCid"));
|
|
26
34
|
export type PostCid = typeof PostCid.Type;
|
|
27
35
|
|
|
28
|
-
export const Did = Schema.String.pipe(
|
|
36
|
+
export const Did = Schema.String.pipe(
|
|
37
|
+
Schema.pattern(/^did:\S+$/),
|
|
38
|
+
Schema.brand("Did")
|
|
39
|
+
);
|
|
29
40
|
export type Did = typeof Did.Type;
|
|
30
41
|
|
|
42
|
+
export const ActorId = Schema.String.pipe(
|
|
43
|
+
Schema.pattern(/^(did:\S+|[a-z0-9][a-z0-9.-]{1,251})$/),
|
|
44
|
+
Schema.brand("ActorId")
|
|
45
|
+
);
|
|
46
|
+
export type ActorId = typeof ActorId.Type;
|
|
47
|
+
|
|
31
48
|
export const Timestamp = Schema.Union(
|
|
32
49
|
Schema.DateFromString,
|
|
33
50
|
Schema.DateFromSelf
|