@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/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# skygent-bsky
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Create a local `.env` from `.env.example` and set credentials:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cp .env.example .env
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
To run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun run index.ts
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# List stores (compact JSON for agents)
|
|
25
|
+
bun run index.ts store list --compact
|
|
26
|
+
|
|
27
|
+
# Sync timeline into a store
|
|
28
|
+
bun run index.ts sync timeline --store my-store --quiet
|
|
29
|
+
|
|
30
|
+
# Query recent posts as a table
|
|
31
|
+
bun run index.ts query my-store --limit 10 --format table
|
|
32
|
+
|
|
33
|
+
# Stream Jetstream posts
|
|
34
|
+
bun run index.ts watch jetstream --store my-store --quiet
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Tips:
|
|
38
|
+
- Add `--compact` to reduce JSON output size.
|
|
39
|
+
- Add `--quiet` to suppress progress logs during sync/watch commands.
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
- docs/README.md
|
|
44
|
+
- docs/getting-started.md
|
|
45
|
+
- docs/cli.md
|
|
46
|
+
- docs/filters.md
|
|
47
|
+
- docs/configuration.md
|
|
48
|
+
- docs/credentials.md
|
|
49
|
+
- docs/stores.md
|
|
50
|
+
- docs/outputs.md
|
|
51
|
+
|
|
52
|
+
## Security notes
|
|
53
|
+
|
|
54
|
+
- Credentials are loaded via Effect `Redacted` config and are not logged.
|
|
55
|
+
- Encrypted credential storage uses `SKYGENT_CREDENTIALS_KEY` (AES-GCM).
|
|
56
|
+
- Avoid putting passwords in `config.json`; use env or the credential store instead.
|
|
57
|
+
- Resource warnings can be configured via `SKYGENT_RESOURCE_*` env vars.
|
|
58
|
+
|
|
59
|
+
This project was created using `bun init` in bun v1.3.4. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/index.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command, HelpDoc, ValidationError } from "@effect/cli";
|
|
3
|
+
import { BunContext, BunRuntime } from "@effect/platform-bun";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import { app } from "./src/cli/app.js";
|
|
6
|
+
import pkg from "./package.json" with { type: "json" };
|
|
7
|
+
import {
|
|
8
|
+
type AgentErrorPayload,
|
|
9
|
+
CliInputError,
|
|
10
|
+
CliJsonError,
|
|
11
|
+
parseAgentErrorPayload
|
|
12
|
+
} from "./src/cli/errors.js";
|
|
13
|
+
import { logErrorEvent } from "./src/cli/logging.js";
|
|
14
|
+
import { CliOutput } from "./src/cli/output.js";
|
|
15
|
+
import { exitCodeFor, exitCodeFromExit } from "./src/cli/exit-codes.js";
|
|
16
|
+
import { BskyError, StoreNotFound } from "./src/domain/errors.js";
|
|
17
|
+
|
|
18
|
+
const cli = Command.run(app, {
|
|
19
|
+
name: "skygent",
|
|
20
|
+
version: pkg.version
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const stripAnsi = (value: string) =>
|
|
24
|
+
value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
25
|
+
|
|
26
|
+
const formatValidationError = (error: ValidationError.ValidationError) => {
|
|
27
|
+
const text = HelpDoc.toAnsiText(error.error).trimEnd();
|
|
28
|
+
return stripAnsi(text.length > 0 ? text : "Invalid command input. Use --help for usage.");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getAgentPayload = (error: unknown): AgentErrorPayload | undefined => {
|
|
32
|
+
if (error instanceof CliJsonError || error instanceof CliInputError) {
|
|
33
|
+
return parseAgentErrorPayload(error.message);
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const formatError = (error: unknown, agentPayload?: AgentErrorPayload) => {
|
|
39
|
+
if (agentPayload) {
|
|
40
|
+
return agentPayload.message;
|
|
41
|
+
}
|
|
42
|
+
if (ValidationError.isValidationError(error)) {
|
|
43
|
+
return formatValidationError(error);
|
|
44
|
+
}
|
|
45
|
+
if (error instanceof CliJsonError) {
|
|
46
|
+
return error.message;
|
|
47
|
+
}
|
|
48
|
+
if (error instanceof CliInputError) {
|
|
49
|
+
return error.message;
|
|
50
|
+
}
|
|
51
|
+
if (error instanceof StoreNotFound) {
|
|
52
|
+
return `Store \"${error.name}\" does not exist.`;
|
|
53
|
+
}
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
return error.message;
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
typeof error === "object" &&
|
|
59
|
+
error !== null &&
|
|
60
|
+
"message" in error &&
|
|
61
|
+
typeof (error as { readonly message?: unknown }).message === "string"
|
|
62
|
+
) {
|
|
63
|
+
return (error as { readonly message: string }).message;
|
|
64
|
+
}
|
|
65
|
+
if (typeof error === "object" && error !== null && "_tag" in error) {
|
|
66
|
+
return JSON.stringify(error);
|
|
67
|
+
}
|
|
68
|
+
return String(error);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const errorType = (error: unknown, agentPayload?: AgentErrorPayload): string => {
|
|
72
|
+
if (agentPayload) return agentPayload.error;
|
|
73
|
+
if (ValidationError.isValidationError(error)) return "ValidationError";
|
|
74
|
+
if (error instanceof Error) return error.name;
|
|
75
|
+
if (typeof error === "object" && error !== null && "_tag" in error) {
|
|
76
|
+
return String((error as { readonly _tag?: unknown })._tag ?? "UnknownError");
|
|
77
|
+
}
|
|
78
|
+
return "UnknownError";
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const errorDetails = (
|
|
82
|
+
error: unknown,
|
|
83
|
+
agentPayload?: AgentErrorPayload
|
|
84
|
+
): Record<string, unknown> | undefined => {
|
|
85
|
+
if (agentPayload) {
|
|
86
|
+
return { error: agentPayload };
|
|
87
|
+
}
|
|
88
|
+
if (error instanceof StoreNotFound) {
|
|
89
|
+
return { error: { _tag: "StoreNotFound", name: error.name } };
|
|
90
|
+
}
|
|
91
|
+
if (error instanceof BskyError) {
|
|
92
|
+
return {
|
|
93
|
+
error: {
|
|
94
|
+
_tag: "BskyError",
|
|
95
|
+
...(error.operation ? { operation: error.operation } : {}),
|
|
96
|
+
...(typeof error.status === "number" ? { status: error.status } : {})
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (error instanceof CliJsonError || error instanceof CliInputError) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
if (typeof error === "object" && error !== null && "_tag" in error) {
|
|
104
|
+
return { error };
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const errorSuggestion = (
|
|
110
|
+
error: unknown,
|
|
111
|
+
agentPayload?: AgentErrorPayload
|
|
112
|
+
): string | undefined => {
|
|
113
|
+
if (agentPayload?.fix) return agentPayload.fix;
|
|
114
|
+
if (error instanceof StoreNotFound) {
|
|
115
|
+
return `Run: skygent store create ${error.name}`;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const program = cli(process.argv).pipe(
|
|
121
|
+
Effect.tapError((error) => {
|
|
122
|
+
if (ValidationError.isValidationError(error)) {
|
|
123
|
+
return Effect.void;
|
|
124
|
+
}
|
|
125
|
+
const agentPayload = getAgentPayload(error);
|
|
126
|
+
return logErrorEvent(formatError(error, agentPayload), {
|
|
127
|
+
code: exitCodeFor(error),
|
|
128
|
+
type: errorType(error, agentPayload),
|
|
129
|
+
suggestion: errorSuggestion(error, agentPayload),
|
|
130
|
+
...errorDetails(error, agentPayload)
|
|
131
|
+
});
|
|
132
|
+
}),
|
|
133
|
+
Effect.provide(Layer.mergeAll(BunContext.layer, CliOutput.layer))
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
BunRuntime.runMain({
|
|
137
|
+
disableErrorReporting: true,
|
|
138
|
+
disablePrettyLogger: true,
|
|
139
|
+
teardown: (exit, onExit) => {
|
|
140
|
+
const code = exitCodeFromExit(exit);
|
|
141
|
+
onExit(code);
|
|
142
|
+
if (typeof process !== "undefined") {
|
|
143
|
+
process.exit(code);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
})(program);
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mepuka/skygent",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Composable Bluesky data filtering and monitoring CLI built with Effect",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"skygent": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src/",
|
|
13
|
+
"package.json"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/mepuka/skygent-bsky.git"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"prepare": "effect-language-service patch",
|
|
21
|
+
"build": "tsc --build",
|
|
22
|
+
"build:clean": "rm -rf dist .tsbuildinfo && bun run build",
|
|
23
|
+
"build:binary": "bun build --compile index.ts --outfile skygent",
|
|
24
|
+
"build:binary:linux-x64": "bun build --compile --target=bun-linux-x64 index.ts --outfile skygent-linux-x64",
|
|
25
|
+
"build:binary:linux-arm64": "bun build --compile --target=bun-linux-arm64 index.ts --outfile skygent-linux-arm64",
|
|
26
|
+
"build:binary:darwin-x64": "bun build --compile --target=bun-darwin-x64 index.ts --outfile skygent-darwin-x64",
|
|
27
|
+
"build:binary:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 index.ts --outfile skygent-darwin-arm64",
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"test:watch": "bun test --watch",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"changeset": "changeset",
|
|
32
|
+
"version": "changeset version",
|
|
33
|
+
"release": "bun run build && changeset publish"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@changesets/cli": "^2.29.8",
|
|
37
|
+
"@effect/language-service": "^0.72.0",
|
|
38
|
+
"@types/bun": "latest"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"typescript": "^5"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@atproto/api": "^0.18.17",
|
|
45
|
+
"@effect/cli": "^0.73.1",
|
|
46
|
+
"@effect/platform": "^0.94.2",
|
|
47
|
+
"@effect/platform-bun": "^0.87.1",
|
|
48
|
+
"@effect/printer": "^0.47.0",
|
|
49
|
+
"@effect/printer-ansi": "^0.47.0",
|
|
50
|
+
"@effect/sql": "^0.49.0",
|
|
51
|
+
"@effect/sql-sqlite-bun": "^0.50.0",
|
|
52
|
+
"@effect/typeclass": "^0.38.0",
|
|
53
|
+
"effect": "^3.19.15",
|
|
54
|
+
"effect-jetstream": "^1.0.4"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/cli/app.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { Layer, Option } from "effect";
|
|
3
|
+
import { CliLive } from "./layers.js";
|
|
4
|
+
import { ConfigOverrides } from "../services/app-config.js";
|
|
5
|
+
import { CredentialsOverrides } from "../services/credential-store.js";
|
|
6
|
+
import { DerivationSettingsOverrides } from "../services/derivation-settings.js";
|
|
7
|
+
import { SyncSettingsOverrides } from "../services/sync-settings.js";
|
|
8
|
+
import { CliPreferences } from "./preferences.js";
|
|
9
|
+
import { storeCommand } from "./store.js";
|
|
10
|
+
import { syncCommand } from "./sync.js";
|
|
11
|
+
import { queryCommand } from "./query.js";
|
|
12
|
+
import { watchCommand } from "./watch.js";
|
|
13
|
+
import { deriveCommand } from "./derive.js";
|
|
14
|
+
import { viewCommand } from "./view.js";
|
|
15
|
+
import { filterCommand } from "./filter.js";
|
|
16
|
+
import { searchCommand } from "./search.js";
|
|
17
|
+
import { graphCommand } from "./graph.js";
|
|
18
|
+
import { feedCommand } from "./feed.js";
|
|
19
|
+
import { postCommand } from "./post.js";
|
|
20
|
+
import { configCommand } from "./config-command.js";
|
|
21
|
+
import {
|
|
22
|
+
configOptions,
|
|
23
|
+
toConfigOverrides,
|
|
24
|
+
toCredentialsOverrides,
|
|
25
|
+
toSyncSettingsOverrides
|
|
26
|
+
} from "./config.js";
|
|
27
|
+
import { withExamples } from "./help.js";
|
|
28
|
+
|
|
29
|
+
export const app = Command.make("skygent", configOptions).pipe(
|
|
30
|
+
Command.withSubcommands([
|
|
31
|
+
configCommand,
|
|
32
|
+
storeCommand,
|
|
33
|
+
syncCommand,
|
|
34
|
+
queryCommand,
|
|
35
|
+
watchCommand,
|
|
36
|
+
deriveCommand,
|
|
37
|
+
viewCommand,
|
|
38
|
+
filterCommand,
|
|
39
|
+
searchCommand,
|
|
40
|
+
graphCommand,
|
|
41
|
+
feedCommand,
|
|
42
|
+
postCommand
|
|
43
|
+
]),
|
|
44
|
+
Command.provide((config) =>
|
|
45
|
+
Layer.mergeAll(
|
|
46
|
+
CliLive.pipe(
|
|
47
|
+
Layer.provide(
|
|
48
|
+
Layer.mergeAll(
|
|
49
|
+
Layer.succeed(ConfigOverrides, toConfigOverrides(config)),
|
|
50
|
+
Layer.succeed(CredentialsOverrides, toCredentialsOverrides(config)),
|
|
51
|
+
Layer.succeed(SyncSettingsOverrides, toSyncSettingsOverrides(config)),
|
|
52
|
+
DerivationSettingsOverrides.layer
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
Layer.succeed(
|
|
57
|
+
CliPreferences,
|
|
58
|
+
Option.match(config.logFormat, {
|
|
59
|
+
onNone: () => ({ compact: config.compact }),
|
|
60
|
+
onSome: (logFormat) => ({ compact: config.compact, logFormat })
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
),
|
|
65
|
+
Command.withDescription(
|
|
66
|
+
withExamples(
|
|
67
|
+
"Skygent CLI for Bluesky monitoring",
|
|
68
|
+
[
|
|
69
|
+
"skygent store list --compact",
|
|
70
|
+
"skygent sync timeline --store my-store --quiet"
|
|
71
|
+
],
|
|
72
|
+
["Tip: add --compact for shorter JSON output."]
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Command } from "@effect/cli";
|
|
2
|
+
import { FileSystem, Path } from "@effect/platform";
|
|
3
|
+
import { Options } from "@effect/cli";
|
|
4
|
+
import { Clock, Effect, Option, Stream } from "effect";
|
|
5
|
+
import { withExamples } from "./help.js";
|
|
6
|
+
import { writeJson, writeText } from "./output.js";
|
|
7
|
+
import { renderTableLegacy } from "./doc/table.js";
|
|
8
|
+
import { AppConfigService } from "../services/app-config.js";
|
|
9
|
+
import { CredentialStore } from "../services/credential-store.js";
|
|
10
|
+
import { BskyClient } from "../services/bsky-client.js";
|
|
11
|
+
import { jsonTableFormats, resolveOutputFormat } from "./output-format.js";
|
|
12
|
+
|
|
13
|
+
type CheckStatus = "ok" | "warn" | "error";
|
|
14
|
+
|
|
15
|
+
type CheckResult = {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly status: CheckStatus;
|
|
18
|
+
readonly message?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const checkOk = (name: string, message?: string): CheckResult =>
|
|
22
|
+
message ? { name, status: "ok", message } : { name, status: "ok" };
|
|
23
|
+
|
|
24
|
+
const checkWarn = (name: string, message: string): CheckResult => ({
|
|
25
|
+
name,
|
|
26
|
+
status: "warn",
|
|
27
|
+
message
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const checkError = (name: string, message: string): CheckResult => ({
|
|
31
|
+
name,
|
|
32
|
+
status: "error",
|
|
33
|
+
message
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const checkFormatOption = Options.choice("format", jsonTableFormats).pipe(
|
|
37
|
+
Options.withDescription("Output format (default: json)"),
|
|
38
|
+
Options.optional
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const configCheckCommand = Command.make("check", { format: checkFormatOption }, ({ format }) =>
|
|
42
|
+
Effect.gen(function* () {
|
|
43
|
+
const results: Array<CheckResult> = [];
|
|
44
|
+
|
|
45
|
+
const fs = yield* FileSystem.FileSystem;
|
|
46
|
+
const path = yield* Path.Path;
|
|
47
|
+
const config = yield* AppConfigService;
|
|
48
|
+
const credentials = yield* CredentialStore;
|
|
49
|
+
const bsky = yield* BskyClient;
|
|
50
|
+
// Store root writable
|
|
51
|
+
const rootCheck = yield* Effect.gen(function* () {
|
|
52
|
+
yield* fs.makeDirectory(config.storeRoot, { recursive: true });
|
|
53
|
+
const now = yield* Clock.currentTimeMillis;
|
|
54
|
+
const probePath = path.join(
|
|
55
|
+
config.storeRoot,
|
|
56
|
+
`.skygent-check-${now}`
|
|
57
|
+
);
|
|
58
|
+
yield* fs.writeFileString(probePath, "ok");
|
|
59
|
+
yield* fs.remove(probePath);
|
|
60
|
+
return checkOk("store-root", "Store root is writable.");
|
|
61
|
+
}).pipe(
|
|
62
|
+
Effect.catchAll((error) =>
|
|
63
|
+
Effect.succeed(
|
|
64
|
+
checkError(
|
|
65
|
+
"store-root",
|
|
66
|
+
error instanceof Error ? error.message : String(error)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
results.push(rootCheck);
|
|
72
|
+
|
|
73
|
+
// Credential file + key
|
|
74
|
+
const credentialCheck = yield* credentials.get().pipe(
|
|
75
|
+
Effect.match({
|
|
76
|
+
onFailure: (error) =>
|
|
77
|
+
checkError("credentials", error.message ?? "Failed to load credentials"),
|
|
78
|
+
onSuccess: (value) =>
|
|
79
|
+
Option.isSome(value)
|
|
80
|
+
? checkOk("credentials", "Credentials loaded.")
|
|
81
|
+
: checkWarn("credentials", "No credentials configured.")
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
results.push(credentialCheck);
|
|
85
|
+
|
|
86
|
+
// Bluesky auth
|
|
87
|
+
if (credentialCheck.status === "ok") {
|
|
88
|
+
const bskyCheck = yield* bsky
|
|
89
|
+
.getTimeline({ limit: 1 })
|
|
90
|
+
.pipe(
|
|
91
|
+
Stream.take(1),
|
|
92
|
+
Stream.runCollect,
|
|
93
|
+
Effect.as(checkOk("bluesky", "Bluesky login succeeded.")),
|
|
94
|
+
Effect.catchAll((error) =>
|
|
95
|
+
Effect.succeed(
|
|
96
|
+
checkError(
|
|
97
|
+
"bluesky",
|
|
98
|
+
error instanceof Error ? error.message : String(error)
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
results.push(bskyCheck);
|
|
104
|
+
} else {
|
|
105
|
+
results.push(
|
|
106
|
+
checkWarn("bluesky", "Skipped Bluesky login (credentials missing).")
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ok = results.every((result) => result.status !== "error");
|
|
111
|
+
const outputFormat = resolveOutputFormat(
|
|
112
|
+
format,
|
|
113
|
+
config.outputFormat,
|
|
114
|
+
jsonTableFormats,
|
|
115
|
+
"json"
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (outputFormat === "table") {
|
|
119
|
+
const rows = results.map((r) => [
|
|
120
|
+
r.status === "ok" ? "✓" : r.status === "warn" ? "⚠" : "✗",
|
|
121
|
+
r.name,
|
|
122
|
+
r.message || r.status
|
|
123
|
+
]);
|
|
124
|
+
const table = renderTableLegacy(["STATUS", "CHECK", "DETAILS"], rows);
|
|
125
|
+
yield* writeText(`${ok ? "✓ Config valid" : "✗ Config has errors"}\n\n${table}`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
yield* writeJson({ ok, checks: results });
|
|
130
|
+
})
|
|
131
|
+
).pipe(
|
|
132
|
+
Command.withDescription("Run health checks (store root, credentials, Bluesky auth)")
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
export const configCommand = Command.make("config", {}).pipe(
|
|
136
|
+
Command.withSubcommands([configCheckCommand]),
|
|
137
|
+
Command.withDescription(
|
|
138
|
+
withExamples("Configuration helpers", ["skygent config check"])
|
|
139
|
+
)
|
|
140
|
+
);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Options } from "@effect/cli";
|
|
2
|
+
import { Option, Redacted } from "effect";
|
|
3
|
+
import { pickDefined } from "../services/shared.js";
|
|
4
|
+
import { OutputFormat } from "../domain/config.js";
|
|
5
|
+
import { AppConfig } from "../domain/config.js";
|
|
6
|
+
import type { LogFormat } from "./logging.js";
|
|
7
|
+
import type { SyncSettingsValue } from "../services/sync-settings.js";
|
|
8
|
+
import type { CredentialsOverridesValue } from "../services/credential-store.js";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export const configOptions = {
|
|
12
|
+
service: Options.text("service").pipe(
|
|
13
|
+
Options.optional,
|
|
14
|
+
Options.withDescription("Override the Bluesky service URL")
|
|
15
|
+
),
|
|
16
|
+
storeRoot: Options.text("store-root").pipe(
|
|
17
|
+
Options.optional,
|
|
18
|
+
Options.withDescription("Override the root storage directory")
|
|
19
|
+
),
|
|
20
|
+
outputFormat: Options.choice("output-format", [
|
|
21
|
+
"json",
|
|
22
|
+
"ndjson",
|
|
23
|
+
"markdown",
|
|
24
|
+
"table"
|
|
25
|
+
]).pipe(Options.optional, Options.withDescription("Default output format")),
|
|
26
|
+
identifier: Options.text("identifier").pipe(
|
|
27
|
+
Options.optional,
|
|
28
|
+
Options.withDescription("Override Bluesky identifier")
|
|
29
|
+
),
|
|
30
|
+
password: Options.redacted("password").pipe(
|
|
31
|
+
Options.optional,
|
|
32
|
+
Options.withDescription("Override Bluesky password (redacted)")
|
|
33
|
+
),
|
|
34
|
+
compact: Options.boolean("compact").pipe(
|
|
35
|
+
Options.withDescription("Reduce JSON output verbosity for agent consumption")
|
|
36
|
+
),
|
|
37
|
+
logFormat: Options.choice("log-format", ["json", "human"]).pipe(
|
|
38
|
+
Options.optional,
|
|
39
|
+
Options.withDescription("Override log format (json or human)")
|
|
40
|
+
),
|
|
41
|
+
syncConcurrency: Options.integer("sync-concurrency").pipe(
|
|
42
|
+
Options.optional,
|
|
43
|
+
Options.withDescription("Concurrent sync preparation workers (default: 5)")
|
|
44
|
+
),
|
|
45
|
+
checkpointEvery: Options.integer("checkpoint-every").pipe(
|
|
46
|
+
Options.optional,
|
|
47
|
+
Options.withDescription("Checkpoint every N processed posts (default: 100)")
|
|
48
|
+
),
|
|
49
|
+
checkpointIntervalMs: Options.integer("checkpoint-interval-ms").pipe(
|
|
50
|
+
Options.optional,
|
|
51
|
+
Options.withDescription("Checkpoint interval in milliseconds (default: 5000)")
|
|
52
|
+
)
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ConfigOptions = {
|
|
56
|
+
readonly service: Option.Option<string>;
|
|
57
|
+
readonly storeRoot: Option.Option<string>;
|
|
58
|
+
readonly outputFormat: Option.Option<OutputFormat>;
|
|
59
|
+
readonly identifier: Option.Option<string>;
|
|
60
|
+
readonly password: Option.Option<Redacted.Redacted<string>>;
|
|
61
|
+
readonly compact: boolean;
|
|
62
|
+
readonly logFormat: Option.Option<LogFormat>;
|
|
63
|
+
readonly syncConcurrency: Option.Option<number>;
|
|
64
|
+
readonly checkpointEvery: Option.Option<number>;
|
|
65
|
+
readonly checkpointIntervalMs: Option.Option<number>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const toConfigOverrides = (options: ConfigOptions): Partial<AppConfig> =>
|
|
69
|
+
pickDefined({
|
|
70
|
+
service: Option.getOrUndefined(options.service),
|
|
71
|
+
storeRoot: Option.getOrUndefined(options.storeRoot),
|
|
72
|
+
outputFormat: Option.getOrUndefined(options.outputFormat),
|
|
73
|
+
identifier: Option.getOrUndefined(options.identifier)
|
|
74
|
+
}) as Partial<AppConfig>;
|
|
75
|
+
|
|
76
|
+
export const toCredentialsOverrides = (
|
|
77
|
+
options: ConfigOptions
|
|
78
|
+
): CredentialsOverridesValue =>
|
|
79
|
+
pickDefined({
|
|
80
|
+
identifier: Option.getOrUndefined(options.identifier),
|
|
81
|
+
password: Option.getOrUndefined(options.password)
|
|
82
|
+
}) as CredentialsOverridesValue;
|
|
83
|
+
|
|
84
|
+
export const toSyncSettingsOverrides = (
|
|
85
|
+
options: ConfigOptions
|
|
86
|
+
): Partial<SyncSettingsValue> =>
|
|
87
|
+
pickDefined({
|
|
88
|
+
concurrency: Option.getOrUndefined(options.syncConcurrency),
|
|
89
|
+
checkpointEvery: Option.getOrUndefined(options.checkpointEvery),
|
|
90
|
+
checkpointIntervalMs: Option.getOrUndefined(options.checkpointIntervalMs)
|
|
91
|
+
}) as Partial<SyncSettingsValue>;
|