agent-tail-core 0.0.1
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/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +106 -0
- package/dist/commands-BO9MYtxZ.mjs +291 -0
- package/dist/index.d.mts +89 -0
- package/dist/index.mjs +105 -0
- package/package.json +32 -0
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as cmd_run, r as cmd_wrap, t as cmd_init } from "./commands-BO9MYtxZ.mjs";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
|
|
5
|
+
//#region src/cli.ts
|
|
6
|
+
const HELP = `
|
|
7
|
+
\x1b[1magent-tail\x1b[0m — Pipe any dev server's output into your unified log session
|
|
8
|
+
|
|
9
|
+
\x1b[1mUsage:\x1b[0m
|
|
10
|
+
agent-tail init Create a new log session
|
|
11
|
+
agent-tail wrap <name> -- <command...> Wrap a command, pipe output to <name>.log
|
|
12
|
+
agent-tail run <config...> Run multiple services concurrently
|
|
13
|
+
|
|
14
|
+
\x1b[1mOptions:\x1b[0m
|
|
15
|
+
--log-dir <dir> Log directory relative to cwd (default: tmp/logs)
|
|
16
|
+
--max-sessions <n> Max sessions to keep (default: 10)
|
|
17
|
+
--no-combined Don't write to combined.log
|
|
18
|
+
-h, --help Show this help
|
|
19
|
+
|
|
20
|
+
\x1b[1mExamples:\x1b[0m
|
|
21
|
+
agent-tail init
|
|
22
|
+
agent-tail wrap api -- uv run fastapi-server
|
|
23
|
+
agent-tail wrap worker -- python -m celery worker
|
|
24
|
+
agent-tail run "fe: npm run dev" "api: uv run server" "worker: uv run worker"
|
|
25
|
+
`;
|
|
26
|
+
function parse_cli_options(args) {
|
|
27
|
+
const dash_index = args.indexOf("--");
|
|
28
|
+
const before_dash = dash_index >= 0 ? args.slice(0, dash_index) : args;
|
|
29
|
+
const rest = dash_index >= 0 ? args.slice(dash_index + 1) : [];
|
|
30
|
+
const { values, positionals } = parseArgs({
|
|
31
|
+
args: before_dash,
|
|
32
|
+
options: {
|
|
33
|
+
"log-dir": {
|
|
34
|
+
type: "string",
|
|
35
|
+
default: "tmp/logs"
|
|
36
|
+
},
|
|
37
|
+
"max-sessions": {
|
|
38
|
+
type: "string",
|
|
39
|
+
default: "10"
|
|
40
|
+
},
|
|
41
|
+
"no-combined": {
|
|
42
|
+
type: "boolean",
|
|
43
|
+
default: false
|
|
44
|
+
},
|
|
45
|
+
help: {
|
|
46
|
+
type: "boolean",
|
|
47
|
+
short: "h",
|
|
48
|
+
default: false
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
allowPositionals: true,
|
|
52
|
+
strict: false
|
|
53
|
+
});
|
|
54
|
+
if (values.help) {
|
|
55
|
+
console.log(HELP);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
options: {
|
|
60
|
+
log_dir: values["log-dir"] ?? "tmp/logs",
|
|
61
|
+
max_sessions: parseInt(values["max-sessions"] ?? "10", 10),
|
|
62
|
+
combined: !values["no-combined"]
|
|
63
|
+
},
|
|
64
|
+
positionals,
|
|
65
|
+
rest
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function main() {
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
|
|
71
|
+
console.log(HELP);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
const subcommand = args[0];
|
|
75
|
+
const { options, positionals, rest } = parse_cli_options(args.slice(1));
|
|
76
|
+
const project_root = process.cwd();
|
|
77
|
+
try {
|
|
78
|
+
switch (subcommand) {
|
|
79
|
+
case "init": {
|
|
80
|
+
const session_dir = cmd_init(project_root, options);
|
|
81
|
+
console.log(session_dir);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "wrap": {
|
|
85
|
+
const code = await cmd_wrap(project_root, positionals[0], rest, options);
|
|
86
|
+
process.exit(code);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "run":
|
|
90
|
+
await cmd_run(project_root, positionals, options);
|
|
91
|
+
process.exit(0);
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
console.error(`\x1b[36m[agent-tail]\x1b[0m Unknown command: ${subcommand}`);
|
|
95
|
+
console.log(HELP);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(`\x1b[36m[agent-tail]\x1b[0m Error: ${err.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
main();
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { };
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
//#region src/log-manager.ts
|
|
6
|
+
const PLUGIN_PREFIX = "\x1B[36m[agent-tail]\x1B[0m";
|
|
7
|
+
let session_counter = 0;
|
|
8
|
+
var LogManager = class {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
initialize(project_root) {
|
|
13
|
+
const log_dir = path.resolve(project_root, this.options.logDir);
|
|
14
|
+
let session_name = this.create_session_name();
|
|
15
|
+
let session_dir = path.join(log_dir, session_name);
|
|
16
|
+
while (fs.existsSync(session_dir)) {
|
|
17
|
+
session_counter++;
|
|
18
|
+
session_name = `${this.create_session_name()}-${session_counter}`;
|
|
19
|
+
session_dir = path.join(log_dir, session_name);
|
|
20
|
+
}
|
|
21
|
+
fs.mkdirSync(session_dir, { recursive: true });
|
|
22
|
+
this.update_latest_symlink(log_dir, session_dir);
|
|
23
|
+
this.prune_sessions(log_dir);
|
|
24
|
+
const log_file = path.join(session_dir, this.options.logFileName);
|
|
25
|
+
fs.writeFileSync(log_file, "");
|
|
26
|
+
if (this.options.warnOnMissingGitignore) this.check_gitignore(project_root);
|
|
27
|
+
console.log(`${PLUGIN_PREFIX} Writing to ${log_file}`);
|
|
28
|
+
return log_file;
|
|
29
|
+
}
|
|
30
|
+
create_session_name() {
|
|
31
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
32
|
+
}
|
|
33
|
+
update_latest_symlink(log_dir, session_dir) {
|
|
34
|
+
const latest_link = path.join(log_dir, "latest");
|
|
35
|
+
try {
|
|
36
|
+
fs.unlinkSync(latest_link);
|
|
37
|
+
} catch {}
|
|
38
|
+
const relative_target = path.relative(log_dir, session_dir);
|
|
39
|
+
fs.symlinkSync(relative_target, latest_link);
|
|
40
|
+
}
|
|
41
|
+
prune_sessions(log_dir) {
|
|
42
|
+
try {
|
|
43
|
+
const session_dirs = fs.readdirSync(log_dir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== "latest").map((e) => e.name).sort();
|
|
44
|
+
const to_remove = session_dirs.slice(0, Math.max(0, session_dirs.length - this.options.maxLogSessions));
|
|
45
|
+
for (const dir_name of to_remove) {
|
|
46
|
+
const dir_path = path.join(log_dir, dir_name);
|
|
47
|
+
fs.rmSync(dir_path, {
|
|
48
|
+
recursive: true,
|
|
49
|
+
force: true
|
|
50
|
+
});
|
|
51
|
+
console.log(`${PLUGIN_PREFIX} Pruned old session: ${dir_name}`);
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the current session directory. If a `latest` symlink exists and
|
|
57
|
+
* points to a valid directory, return it. Otherwise create a new session.
|
|
58
|
+
*/
|
|
59
|
+
resolve_session(project_root) {
|
|
60
|
+
const log_dir = path.resolve(project_root, this.options.logDir);
|
|
61
|
+
const latest_link = path.join(log_dir, "latest");
|
|
62
|
+
try {
|
|
63
|
+
const real = fs.realpathSync(latest_link);
|
|
64
|
+
if (fs.statSync(real).isDirectory()) return real;
|
|
65
|
+
} catch {}
|
|
66
|
+
const log_path = this.initialize(project_root);
|
|
67
|
+
return path.dirname(log_path);
|
|
68
|
+
}
|
|
69
|
+
check_gitignore(project_root) {
|
|
70
|
+
const gitignore_path = path.join(project_root, ".gitignore");
|
|
71
|
+
try {
|
|
72
|
+
const lines = fs.readFileSync(gitignore_path, "utf-8").split("\n").map((l) => l.trim());
|
|
73
|
+
const log_dir = this.options.logDir;
|
|
74
|
+
const parts = log_dir.split("/");
|
|
75
|
+
let covered = false;
|
|
76
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
77
|
+
const prefix = parts.slice(0, i).join("/");
|
|
78
|
+
if (lines.includes(prefix) || lines.includes(prefix + "/") || lines.includes("/" + prefix) || lines.includes("/" + prefix + "/")) {
|
|
79
|
+
covered = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!covered) console.warn(`${PLUGIN_PREFIX} \x1b[33mWarning:\x1b[0m "${log_dir}" is not in your .gitignore. Add "${log_dir}/" to your .gitignore to avoid committing log files.`);
|
|
84
|
+
} catch {
|
|
85
|
+
console.warn(`${PLUGIN_PREFIX} \x1b[33mWarning:\x1b[0m No .gitignore found. Consider adding one with "${this.options.logDir}/" to avoid committing log files.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/types.ts
|
|
92
|
+
const DEFAULT_OPTIONS = {
|
|
93
|
+
logDir: "tmp/logs",
|
|
94
|
+
logFileName: "browser.log",
|
|
95
|
+
maxLogSessions: 10,
|
|
96
|
+
endpoint: "/__browser-logs",
|
|
97
|
+
flushInterval: 500,
|
|
98
|
+
maxBatchSize: 50,
|
|
99
|
+
maxSerializeLength: 2e3,
|
|
100
|
+
warnOnMissingGitignore: true,
|
|
101
|
+
levels: [
|
|
102
|
+
"log",
|
|
103
|
+
"warn",
|
|
104
|
+
"error",
|
|
105
|
+
"info",
|
|
106
|
+
"debug"
|
|
107
|
+
],
|
|
108
|
+
captureErrors: true,
|
|
109
|
+
captureRejections: true
|
|
110
|
+
};
|
|
111
|
+
function resolve_options(user_options) {
|
|
112
|
+
return {
|
|
113
|
+
...DEFAULT_OPTIONS,
|
|
114
|
+
...user_options
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/commands.ts
|
|
120
|
+
const PREFIX = "\x1B[36m[agent-tail]\x1B[0m";
|
|
121
|
+
const DEFAULT_CLI_OPTIONS = {
|
|
122
|
+
log_dir: "tmp/logs",
|
|
123
|
+
max_sessions: 10,
|
|
124
|
+
combined: true
|
|
125
|
+
};
|
|
126
|
+
function create_manager(options) {
|
|
127
|
+
return new LogManager(resolve_options({
|
|
128
|
+
logDir: options.log_dir,
|
|
129
|
+
maxLogSessions: options.max_sessions
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Create a new log session. Returns the session directory path.
|
|
134
|
+
*/
|
|
135
|
+
function cmd_init(project_root, options = DEFAULT_CLI_OPTIONS) {
|
|
136
|
+
const log_path = create_manager(options).initialize(project_root);
|
|
137
|
+
return path.dirname(log_path);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolve the current session directory (reuse if exists, create if not).
|
|
141
|
+
*/
|
|
142
|
+
function resolve_session_dir(project_root, options = DEFAULT_CLI_OPTIONS) {
|
|
143
|
+
return create_manager(options).resolve_session(project_root);
|
|
144
|
+
}
|
|
145
|
+
function parse_service_configs(args) {
|
|
146
|
+
return args.map((arg) => {
|
|
147
|
+
const colon_index = arg.indexOf(":");
|
|
148
|
+
if (colon_index === -1) throw new Error(`Invalid service format "${arg}". Expected "name: command", e.g. "api: uv run server"`);
|
|
149
|
+
return {
|
|
150
|
+
name: arg.slice(0, colon_index).trim(),
|
|
151
|
+
command: arg.slice(colon_index + 1).trim()
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Write data to a log stream and optionally to combined.log with a prefix.
|
|
157
|
+
*/
|
|
158
|
+
function write_to_logs(chunk, name, log_stream, combined_stream) {
|
|
159
|
+
log_stream.write(chunk);
|
|
160
|
+
if (combined_stream) {
|
|
161
|
+
const lines = chunk.toString().split("\n");
|
|
162
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].length > 0) combined_stream.write(`[${name}] ${lines[i]}\n`);
|
|
163
|
+
else if (i < lines.length - 1) combined_stream.write("\n");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Wrap a single command, piping its output to {name}.log in the session.
|
|
168
|
+
* Returns a promise that resolves with the exit code.
|
|
169
|
+
*/
|
|
170
|
+
function cmd_wrap(project_root, name, command, options = DEFAULT_CLI_OPTIONS) {
|
|
171
|
+
if (!name) throw new Error("wrap requires a service name");
|
|
172
|
+
if (command.length === 0) throw new Error("wrap requires a command after --");
|
|
173
|
+
const session_dir = resolve_session_dir(project_root, options);
|
|
174
|
+
const log_file = path.join(session_dir, `${name}.log`);
|
|
175
|
+
const log_stream = fs.createWriteStream(log_file, { flags: "a" });
|
|
176
|
+
let combined_stream = null;
|
|
177
|
+
if (options.combined) {
|
|
178
|
+
const combined_file = path.join(session_dir, "combined.log");
|
|
179
|
+
combined_stream = fs.createWriteStream(combined_file, { flags: "a" });
|
|
180
|
+
}
|
|
181
|
+
console.log(`${PREFIX} ${name} → ${log_file}`);
|
|
182
|
+
const child = spawn("sh", ["-c", command.join(" ")], {
|
|
183
|
+
stdio: [
|
|
184
|
+
"inherit",
|
|
185
|
+
"pipe",
|
|
186
|
+
"pipe"
|
|
187
|
+
],
|
|
188
|
+
env: { ...process.env }
|
|
189
|
+
});
|
|
190
|
+
child.stdout?.on("data", (chunk) => {
|
|
191
|
+
process.stdout.write(chunk);
|
|
192
|
+
write_to_logs(chunk, name, log_stream, combined_stream);
|
|
193
|
+
});
|
|
194
|
+
child.stderr?.on("data", (chunk) => {
|
|
195
|
+
process.stderr.write(chunk);
|
|
196
|
+
write_to_logs(chunk, name, log_stream, combined_stream);
|
|
197
|
+
});
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
child.on("close", (code) => {
|
|
200
|
+
log_stream.end();
|
|
201
|
+
combined_stream?.end();
|
|
202
|
+
resolve(code ?? 0);
|
|
203
|
+
});
|
|
204
|
+
child.on("error", (err) => {
|
|
205
|
+
log_stream.end();
|
|
206
|
+
combined_stream?.end();
|
|
207
|
+
reject(err);
|
|
208
|
+
});
|
|
209
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => {
|
|
210
|
+
child.kill(signal);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const COLORS = [
|
|
215
|
+
"\x1B[34m",
|
|
216
|
+
"\x1B[32m",
|
|
217
|
+
"\x1B[33m",
|
|
218
|
+
"\x1B[35m",
|
|
219
|
+
"\x1B[36m",
|
|
220
|
+
"\x1B[31m"
|
|
221
|
+
];
|
|
222
|
+
const RESET = "\x1B[0m";
|
|
223
|
+
/**
|
|
224
|
+
* Run multiple services concurrently.
|
|
225
|
+
* Returns a promise that resolves when all services exit.
|
|
226
|
+
*/
|
|
227
|
+
function cmd_run(project_root, service_args, options = DEFAULT_CLI_OPTIONS) {
|
|
228
|
+
if (service_args.length === 0) throw new Error("run requires at least one service");
|
|
229
|
+
const services = parse_service_configs(service_args);
|
|
230
|
+
const log_path = create_manager(options).initialize(project_root);
|
|
231
|
+
const session_dir = path.dirname(log_path);
|
|
232
|
+
let combined_stream = null;
|
|
233
|
+
if (options.combined) {
|
|
234
|
+
const combined_file = path.join(session_dir, "combined.log");
|
|
235
|
+
combined_stream = fs.createWriteStream(combined_file, { flags: "a" });
|
|
236
|
+
}
|
|
237
|
+
console.log(`${PREFIX} Session: ${session_dir}`);
|
|
238
|
+
for (const svc of services) console.log(`${PREFIX} ${svc.name} → ${svc.name}.log`);
|
|
239
|
+
if (options.combined) console.log(`${PREFIX} combined → combined.log`);
|
|
240
|
+
console.log("");
|
|
241
|
+
const children = [];
|
|
242
|
+
const promises = services.map((svc, i) => {
|
|
243
|
+
const tag = `${COLORS[i % COLORS.length]}[${svc.name}]${RESET}`;
|
|
244
|
+
const log_file = path.join(session_dir, `${svc.name}.log`);
|
|
245
|
+
const log_stream = fs.createWriteStream(log_file, { flags: "a" });
|
|
246
|
+
const child = spawn("sh", ["-c", svc.command], {
|
|
247
|
+
stdio: [
|
|
248
|
+
"inherit",
|
|
249
|
+
"pipe",
|
|
250
|
+
"pipe"
|
|
251
|
+
],
|
|
252
|
+
env: { ...process.env }
|
|
253
|
+
});
|
|
254
|
+
function handle(target, chunk) {
|
|
255
|
+
const text = chunk.toString();
|
|
256
|
+
log_stream.write(chunk);
|
|
257
|
+
const lines = text.split("\n");
|
|
258
|
+
for (let j = 0; j < lines.length; j++) if (lines[j].length > 0) {
|
|
259
|
+
target.write(`${tag} ${lines[j]}\n`);
|
|
260
|
+
combined_stream?.write(`[${svc.name}] ${lines[j]}\n`);
|
|
261
|
+
} else if (j < lines.length - 1) {
|
|
262
|
+
target.write("\n");
|
|
263
|
+
combined_stream?.write("\n");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
child.stdout?.on("data", (chunk) => handle(process.stdout, chunk));
|
|
267
|
+
child.stderr?.on("data", (chunk) => handle(process.stderr, chunk));
|
|
268
|
+
children.push(child);
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
child.on("close", (code) => {
|
|
271
|
+
log_stream.end();
|
|
272
|
+
if (code !== 0 && code !== null) console.log(`${tag} exited with code ${code}`);
|
|
273
|
+
resolve();
|
|
274
|
+
});
|
|
275
|
+
child.on("error", (err) => {
|
|
276
|
+
console.error(`${tag} Failed to start: ${err.message}`);
|
|
277
|
+
log_stream.end();
|
|
278
|
+
resolve();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => {
|
|
283
|
+
for (const child of children) child.kill(signal);
|
|
284
|
+
});
|
|
285
|
+
return Promise.all(promises).then(() => {
|
|
286
|
+
combined_stream?.end();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
//#endregion
|
|
291
|
+
export { resolve_session_dir as a, LogManager as c, parse_service_configs as i, cmd_run as n, DEFAULT_OPTIONS as o, cmd_wrap as r, resolve_options as s, cmd_init as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
interface BrowserLogsOptions {
|
|
3
|
+
/** Directory for log storage, relative to project root. Default: "tmp/logs" */
|
|
4
|
+
logDir?: string;
|
|
5
|
+
/** Log file name within each session directory. Default: "browser.log" */
|
|
6
|
+
logFileName?: string;
|
|
7
|
+
/** Maximum number of log session directories to retain. Default: 10 */
|
|
8
|
+
maxLogSessions?: number;
|
|
9
|
+
/** Server endpoint path for receiving log batches. Default: "/__browser-logs" */
|
|
10
|
+
endpoint?: string;
|
|
11
|
+
/** Client-side flush interval in milliseconds. Default: 500 */
|
|
12
|
+
flushInterval?: number;
|
|
13
|
+
/** Client-side max batch size before immediate flush. Default: 50 */
|
|
14
|
+
maxBatchSize?: number;
|
|
15
|
+
/** Max character length for serialized objects in client. Default: 2000 */
|
|
16
|
+
maxSerializeLength?: number;
|
|
17
|
+
/** Warn in terminal if logDir is not in .gitignore. Default: true */
|
|
18
|
+
warnOnMissingGitignore?: boolean;
|
|
19
|
+
/** Console methods to intercept. Default: ["log", "warn", "error", "info", "debug"] */
|
|
20
|
+
levels?: string[];
|
|
21
|
+
/** Capture window unhandled errors. Default: true */
|
|
22
|
+
captureErrors?: boolean;
|
|
23
|
+
/** Capture unhandled promise rejections. Default: true */
|
|
24
|
+
captureRejections?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface LogEntry {
|
|
27
|
+
level: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
timestamp: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
stack?: string;
|
|
32
|
+
}
|
|
33
|
+
type ResolvedOptions = Required<BrowserLogsOptions>;
|
|
34
|
+
declare const DEFAULT_OPTIONS: ResolvedOptions;
|
|
35
|
+
declare function resolve_options(user_options?: BrowserLogsOptions): ResolvedOptions;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/formatter.d.ts
|
|
38
|
+
declare function format_log_line(entry: LogEntry): string;
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/client.d.ts
|
|
41
|
+
declare function generate_client_script(options: ResolvedOptions): string;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/log-manager.d.ts
|
|
44
|
+
declare class LogManager {
|
|
45
|
+
private options;
|
|
46
|
+
constructor(options: ResolvedOptions);
|
|
47
|
+
initialize(project_root: string): string;
|
|
48
|
+
create_session_name(): string;
|
|
49
|
+
update_latest_symlink(log_dir: string, session_dir: string): void;
|
|
50
|
+
prune_sessions(log_dir: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the current session directory. If a `latest` symlink exists and
|
|
53
|
+
* points to a valid directory, return it. Otherwise create a new session.
|
|
54
|
+
*/
|
|
55
|
+
resolve_session(project_root: string): string;
|
|
56
|
+
check_gitignore(project_root: string): void;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/commands.d.ts
|
|
60
|
+
interface CliOptions {
|
|
61
|
+
log_dir: string;
|
|
62
|
+
max_sessions: number;
|
|
63
|
+
combined: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create a new log session. Returns the session directory path.
|
|
67
|
+
*/
|
|
68
|
+
declare function cmd_init(project_root: string, options?: CliOptions): string;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the current session directory (reuse if exists, create if not).
|
|
71
|
+
*/
|
|
72
|
+
declare function resolve_session_dir(project_root: string, options?: CliOptions): string;
|
|
73
|
+
interface ServiceConfig {
|
|
74
|
+
name: string;
|
|
75
|
+
command: string;
|
|
76
|
+
}
|
|
77
|
+
declare function parse_service_configs(args: string[]): ServiceConfig[];
|
|
78
|
+
/**
|
|
79
|
+
* Wrap a single command, piping its output to {name}.log in the session.
|
|
80
|
+
* Returns a promise that resolves with the exit code.
|
|
81
|
+
*/
|
|
82
|
+
declare function cmd_wrap(project_root: string, name: string, command: string[], options?: CliOptions): Promise<number>;
|
|
83
|
+
/**
|
|
84
|
+
* Run multiple services concurrently.
|
|
85
|
+
* Returns a promise that resolves when all services exit.
|
|
86
|
+
*/
|
|
87
|
+
declare function cmd_run(project_root: string, service_args: string[], options?: CliOptions): Promise<void>;
|
|
88
|
+
//#endregion
|
|
89
|
+
export { type BrowserLogsOptions, type CliOptions, DEFAULT_OPTIONS, type LogEntry, LogManager, type ResolvedOptions, type ServiceConfig, cmd_init, cmd_run, cmd_wrap, format_log_line, generate_client_script, parse_service_configs, resolve_options, resolve_session_dir };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { a as resolve_session_dir, c as LogManager, i as parse_service_configs, n as cmd_run, o as DEFAULT_OPTIONS, r as cmd_wrap, s as resolve_options, t as cmd_init } from "./commands-BO9MYtxZ.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/formatter.ts
|
|
4
|
+
function format_log_line(entry) {
|
|
5
|
+
return `[${entry.timestamp}] [${entry.level.toUpperCase().padEnd(7)}] ${entry.args.join(" ")}${entry.url ? ` (${entry.url})` : ""}${entry.stack ? `\n ${entry.stack.split("\n").join("\n ")}` : ""}\n`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/client.ts
|
|
10
|
+
function generate_client_script(options) {
|
|
11
|
+
return `(function() {
|
|
12
|
+
var BATCH = [];
|
|
13
|
+
var FLUSH_INTERVAL = ${options.flushInterval};
|
|
14
|
+
var MAX_BATCH = ${options.maxBatchSize};
|
|
15
|
+
var MAX_SERIALIZE = ${options.maxSerializeLength};
|
|
16
|
+
var ENDPOINT = ${JSON.stringify(options.endpoint)};
|
|
17
|
+
var LEVELS = ${JSON.stringify(options.levels)};
|
|
18
|
+
var CAPTURE_ERRORS = ${options.captureErrors};
|
|
19
|
+
var CAPTURE_REJECTIONS = ${options.captureRejections};
|
|
20
|
+
var timer = null;
|
|
21
|
+
|
|
22
|
+
function serialize(arg) {
|
|
23
|
+
if (arg === null) return "null";
|
|
24
|
+
if (arg === undefined) return "undefined";
|
|
25
|
+
if (arg instanceof Error) return arg.stack || arg.message || String(arg);
|
|
26
|
+
if (typeof arg === "string") return arg;
|
|
27
|
+
try {
|
|
28
|
+
var s = JSON.stringify(arg, null, 2);
|
|
29
|
+
return s.length > MAX_SERIALIZE ? s.slice(0, MAX_SERIALIZE) + "..." : s;
|
|
30
|
+
} catch(e) {
|
|
31
|
+
return String(arg);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function get_timestamp() {
|
|
36
|
+
var d = new Date();
|
|
37
|
+
return d.toTimeString().slice(0, 8) + "." + String(d.getMilliseconds()).padStart(3, "0");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function queue(level, args, extra) {
|
|
41
|
+
var entry = {
|
|
42
|
+
level: level,
|
|
43
|
+
args: Array.prototype.map.call(args, serialize),
|
|
44
|
+
timestamp: get_timestamp()
|
|
45
|
+
};
|
|
46
|
+
if (extra) {
|
|
47
|
+
if (extra.url) entry.url = extra.url;
|
|
48
|
+
if (extra.stack) entry.stack = extra.stack;
|
|
49
|
+
}
|
|
50
|
+
BATCH.push(entry);
|
|
51
|
+
if (BATCH.length >= MAX_BATCH) flush();
|
|
52
|
+
else if (!timer) timer = setTimeout(flush, FLUSH_INTERVAL);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function flush() {
|
|
56
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
57
|
+
if (!BATCH.length) return;
|
|
58
|
+
var payload = JSON.stringify(BATCH);
|
|
59
|
+
BATCH = [];
|
|
60
|
+
try {
|
|
61
|
+
navigator.sendBeacon(ENDPOINT, payload);
|
|
62
|
+
} catch(e) {
|
|
63
|
+
fetch(ENDPOINT, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: payload,
|
|
67
|
+
keepalive: true
|
|
68
|
+
}).catch(function() {});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
LEVELS.forEach(function(level) {
|
|
73
|
+
var original = console[level];
|
|
74
|
+
if (!original) return;
|
|
75
|
+
console[level] = function() {
|
|
76
|
+
queue(level, arguments);
|
|
77
|
+
return original.apply(console, arguments);
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (CAPTURE_ERRORS) {
|
|
82
|
+
window.addEventListener("error", function(e) {
|
|
83
|
+
queue("uncaught_error", [e.message], {
|
|
84
|
+
url: e.filename + ":" + e.lineno + ":" + e.colno,
|
|
85
|
+
stack: e.error && e.error.stack ? e.error.stack : undefined
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (CAPTURE_REJECTIONS) {
|
|
91
|
+
window.addEventListener("unhandledrejection", function(e) {
|
|
92
|
+
var reason = e.reason;
|
|
93
|
+
var msg = reason instanceof Error ? reason.message : String(reason);
|
|
94
|
+
var stack = reason instanceof Error ? reason.stack : undefined;
|
|
95
|
+
queue("unhandled_rejection", [msg], { stack: stack });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
window.addEventListener("beforeunload", flush);
|
|
100
|
+
window.addEventListener("pagehide", flush);
|
|
101
|
+
})();`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { DEFAULT_OPTIONS, LogManager, cmd_init, cmd_run, cmd_wrap, format_log_line, generate_client_script, parse_service_configs, resolve_options, resolve_session_dir };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-tail-core",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Core utilities for agent-tail log capture plugins.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/gillkyle/agent-tail",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"author": "Kyle Gill",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"types": "./dist/index.d.mts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.mts",
|
|
21
|
+
"bin": {
|
|
22
|
+
"agent-tail": "./dist/cli.mjs"
|
|
23
|
+
},
|
|
24
|
+
"files": ["dist"],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tsdown": "^0.18.1"
|
|
31
|
+
}
|
|
32
|
+
}
|