@letsrunit/journal 0.1.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 +49 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +318 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/index.ts +3 -0
- package/src/journal-batch.ts +67 -0
- package/src/journal.ts +85 -0
- package/src/sink/cli-sink.ts +153 -0
- package/src/sink/index.ts +3 -0
- package/src/sink/no-sink.ts +6 -0
- package/src/sink/supabase-sink.ts +107 -0
- package/src/types.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Journal Package (`@letsrunit/journal`)
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @letsrunit/journal
|
|
7
|
+
# or
|
|
8
|
+
yarn add @letsrunit/journal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Logging and reporting infrastructure for test execution and AI decision processes. It provides a structured way to capture logs, artifacts (like screenshots), and metadata during automation runs.
|
|
12
|
+
|
|
13
|
+
## Exported Classes
|
|
14
|
+
|
|
15
|
+
### `Journal<TSink>`
|
|
16
|
+
|
|
17
|
+
The main class for logging. It requires a `Sink` to publish entries.
|
|
18
|
+
|
|
19
|
+
#### Methods:
|
|
20
|
+
|
|
21
|
+
- **`static nil()`**: Creates a journal with a `NoSink` (logs are ignored).
|
|
22
|
+
- **`log(message, options)`**: The base logging method.
|
|
23
|
+
- **`debug|info|warn|error(message, options)`**: Standard logging levels.
|
|
24
|
+
- **`title(message, options)`**: Logs a title/header.
|
|
25
|
+
- **`start|success|failure(message, options)`**: Methods to track the lifecycle of an action.
|
|
26
|
+
- **`prepare(message, options)`**: Logs a preparation step.
|
|
27
|
+
- **`do(message, callback, metaFn)`**: A wrapper for executing an action. It automatically logs `start`, `success` (with meta/artifacts), or `failure`.
|
|
28
|
+
- **`batch()`**: Returns a `JournalBatch` instance for fluent, batched logging.
|
|
29
|
+
|
|
30
|
+
### `JournalBatch`
|
|
31
|
+
|
|
32
|
+
A builder-like class for logging multiple entries at once.
|
|
33
|
+
|
|
34
|
+
## Sinks
|
|
35
|
+
|
|
36
|
+
Sinks define where the logs are sent:
|
|
37
|
+
|
|
38
|
+
- **`NoSink`**: Discards all logs.
|
|
39
|
+
- **`ConsoleSink`**: Logs to the terminal.
|
|
40
|
+
- **`CliSink`**: Advanced console logging with progress indicators and verbosity control.
|
|
41
|
+
- **`SupabaseSink`**: Sends logs and artifacts to Supabase for persistence and real-time dashboard updates.
|
|
42
|
+
|
|
43
|
+
## Testing
|
|
44
|
+
|
|
45
|
+
Run tests for this package:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
yarn test
|
|
49
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
|
|
4
|
+
interface JournalEntry {
|
|
5
|
+
timestamp: number;
|
|
6
|
+
type: 'debug' | 'info' | 'title' | 'warn' | 'error' | 'prepare' | 'start' | 'success' | 'failure';
|
|
7
|
+
message: string;
|
|
8
|
+
artifacts: File[];
|
|
9
|
+
meta: Record<string, any>;
|
|
10
|
+
}
|
|
11
|
+
interface Sink {
|
|
12
|
+
publish(...entries: JournalEntry[]): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Options$1 = Partial<Pick<JournalEntry, 'artifacts' | 'meta'>>;
|
|
16
|
+
declare class JournalBatch {
|
|
17
|
+
private sink;
|
|
18
|
+
private entries;
|
|
19
|
+
constructor(sink: Sink);
|
|
20
|
+
log(message: string | undefined, options: Options$1 & {
|
|
21
|
+
type: JournalEntry['type'];
|
|
22
|
+
}): JournalBatch;
|
|
23
|
+
flush(): Promise<void>;
|
|
24
|
+
debug(message: string | undefined, options?: Options$1): JournalBatch;
|
|
25
|
+
info(message: string | undefined, options?: Options$1): JournalBatch;
|
|
26
|
+
title(message: string | undefined, options?: Options$1): JournalBatch;
|
|
27
|
+
warn(message: string | undefined, options?: Options$1): JournalBatch;
|
|
28
|
+
error(message: string | undefined, options?: Options$1): JournalBatch;
|
|
29
|
+
prepare(message: string | undefined, options?: Options$1): JournalBatch;
|
|
30
|
+
success(message: string | undefined, options?: Options$1): JournalBatch;
|
|
31
|
+
failure(message: string | undefined, options?: Options$1): JournalBatch;
|
|
32
|
+
each<T>(items: Array<T>, fn: (journal: JournalBatch, item: T) => void): JournalBatch;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type WriteFile = typeof writeFile;
|
|
36
|
+
interface CliSinkOptions {
|
|
37
|
+
stream?: NodeJS.WriteStream;
|
|
38
|
+
verbosity?: number;
|
|
39
|
+
artifactPath?: string;
|
|
40
|
+
writeFile?: WriteFile;
|
|
41
|
+
}
|
|
42
|
+
declare class CliSink implements Sink {
|
|
43
|
+
readonly stream: NodeJS.WriteStream;
|
|
44
|
+
readonly verbosity: number;
|
|
45
|
+
readonly artifactPath: string | undefined;
|
|
46
|
+
private readonly writeFile;
|
|
47
|
+
private entries;
|
|
48
|
+
constructor(options?: CliSinkOptions);
|
|
49
|
+
publish(...entries: JournalEntry[]): Promise<void>;
|
|
50
|
+
endSection(): void;
|
|
51
|
+
private replace;
|
|
52
|
+
private append;
|
|
53
|
+
private format;
|
|
54
|
+
private stripAnsi;
|
|
55
|
+
private countDisplayLines;
|
|
56
|
+
private storeArtifacts;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
declare class NoSink implements Sink {
|
|
60
|
+
publish(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface SupabaseSinkOptions {
|
|
64
|
+
supabase: SupabaseClient;
|
|
65
|
+
run: {
|
|
66
|
+
id: string;
|
|
67
|
+
projectId: string;
|
|
68
|
+
};
|
|
69
|
+
tableName?: string;
|
|
70
|
+
bucket?: string;
|
|
71
|
+
console?: {
|
|
72
|
+
error: (...args: any[]) => void;
|
|
73
|
+
warn: (...args: any[]) => void;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
declare class SupabaseSink implements Sink {
|
|
77
|
+
private readonly supabase;
|
|
78
|
+
private readonly projectId;
|
|
79
|
+
private readonly runId;
|
|
80
|
+
private readonly tableName;
|
|
81
|
+
private readonly bucket?;
|
|
82
|
+
private readonly console;
|
|
83
|
+
private bucketEnsured;
|
|
84
|
+
constructor(options: SupabaseSinkOptions);
|
|
85
|
+
publish(...entries: JournalEntry[]): Promise<void>;
|
|
86
|
+
private ensureBucket;
|
|
87
|
+
private storeArtifacts;
|
|
88
|
+
private artifactExists;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type Options = Partial<Pick<JournalEntry, 'artifacts' | 'meta'>>;
|
|
92
|
+
declare class Journal<TSink extends Sink = Sink> {
|
|
93
|
+
readonly sink: TSink;
|
|
94
|
+
constructor(sink: TSink);
|
|
95
|
+
static nil(): Journal<NoSink>;
|
|
96
|
+
log(message: string, options: Options & {
|
|
97
|
+
type: JournalEntry['type'];
|
|
98
|
+
}): Promise<void>;
|
|
99
|
+
batch(): JournalBatch;
|
|
100
|
+
do<T>(message: string, callback: () => T | Promise<T>, metaFn?: (result: T) => {
|
|
101
|
+
meta?: Record<string, any>;
|
|
102
|
+
artifacts?: File[];
|
|
103
|
+
}): Promise<T>;
|
|
104
|
+
debug(message: string, options?: Options): Promise<void>;
|
|
105
|
+
info(message: string, options?: Options): Promise<void>;
|
|
106
|
+
warn(message: string, options?: Options): Promise<void>;
|
|
107
|
+
error(message: string, options?: Options): Promise<void>;
|
|
108
|
+
title(message: string, options?: Options): Promise<void>;
|
|
109
|
+
prepare(message: string, options?: Options): Promise<void>;
|
|
110
|
+
start(message: string, options?: Options): Promise<void>;
|
|
111
|
+
success(message: string, options?: Options): Promise<void>;
|
|
112
|
+
failure(message: string, options?: Options): Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { CliSink, Journal, type JournalEntry, NoSink, type Sink, SupabaseSink };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { statusSymbol } from '@letsrunit/utils';
|
|
2
|
+
import { cursorUp, cursorLeft, eraseDown } from 'ansi-escapes';
|
|
3
|
+
import { writeFile } from 'fs/promises';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
|
|
6
|
+
// src/journal-batch.ts
|
|
7
|
+
var JournalBatch = class {
|
|
8
|
+
constructor(sink) {
|
|
9
|
+
this.sink = sink;
|
|
10
|
+
}
|
|
11
|
+
entries = [];
|
|
12
|
+
log(message, options) {
|
|
13
|
+
if (message) {
|
|
14
|
+
this.entries.push({
|
|
15
|
+
timestamp: Date.now(),
|
|
16
|
+
message,
|
|
17
|
+
type: options.type,
|
|
18
|
+
artifacts: options.artifacts ?? [],
|
|
19
|
+
meta: options.meta ?? {}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
async flush() {
|
|
25
|
+
await this.sink.publish(...this.entries);
|
|
26
|
+
}
|
|
27
|
+
debug(message, options = {}) {
|
|
28
|
+
return this.log(message, { ...options, type: "debug" });
|
|
29
|
+
}
|
|
30
|
+
info(message, options = {}) {
|
|
31
|
+
return this.log(message, { ...options, type: "info" });
|
|
32
|
+
}
|
|
33
|
+
title(message, options = {}) {
|
|
34
|
+
return this.log(message, { ...options, type: "title" });
|
|
35
|
+
}
|
|
36
|
+
warn(message, options = {}) {
|
|
37
|
+
return this.log(message, { ...options, type: "warn" });
|
|
38
|
+
}
|
|
39
|
+
error(message, options = {}) {
|
|
40
|
+
return this.log(message, { ...options, type: "error" });
|
|
41
|
+
}
|
|
42
|
+
prepare(message, options = {}) {
|
|
43
|
+
return this.log(message, { ...options, type: "prepare" });
|
|
44
|
+
}
|
|
45
|
+
success(message, options = {}) {
|
|
46
|
+
return this.log(message, { ...options, type: "success" });
|
|
47
|
+
}
|
|
48
|
+
failure(message, options = {}) {
|
|
49
|
+
return this.log(message, { ...options, type: "failure" });
|
|
50
|
+
}
|
|
51
|
+
each(items, fn) {
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
fn(this, item);
|
|
54
|
+
}
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function colorize(type, text) {
|
|
59
|
+
switch (type) {
|
|
60
|
+
case "debug":
|
|
61
|
+
case "prepare":
|
|
62
|
+
return `\x1B[90m${text}\x1B[0m`;
|
|
63
|
+
case "warn":
|
|
64
|
+
return `\x1B[33m${text}\x1B[0m`;
|
|
65
|
+
case "error":
|
|
66
|
+
case "failure":
|
|
67
|
+
return `\x1B[31m${text}\x1B[0m`;
|
|
68
|
+
case "success":
|
|
69
|
+
return `\x1B[32m${text}\x1B[0m`;
|
|
70
|
+
case "title":
|
|
71
|
+
return `\x1B[1m${text}\x1B[0m`;
|
|
72
|
+
case "info":
|
|
73
|
+
default:
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
var CliSink = class {
|
|
78
|
+
stream;
|
|
79
|
+
verbosity;
|
|
80
|
+
artifactPath;
|
|
81
|
+
writeFile;
|
|
82
|
+
entries = [];
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.stream = options.stream ?? process.stdout;
|
|
85
|
+
this.verbosity = options.verbosity ?? 1;
|
|
86
|
+
this.artifactPath = options.artifactPath;
|
|
87
|
+
this.writeFile = options.writeFile ?? writeFile;
|
|
88
|
+
}
|
|
89
|
+
async publish(...entries) {
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (entry.type === "debug" && this.verbosity < 2) continue;
|
|
92
|
+
if (entry.type !== "error" && this.verbosity < 1) continue;
|
|
93
|
+
if (entry.type === "title") this.endSection();
|
|
94
|
+
this.replace(entry) || this.append(entry);
|
|
95
|
+
await this.storeArtifacts(entry.artifacts);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
endSection() {
|
|
99
|
+
this.entries = [];
|
|
100
|
+
}
|
|
101
|
+
replace(entry) {
|
|
102
|
+
if (entry.type !== "start" && entry.type !== "success" && entry.type !== "failure") return false;
|
|
103
|
+
const index = this.entries.findLastIndex(
|
|
104
|
+
(e) => e.message === entry.message && (e.type === "prepare" || e.type === "start" && entry.type !== "start")
|
|
105
|
+
);
|
|
106
|
+
if (index < 0) return false;
|
|
107
|
+
const oldTexts = this.entries.slice(index).map((e) => this.format(e));
|
|
108
|
+
const columns = this.stream.columns ?? 0;
|
|
109
|
+
const oldLength = oldTexts.reduce((total, current) => total + this.countDisplayLines(current, columns), 0);
|
|
110
|
+
this.entries[index] = entry;
|
|
111
|
+
const newTexts = this.entries.slice(index).map((e) => this.format(e));
|
|
112
|
+
this.stream.write(cursorUp(oldLength));
|
|
113
|
+
this.stream.write(cursorLeft);
|
|
114
|
+
this.stream.write(newTexts.join("\n") + "\n");
|
|
115
|
+
this.stream.write(eraseDown);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
append(entry) {
|
|
119
|
+
this.entries.push(entry);
|
|
120
|
+
const text = this.format(entry);
|
|
121
|
+
this.stream.write(text + "\n");
|
|
122
|
+
}
|
|
123
|
+
format(entry) {
|
|
124
|
+
const lines = [];
|
|
125
|
+
const message = entry.message.trim();
|
|
126
|
+
if (["prepare", "start", "success", "failure"].includes(entry.type)) {
|
|
127
|
+
const prefix = colorize(entry.type, statusSymbol(entry.type));
|
|
128
|
+
lines.push(`${prefix} ${message}`);
|
|
129
|
+
} else {
|
|
130
|
+
lines.push(colorize(entry.type, message));
|
|
131
|
+
}
|
|
132
|
+
if (this.verbosity >= 3) {
|
|
133
|
+
if (entry.meta && Object.values(entry.meta).length && Object.keys(entry.meta).length) {
|
|
134
|
+
const yaml = YAML.stringify(entry.meta).trimEnd();
|
|
135
|
+
lines.push(colorize("debug", `[Meta]
|
|
136
|
+
${yaml}`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
stripAnsi(input) {
|
|
142
|
+
const pattern = /[\u001B\u009B][[\]()#;?]*(?:((?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])/g;
|
|
143
|
+
return input.replace(pattern, "");
|
|
144
|
+
}
|
|
145
|
+
countDisplayLines(text, columns) {
|
|
146
|
+
if (!columns || columns <= 0 || !Number.isFinite(columns)) {
|
|
147
|
+
return text.split("\n").length;
|
|
148
|
+
}
|
|
149
|
+
let count = 0;
|
|
150
|
+
const lines = text.split("\n");
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const visible = this.stripAnsi(line);
|
|
153
|
+
const len = [...visible].length;
|
|
154
|
+
const wraps = Math.max(1, Math.ceil(len / columns));
|
|
155
|
+
count += wraps;
|
|
156
|
+
}
|
|
157
|
+
return count;
|
|
158
|
+
}
|
|
159
|
+
async storeArtifacts(artifacts) {
|
|
160
|
+
if (!this.artifactPath || artifacts.length === 0) return;
|
|
161
|
+
for (const artifact of artifacts) {
|
|
162
|
+
const data = await artifact.bytes();
|
|
163
|
+
await this.writeFile(`${this.artifactPath}/${artifact.name}`, data);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/sink/no-sink.ts
|
|
169
|
+
var NoSink = class {
|
|
170
|
+
async publish() {
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/sink/supabase-sink.ts
|
|
175
|
+
var SupabaseSink = class {
|
|
176
|
+
supabase;
|
|
177
|
+
projectId;
|
|
178
|
+
runId;
|
|
179
|
+
tableName;
|
|
180
|
+
bucket;
|
|
181
|
+
console;
|
|
182
|
+
bucketEnsured = false;
|
|
183
|
+
constructor(options) {
|
|
184
|
+
this.supabase = options.supabase;
|
|
185
|
+
this.runId = options.run.id;
|
|
186
|
+
this.projectId = options.run.projectId;
|
|
187
|
+
this.tableName = options.tableName ?? "log_entries";
|
|
188
|
+
this.bucket = options.bucket;
|
|
189
|
+
this.console = options.console || console;
|
|
190
|
+
}
|
|
191
|
+
async publish(...entries) {
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
const artifactList = await this.storeArtifacts(entry.artifacts);
|
|
194
|
+
const { error } = await this.supabase.from(this.tableName).insert({
|
|
195
|
+
run_id: this.runId,
|
|
196
|
+
type: entry.type,
|
|
197
|
+
message: entry.message,
|
|
198
|
+
meta: entry.meta ?? {},
|
|
199
|
+
artifacts: artifactList,
|
|
200
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
201
|
+
});
|
|
202
|
+
if (error) {
|
|
203
|
+
this.console.error("SupabaseSink insert failed:", error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async ensureBucket() {
|
|
208
|
+
if (!this.bucket || this.bucketEnsured) return;
|
|
209
|
+
try {
|
|
210
|
+
await this.supabase.storage?.createBucket?.(this.bucket, { public: true });
|
|
211
|
+
this.bucketEnsured = true;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.console.warn(error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async storeArtifacts(artifacts) {
|
|
217
|
+
if (!this.bucket || artifacts.length === 0) return [];
|
|
218
|
+
await this.ensureBucket();
|
|
219
|
+
const stored = [];
|
|
220
|
+
const uniqueArtifacts = artifacts.filter(
|
|
221
|
+
(artifact, index, self) => index === self.findIndex((a) => a.name === artifact.name)
|
|
222
|
+
);
|
|
223
|
+
for (const artifact of uniqueArtifacts) {
|
|
224
|
+
try {
|
|
225
|
+
const path = `${this.projectId}/${artifact.name}`;
|
|
226
|
+
const { data } = this.supabase.storage.from(this.bucket).getPublicUrl(path);
|
|
227
|
+
const publicUrl = data.publicUrl;
|
|
228
|
+
const exists = await this.artifactExists(publicUrl);
|
|
229
|
+
if (!exists) {
|
|
230
|
+
const { error } = await this.supabase.storage.from(this.bucket).upload(path, await artifact.bytes(), { contentType: artifact.type, upsert: true });
|
|
231
|
+
if (error) throw error;
|
|
232
|
+
}
|
|
233
|
+
stored.push({
|
|
234
|
+
name: artifact.name,
|
|
235
|
+
url: publicUrl,
|
|
236
|
+
size: artifact.size
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
this.console.warn("SupabaseSink upload failed:", error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return stored;
|
|
243
|
+
}
|
|
244
|
+
async artifactExists(url) {
|
|
245
|
+
try {
|
|
246
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
247
|
+
return response.ok;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/journal.ts
|
|
255
|
+
var Journal = class _Journal {
|
|
256
|
+
constructor(sink) {
|
|
257
|
+
this.sink = sink;
|
|
258
|
+
}
|
|
259
|
+
static nil() {
|
|
260
|
+
return new _Journal(new NoSink());
|
|
261
|
+
}
|
|
262
|
+
async log(message, options) {
|
|
263
|
+
const entry = {
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
message,
|
|
266
|
+
type: options.type,
|
|
267
|
+
artifacts: options.artifacts ?? [],
|
|
268
|
+
meta: options.meta ?? {}
|
|
269
|
+
};
|
|
270
|
+
await this.sink.publish(entry);
|
|
271
|
+
}
|
|
272
|
+
batch() {
|
|
273
|
+
return new JournalBatch(this.sink);
|
|
274
|
+
}
|
|
275
|
+
async do(message, callback, metaFn) {
|
|
276
|
+
try {
|
|
277
|
+
await this.start(message);
|
|
278
|
+
const result = await callback();
|
|
279
|
+
const { meta, artifacts } = metaFn?.(result) ?? {};
|
|
280
|
+
await this.success(message, { meta, artifacts });
|
|
281
|
+
return result;
|
|
282
|
+
} catch (e) {
|
|
283
|
+
await this.failure(message);
|
|
284
|
+
throw e;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async debug(message, options = {}) {
|
|
288
|
+
await this.log(message, { ...options, type: "debug" });
|
|
289
|
+
}
|
|
290
|
+
async info(message, options = {}) {
|
|
291
|
+
await this.log(message, { ...options, type: "info" });
|
|
292
|
+
}
|
|
293
|
+
async warn(message, options = {}) {
|
|
294
|
+
await this.log(message, { ...options, type: "warn" });
|
|
295
|
+
}
|
|
296
|
+
async error(message, options = {}) {
|
|
297
|
+
await this.log(message, { ...options, type: "error" });
|
|
298
|
+
}
|
|
299
|
+
async title(message, options = {}) {
|
|
300
|
+
await this.log(message, { ...options, type: "title" });
|
|
301
|
+
}
|
|
302
|
+
async prepare(message, options = {}) {
|
|
303
|
+
await this.log(message, { ...options, type: "prepare" });
|
|
304
|
+
}
|
|
305
|
+
async start(message, options = {}) {
|
|
306
|
+
await this.log(message, { ...options, type: "start" });
|
|
307
|
+
}
|
|
308
|
+
async success(message, options = {}) {
|
|
309
|
+
await this.log(message, { ...options, type: "success" });
|
|
310
|
+
}
|
|
311
|
+
async failure(message, options = {}) {
|
|
312
|
+
await this.log(message, { ...options, type: "failure" });
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export { CliSink, Journal, NoSink, SupabaseSink };
|
|
317
|
+
//# sourceMappingURL=index.js.map
|
|
318
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/journal-batch.ts","../src/sink/cli-sink.ts","../src/sink/no-sink.ts","../src/sink/supabase-sink.ts","../src/journal.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { Sink, JournalEntry } from './types';\n\ntype Options = Partial<Pick<JournalEntry, 'artifacts' | 'meta'>>;\n\nexport class JournalBatch {\n private entries: JournalEntry[] = [];\n\n constructor(private sink: Sink) {}\n\n log(message: string | undefined, options: Options & { type: JournalEntry['type'] }): JournalBatch {\n if (message) {\n this.entries.push({\n timestamp: Date.now(),\n message,\n type: options.type,\n artifacts: options.artifacts ?? [],\n meta: options.meta ?? {},\n });\n }\n\n return this;\n }\n\n async flush() {\n await this.sink.publish(...this.entries);\n }\n\n debug(message: string | undefined, options: Options = {}): JournalBatch {\n return this.log(message, { ...options, type: 'debug' });\n }\n\n info(message: string | undefined, options: Options = {}): JournalBatch {\n return this.log(message, { ...options, type: 'info' });\n }\n\n title(message: string | undefined, options: Options = {}): JournalBatch {\n return this.log(message, { ...options, type: 'title' });\n }\n\n warn(message: string | undefined, options: Options = {}): JournalBatch {\n return this.log(message, { ...options, type: 'warn' });\n }\n\n error(message: string | undefined, options: Options = {}): JournalBatch {\n return this.log(message, { ...options, type: 'error' });\n }\n\n prepare(message: string | undefined, options: Options = {}): JournalBatch {\n return this.log(message, { ...options, type: 'prepare' });\n }\n\n success(message: string | undefined, options: Options = {}): JournalBatch {\n return this.log(message, { ...options, type: 'success' });\n }\n\n failure(message: string | undefined, options: Options = {}): JournalBatch {\n return this.log(message, { ...options, type: 'failure' });\n }\n\n each<T>(items: Array<T>, fn: (journal: JournalBatch, item: T) => void): JournalBatch {\n for (const item of items) {\n fn(this, item);\n }\n\n return this;\n }\n}\n","import { statusSymbol } from '@letsrunit/utils';\nimport { cursorLeft, cursorUp, eraseDown } from 'ansi-escapes';\nimport { writeFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport type { JournalEntry, Sink } from '../types';\n\ntype WriteFile = typeof writeFile;\n\ninterface CliSinkOptions {\n stream?: NodeJS.WriteStream;\n verbosity?: number;\n artifactPath?: string;\n writeFile?: WriteFile;\n}\n\nfunction colorize(type: JournalEntry['type'], text: string): string {\n // ANSI escape codes: debug=bright black (gray), warn=yellow, error=red, info=no color\n switch (type) {\n case 'debug':\n case 'prepare':\n return `\\x1b[90m${text}\\x1b[0m`;\n case 'warn':\n return `\\x1b[33m${text}\\x1b[0m`;\n case 'error':\n case 'failure':\n return `\\x1b[31m${text}\\x1b[0m`;\n case 'success':\n return `\\x1b[32m${text}\\x1b[0m`;\n case 'title':\n return `\\x1b[1m${text}\\x1b[0m`;\n case 'info':\n default:\n return text;\n }\n}\n\nexport class CliSink implements Sink {\n public readonly stream: NodeJS.WriteStream;\n public readonly verbosity: number;\n public readonly artifactPath: string | undefined;\n private readonly writeFile: WriteFile;\n\n private entries: JournalEntry[] = [];\n\n constructor(options: CliSinkOptions = {}) {\n this.stream = options.stream ?? process.stdout;\n this.verbosity = options.verbosity ?? 1;\n this.artifactPath = options.artifactPath;\n this.writeFile = options.writeFile ?? writeFile;\n }\n\n async publish(...entries: JournalEntry[]): Promise<void> {\n for (const entry of entries) {\n if (entry.type === 'debug' && this.verbosity < 2) continue;\n if (entry.type !== 'error' && this.verbosity < 1) continue;\n\n if (entry.type === 'title') this.endSection();\n\n this.replace(entry) || this.append(entry);\n\n await this.storeArtifacts(entry.artifacts);\n }\n }\n\n endSection() {\n this.entries = [];\n }\n\n private replace(entry: JournalEntry): boolean {\n if (entry.type !== 'start' && entry.type !== 'success' && entry.type !== 'failure') return false;\n\n const index = this.entries.findLastIndex(\n (e) => e.message === entry.message &&\n (e.type === 'prepare' || (e.type === 'start' && entry.type !== 'start'))\n );\n if (index < 0) return false;\n\n const oldTexts = this.entries.slice(index).map((e) => this.format(e));\n const columns = this.stream.columns ?? 0;\n const oldLength = oldTexts.reduce((total, current) => total + this.countDisplayLines(current, columns), 0);\n\n this.entries[index] = entry;\n const newTexts = this.entries.slice(index).map((e) => this.format(e));\n\n this.stream.write(cursorUp(oldLength));\n this.stream.write(cursorLeft);\n this.stream.write(newTexts.join('\\n') + '\\n');\n this.stream.write(eraseDown);\n\n return true;\n }\n\n private append(entry: JournalEntry): void {\n this.entries.push(entry);\n\n const text = this.format(entry);\n this.stream.write(text + '\\n');\n }\n\n private format(entry: JournalEntry): string {\n const lines = [];\n\n const message = entry.message.trim();\n\n if (['prepare', 'start', 'success', 'failure'].includes(entry.type)) {\n const prefix = colorize(entry.type, statusSymbol(entry.type));\n lines.push(`${prefix} ${message}`);\n } else {\n lines.push(colorize(entry.type, message));\n }\n\n if (this.verbosity >= 3) {\n if (entry.meta && Object.values(entry.meta).length && Object.keys(entry.meta).length) {\n const yaml = YAML.stringify(entry.meta).trimEnd();\n lines.push(colorize('debug', `[Meta]\\n${yaml}`));\n }\n }\n\n return lines.join('\\n');\n }\n\n private stripAnsi(input: string): string {\n // Regex from chalk/strip-ansi to remove ANSI escape sequences\n const pattern = /[\\u001B\\u009B][[\\]()#;?]*(?:((?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)|(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~])/g;\n return input.replace(pattern, '');\n }\n\n private countDisplayLines(text: string, columns: number): number {\n // If columns is not available (non-TTY), fall back to newline-based counting\n if (!columns || columns <= 0 || !Number.isFinite(columns)) {\n return text.split('\\n').length;\n }\n\n let count = 0;\n const lines = text.split('\\n');\n for (const line of lines) {\n const visible = this.stripAnsi(line);\n const len = [...visible].length; // handles surrogate pairs reasonably\n const wraps = Math.max(1, Math.ceil(len / columns));\n count += wraps;\n }\n return count;\n }\n\n private async storeArtifacts(artifacts: File[]) {\n if (!this.artifactPath || artifacts.length === 0) return;\n\n for (const artifact of artifacts) {\n const data = await artifact.bytes();\n await this.writeFile(`${this.artifactPath}/${artifact.name}`, data);\n }\n }\n}\n","import type { Sink } from '../types';\n\n/* v8 ignore start */\nexport class NoSink implements Sink {\n async publish(): Promise<void> {}\n}\n","import { SupabaseClient } from '@supabase/supabase-js';\nimport type { JournalEntry, Sink } from '../types';\n\ninterface SupabaseSinkOptions {\n supabase: SupabaseClient;\n run: { id: string; projectId: string };\n tableName?: string;\n bucket?: string;\n console?: { error: (...args: any[]) => void; warn: (...args: any[]) => void };\n}\n\nexport class SupabaseSink implements Sink {\n private readonly supabase: SupabaseClient;\n private readonly projectId: string;\n private readonly runId: string;\n private readonly tableName: string;\n private readonly bucket?: string;\n private readonly console: { error: (...args: any[]) => void; warn: (...args: any[]) => void };\n private bucketEnsured = false;\n\n constructor(options: SupabaseSinkOptions) {\n this.supabase = options.supabase;\n this.runId = options.run.id;\n this.projectId = options.run.projectId;\n this.tableName = options.tableName ?? 'log_entries';\n this.bucket = options.bucket;\n this.console = options.console || console;\n }\n\n async publish(...entries: JournalEntry[]): Promise<void> {\n for (const entry of entries) {\n const artifactList = await this.storeArtifacts(entry.artifacts);\n\n const { error } = await this.supabase.from(this.tableName).insert({\n run_id: this.runId,\n type: entry.type,\n message: entry.message,\n meta: entry.meta ?? {},\n artifacts: artifactList,\n created_at: new Date().toISOString(),\n });\n\n if (error) {\n this.console.error('SupabaseSink insert failed:', error);\n }\n }\n }\n\n private async ensureBucket() {\n if (!this.bucket || this.bucketEnsured) return;\n\n try {\n await this.supabase.storage?.createBucket?.(this.bucket, { public: true });\n this.bucketEnsured = true;\n } catch (error) {\n this.console.warn(error);\n }\n }\n\n private async storeArtifacts(artifacts: File[]): Promise<any[]> {\n if (!this.bucket || artifacts.length === 0) return [];\n\n await this.ensureBucket();\n\n const stored: any[] = [];\n\n const uniqueArtifacts = artifacts.filter(\n (artifact, index, self) => index === self.findIndex((a) => a.name === artifact.name),\n );\n\n for (const artifact of uniqueArtifacts) {\n try {\n const path = `${this.projectId}/${artifact.name}`;\n const { data } = this.supabase.storage.from(this.bucket).getPublicUrl(path);\n const publicUrl = data.publicUrl;\n\n const exists = await this.artifactExists(publicUrl);\n\n if (!exists) {\n const { error } = await this.supabase.storage\n .from(this.bucket)\n .upload(path, await artifact.bytes(), { contentType: artifact.type, upsert: true });\n if (error) throw error;\n }\n\n stored.push({\n name: artifact.name,\n url: publicUrl,\n size: artifact.size,\n });\n } catch (error) {\n this.console.warn('SupabaseSink upload failed:', error);\n }\n }\n\n return stored;\n }\n\n private async artifactExists(url: string): Promise<boolean> {\n try {\n const response = await fetch(url, { method: 'HEAD' });\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n","import { JournalBatch } from './journal-batch';\nimport { NoSink } from './sink';\nimport type { JournalEntry, Sink } from './types';\n\ntype Options = Partial<Pick<JournalEntry, 'artifacts' | 'meta'>>;\n\nexport class Journal<TSink extends Sink = Sink> {\n constructor(readonly sink: TSink) {}\n\n static nil() {\n return new Journal(new NoSink());\n }\n\n async log(message: string, options: Options & { type: JournalEntry['type'] }): Promise<void> {\n const entry: JournalEntry = {\n timestamp: Date.now(),\n message,\n type: options.type,\n artifacts: options.artifacts ?? [],\n meta: options.meta ?? {},\n };\n\n await this.sink.publish(entry);\n }\n\n batch() {\n return new JournalBatch(this.sink);\n }\n\n async do<T>(\n message: string,\n callback: () => T | Promise<T>,\n metaFn?: (result: T) => { meta?: Record<string, any>; artifacts?: File[] },\n ): Promise<T> {\n try {\n await this.start(message);\n\n const result = await callback();\n\n const { meta, artifacts } = metaFn?.(result) ?? {};\n await this.success(message, { meta, artifacts });\n\n return result;\n } catch (e) {\n await this.failure(message);\n throw e;\n }\n }\n\n async debug(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'debug' });\n }\n\n async info(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'info' });\n }\n\n async warn(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'warn' });\n }\n\n async error(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'error' });\n }\n\n async title(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'title' });\n }\n\n async prepare(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'prepare' });\n }\n\n async start(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'start' });\n }\n\n async success(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'success' });\n }\n\n async failure(message: string, options: Options = {}): Promise<void> {\n await this.log(message, { ...options, type: 'failure' });\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@letsrunit/journal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Logging and journaling system for letsrunit",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"testing",
|
|
7
|
+
"logging",
|
|
8
|
+
"journal",
|
|
9
|
+
"letsrunit"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/letsrunit/letsrunit.git",
|
|
15
|
+
"directory": "packages/journal"
|
|
16
|
+
},
|
|
17
|
+
"bugs": "https://github.com/letsrunit/letsrunit/issues",
|
|
18
|
+
"homepage": "https://github.com/letsrunit/letsrunit#readme",
|
|
19
|
+
"private": false,
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "../../node_modules/.bin/tsup",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:cov": "vitest run --coverage",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"packageManager": "yarn@4.10.3",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@letsrunit/utils": "workspace:*",
|
|
39
|
+
"@supabase/supabase-js": "^2.91.0",
|
|
40
|
+
"ansi-escapes": "^7.2.0",
|
|
41
|
+
"yaml": "^2.8.2"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^25.0.9",
|
|
45
|
+
"@vitest/coverage-v8": "4.0.17",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vitest": "^4.0.17"
|
|
48
|
+
},
|
|
49
|
+
"module": "./dist/index.js",
|
|
50
|
+
"types": "./dist/index.d.ts",
|
|
51
|
+
"exports": {
|
|
52
|
+
".": {
|
|
53
|
+
"types": "./dist/index.d.ts",
|
|
54
|
+
"import": "./dist/index.js"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Sink, JournalEntry } from './types';
|
|
2
|
+
|
|
3
|
+
type Options = Partial<Pick<JournalEntry, 'artifacts' | 'meta'>>;
|
|
4
|
+
|
|
5
|
+
export class JournalBatch {
|
|
6
|
+
private entries: JournalEntry[] = [];
|
|
7
|
+
|
|
8
|
+
constructor(private sink: Sink) {}
|
|
9
|
+
|
|
10
|
+
log(message: string | undefined, options: Options & { type: JournalEntry['type'] }): JournalBatch {
|
|
11
|
+
if (message) {
|
|
12
|
+
this.entries.push({
|
|
13
|
+
timestamp: Date.now(),
|
|
14
|
+
message,
|
|
15
|
+
type: options.type,
|
|
16
|
+
artifacts: options.artifacts ?? [],
|
|
17
|
+
meta: options.meta ?? {},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async flush() {
|
|
25
|
+
await this.sink.publish(...this.entries);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
debug(message: string | undefined, options: Options = {}): JournalBatch {
|
|
29
|
+
return this.log(message, { ...options, type: 'debug' });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
info(message: string | undefined, options: Options = {}): JournalBatch {
|
|
33
|
+
return this.log(message, { ...options, type: 'info' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
title(message: string | undefined, options: Options = {}): JournalBatch {
|
|
37
|
+
return this.log(message, { ...options, type: 'title' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
warn(message: string | undefined, options: Options = {}): JournalBatch {
|
|
41
|
+
return this.log(message, { ...options, type: 'warn' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
error(message: string | undefined, options: Options = {}): JournalBatch {
|
|
45
|
+
return this.log(message, { ...options, type: 'error' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
prepare(message: string | undefined, options: Options = {}): JournalBatch {
|
|
49
|
+
return this.log(message, { ...options, type: 'prepare' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
success(message: string | undefined, options: Options = {}): JournalBatch {
|
|
53
|
+
return this.log(message, { ...options, type: 'success' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
failure(message: string | undefined, options: Options = {}): JournalBatch {
|
|
57
|
+
return this.log(message, { ...options, type: 'failure' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
each<T>(items: Array<T>, fn: (journal: JournalBatch, item: T) => void): JournalBatch {
|
|
61
|
+
for (const item of items) {
|
|
62
|
+
fn(this, item);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/journal.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { JournalBatch } from './journal-batch';
|
|
2
|
+
import { NoSink } from './sink';
|
|
3
|
+
import type { JournalEntry, Sink } from './types';
|
|
4
|
+
|
|
5
|
+
type Options = Partial<Pick<JournalEntry, 'artifacts' | 'meta'>>;
|
|
6
|
+
|
|
7
|
+
export class Journal<TSink extends Sink = Sink> {
|
|
8
|
+
constructor(readonly sink: TSink) {}
|
|
9
|
+
|
|
10
|
+
static nil() {
|
|
11
|
+
return new Journal(new NoSink());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async log(message: string, options: Options & { type: JournalEntry['type'] }): Promise<void> {
|
|
15
|
+
const entry: JournalEntry = {
|
|
16
|
+
timestamp: Date.now(),
|
|
17
|
+
message,
|
|
18
|
+
type: options.type,
|
|
19
|
+
artifacts: options.artifacts ?? [],
|
|
20
|
+
meta: options.meta ?? {},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
await this.sink.publish(entry);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
batch() {
|
|
27
|
+
return new JournalBatch(this.sink);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async do<T>(
|
|
31
|
+
message: string,
|
|
32
|
+
callback: () => T | Promise<T>,
|
|
33
|
+
metaFn?: (result: T) => { meta?: Record<string, any>; artifacts?: File[] },
|
|
34
|
+
): Promise<T> {
|
|
35
|
+
try {
|
|
36
|
+
await this.start(message);
|
|
37
|
+
|
|
38
|
+
const result = await callback();
|
|
39
|
+
|
|
40
|
+
const { meta, artifacts } = metaFn?.(result) ?? {};
|
|
41
|
+
await this.success(message, { meta, artifacts });
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
await this.failure(message);
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async debug(message: string, options: Options = {}): Promise<void> {
|
|
51
|
+
await this.log(message, { ...options, type: 'debug' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async info(message: string, options: Options = {}): Promise<void> {
|
|
55
|
+
await this.log(message, { ...options, type: 'info' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async warn(message: string, options: Options = {}): Promise<void> {
|
|
59
|
+
await this.log(message, { ...options, type: 'warn' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async error(message: string, options: Options = {}): Promise<void> {
|
|
63
|
+
await this.log(message, { ...options, type: 'error' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async title(message: string, options: Options = {}): Promise<void> {
|
|
67
|
+
await this.log(message, { ...options, type: 'title' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async prepare(message: string, options: Options = {}): Promise<void> {
|
|
71
|
+
await this.log(message, { ...options, type: 'prepare' });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async start(message: string, options: Options = {}): Promise<void> {
|
|
75
|
+
await this.log(message, { ...options, type: 'start' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async success(message: string, options: Options = {}): Promise<void> {
|
|
79
|
+
await this.log(message, { ...options, type: 'success' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async failure(message: string, options: Options = {}): Promise<void> {
|
|
83
|
+
await this.log(message, { ...options, type: 'failure' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { statusSymbol } from '@letsrunit/utils';
|
|
2
|
+
import { cursorLeft, cursorUp, eraseDown } from 'ansi-escapes';
|
|
3
|
+
import { writeFile } from 'node:fs/promises';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import type { JournalEntry, Sink } from '../types';
|
|
6
|
+
|
|
7
|
+
type WriteFile = typeof writeFile;
|
|
8
|
+
|
|
9
|
+
interface CliSinkOptions {
|
|
10
|
+
stream?: NodeJS.WriteStream;
|
|
11
|
+
verbosity?: number;
|
|
12
|
+
artifactPath?: string;
|
|
13
|
+
writeFile?: WriteFile;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function colorize(type: JournalEntry['type'], text: string): string {
|
|
17
|
+
// ANSI escape codes: debug=bright black (gray), warn=yellow, error=red, info=no color
|
|
18
|
+
switch (type) {
|
|
19
|
+
case 'debug':
|
|
20
|
+
case 'prepare':
|
|
21
|
+
return `\x1b[90m${text}\x1b[0m`;
|
|
22
|
+
case 'warn':
|
|
23
|
+
return `\x1b[33m${text}\x1b[0m`;
|
|
24
|
+
case 'error':
|
|
25
|
+
case 'failure':
|
|
26
|
+
return `\x1b[31m${text}\x1b[0m`;
|
|
27
|
+
case 'success':
|
|
28
|
+
return `\x1b[32m${text}\x1b[0m`;
|
|
29
|
+
case 'title':
|
|
30
|
+
return `\x1b[1m${text}\x1b[0m`;
|
|
31
|
+
case 'info':
|
|
32
|
+
default:
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class CliSink implements Sink {
|
|
38
|
+
public readonly stream: NodeJS.WriteStream;
|
|
39
|
+
public readonly verbosity: number;
|
|
40
|
+
public readonly artifactPath: string | undefined;
|
|
41
|
+
private readonly writeFile: WriteFile;
|
|
42
|
+
|
|
43
|
+
private entries: JournalEntry[] = [];
|
|
44
|
+
|
|
45
|
+
constructor(options: CliSinkOptions = {}) {
|
|
46
|
+
this.stream = options.stream ?? process.stdout;
|
|
47
|
+
this.verbosity = options.verbosity ?? 1;
|
|
48
|
+
this.artifactPath = options.artifactPath;
|
|
49
|
+
this.writeFile = options.writeFile ?? writeFile;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async publish(...entries: JournalEntry[]): Promise<void> {
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.type === 'debug' && this.verbosity < 2) continue;
|
|
55
|
+
if (entry.type !== 'error' && this.verbosity < 1) continue;
|
|
56
|
+
|
|
57
|
+
if (entry.type === 'title') this.endSection();
|
|
58
|
+
|
|
59
|
+
this.replace(entry) || this.append(entry);
|
|
60
|
+
|
|
61
|
+
await this.storeArtifacts(entry.artifacts);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
endSection() {
|
|
66
|
+
this.entries = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private replace(entry: JournalEntry): boolean {
|
|
70
|
+
if (entry.type !== 'start' && entry.type !== 'success' && entry.type !== 'failure') return false;
|
|
71
|
+
|
|
72
|
+
const index = this.entries.findLastIndex(
|
|
73
|
+
(e) => e.message === entry.message &&
|
|
74
|
+
(e.type === 'prepare' || (e.type === 'start' && entry.type !== 'start'))
|
|
75
|
+
);
|
|
76
|
+
if (index < 0) return false;
|
|
77
|
+
|
|
78
|
+
const oldTexts = this.entries.slice(index).map((e) => this.format(e));
|
|
79
|
+
const columns = this.stream.columns ?? 0;
|
|
80
|
+
const oldLength = oldTexts.reduce((total, current) => total + this.countDisplayLines(current, columns), 0);
|
|
81
|
+
|
|
82
|
+
this.entries[index] = entry;
|
|
83
|
+
const newTexts = this.entries.slice(index).map((e) => this.format(e));
|
|
84
|
+
|
|
85
|
+
this.stream.write(cursorUp(oldLength));
|
|
86
|
+
this.stream.write(cursorLeft);
|
|
87
|
+
this.stream.write(newTexts.join('\n') + '\n');
|
|
88
|
+
this.stream.write(eraseDown);
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private append(entry: JournalEntry): void {
|
|
94
|
+
this.entries.push(entry);
|
|
95
|
+
|
|
96
|
+
const text = this.format(entry);
|
|
97
|
+
this.stream.write(text + '\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private format(entry: JournalEntry): string {
|
|
101
|
+
const lines = [];
|
|
102
|
+
|
|
103
|
+
const message = entry.message.trim();
|
|
104
|
+
|
|
105
|
+
if (['prepare', 'start', 'success', 'failure'].includes(entry.type)) {
|
|
106
|
+
const prefix = colorize(entry.type, statusSymbol(entry.type));
|
|
107
|
+
lines.push(`${prefix} ${message}`);
|
|
108
|
+
} else {
|
|
109
|
+
lines.push(colorize(entry.type, message));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (this.verbosity >= 3) {
|
|
113
|
+
if (entry.meta && Object.values(entry.meta).length && Object.keys(entry.meta).length) {
|
|
114
|
+
const yaml = YAML.stringify(entry.meta).trimEnd();
|
|
115
|
+
lines.push(colorize('debug', `[Meta]\n${yaml}`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private stripAnsi(input: string): string {
|
|
123
|
+
// Regex from chalk/strip-ansi to remove ANSI escape sequences
|
|
124
|
+
const pattern = /[\u001B\u009B][[\]()#;?]*(?:((?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])/g;
|
|
125
|
+
return input.replace(pattern, '');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private countDisplayLines(text: string, columns: number): number {
|
|
129
|
+
// If columns is not available (non-TTY), fall back to newline-based counting
|
|
130
|
+
if (!columns || columns <= 0 || !Number.isFinite(columns)) {
|
|
131
|
+
return text.split('\n').length;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let count = 0;
|
|
135
|
+
const lines = text.split('\n');
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
const visible = this.stripAnsi(line);
|
|
138
|
+
const len = [...visible].length; // handles surrogate pairs reasonably
|
|
139
|
+
const wraps = Math.max(1, Math.ceil(len / columns));
|
|
140
|
+
count += wraps;
|
|
141
|
+
}
|
|
142
|
+
return count;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async storeArtifacts(artifacts: File[]) {
|
|
146
|
+
if (!this.artifactPath || artifacts.length === 0) return;
|
|
147
|
+
|
|
148
|
+
for (const artifact of artifacts) {
|
|
149
|
+
const data = await artifact.bytes();
|
|
150
|
+
await this.writeFile(`${this.artifactPath}/${artifact.name}`, data);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { JournalEntry, Sink } from '../types';
|
|
3
|
+
|
|
4
|
+
interface SupabaseSinkOptions {
|
|
5
|
+
supabase: SupabaseClient;
|
|
6
|
+
run: { id: string; projectId: string };
|
|
7
|
+
tableName?: string;
|
|
8
|
+
bucket?: string;
|
|
9
|
+
console?: { error: (...args: any[]) => void; warn: (...args: any[]) => void };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class SupabaseSink implements Sink {
|
|
13
|
+
private readonly supabase: SupabaseClient;
|
|
14
|
+
private readonly projectId: string;
|
|
15
|
+
private readonly runId: string;
|
|
16
|
+
private readonly tableName: string;
|
|
17
|
+
private readonly bucket?: string;
|
|
18
|
+
private readonly console: { error: (...args: any[]) => void; warn: (...args: any[]) => void };
|
|
19
|
+
private bucketEnsured = false;
|
|
20
|
+
|
|
21
|
+
constructor(options: SupabaseSinkOptions) {
|
|
22
|
+
this.supabase = options.supabase;
|
|
23
|
+
this.runId = options.run.id;
|
|
24
|
+
this.projectId = options.run.projectId;
|
|
25
|
+
this.tableName = options.tableName ?? 'log_entries';
|
|
26
|
+
this.bucket = options.bucket;
|
|
27
|
+
this.console = options.console || console;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async publish(...entries: JournalEntry[]): Promise<void> {
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const artifactList = await this.storeArtifacts(entry.artifacts);
|
|
33
|
+
|
|
34
|
+
const { error } = await this.supabase.from(this.tableName).insert({
|
|
35
|
+
run_id: this.runId,
|
|
36
|
+
type: entry.type,
|
|
37
|
+
message: entry.message,
|
|
38
|
+
meta: entry.meta ?? {},
|
|
39
|
+
artifacts: artifactList,
|
|
40
|
+
created_at: new Date().toISOString(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (error) {
|
|
44
|
+
this.console.error('SupabaseSink insert failed:', error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async ensureBucket() {
|
|
50
|
+
if (!this.bucket || this.bucketEnsured) return;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await this.supabase.storage?.createBucket?.(this.bucket, { public: true });
|
|
54
|
+
this.bucketEnsured = true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
this.console.warn(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async storeArtifacts(artifacts: File[]): Promise<any[]> {
|
|
61
|
+
if (!this.bucket || artifacts.length === 0) return [];
|
|
62
|
+
|
|
63
|
+
await this.ensureBucket();
|
|
64
|
+
|
|
65
|
+
const stored: any[] = [];
|
|
66
|
+
|
|
67
|
+
const uniqueArtifacts = artifacts.filter(
|
|
68
|
+
(artifact, index, self) => index === self.findIndex((a) => a.name === artifact.name),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const artifact of uniqueArtifacts) {
|
|
72
|
+
try {
|
|
73
|
+
const path = `${this.projectId}/${artifact.name}`;
|
|
74
|
+
const { data } = this.supabase.storage.from(this.bucket).getPublicUrl(path);
|
|
75
|
+
const publicUrl = data.publicUrl;
|
|
76
|
+
|
|
77
|
+
const exists = await this.artifactExists(publicUrl);
|
|
78
|
+
|
|
79
|
+
if (!exists) {
|
|
80
|
+
const { error } = await this.supabase.storage
|
|
81
|
+
.from(this.bucket)
|
|
82
|
+
.upload(path, await artifact.bytes(), { contentType: artifact.type, upsert: true });
|
|
83
|
+
if (error) throw error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
stored.push({
|
|
87
|
+
name: artifact.name,
|
|
88
|
+
url: publicUrl,
|
|
89
|
+
size: artifact.size,
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.console.warn('SupabaseSink upload failed:', error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return stored;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async artifactExists(url: string): Promise<boolean> {
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(url, { method: 'HEAD' });
|
|
102
|
+
return response.ok;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface JournalEntry {
|
|
2
|
+
timestamp: number;
|
|
3
|
+
type: 'debug' | 'info' | 'title' | 'warn' | 'error' | 'prepare' | 'start' | 'success' | 'failure';
|
|
4
|
+
message: string;
|
|
5
|
+
artifacts: File[];
|
|
6
|
+
meta: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Sink {
|
|
10
|
+
publish(...entries: JournalEntry[]): Promise<void>
|
|
11
|
+
}
|