@mepuka/skygent 0.2.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 +59 -0
- package/index.ts +146 -0
- package/package.json +56 -0
- package/src/cli/app.ts +75 -0
- package/src/cli/config-command.ts +140 -0
- package/src/cli/config.ts +91 -0
- package/src/cli/derive.ts +205 -0
- package/src/cli/doc/annotation.ts +36 -0
- package/src/cli/doc/filter.ts +69 -0
- package/src/cli/doc/index.ts +9 -0
- package/src/cli/doc/post.ts +155 -0
- package/src/cli/doc/primitives.ts +25 -0
- package/src/cli/doc/render.ts +18 -0
- package/src/cli/doc/table.ts +114 -0
- package/src/cli/doc/thread.ts +46 -0
- package/src/cli/doc/tree.ts +126 -0
- package/src/cli/errors.ts +59 -0
- package/src/cli/exit-codes.ts +52 -0
- package/src/cli/feed.ts +177 -0
- package/src/cli/filter-dsl.ts +1411 -0
- package/src/cli/filter-errors.ts +208 -0
- package/src/cli/filter-help.ts +70 -0
- package/src/cli/filter-input.ts +54 -0
- package/src/cli/filter.ts +435 -0
- package/src/cli/graph.ts +472 -0
- package/src/cli/help.ts +14 -0
- package/src/cli/interval.ts +35 -0
- package/src/cli/jetstream.ts +173 -0
- package/src/cli/layers.ts +180 -0
- package/src/cli/logging.ts +136 -0
- package/src/cli/output-format.ts +26 -0
- package/src/cli/output.ts +82 -0
- package/src/cli/parse.ts +80 -0
- package/src/cli/post.ts +193 -0
- package/src/cli/preferences.ts +11 -0
- package/src/cli/query-fields.ts +247 -0
- package/src/cli/query.ts +415 -0
- package/src/cli/range.ts +44 -0
- package/src/cli/search.ts +465 -0
- package/src/cli/shared-options.ts +169 -0
- package/src/cli/shared.ts +20 -0
- package/src/cli/store-errors.ts +80 -0
- package/src/cli/store-tree.ts +392 -0
- package/src/cli/store.ts +395 -0
- package/src/cli/sync-factory.ts +107 -0
- package/src/cli/sync.ts +366 -0
- package/src/cli/view-thread.ts +196 -0
- package/src/cli/view.ts +47 -0
- package/src/cli/watch.ts +344 -0
- package/src/db/migrations/store-catalog/001_init.ts +14 -0
- package/src/db/migrations/store-index/001_init.ts +34 -0
- package/src/db/migrations/store-index/002_event_log.ts +24 -0
- package/src/db/migrations/store-index/003_fts_and_derived.ts +52 -0
- package/src/db/migrations/store-index/004_query_indexes.ts +9 -0
- package/src/db/migrations/store-index/005_post_lang.ts +15 -0
- package/src/db/migrations/store-index/006_has_embed.ts +10 -0
- package/src/db/migrations/store-index/007_event_seq_and_checkpoints.ts +68 -0
- package/src/domain/bsky.ts +467 -0
- package/src/domain/config.ts +11 -0
- package/src/domain/credentials.ts +6 -0
- package/src/domain/defaults.ts +8 -0
- package/src/domain/derivation.ts +55 -0
- package/src/domain/errors.ts +71 -0
- package/src/domain/events.ts +55 -0
- package/src/domain/extract.ts +64 -0
- package/src/domain/filter-describe.ts +551 -0
- package/src/domain/filter-explain.ts +9 -0
- package/src/domain/filter.ts +797 -0
- package/src/domain/format.ts +91 -0
- package/src/domain/index.ts +13 -0
- package/src/domain/indexes.ts +17 -0
- package/src/domain/policies.ts +16 -0
- package/src/domain/post.ts +88 -0
- package/src/domain/primitives.ts +50 -0
- package/src/domain/raw.ts +140 -0
- package/src/domain/store.ts +103 -0
- package/src/domain/sync.ts +211 -0
- package/src/domain/text-width.ts +56 -0
- package/src/services/app-config.ts +278 -0
- package/src/services/bsky-client.ts +2113 -0
- package/src/services/credential-store.ts +408 -0
- package/src/services/derivation-engine.ts +502 -0
- package/src/services/derivation-settings.ts +61 -0
- package/src/services/derivation-validator.ts +68 -0
- package/src/services/filter-compiler.ts +269 -0
- package/src/services/filter-library.ts +371 -0
- package/src/services/filter-runtime.ts +821 -0
- package/src/services/filter-settings.ts +30 -0
- package/src/services/identity-resolver.ts +563 -0
- package/src/services/jetstream-sync.ts +636 -0
- package/src/services/lineage-store.ts +89 -0
- package/src/services/link-validator.ts +244 -0
- package/src/services/output-manager.ts +274 -0
- package/src/services/post-parser.ts +62 -0
- package/src/services/profile-resolver.ts +223 -0
- package/src/services/resource-monitor.ts +106 -0
- package/src/services/shared.ts +69 -0
- package/src/services/store-cleaner.ts +43 -0
- package/src/services/store-commit.ts +168 -0
- package/src/services/store-db.ts +248 -0
- package/src/services/store-event-log.ts +285 -0
- package/src/services/store-index-sql.ts +289 -0
- package/src/services/store-index.ts +1152 -0
- package/src/services/store-keys.ts +4 -0
- package/src/services/store-manager.ts +358 -0
- package/src/services/store-stats.ts +522 -0
- package/src/services/store-writer.ts +200 -0
- package/src/services/sync-checkpoint-store.ts +169 -0
- package/src/services/sync-engine.ts +547 -0
- package/src/services/sync-reporter.ts +16 -0
- package/src/services/sync-settings.ts +72 -0
- package/src/services/trending-topics.ts +226 -0
- package/src/services/view-checkpoint-store.ts +238 -0
- package/src/typeclass/chunk.ts +84 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ParseResult } from "effect";
|
|
2
|
+
import { safeParseJson, issueDetails } from "./shared.js";
|
|
3
|
+
import { formatAgentError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
const storeConfigExample = {
|
|
6
|
+
format: { json: true, markdown: false },
|
|
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
|
+
]
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const hasPath = (issue: { readonly path: ReadonlyArray<unknown> }, key: string) =>
|
|
19
|
+
issue.path.length > 0 && issue.path[0] === key;
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
export const formatStoreConfigParseError = (
|
|
23
|
+
error: ParseResult.ParseError,
|
|
24
|
+
raw: string
|
|
25
|
+
): string => {
|
|
26
|
+
const issues = ParseResult.ArrayFormatter.formatErrorSync(error);
|
|
27
|
+
const received = safeParseJson(raw);
|
|
28
|
+
const receivedValue = received === undefined ? raw : received;
|
|
29
|
+
|
|
30
|
+
const jsonParseIssue = issues.find(
|
|
31
|
+
(issue) =>
|
|
32
|
+
issue._tag === "Transformation" &&
|
|
33
|
+
typeof issue.message === "string" &&
|
|
34
|
+
issue.message.startsWith("JSON Parse error")
|
|
35
|
+
);
|
|
36
|
+
if (jsonParseIssue) {
|
|
37
|
+
return formatAgentError({
|
|
38
|
+
error: "StoreConfigJsonParseError",
|
|
39
|
+
message: "Invalid JSON in --config-json.",
|
|
40
|
+
received: raw,
|
|
41
|
+
details: [
|
|
42
|
+
jsonParseIssue.message,
|
|
43
|
+
"Tip: wrap JSON in single quotes to avoid shell escaping issues."
|
|
44
|
+
],
|
|
45
|
+
expected: storeConfigExample
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (issues.some((issue) => issue._tag === "Missing" && hasPath(issue, "filters"))) {
|
|
50
|
+
return formatAgentError({
|
|
51
|
+
error: "StoreConfigValidationError",
|
|
52
|
+
message: "Store config requires a filters array.",
|
|
53
|
+
received: receivedValue,
|
|
54
|
+
expected: storeConfigExample,
|
|
55
|
+
fix:
|
|
56
|
+
"Add a filters array. Store config filters are materialized views; use --filter/--filter-json for sync-time filters."
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (issues.some((issue) => hasPath(issue, "filters"))) {
|
|
61
|
+
return formatAgentError({
|
|
62
|
+
error: "StoreConfigValidationError",
|
|
63
|
+
message: "Store config filters must include name, expr, and output fields.",
|
|
64
|
+
received: receivedValue,
|
|
65
|
+
expected: storeConfigExample,
|
|
66
|
+
fix: "Each filter requires name, expr (filter JSON), and output (path/json/markdown).",
|
|
67
|
+
details: issueDetails(issues)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return formatAgentError({
|
|
72
|
+
error: "StoreConfigValidationError",
|
|
73
|
+
message: "Store config failed validation.",
|
|
74
|
+
received: receivedValue,
|
|
75
|
+
expected: storeConfigExample,
|
|
76
|
+
details: issueDetails(issues),
|
|
77
|
+
fix:
|
|
78
|
+
"Check required fields (format, autoSync, filters). For ingestion filters, use --filter/--filter-json on sync/query."
|
|
79
|
+
});
|
|
80
|
+
};
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import * as Doc from "@effect/printer/Doc";
|
|
2
|
+
import { Chunk, Context, Effect, Option } from "effect";
|
|
3
|
+
import { StoreIndex } from "../services/store-index.js";
|
|
4
|
+
import { StoreManager } from "../services/store-manager.js";
|
|
5
|
+
import { LineageStore } from "../services/lineage-store.js";
|
|
6
|
+
import { DerivationValidator } from "../services/derivation-validator.js";
|
|
7
|
+
import { SyncCheckpointStore } from "../services/sync-checkpoint-store.js";
|
|
8
|
+
import { StoreEventLog } from "../services/store-event-log.js";
|
|
9
|
+
import { DataSource } from "../domain/sync.js";
|
|
10
|
+
import type { FilterExpr } from "../domain/filter.js";
|
|
11
|
+
import { formatFilterExpr } from "../domain/filter-describe.js";
|
|
12
|
+
import type { StoreName } from "../domain/primitives.js";
|
|
13
|
+
import type { StoreRef } from "../domain/store.js";
|
|
14
|
+
import type { StoreLineage } from "../domain/derivation.js";
|
|
15
|
+
import type { Annotation } from "./doc/annotation.js";
|
|
16
|
+
import { renderPlain, renderAnsi } from "./doc/render.js";
|
|
17
|
+
import { ann, label, field } from "./doc/primitives.js";
|
|
18
|
+
import { renderTree } from "./doc/tree.js";
|
|
19
|
+
import { renderTableLegacy } from "./doc/table.js";
|
|
20
|
+
|
|
21
|
+
export type StoreTreeFormat = "tree" | "table" | "json";
|
|
22
|
+
|
|
23
|
+
export type StoreTreeRenderOptions = {
|
|
24
|
+
readonly width?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type StoreTreeNode = {
|
|
28
|
+
readonly name: StoreName;
|
|
29
|
+
readonly posts: number;
|
|
30
|
+
readonly derived: boolean;
|
|
31
|
+
readonly status: "source" | "ready" | "stale" | "unknown";
|
|
32
|
+
readonly syncStatus?: "current" | "stale" | "unknown" | "empty";
|
|
33
|
+
readonly lastSync?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type StoreTreeEdge = {
|
|
37
|
+
readonly source: StoreName;
|
|
38
|
+
readonly target: StoreName;
|
|
39
|
+
readonly filter: FilterExpr;
|
|
40
|
+
readonly mode: string;
|
|
41
|
+
readonly derivedAt?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type StoreTreeData = {
|
|
45
|
+
readonly roots: ReadonlyArray<StoreName>;
|
|
46
|
+
readonly stores: ReadonlyArray<StoreTreeNode>;
|
|
47
|
+
readonly edges: ReadonlyArray<StoreTreeEdge>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const formatCount = (value: number) =>
|
|
51
|
+
new Intl.NumberFormat("en-US").format(value);
|
|
52
|
+
|
|
53
|
+
const formatPercent = (value: number) => {
|
|
54
|
+
if (!Number.isFinite(value)) return "0%";
|
|
55
|
+
const rounded = value < 1 ? value.toFixed(2) : value.toFixed(1);
|
|
56
|
+
return `${rounded}%`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const statusLabel = (status: StoreTreeNode["status"]) =>
|
|
60
|
+
status === "source" ? "SOURCE" : status.toUpperCase();
|
|
61
|
+
|
|
62
|
+
const syncLabel = (status: StoreTreeNode["syncStatus"]) =>
|
|
63
|
+
status ? status.toUpperCase() : "UNKNOWN";
|
|
64
|
+
|
|
65
|
+
const formatMode = (mode: string) =>
|
|
66
|
+
mode === "EventTime"
|
|
67
|
+
? "event-time"
|
|
68
|
+
: mode === "DeriveTime"
|
|
69
|
+
? "derive-time"
|
|
70
|
+
: mode.toLowerCase();
|
|
71
|
+
|
|
72
|
+
const formatMatchRate = (
|
|
73
|
+
source: StoreTreeNode | undefined,
|
|
74
|
+
target: StoreTreeNode | undefined
|
|
75
|
+
) => {
|
|
76
|
+
if (!source || !target || source.posts <= 0) return "-";
|
|
77
|
+
const rate = (target.posts / source.posts) * 100;
|
|
78
|
+
return formatPercent(rate);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type DerivationValidatorService = Context.Tag.Service<typeof DerivationValidator>;
|
|
82
|
+
type StoreEventLogService = Context.Tag.Service<typeof StoreEventLog>;
|
|
83
|
+
type SyncCheckpointStoreService = Context.Tag.Service<typeof SyncCheckpointStore>;
|
|
84
|
+
|
|
85
|
+
const resolveDerivedStatus = (
|
|
86
|
+
store: StoreName,
|
|
87
|
+
lineage: Option.Option<StoreLineage>,
|
|
88
|
+
validator: DerivationValidatorService
|
|
89
|
+
) =>
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
if (Option.isNone(lineage) || !lineage.value.isDerived) {
|
|
92
|
+
return "source" as const;
|
|
93
|
+
}
|
|
94
|
+
const sources = lineage.value.sources;
|
|
95
|
+
if (sources.length === 0) {
|
|
96
|
+
return "unknown" as const;
|
|
97
|
+
}
|
|
98
|
+
const staleFlags = yield* Effect.forEach(
|
|
99
|
+
sources,
|
|
100
|
+
(source) => validator.isStale(store, source.storeName),
|
|
101
|
+
{ discard: false }
|
|
102
|
+
);
|
|
103
|
+
return staleFlags.some(Boolean) ? ("stale" as const) : ("ready" as const);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const resolveSyncInfo = (
|
|
107
|
+
storeRef: StoreRef,
|
|
108
|
+
eventLog: StoreEventLogService,
|
|
109
|
+
checkpoints: SyncCheckpointStoreService
|
|
110
|
+
) =>
|
|
111
|
+
Effect.gen(function* () {
|
|
112
|
+
const lastEventSeqOption = yield* eventLog.getLastEventSeq(storeRef);
|
|
113
|
+
if (Option.isNone(lastEventSeqOption)) {
|
|
114
|
+
return { syncStatus: "empty" as const };
|
|
115
|
+
}
|
|
116
|
+
const [timelineCheckpoint, notificationsCheckpoint] = yield* Effect.all([
|
|
117
|
+
checkpoints.load(storeRef, DataSource.timeline()),
|
|
118
|
+
checkpoints.load(storeRef, DataSource.notifications())
|
|
119
|
+
]);
|
|
120
|
+
const candidates = [timelineCheckpoint, notificationsCheckpoint]
|
|
121
|
+
.filter(Option.isSome)
|
|
122
|
+
.map((option) => option.value);
|
|
123
|
+
if (candidates.length === 0) {
|
|
124
|
+
return { syncStatus: "unknown" as const };
|
|
125
|
+
}
|
|
126
|
+
const latest = candidates.sort(
|
|
127
|
+
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
|
|
128
|
+
)[0];
|
|
129
|
+
if (!latest) {
|
|
130
|
+
return { syncStatus: "unknown" as const };
|
|
131
|
+
}
|
|
132
|
+
const current =
|
|
133
|
+
latest.lastEventSeq && latest.lastEventSeq === lastEventSeqOption.value
|
|
134
|
+
? ("current" as const)
|
|
135
|
+
: ("stale" as const);
|
|
136
|
+
return { syncStatus: current, lastSync: latest.updatedAt.toISOString() };
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export const buildStoreTreeData = Effect.gen(function* () {
|
|
140
|
+
const index = yield* StoreIndex;
|
|
141
|
+
const manager = yield* StoreManager;
|
|
142
|
+
const lineageStore = yield* LineageStore;
|
|
143
|
+
const validator = yield* DerivationValidator;
|
|
144
|
+
const eventLog = yield* StoreEventLog;
|
|
145
|
+
const checkpoints = yield* SyncCheckpointStore;
|
|
146
|
+
|
|
147
|
+
const stores = yield* manager.listStores();
|
|
148
|
+
const storeRefs = Chunk.toReadonlyArray(stores).map((meta) => ({
|
|
149
|
+
name: meta.name,
|
|
150
|
+
root: meta.root
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
const storeInfo = yield* Effect.forEach(
|
|
154
|
+
storeRefs,
|
|
155
|
+
(storeRef) =>
|
|
156
|
+
Effect.gen(function* () {
|
|
157
|
+
const posts = yield* index.count(storeRef);
|
|
158
|
+
const lineage = yield* lineageStore.get(storeRef.name);
|
|
159
|
+
const status = yield* resolveDerivedStatus(storeRef.name, lineage, validator);
|
|
160
|
+
const syncInfo =
|
|
161
|
+
status === "source"
|
|
162
|
+
? yield* resolveSyncInfo(storeRef, eventLog, checkpoints)
|
|
163
|
+
: undefined;
|
|
164
|
+
|
|
165
|
+
const info: StoreTreeNode = {
|
|
166
|
+
name: storeRef.name,
|
|
167
|
+
posts,
|
|
168
|
+
derived: Option.isSome(lineage) && lineage.value.isDerived,
|
|
169
|
+
status,
|
|
170
|
+
...(syncInfo ? syncInfo : {})
|
|
171
|
+
} satisfies StoreTreeNode;
|
|
172
|
+
return info;
|
|
173
|
+
}),
|
|
174
|
+
{ discard: false }
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const lineageEntries = yield* Effect.forEach(
|
|
178
|
+
storeRefs,
|
|
179
|
+
(storeRef) =>
|
|
180
|
+
lineageStore.get(storeRef.name).pipe(
|
|
181
|
+
Effect.map((lineage) => ({ store: storeRef.name, lineage }))
|
|
182
|
+
),
|
|
183
|
+
{ discard: false }
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const edges: StoreTreeEdge[] = [];
|
|
187
|
+
for (const entry of lineageEntries) {
|
|
188
|
+
if (Option.isNone(entry.lineage) || !entry.lineage.value.isDerived) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
for (const source of entry.lineage.value.sources) {
|
|
192
|
+
edges.push({
|
|
193
|
+
source: source.storeName,
|
|
194
|
+
target: entry.store,
|
|
195
|
+
filter: source.filter,
|
|
196
|
+
mode: source.evaluationMode,
|
|
197
|
+
derivedAt: source.derivedAt.toISOString()
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const targets = new Set(edges.map((edge) => edge.target));
|
|
203
|
+
const roots = storeRefs
|
|
204
|
+
.map((store) => store.name)
|
|
205
|
+
.filter((name) => !targets.has(name));
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
roots,
|
|
209
|
+
stores: storeInfo,
|
|
210
|
+
edges
|
|
211
|
+
} satisfies StoreTreeData;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const buildMaps = (data: StoreTreeData) => {
|
|
215
|
+
const storeMap = new Map<string, StoreTreeNode>(
|
|
216
|
+
data.stores.map((store) => [store.name, store])
|
|
217
|
+
);
|
|
218
|
+
const edgeMap = new Map<string, StoreTreeEdge[]>();
|
|
219
|
+
for (const edge of data.edges) {
|
|
220
|
+
const key = edge.source;
|
|
221
|
+
const existing = edgeMap.get(key) ?? [];
|
|
222
|
+
edgeMap.set(key, [...existing, edge]);
|
|
223
|
+
}
|
|
224
|
+
return { storeMap, edgeMap };
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
type SDoc = Doc.Doc<Annotation>;
|
|
228
|
+
|
|
229
|
+
const statusAnnotation: Record<StoreTreeNode["status"], Annotation> = {
|
|
230
|
+
source: "status:source",
|
|
231
|
+
ready: "status:ready",
|
|
232
|
+
stale: "status:stale",
|
|
233
|
+
unknown: "status:unknown"
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const syncAnnotation = (status: StoreTreeNode["syncStatus"]): Annotation => {
|
|
237
|
+
switch (status) {
|
|
238
|
+
case "current": return "sync:current";
|
|
239
|
+
case "stale": return "sync:stale";
|
|
240
|
+
case "empty": return "sync:empty";
|
|
241
|
+
default: return "sync:unknown";
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const buildStoreTreeDoc = (data: StoreTreeData): SDoc => {
|
|
246
|
+
const { storeMap, edgeMap } = buildMaps(data);
|
|
247
|
+
|
|
248
|
+
return renderTree<StoreName, StoreTreeEdge>(data.roots, {
|
|
249
|
+
children: (name) =>
|
|
250
|
+
(edgeMap.get(name) ?? []).map((e) => ({ node: e.target, edge: e })),
|
|
251
|
+
|
|
252
|
+
renderNode: (name, { isRoot, edge }) => {
|
|
253
|
+
const info = storeMap.get(name);
|
|
254
|
+
const derived = info?.derived ?? false;
|
|
255
|
+
const parts: SDoc[] = [];
|
|
256
|
+
|
|
257
|
+
if (edge) {
|
|
258
|
+
const sourceInfo = storeMap.get(edge.source);
|
|
259
|
+
const edgeParts = [
|
|
260
|
+
`filter:${formatFilterExpr(edge.filter)}`,
|
|
261
|
+
`mode:${formatMode(edge.mode)}`
|
|
262
|
+
];
|
|
263
|
+
const match = formatMatchRate(sourceInfo, info);
|
|
264
|
+
if (match !== "-") edgeParts.push(`match:${match}`);
|
|
265
|
+
parts.push(ann("dim", Doc.text(`[${edgeParts.join(" | ")}] ->`)));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const nameAnn: Annotation = isRoot ? "storeName:root"
|
|
269
|
+
: derived ? "storeName:derived"
|
|
270
|
+
: "storeName";
|
|
271
|
+
parts.push(ann(nameAnn, Doc.text(name)));
|
|
272
|
+
parts.push(ann(derived ? "storeName:derived" : "storeName", Doc.text(`(${derived ? "derived" : "source"})`)));
|
|
273
|
+
|
|
274
|
+
return Doc.hsep(parts);
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
details: (name, { edge }) => {
|
|
278
|
+
const info = storeMap.get(name);
|
|
279
|
+
if (!info) return [];
|
|
280
|
+
const lines: SDoc[] = [];
|
|
281
|
+
const derived = info.derived;
|
|
282
|
+
|
|
283
|
+
let postsVal = formatCount(info.posts);
|
|
284
|
+
if (edge) {
|
|
285
|
+
const sourceInfo = storeMap.get(edge.source);
|
|
286
|
+
const match = formatMatchRate(sourceInfo, info);
|
|
287
|
+
if (match !== "-") postsVal += ` (${match} match)`;
|
|
288
|
+
}
|
|
289
|
+
lines.push(field("Posts", postsVal));
|
|
290
|
+
|
|
291
|
+
if (derived) {
|
|
292
|
+
lines.push(Doc.hsep([label("Status:"), ann(statusAnnotation[info.status], Doc.text(statusLabel(info.status)))]));
|
|
293
|
+
} else {
|
|
294
|
+
lines.push(Doc.hsep([label("Sync:"), ann(syncAnnotation(info.syncStatus), Doc.text(syncLabel(info.syncStatus)))]));
|
|
295
|
+
if (info.lastSync) lines.push(field("Last sync", info.lastSync));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (edge?.derivedAt) lines.push(field("Derived at", edge.derivedAt));
|
|
299
|
+
return lines;
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
key: (name) => name,
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export const renderStoreTree = (
|
|
307
|
+
data: StoreTreeData,
|
|
308
|
+
options?: StoreTreeRenderOptions
|
|
309
|
+
): string => renderPlain(buildStoreTreeDoc(data), options?.width);
|
|
310
|
+
|
|
311
|
+
const renderTableSection = (
|
|
312
|
+
sectionLabel: string,
|
|
313
|
+
headers: ReadonlyArray<string>,
|
|
314
|
+
rows: ReadonlyArray<ReadonlyArray<string>>
|
|
315
|
+
) => {
|
|
316
|
+
if (rows.length === 0) {
|
|
317
|
+
return `${sectionLabel}\n(no rows)`;
|
|
318
|
+
}
|
|
319
|
+
// Note: store-tree uses trimEnd: false to preserve trailing whitespace for alignment
|
|
320
|
+
return `${sectionLabel}\n${renderTableLegacy(headers, rows, false)}`;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export const renderStoreTreeTable = (data: StoreTreeData): string => {
|
|
324
|
+
const rows: Array<ReadonlyArray<string>> = [];
|
|
325
|
+
const edgeRows: Array<ReadonlyArray<string>> = [];
|
|
326
|
+
|
|
327
|
+
const rootSet = new Set(data.roots);
|
|
328
|
+
const { storeMap } = buildMaps(data);
|
|
329
|
+
|
|
330
|
+
const storeRows = [...data.stores].sort((a, b) =>
|
|
331
|
+
String(a.name).localeCompare(String(b.name))
|
|
332
|
+
);
|
|
333
|
+
for (const store of storeRows) {
|
|
334
|
+
rows.push([
|
|
335
|
+
store.name,
|
|
336
|
+
store.derived ? "derived" : "source",
|
|
337
|
+
rootSet.has(store.name) ? "yes" : "no",
|
|
338
|
+
formatCount(store.posts),
|
|
339
|
+
statusLabel(store.status),
|
|
340
|
+
store.derived ? "-" : syncLabel(store.syncStatus),
|
|
341
|
+
store.derived ? "-" : store.lastSync ?? "-"
|
|
342
|
+
]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const sortedEdges = [...data.edges].sort((a, b) =>
|
|
346
|
+
`${a.source}-${a.target}`.localeCompare(`${b.source}-${b.target}`)
|
|
347
|
+
);
|
|
348
|
+
for (const edge of sortedEdges) {
|
|
349
|
+
const sourceInfo = storeMap.get(edge.source);
|
|
350
|
+
const targetInfo = storeMap.get(edge.target);
|
|
351
|
+
edgeRows.push([
|
|
352
|
+
edge.source,
|
|
353
|
+
edge.target,
|
|
354
|
+
formatFilterExpr(edge.filter),
|
|
355
|
+
formatMode(edge.mode),
|
|
356
|
+
formatMatchRate(sourceInfo, targetInfo),
|
|
357
|
+
edge.derivedAt ?? "-"
|
|
358
|
+
]);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const sections = [
|
|
362
|
+
renderTableSection(
|
|
363
|
+
"Stores",
|
|
364
|
+
["Store", "Kind", "Root", "Posts", "Status", "Sync", "Last Sync"],
|
|
365
|
+
rows
|
|
366
|
+
),
|
|
367
|
+
renderTableSection(
|
|
368
|
+
"Derivations",
|
|
369
|
+
["Source", "Target", "Filter", "Mode", "Match", "Derived At"],
|
|
370
|
+
edgeRows
|
|
371
|
+
)
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
return sections.join("\n\n");
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export const renderStoreTreeAnsi = (
|
|
378
|
+
data: StoreTreeData,
|
|
379
|
+
options?: StoreTreeRenderOptions
|
|
380
|
+
): string => renderAnsi(buildStoreTreeDoc(data), options?.width);
|
|
381
|
+
|
|
382
|
+
export const renderStoreTreeJson = (data: StoreTreeData) => ({
|
|
383
|
+
roots: data.roots,
|
|
384
|
+
stores: data.stores,
|
|
385
|
+
edges: data.edges.map((edge) => ({
|
|
386
|
+
source: edge.source,
|
|
387
|
+
target: edge.target,
|
|
388
|
+
filter: formatFilterExpr(edge.filter),
|
|
389
|
+
mode: formatMode(edge.mode),
|
|
390
|
+
derivedAt: edge.derivedAt
|
|
391
|
+
}))
|
|
392
|
+
});
|