@rikalabs/logpoint 0.0.2

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.
@@ -0,0 +1,249 @@
1
+ import { Effect } from "effect";
2
+ import { ParseResult, Schema, TreeFormatter } from "@effect/schema";
3
+ import { ManifestError, ParseError, ValidationError } from "./errors.js";
4
+ import { SupportedLanguages, type Language } from "./language.js";
5
+
6
+ const PositiveInt = Schema.Number.pipe(
7
+ Schema.filter((value): value is number => Number.isInteger(value) && value > 0),
8
+ );
9
+
10
+ const ValidPort = Schema.Number.pipe(
11
+ Schema.filter((value): value is number => Number.isInteger(value) && value >= 1024 && value <= 65535),
12
+ );
13
+
14
+ const NonEmptyString = Schema.String.pipe(Schema.minLength(1));
15
+
16
+ const NonEmptyStringArray = Schema.Array(Schema.String).pipe(
17
+ Schema.filter((items): items is readonly string[] => items.length > 0),
18
+ );
19
+
20
+ const LogpointId = Schema.String.pipe(Schema.pattern(/^[a-z0-9_-]+$/));
21
+
22
+ export const LanguageSchema = Schema.Literal(...SupportedLanguages);
23
+
24
+ export const LogpointDefSchema = Schema.Struct({
25
+ id: LogpointId,
26
+ file: NonEmptyString,
27
+ line: PositiveInt,
28
+ label: Schema.String,
29
+ hypothesis: Schema.String,
30
+ capture: NonEmptyStringArray,
31
+ maxHits: Schema.optional(PositiveInt),
32
+ });
33
+
34
+ export type LogpointDefInput = typeof LogpointDefSchema.Type;
35
+
36
+ export type LogpointDef = Omit<LogpointDefInput, "maxHits"> & {
37
+ readonly maxHits: number;
38
+ };
39
+
40
+ export const ManifestSchema = Schema.Struct({
41
+ port: Schema.optional(ValidPort),
42
+ projectRoot: Schema.optional(Schema.String),
43
+ language: Schema.optional(LanguageSchema),
44
+ logpoints: Schema.Array(LogpointDefSchema).pipe(
45
+ Schema.filter((items): items is readonly LogpointDefInput[] => items.length > 0),
46
+ ),
47
+ });
48
+
49
+ export type ManifestInput = typeof ManifestSchema.Type;
50
+
51
+ export type Manifest = {
52
+ readonly port: number;
53
+ readonly projectRoot: string;
54
+ readonly language?: Language;
55
+ readonly logpoints: readonly LogpointDef[];
56
+ };
57
+
58
+ export const SnapshotSchema = Schema.Struct({
59
+ id: Schema.String,
60
+ file: Schema.String,
61
+ line: Schema.Number,
62
+ label: Schema.String,
63
+ hypothesis: Schema.String,
64
+ timestamp: Schema.String,
65
+ vars: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
66
+ hit: Schema.optional(Schema.Number),
67
+ maxHits: Schema.optional(PositiveInt),
68
+ });
69
+
70
+ export type Snapshot = typeof SnapshotSchema.Type;
71
+
72
+ export const CollectorConfigSchema = Schema.Struct({
73
+ port: Schema.optional(ValidPort),
74
+ output: Schema.optional(Schema.String),
75
+ timeout: Schema.optional(PositiveInt),
76
+ corsOrigin: Schema.optional(Schema.String),
77
+ });
78
+
79
+ export type CollectorConfigInput = typeof CollectorConfigSchema.Type;
80
+
81
+ export type CollectorConfig = {
82
+ readonly port: number;
83
+ readonly output: string;
84
+ readonly timeout: number;
85
+ readonly corsOrigin: string;
86
+ };
87
+
88
+ export const InjectConfigSchema = Schema.Struct({
89
+ manifest: NonEmptyString,
90
+ projectRoot: Schema.optional(Schema.String),
91
+ language: Schema.optional(LanguageSchema),
92
+ dryRun: Schema.optional(Schema.Boolean),
93
+ });
94
+
95
+ export type InjectConfigInput = typeof InjectConfigSchema.Type;
96
+
97
+ export type InjectConfig = {
98
+ readonly manifest: string;
99
+ readonly projectRoot?: string;
100
+ readonly language?: Language;
101
+ readonly dryRun: boolean;
102
+ };
103
+
104
+ export const CleanupConfigSchema = Schema.Struct({
105
+ dir: Schema.optional(Schema.String),
106
+ ids: Schema.optional(Schema.Array(LogpointId)),
107
+ dryRun: Schema.optional(Schema.Boolean),
108
+ verify: Schema.optional(Schema.Boolean),
109
+ });
110
+
111
+ export type CleanupConfigInput = typeof CleanupConfigSchema.Type;
112
+
113
+ export type CleanupConfig = {
114
+ readonly dir: string;
115
+ readonly ids?: readonly string[];
116
+ readonly dryRun: boolean;
117
+ readonly verify: boolean;
118
+ };
119
+
120
+ export const AnalyzeFormatSchema = Schema.Literal("markdown", "json");
121
+
122
+ export type AnalyzeFormat = typeof AnalyzeFormatSchema.Type;
123
+
124
+ export const AnalyzeConfigSchema = Schema.Struct({
125
+ input: Schema.optional(Schema.String),
126
+ format: Schema.optional(AnalyzeFormatSchema),
127
+ });
128
+
129
+ export type AnalyzeConfigInput = typeof AnalyzeConfigSchema.Type;
130
+
131
+ export type AnalyzeConfig = {
132
+ readonly input: string;
133
+ readonly format: AnalyzeFormat;
134
+ };
135
+
136
+ export const formatParseError = (error: ParseResult.ParseError): string => {
137
+ try {
138
+ return TreeFormatter.formatErrorSync(error);
139
+ } catch {
140
+ return error.message;
141
+ }
142
+ };
143
+
144
+ const decode = <T>(
145
+ result: ReturnType<ReturnType<typeof Schema.decodeUnknownEither<T, unknown>>>,
146
+ toError: (message: string, cause: ParseResult.ParseError) => ManifestError | ValidationError,
147
+ ): Effect.Effect<T, ManifestError | ValidationError, never> => {
148
+ if (result._tag === "Left") {
149
+ return Effect.fail(toError(formatParseError(result.left), result.left));
150
+ }
151
+ return Effect.succeed(result.right);
152
+ };
153
+
154
+ export const parseJsonString = (input: string): Effect.Effect<unknown, ParseError, never> =>
155
+ Effect.try({
156
+ try: () => JSON.parse(input) as unknown,
157
+ catch: (cause) => new ParseError({ input, cause }),
158
+ });
159
+
160
+ const withLogpointDefaults = (input: LogpointDefInput): LogpointDef => ({
161
+ ...input,
162
+ maxHits: input.maxHits ?? 100,
163
+ });
164
+
165
+ export const withManifestDefaults = (input: ManifestInput): Manifest => ({
166
+ port: input.port ?? 9111,
167
+ projectRoot: input.projectRoot ?? ".",
168
+ ...(input.language === undefined ? {} : { language: input.language }),
169
+ logpoints: input.logpoints.map(withLogpointDefaults),
170
+ });
171
+
172
+ export const withCollectorDefaults = (input: CollectorConfigInput): CollectorConfig => ({
173
+ port: input.port ?? 9111,
174
+ output: input.output ?? "/tmp/debug-logpoints.jsonl",
175
+ timeout: input.timeout ?? 300,
176
+ corsOrigin: input.corsOrigin ?? "*",
177
+ });
178
+
179
+ export const withInjectDefaults = (input: InjectConfigInput): InjectConfig => ({
180
+ manifest: input.manifest,
181
+ ...(input.projectRoot === undefined ? {} : { projectRoot: input.projectRoot }),
182
+ ...(input.language === undefined ? {} : { language: input.language }),
183
+ dryRun: input.dryRun ?? false,
184
+ });
185
+
186
+ export const withCleanupDefaults = (input: CleanupConfigInput): CleanupConfig => ({
187
+ dir: input.dir ?? ".",
188
+ ...(input.ids === undefined ? {} : { ids: input.ids }),
189
+ dryRun: input.dryRun ?? false,
190
+ verify: input.verify ?? true,
191
+ });
192
+
193
+ export const withAnalyzeDefaults = (input: AnalyzeConfigInput): AnalyzeConfig => ({
194
+ input: input.input ?? "/tmp/debug-logpoints.jsonl",
195
+ format: input.format ?? "markdown",
196
+ });
197
+
198
+ export const decodeManifestUnknown = (input: unknown): Effect.Effect<Manifest, ManifestError, never> => {
199
+ const parsed = Schema.decodeUnknownEither(ManifestSchema)(input);
200
+ return decode(parsed, (message, cause) => new ManifestError({ message, cause })).pipe(
201
+ Effect.map((value) => withManifestDefaults(value as ManifestInput)),
202
+ Effect.mapError((error) => (error._tag === "ManifestError" ? error : new ManifestError({ message: error.message, cause: error }))),
203
+ );
204
+ };
205
+
206
+ export const decodeSnapshotUnknown = (input: unknown): Effect.Effect<Snapshot, ValidationError, never> => {
207
+ const parsed = Schema.decodeUnknownEither(SnapshotSchema)(input);
208
+ return decode(parsed, (message) => new ValidationError({ message })).pipe(
209
+ Effect.mapError((error) =>
210
+ error._tag === "ValidationError" ? error : new ValidationError({ message: error.message }),
211
+ ),
212
+ );
213
+ };
214
+
215
+ export const decodeCollectorConfigUnknown = (
216
+ input: unknown,
217
+ ): Effect.Effect<CollectorConfig, ManifestError, never> => {
218
+ const parsed = Schema.decodeUnknownEither(CollectorConfigSchema)(input);
219
+ return decode(parsed, (message, cause) => new ManifestError({ message, cause })).pipe(
220
+ Effect.map((value) => withCollectorDefaults(value as CollectorConfigInput)),
221
+ Effect.mapError((error) => (error._tag === "ManifestError" ? error : new ManifestError({ message: error.message, cause: error }))),
222
+ );
223
+ };
224
+
225
+ export const decodeInjectConfigUnknown = (input: unknown): Effect.Effect<InjectConfig, ManifestError, never> => {
226
+ const parsed = Schema.decodeUnknownEither(InjectConfigSchema)(input);
227
+ return decode(parsed, (message, cause) => new ManifestError({ message, cause })).pipe(
228
+ Effect.map((value) => withInjectDefaults(value as InjectConfigInput)),
229
+ Effect.mapError((error) => (error._tag === "ManifestError" ? error : new ManifestError({ message: error.message, cause: error }))),
230
+ );
231
+ };
232
+
233
+ export const decodeCleanupConfigUnknown = (
234
+ input: unknown,
235
+ ): Effect.Effect<CleanupConfig, ManifestError, never> => {
236
+ const parsed = Schema.decodeUnknownEither(CleanupConfigSchema)(input);
237
+ return decode(parsed, (message, cause) => new ManifestError({ message, cause })).pipe(
238
+ Effect.map((value) => withCleanupDefaults(value as CleanupConfigInput)),
239
+ Effect.mapError((error) => (error._tag === "ManifestError" ? error : new ManifestError({ message: error.message, cause: error }))),
240
+ );
241
+ };
242
+
243
+ export const decodeAnalyzeConfigUnknown = (input: unknown): Effect.Effect<AnalyzeConfig, ManifestError, never> => {
244
+ const parsed = Schema.decodeUnknownEither(AnalyzeConfigSchema)(input);
245
+ return decode(parsed, (message, cause) => new ManifestError({ message, cause })).pipe(
246
+ Effect.map((value) => withAnalyzeDefaults(value as AnalyzeConfigInput)),
247
+ Effect.mapError((error) => (error._tag === "ManifestError" ? error : new ManifestError({ message: error.message, cause: error }))),
248
+ );
249
+ };
@@ -0,0 +1,4 @@
1
+ const secretPattern =
2
+ /password|secret|token|api[_-]?key|private[_-]?key|auth|credential|jwt|bearer|ssh|cookie|csrf/i;
3
+
4
+ export const isSecretVariable = (name: string): boolean => secretPattern.test(name);
@@ -0,0 +1,107 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { Context, Effect, Layer } from "effect";
4
+ import { FileNotFound, FileReadError, FileWriteError } from "./errors.js";
5
+
6
+ export class FileSystem extends Context.Tag("FileSystem")<
7
+ FileSystem,
8
+ {
9
+ readonly readFile: (path: string) => Effect.Effect<string, FileNotFound | FileReadError, never>;
10
+ readonly writeFile: (path: string, content: string) => Effect.Effect<void, FileWriteError, never>;
11
+ readonly exists: (path: string) => Effect.Effect<boolean, never, never>;
12
+ readonly glob: (pattern: string, cwd: string) => Effect.Effect<readonly string[], FileReadError, never>;
13
+ readonly removeFile: (path: string) => Effect.Effect<void, FileWriteError, never>;
14
+ readonly mkdirp: (path: string) => Effect.Effect<void, FileWriteError, never>;
15
+ }
16
+ >() {}
17
+
18
+ export class Logger extends Context.Tag("Logger")<
19
+ Logger,
20
+ {
21
+ readonly info: (message: string) => Effect.Effect<void, never, never>;
22
+ readonly warn: (message: string) => Effect.Effect<void, never, never>;
23
+ readonly error: (message: string) => Effect.Effect<void, never, never>;
24
+ readonly json: (value: unknown) => Effect.Effect<void, never, never>;
25
+ }
26
+ >() {}
27
+
28
+ export class Clock extends Context.Tag("Clock")<
29
+ Clock,
30
+ {
31
+ readonly now: () => Effect.Effect<Date, never, never>;
32
+ }
33
+ >() {}
34
+
35
+ export class RuntimeEnv extends Context.Tag("RuntimeEnv")<
36
+ RuntimeEnv,
37
+ {
38
+ readonly cwd: string;
39
+ readonly tmpDir: string;
40
+ }
41
+ >() {}
42
+
43
+ export const FileSystemLive = Layer.succeed(FileSystem, {
44
+ readFile: (path: string) =>
45
+ Effect.gen(function* () {
46
+ const exists = yield* Effect.promise(() => Bun.file(path).exists());
47
+ if (!exists) {
48
+ return yield* Effect.fail(new FileNotFound({ path }));
49
+ }
50
+ return yield* Effect.tryPromise({
51
+ try: () => Bun.file(path).text(),
52
+ catch: (cause) => new FileReadError({ path, cause }),
53
+ });
54
+ }),
55
+ writeFile: (path: string, content: string) =>
56
+ Effect.tryPromise({
57
+ try: async () => {
58
+ await Bun.write(path, content);
59
+ },
60
+ catch: (cause) => new FileWriteError({ path, cause }),
61
+ }),
62
+ exists: (path: string) => Effect.promise(() => Bun.file(path).exists()),
63
+ glob: (pattern: string, cwd: string) =>
64
+ Effect.tryPromise({
65
+ try: async () => {
66
+ const matches: string[] = [];
67
+ const glob = new Bun.Glob(pattern);
68
+ for await (const entry of glob.scan({ cwd, onlyFiles: true, dot: false })) {
69
+ matches.push(join(cwd, entry));
70
+ }
71
+ return matches;
72
+ },
73
+ catch: (cause) => new FileReadError({ path: cwd, cause }),
74
+ }),
75
+ removeFile: (path: string) =>
76
+ Effect.tryPromise({
77
+ try: async () => {
78
+ await Bun.file(path).delete();
79
+ },
80
+ catch: (cause) => new FileWriteError({ path, cause }),
81
+ }),
82
+ mkdirp: (path: string) =>
83
+ Effect.tryPromise({
84
+ try: async () => {
85
+ await mkdir(path, { recursive: true });
86
+ },
87
+ catch: (cause) => new FileWriteError({ path, cause }),
88
+ }),
89
+ });
90
+
91
+ export const LoggerLive = Layer.succeed(Logger, {
92
+ info: (message: string) => Effect.sync(() => console.log(message)),
93
+ warn: (message: string) => Effect.sync(() => console.warn(message)),
94
+ error: (message: string) => Effect.sync(() => console.error(message)),
95
+ json: (value: unknown) => Effect.sync(() => console.log(JSON.stringify(value))),
96
+ });
97
+
98
+ export const ClockLive = Layer.succeed(Clock, {
99
+ now: () => Effect.sync(() => new Date()),
100
+ });
101
+
102
+ export const RuntimeEnvLive = Layer.succeed(RuntimeEnv, {
103
+ cwd: process.cwd(),
104
+ tmpDir: process.env["TMPDIR"] ?? "/tmp",
105
+ });
106
+
107
+ export const AppLive = Layer.mergeAll(FileSystemLive, LoggerLive, ClockLive, RuntimeEnvLive);