@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
package/src/cli/store.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Chunk, Effect, Option } from "effect";
|
|
3
|
+
import { StoreManager } from "../services/store-manager.js";
|
|
4
|
+
import { AppConfigService } from "../services/app-config.js";
|
|
5
|
+
import { Terminal } from "@effect/platform";
|
|
6
|
+
import { StoreNotFound } from "../domain/errors.js";
|
|
7
|
+
import { StoreName } from "../domain/primitives.js";
|
|
8
|
+
import { StoreConfig, StoreMetadata, StoreRef } from "../domain/store.js";
|
|
9
|
+
import type { StoreLineage } from "../domain/derivation.js";
|
|
10
|
+
import { defaultStoreConfig } from "../domain/defaults.js";
|
|
11
|
+
import { decodeJson } from "./parse.js";
|
|
12
|
+
import { writeJson, writeText } from "./output.js";
|
|
13
|
+
import { StoreCleaner } from "../services/store-cleaner.js";
|
|
14
|
+
import { LineageStore } from "../services/lineage-store.js";
|
|
15
|
+
import { CliInputError } from "./errors.js";
|
|
16
|
+
import { OutputManager } from "../services/output-manager.js";
|
|
17
|
+
import { formatStoreConfigParseError } from "./store-errors.js";
|
|
18
|
+
import { formatFilterExpr } from "../domain/filter-describe.js";
|
|
19
|
+
import { CliPreferences } from "./preferences.js";
|
|
20
|
+
import { StoreStats } from "../services/store-stats.js";
|
|
21
|
+
import { withExamples } from "./help.js";
|
|
22
|
+
import { resolveOutputFormat, treeTableJsonFormats } from "./output-format.js";
|
|
23
|
+
import {
|
|
24
|
+
buildStoreTreeData,
|
|
25
|
+
renderStoreTree,
|
|
26
|
+
renderStoreTreeAnsi,
|
|
27
|
+
renderStoreTreeJson,
|
|
28
|
+
renderStoreTreeTable,
|
|
29
|
+
type StoreTreeRenderOptions
|
|
30
|
+
} from "./store-tree.js";
|
|
31
|
+
|
|
32
|
+
const storeNameArg = Args.text({ name: "name" }).pipe(
|
|
33
|
+
Args.withSchema(StoreName),
|
|
34
|
+
Args.withDescription("Store name")
|
|
35
|
+
);
|
|
36
|
+
const storeNameOption = Options.text("store").pipe(
|
|
37
|
+
Options.withSchema(StoreName),
|
|
38
|
+
Options.withDescription("Store name")
|
|
39
|
+
);
|
|
40
|
+
const forceOption = Options.boolean("force").pipe(
|
|
41
|
+
Options.withAlias("f"),
|
|
42
|
+
Options.withDescription("Confirm destructive store deletion")
|
|
43
|
+
);
|
|
44
|
+
const filterNameOption = Options.text("filter").pipe(
|
|
45
|
+
Options.withDescription("Filter spec name to materialize"),
|
|
46
|
+
Options.optional
|
|
47
|
+
);
|
|
48
|
+
const treeFormatOption = Options.choice("format", treeTableJsonFormats).pipe(
|
|
49
|
+
Options.withDescription("Output format for store tree (default: tree)"),
|
|
50
|
+
Options.optional
|
|
51
|
+
);
|
|
52
|
+
const treeAnsiOption = Options.boolean("ansi").pipe(
|
|
53
|
+
Options.withDescription("Enable ANSI color output for tree format")
|
|
54
|
+
);
|
|
55
|
+
const treeWidthOption = Options.integer("width").pipe(
|
|
56
|
+
Options.withDescription("Line width for tree rendering (enables wrapping)"),
|
|
57
|
+
Options.optional
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const configJsonOption = Options.text("config-json").pipe(
|
|
61
|
+
Options.withDescription(
|
|
62
|
+
"Store config as JSON string (materialized view filters, not sync filters)"
|
|
63
|
+
),
|
|
64
|
+
Options.optional
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const parseConfig = (configJson: Option.Option<string>) =>
|
|
68
|
+
Option.match(configJson, {
|
|
69
|
+
onNone: () => Effect.succeed(defaultStoreConfig),
|
|
70
|
+
onSome: (raw) =>
|
|
71
|
+
decodeJson(StoreConfig, raw, {
|
|
72
|
+
formatter: formatStoreConfigParseError
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const loadStoreRef = (name: StoreName) =>
|
|
77
|
+
Effect.gen(function* () {
|
|
78
|
+
const manager = yield* StoreManager;
|
|
79
|
+
const store = yield* manager.getStore(name);
|
|
80
|
+
return yield* Option.match(store, {
|
|
81
|
+
onNone: () => Effect.fail(StoreNotFound.make({ name })),
|
|
82
|
+
onSome: Effect.succeed
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const loadStoreConfig = (name: StoreName) =>
|
|
87
|
+
Effect.gen(function* () {
|
|
88
|
+
const manager = yield* StoreManager;
|
|
89
|
+
const config = yield* manager.getConfig(name);
|
|
90
|
+
return Option.getOrElse(config, () => defaultStoreConfig);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const compactLineage = (store: StoreRef, lineage: StoreLineage | undefined) => {
|
|
94
|
+
if (!lineage) {
|
|
95
|
+
return { store: store.name, derived: false, status: "ready" };
|
|
96
|
+
}
|
|
97
|
+
if (!lineage.isDerived || lineage.sources.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
store: store.name,
|
|
100
|
+
derived: lineage.isDerived,
|
|
101
|
+
status: lineage.isDerived ? "derived" : "ready",
|
|
102
|
+
updatedAt: lineage.updatedAt.toISOString()
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const sources = lineage.sources.map((source) => ({
|
|
106
|
+
store: source.storeName,
|
|
107
|
+
filter: formatFilterExpr(source.filter),
|
|
108
|
+
mode: source.evaluationMode,
|
|
109
|
+
derivedAt: source.derivedAt.toISOString()
|
|
110
|
+
}));
|
|
111
|
+
const base = {
|
|
112
|
+
store: store.name,
|
|
113
|
+
derived: true,
|
|
114
|
+
status: "derived",
|
|
115
|
+
updatedAt: lineage.updatedAt.toISOString()
|
|
116
|
+
};
|
|
117
|
+
if (sources.length === 1) {
|
|
118
|
+
const source = sources[0]!;
|
|
119
|
+
return {
|
|
120
|
+
...base,
|
|
121
|
+
source: source.store,
|
|
122
|
+
filter: source.filter,
|
|
123
|
+
mode: source.mode
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return { ...base, sources };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const storeCreate = Command.make(
|
|
130
|
+
"create",
|
|
131
|
+
{ name: storeNameArg, config: configJsonOption },
|
|
132
|
+
({ name, config }) =>
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
const manager = yield* StoreManager;
|
|
135
|
+
const parsed = yield* parseConfig(config);
|
|
136
|
+
const store = yield* manager.createStore(name, parsed);
|
|
137
|
+
yield* writeJson(store);
|
|
138
|
+
})
|
|
139
|
+
).pipe(
|
|
140
|
+
Command.withDescription(
|
|
141
|
+
withExamples("Create or load a store", [
|
|
142
|
+
"skygent store create my-store",
|
|
143
|
+
"skygent store create my-store --config-json '{\"filters\":[]}'"
|
|
144
|
+
])
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
export const storeList = Command.make("list", {}, () =>
|
|
149
|
+
Effect.gen(function* () {
|
|
150
|
+
const manager = yield* StoreManager;
|
|
151
|
+
const preferences = yield* CliPreferences;
|
|
152
|
+
const stores = yield* manager.listStores();
|
|
153
|
+
if (preferences.compact) {
|
|
154
|
+
const names = Chunk.toReadonlyArray(stores).map((store) => store.name);
|
|
155
|
+
yield* writeJson(names);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
yield* writeJson(Chunk.toReadonlyArray(stores) as ReadonlyArray<StoreMetadata>);
|
|
159
|
+
})
|
|
160
|
+
).pipe(
|
|
161
|
+
Command.withDescription(
|
|
162
|
+
withExamples("List known stores", [
|
|
163
|
+
"skygent store list",
|
|
164
|
+
"skygent store list --compact"
|
|
165
|
+
])
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
export const storeShow = Command.make(
|
|
170
|
+
"show",
|
|
171
|
+
{ name: storeNameArg },
|
|
172
|
+
({ name }) =>
|
|
173
|
+
Effect.gen(function* () {
|
|
174
|
+
const manager = yield* StoreManager;
|
|
175
|
+
const lineageStore = yield* LineageStore;
|
|
176
|
+
const preferences = yield* CliPreferences;
|
|
177
|
+
const store = yield* loadStoreRef(name);
|
|
178
|
+
const config = yield* manager.getConfig(name);
|
|
179
|
+
const lineageOption = yield* lineageStore.get(name);
|
|
180
|
+
|
|
181
|
+
if (preferences.compact) {
|
|
182
|
+
const lineage = Option.getOrUndefined(lineageOption);
|
|
183
|
+
yield* writeJson(compactLineage(store, lineage));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const output = Option.match(config, {
|
|
188
|
+
onNone: () => ({ store }),
|
|
189
|
+
onSome: (value) => ({ store, config: value })
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const finalOutput = Option.match(lineageOption, {
|
|
193
|
+
onNone: () => output,
|
|
194
|
+
onSome: (lineage) => ({ ...output, lineage })
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
yield* writeJson(finalOutput);
|
|
198
|
+
})
|
|
199
|
+
).pipe(
|
|
200
|
+
Command.withDescription(
|
|
201
|
+
withExamples("Show store config and metadata", [
|
|
202
|
+
"skygent store show my-store",
|
|
203
|
+
"skygent store show my-store --compact"
|
|
204
|
+
])
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
export const storeDelete = Command.make(
|
|
209
|
+
"delete",
|
|
210
|
+
{ name: storeNameArg, force: forceOption },
|
|
211
|
+
({ name, force }) =>
|
|
212
|
+
Effect.gen(function* () {
|
|
213
|
+
if (!force) {
|
|
214
|
+
const terminal = yield* Terminal.Terminal;
|
|
215
|
+
const isTTY = yield* terminal.isTTY.pipe(Effect.orElseSucceed(() => false));
|
|
216
|
+
if (!isTTY) {
|
|
217
|
+
return yield* CliInputError.make({
|
|
218
|
+
message: "--force is required to delete a store.",
|
|
219
|
+
cause: { name, force }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
yield* terminal.display(
|
|
224
|
+
`Delete store "${name}" and all its data? [y/N] `
|
|
225
|
+
);
|
|
226
|
+
const response = yield* terminal.readLine.pipe(
|
|
227
|
+
Effect.catchAll(() => Effect.succeed(""))
|
|
228
|
+
);
|
|
229
|
+
const normalized = response.trim().toLowerCase();
|
|
230
|
+
const confirmed = normalized === "y" || normalized === "yes";
|
|
231
|
+
if (!confirmed) {
|
|
232
|
+
yield* writeJson({ deleted: false, reason: "cancelled" });
|
|
233
|
+
return yield* CliInputError.make({
|
|
234
|
+
message: `Store "${name}" was not deleted (cancelled by user).`,
|
|
235
|
+
cause: { deleted: false, reason: "cancelled" }
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const cleaner = yield* StoreCleaner;
|
|
240
|
+
const result = yield* cleaner.deleteStore(name);
|
|
241
|
+
if (!result.deleted) {
|
|
242
|
+
return yield* CliInputError.make({
|
|
243
|
+
message: `Store "${name}" was not deleted.`,
|
|
244
|
+
cause: result
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
yield* writeJson(result);
|
|
248
|
+
})
|
|
249
|
+
).pipe(
|
|
250
|
+
Command.withDescription(
|
|
251
|
+
withExamples("Delete a store and its data", [
|
|
252
|
+
"skygent store delete my-store --force"
|
|
253
|
+
])
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
export const storeMaterialize = Command.make(
|
|
258
|
+
"materialize",
|
|
259
|
+
{ name: storeNameArg, filter: filterNameOption },
|
|
260
|
+
({ name, filter }) =>
|
|
261
|
+
Effect.gen(function* () {
|
|
262
|
+
const manager = yield* StoreManager;
|
|
263
|
+
const outputManager = yield* OutputManager;
|
|
264
|
+
const storeRef = yield* loadStoreRef(name);
|
|
265
|
+
const configOption = yield* manager.getConfig(name);
|
|
266
|
+
const config = Option.getOrElse(configOption, () => defaultStoreConfig);
|
|
267
|
+
|
|
268
|
+
if (config.filters.length === 0) {
|
|
269
|
+
return yield* CliInputError.make({
|
|
270
|
+
message: `Store "${name}" has no configured filters to materialize. Update the store config to add filters.`,
|
|
271
|
+
cause: { store: name }
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const selected = yield* Option.match(filter, {
|
|
276
|
+
onNone: () => Effect.succeed(config.filters),
|
|
277
|
+
onSome: (filterName) => {
|
|
278
|
+
const match = config.filters.find((spec) => spec.name === filterName);
|
|
279
|
+
if (!match) {
|
|
280
|
+
return Effect.fail(
|
|
281
|
+
CliInputError.make({
|
|
282
|
+
message: `Unknown filter spec: ${filterName}`,
|
|
283
|
+
cause: { store: name, filter: filterName }
|
|
284
|
+
})
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return Effect.succeed([match]);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
const results = yield* outputManager.materializeFilters(storeRef, selected);
|
|
291
|
+
yield* writeJson({
|
|
292
|
+
store: storeRef.name,
|
|
293
|
+
filters: results
|
|
294
|
+
});
|
|
295
|
+
})
|
|
296
|
+
).pipe(
|
|
297
|
+
Command.withDescription(
|
|
298
|
+
withExamples("Materialize configured filter outputs to disk", [
|
|
299
|
+
"skygent store materialize my-store",
|
|
300
|
+
"skygent store materialize my-store --filter ai-posts"
|
|
301
|
+
])
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
export const storeStats = Command.make(
|
|
306
|
+
"stats",
|
|
307
|
+
{ name: storeNameArg },
|
|
308
|
+
({ name }) =>
|
|
309
|
+
Effect.gen(function* () {
|
|
310
|
+
const stats = yield* StoreStats;
|
|
311
|
+
const storeRef = yield* loadStoreRef(name);
|
|
312
|
+
const result = yield* stats.stats(storeRef);
|
|
313
|
+
yield* writeJson(result);
|
|
314
|
+
})
|
|
315
|
+
).pipe(
|
|
316
|
+
Command.withDescription(
|
|
317
|
+
withExamples("Show summary stats for a store", [
|
|
318
|
+
"skygent store stats my-store"
|
|
319
|
+
])
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
export const storeSummary = Command.make("summary", {}, () =>
|
|
324
|
+
Effect.gen(function* () {
|
|
325
|
+
const stats = yield* StoreStats;
|
|
326
|
+
const result = yield* stats.summary();
|
|
327
|
+
yield* writeJson(result);
|
|
328
|
+
})
|
|
329
|
+
).pipe(
|
|
330
|
+
Command.withDescription(
|
|
331
|
+
withExamples("Summarize all stores with counts and status", [
|
|
332
|
+
"skygent store summary --compact"
|
|
333
|
+
])
|
|
334
|
+
)
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
export const storeTree = Command.make(
|
|
338
|
+
"tree",
|
|
339
|
+
{ format: treeFormatOption, ansi: treeAnsiOption, width: treeWidthOption },
|
|
340
|
+
({ format, ansi, width }) =>
|
|
341
|
+
Effect.gen(function* () {
|
|
342
|
+
const appConfig = yield* AppConfigService;
|
|
343
|
+
const data = yield* buildStoreTreeData;
|
|
344
|
+
const outputFormat = resolveOutputFormat(
|
|
345
|
+
format,
|
|
346
|
+
appConfig.outputFormat,
|
|
347
|
+
treeTableJsonFormats,
|
|
348
|
+
"tree"
|
|
349
|
+
);
|
|
350
|
+
const renderOptions: StoreTreeRenderOptions | undefined = Option.match(width, {
|
|
351
|
+
onNone: () => undefined,
|
|
352
|
+
onSome: (value) => ({ width: value })
|
|
353
|
+
});
|
|
354
|
+
switch (outputFormat) {
|
|
355
|
+
case "json":
|
|
356
|
+
yield* writeJson(renderStoreTreeJson(data));
|
|
357
|
+
return;
|
|
358
|
+
case "table":
|
|
359
|
+
yield* writeText(renderStoreTreeTable(data));
|
|
360
|
+
return;
|
|
361
|
+
default:
|
|
362
|
+
yield* writeText(
|
|
363
|
+
ansi ? renderStoreTreeAnsi(data, renderOptions) : renderStoreTree(data, renderOptions)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
).pipe(
|
|
368
|
+
Command.withDescription(
|
|
369
|
+
withExamples("Visualize store lineage as an ASCII tree", [
|
|
370
|
+
"skygent store tree --format table",
|
|
371
|
+
"skygent store tree --ansi --width 100"
|
|
372
|
+
])
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
export const storeCommand = Command.make("store", {}).pipe(
|
|
377
|
+
Command.withSubcommands([
|
|
378
|
+
storeCreate,
|
|
379
|
+
storeList,
|
|
380
|
+
storeShow,
|
|
381
|
+
storeDelete,
|
|
382
|
+
storeMaterialize,
|
|
383
|
+
storeStats,
|
|
384
|
+
storeSummary,
|
|
385
|
+
storeTree
|
|
386
|
+
]),
|
|
387
|
+
Command.withDescription(
|
|
388
|
+
withExamples("Manage stores and lineage", [
|
|
389
|
+
"skygent store list",
|
|
390
|
+
"skygent store tree --format table"
|
|
391
|
+
])
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
export const storeOptions = { storeNameOption, loadStoreRef, loadStoreConfig };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Effect, Option, Stream } from "effect";
|
|
2
|
+
import { DataSource, SyncResult, WatchConfig } from "../domain/sync.js";
|
|
3
|
+
import { SyncEngine } from "../services/sync-engine.js";
|
|
4
|
+
import { SyncReporter } from "../services/sync-reporter.js";
|
|
5
|
+
import { OutputManager } from "../services/output-manager.js";
|
|
6
|
+
import { ResourceMonitor } from "../services/resource-monitor.js";
|
|
7
|
+
import { parseFilterExpr } from "./filter-input.js";
|
|
8
|
+
import { CliOutput, writeJson, writeJsonStream } from "./output.js";
|
|
9
|
+
import { storeOptions } from "./store.js";
|
|
10
|
+
import { logInfo, logWarn, makeSyncReporter } from "./logging.js";
|
|
11
|
+
import { parseInterval } from "./interval.js";
|
|
12
|
+
import type { StoreName } from "../domain/primitives.js";
|
|
13
|
+
|
|
14
|
+
/** Common options shared by sync and watch API-based commands */
|
|
15
|
+
export interface CommonCommandInput {
|
|
16
|
+
readonly store: StoreName;
|
|
17
|
+
readonly filter: Option.Option<string>;
|
|
18
|
+
readonly filterJson: Option.Option<string>;
|
|
19
|
+
readonly quiet: boolean;
|
|
20
|
+
readonly refresh: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build the command body for a one-shot sync command (timeline, feed, notifications). */
|
|
24
|
+
export const makeSyncCommandBody = (
|
|
25
|
+
sourceName: string,
|
|
26
|
+
makeDataSource: () => DataSource,
|
|
27
|
+
extraLogFields?: Record<string, unknown>
|
|
28
|
+
) =>
|
|
29
|
+
(input: CommonCommandInput) =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const sync = yield* SyncEngine;
|
|
32
|
+
const monitor = yield* ResourceMonitor;
|
|
33
|
+
const output = yield* CliOutput;
|
|
34
|
+
const outputManager = yield* OutputManager;
|
|
35
|
+
const storeRef = yield* storeOptions.loadStoreRef(input.store);
|
|
36
|
+
const storeConfig = yield* storeOptions.loadStoreConfig(input.store);
|
|
37
|
+
const expr = yield* parseFilterExpr(input.filter, input.filterJson);
|
|
38
|
+
const basePolicy = storeConfig.syncPolicy ?? "dedupe";
|
|
39
|
+
const policy = input.refresh ? "refresh" : basePolicy;
|
|
40
|
+
yield* logInfo("Starting sync", { source: sourceName, store: storeRef.name, ...extraLogFields });
|
|
41
|
+
if (policy === "refresh") {
|
|
42
|
+
yield* logWarn("Refresh mode updates existing posts and may grow the event log.", {
|
|
43
|
+
source: sourceName,
|
|
44
|
+
store: storeRef.name
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const result = yield* sync
|
|
48
|
+
.sync(makeDataSource(), storeRef, expr, { policy })
|
|
49
|
+
.pipe(
|
|
50
|
+
Effect.provideService(SyncReporter, makeSyncReporter(input.quiet, monitor, output))
|
|
51
|
+
);
|
|
52
|
+
const materialized = yield* outputManager.materializeStore(storeRef);
|
|
53
|
+
if (materialized.filters.length > 0) {
|
|
54
|
+
yield* logInfo("Materialized filter outputs", {
|
|
55
|
+
store: storeRef.name,
|
|
56
|
+
filters: materialized.filters.map((spec) => spec.name)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
yield* logInfo("Sync complete", { source: sourceName, store: storeRef.name, ...extraLogFields });
|
|
60
|
+
yield* writeJson(result as SyncResult);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/** Common options for watch API-based commands */
|
|
64
|
+
export interface WatchCommandInput extends CommonCommandInput {
|
|
65
|
+
readonly interval: Option.Option<string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Build the command body for a watch command (timeline, feed, notifications). */
|
|
69
|
+
export const makeWatchCommandBody = (
|
|
70
|
+
sourceName: string,
|
|
71
|
+
makeDataSource: () => DataSource,
|
|
72
|
+
extraLogFields?: Record<string, unknown>
|
|
73
|
+
) =>
|
|
74
|
+
(input: WatchCommandInput) =>
|
|
75
|
+
Effect.gen(function* () {
|
|
76
|
+
const sync = yield* SyncEngine;
|
|
77
|
+
const monitor = yield* ResourceMonitor;
|
|
78
|
+
const output = yield* CliOutput;
|
|
79
|
+
const storeRef = yield* storeOptions.loadStoreRef(input.store);
|
|
80
|
+
const storeConfig = yield* storeOptions.loadStoreConfig(input.store);
|
|
81
|
+
const expr = yield* parseFilterExpr(input.filter, input.filterJson);
|
|
82
|
+
const basePolicy = storeConfig.syncPolicy ?? "dedupe";
|
|
83
|
+
const policy = input.refresh ? "refresh" : basePolicy;
|
|
84
|
+
const parsedInterval = yield* parseInterval(input.interval);
|
|
85
|
+
yield* logInfo("Starting watch", { source: sourceName, store: storeRef.name, ...extraLogFields });
|
|
86
|
+
if (policy === "refresh") {
|
|
87
|
+
yield* logWarn("Refresh mode updates existing posts and may grow the event log.", {
|
|
88
|
+
source: sourceName,
|
|
89
|
+
store: storeRef.name
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const stream = sync
|
|
93
|
+
.watch(
|
|
94
|
+
WatchConfig.make({
|
|
95
|
+
source: makeDataSource(),
|
|
96
|
+
store: storeRef,
|
|
97
|
+
filter: expr,
|
|
98
|
+
interval: parsedInterval,
|
|
99
|
+
policy
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
.pipe(
|
|
103
|
+
Stream.map((event) => event.result),
|
|
104
|
+
Stream.provideService(SyncReporter, makeSyncReporter(input.quiet, monitor, output))
|
|
105
|
+
);
|
|
106
|
+
yield* writeJsonStream(stream);
|
|
107
|
+
});
|