@rse/ase 0.0.6 → 0.0.8
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/dst/ase-agent-base.js +16 -25
- package/dst/ase-agent-biz.js +18 -27
- package/dst/ase-agent-dev.js +18 -27
- package/dst/ase-agent-ops.js +18 -27
- package/dst/ase-agent-prd.js +18 -27
- package/dst/ase-agent-prj.js +18 -27
- package/dst/ase-agent.js +19 -29
- package/dst/ase-config.js +375 -44
- package/dst/ase-log.js +69 -0
- package/dst/ase-service.js +376 -216
- package/dst/ase-setup.js +9 -10
- package/dst/ase.1 +51 -4
- package/dst/ase.js +48 -35
- package/package.json +9 -4
- package/README.md +0 -42
package/dst/ase-config.js
CHANGED
|
@@ -3,59 +3,390 @@
|
|
|
3
3
|
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
5
|
*/
|
|
6
|
-
import os from "node:os";
|
|
7
6
|
import path from "node:path";
|
|
8
7
|
import fs from "node:fs";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
import readline from "node:readline/promises";
|
|
9
|
+
import { Document, parseDocument, isMap, isScalar } from "yaml";
|
|
10
|
+
import { execaSync } from "execa";
|
|
11
|
+
import * as v from "valibot";
|
|
12
|
+
import Table from "cli-table3";
|
|
13
|
+
/* classification taxonomy */
|
|
14
|
+
export const projectClassification = {
|
|
15
|
+
source: {
|
|
16
|
+
ambition: ["artist", "craftsman", "engineer"],
|
|
17
|
+
boxing: ["white", "grey", "black"],
|
|
18
|
+
size: ["small", "medium", "large"],
|
|
19
|
+
structure: ["bare", "library", "framework"]
|
|
20
|
+
},
|
|
21
|
+
process: {
|
|
22
|
+
actors: ["person", "team", "crew"],
|
|
23
|
+
control: ["human", "hitl", "agent"],
|
|
24
|
+
drive: ["spec", "code", "test"]
|
|
25
|
+
},
|
|
26
|
+
result: {
|
|
27
|
+
target: ["prototype", "mvp", "product"]
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/* classification presets */
|
|
31
|
+
export const projectClassificationPresets = {
|
|
32
|
+
vibe: {
|
|
33
|
+
"project.id": "example",
|
|
34
|
+
"project.name": "Example Project",
|
|
35
|
+
"project.source.ambition": "engineer",
|
|
36
|
+
"project.source.boxing": "black",
|
|
37
|
+
"project.source.size": "small",
|
|
38
|
+
"project.source.structure": "bare",
|
|
39
|
+
"project.process.actors": "person",
|
|
40
|
+
"project.process.control": "agent",
|
|
41
|
+
"project.process.drive": "spec",
|
|
42
|
+
"project.result.target": "prototype"
|
|
43
|
+
},
|
|
44
|
+
pro: {
|
|
45
|
+
"project.id": "example",
|
|
46
|
+
"project.name": "Example Project",
|
|
47
|
+
"project.source.ambition": "artist",
|
|
48
|
+
"project.source.boxing": "white",
|
|
49
|
+
"project.source.size": "medium",
|
|
50
|
+
"project.source.structure": "framework",
|
|
51
|
+
"project.process.actors": "person",
|
|
52
|
+
"project.process.control": "human",
|
|
53
|
+
"project.process.drive": "code",
|
|
54
|
+
"project.result.target": "product"
|
|
19
55
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
industry: {
|
|
57
|
+
"project.id": "example",
|
|
58
|
+
"project.name": "Example Project",
|
|
59
|
+
"project.source.ambition": "craftsman",
|
|
60
|
+
"project.source.boxing": "grey",
|
|
61
|
+
"project.source.size": "large",
|
|
62
|
+
"project.source.structure": "framework",
|
|
63
|
+
"project.process.actors": "crew",
|
|
64
|
+
"project.process.control": "hitl",
|
|
65
|
+
"project.process.drive": "code",
|
|
66
|
+
"project.result.target": "mvp"
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/* schema for ".ase/config.yaml" */
|
|
70
|
+
export const configSchema = v.nullish(v.strictObject({
|
|
71
|
+
project: v.optional(v.strictObject({
|
|
72
|
+
id: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
73
|
+
name: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
74
|
+
source: v.optional(v.strictObject({
|
|
75
|
+
ambition: v.optional(v.picklist(projectClassification.source.ambition)),
|
|
76
|
+
boxing: v.optional(v.picklist(projectClassification.source.boxing)),
|
|
77
|
+
size: v.optional(v.picklist(projectClassification.source.size)),
|
|
78
|
+
structure: v.optional(v.picklist(projectClassification.source.structure))
|
|
79
|
+
})),
|
|
80
|
+
process: v.optional(v.strictObject({
|
|
81
|
+
actors: v.optional(v.picklist(projectClassification.process.actors)),
|
|
82
|
+
control: v.optional(v.picklist(projectClassification.process.control)),
|
|
83
|
+
drive: v.optional(v.picklist(projectClassification.process.drive))
|
|
84
|
+
})),
|
|
85
|
+
result: v.optional(v.strictObject({
|
|
86
|
+
target: v.optional(v.picklist(projectClassification.result.target))
|
|
87
|
+
}))
|
|
88
|
+
}))
|
|
89
|
+
}));
|
|
90
|
+
/* encapsulate read/write access to a project-local ".ase/<name>.yaml" file */
|
|
91
|
+
export class Config {
|
|
92
|
+
filename;
|
|
93
|
+
doc;
|
|
94
|
+
schema;
|
|
95
|
+
log;
|
|
96
|
+
constructor(name, schema, log) {
|
|
97
|
+
const rel = path.join(".ase", `${name}.yaml`);
|
|
98
|
+
const cwd = process.cwd();
|
|
99
|
+
const top = this.gitToplevel();
|
|
100
|
+
const found = top !== null ?
|
|
101
|
+
this.findUpward(cwd, top, rel) :
|
|
102
|
+
(fs.existsSync(path.join(cwd, rel)) ? path.join(cwd, rel) : null);
|
|
103
|
+
this.filename = found ?? path.join(top ?? cwd, rel);
|
|
104
|
+
this.doc = new Document();
|
|
105
|
+
this.schema = schema ?? null;
|
|
106
|
+
this.log = log;
|
|
107
|
+
}
|
|
108
|
+
/* upward-walk on filesystem for a file path relative to a start directory,
|
|
109
|
+
bounded above (inclusive) by a stop directory */
|
|
110
|
+
findUpward(start, stop, rel) {
|
|
111
|
+
let dir = fs.realpathSync(start);
|
|
112
|
+
const end = fs.realpathSync(stop);
|
|
113
|
+
const between = path.relative(end, dir);
|
|
114
|
+
const steps = between === "" ? 0 : between.split(path.sep).length;
|
|
115
|
+
for (let i = 0; i <= steps; i++) {
|
|
116
|
+
const candidate = path.join(dir, rel);
|
|
117
|
+
if (fs.existsSync(candidate))
|
|
118
|
+
return candidate;
|
|
119
|
+
const parent = path.dirname(dir);
|
|
120
|
+
if (parent === dir)
|
|
121
|
+
return null;
|
|
122
|
+
dir = parent;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/* determine the Git top-level directory, if inside a Git repository */
|
|
127
|
+
gitToplevel() {
|
|
128
|
+
try {
|
|
129
|
+
const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
|
|
130
|
+
stderr: "ignore"
|
|
131
|
+
});
|
|
132
|
+
return result.stdout.trim() || null;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/* read configuration file into memory */
|
|
139
|
+
read(mode = "lenient") {
|
|
140
|
+
const text = fs.existsSync(this.filename) ? fs.readFileSync(this.filename, "utf8") : "";
|
|
141
|
+
this.doc = parseDocument(text);
|
|
142
|
+
if (this.doc.errors.length > 0) {
|
|
143
|
+
const msg = `invalid YAML in ${this.filename}: ${this.doc.errors[0].message}`;
|
|
144
|
+
if (mode === "strict")
|
|
145
|
+
throw new Error(msg);
|
|
146
|
+
this.log.write("warning", msg);
|
|
147
|
+
this.doc = new Document();
|
|
148
|
+
}
|
|
149
|
+
this.validate(mode);
|
|
150
|
+
}
|
|
151
|
+
/* write in-memory configuration back to file */
|
|
152
|
+
write() {
|
|
153
|
+
this.validate("strict");
|
|
154
|
+
fs.mkdirSync(path.dirname(this.filename), { recursive: true });
|
|
155
|
+
fs.writeFileSync(this.filename, this.doc.toString({ indent: 4 }), "utf8");
|
|
156
|
+
}
|
|
157
|
+
/* validate in-memory configuration against the optional schema */
|
|
158
|
+
validate(mode = "strict") {
|
|
159
|
+
if (this.schema === null)
|
|
160
|
+
return;
|
|
161
|
+
for (;;) {
|
|
162
|
+
const result = v.safeParse(this.schema, this.doc.toJS());
|
|
163
|
+
if (result.success)
|
|
164
|
+
return;
|
|
165
|
+
if (mode === "strict") {
|
|
166
|
+
const issues = result.issues.map((i) => {
|
|
167
|
+
const dotPath = (i.path ?? []).map((p) => String(p.key)).join(".");
|
|
168
|
+
return dotPath ? `${dotPath}: ${i.message}` : i.message;
|
|
169
|
+
}).join("; ");
|
|
170
|
+
throw new Error(`invalid configuration in ${this.filename}: ${issues}`);
|
|
171
|
+
}
|
|
172
|
+
let progressed = false;
|
|
173
|
+
for (const i of result.issues) {
|
|
174
|
+
const segs = (i.path ?? []).map((p) => String(p.key));
|
|
175
|
+
const dotPath = segs.join(".");
|
|
176
|
+
this.log.write("warning", `invalid entry in ${this.filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}`);
|
|
177
|
+
if (segs.length > 0) {
|
|
178
|
+
this.doc.deleteIn(segs);
|
|
179
|
+
progressed = true;
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
/* root-level issue is structurally unrecoverable: do not wipe
|
|
183
|
+
the document, let the next strict validate() surface it */
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!progressed)
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/* enumerate all full dotted leaf paths from the attached valibot schema */
|
|
191
|
+
schemaLeafPaths() {
|
|
192
|
+
const unwrap = (s) => {
|
|
193
|
+
while (s !== undefined && s !== null && (s.type === "optional" || s.type === "nullish"
|
|
194
|
+
|| s.type === "nullable" || s.type === "undefinedable"))
|
|
195
|
+
s = s.wrapped;
|
|
196
|
+
return s;
|
|
197
|
+
};
|
|
198
|
+
const walk = (s, prefix) => {
|
|
199
|
+
const u = unwrap(s);
|
|
200
|
+
if (u !== undefined && u !== null
|
|
201
|
+
&& (u.type === "object" || u.type === "strict_object" || u.type === "loose_object")
|
|
202
|
+
&& u.entries !== undefined) {
|
|
203
|
+
const paths = [];
|
|
204
|
+
for (const [k, sub] of Object.entries(u.entries))
|
|
205
|
+
paths.push(...walk(sub, [...prefix, k]));
|
|
206
|
+
return paths;
|
|
207
|
+
}
|
|
208
|
+
return [prefix];
|
|
209
|
+
};
|
|
210
|
+
return walk(this.schema, []);
|
|
211
|
+
}
|
|
212
|
+
/* resolve a (possibly trailing-segment) dotted key to its full schema path */
|
|
213
|
+
resolveKey(key) {
|
|
214
|
+
if (this.schema === null)
|
|
215
|
+
return key;
|
|
216
|
+
const segs = key.split(".");
|
|
217
|
+
const matches = this.schemaLeafPaths().filter((p) => {
|
|
218
|
+
if (p.length < segs.length)
|
|
219
|
+
return false;
|
|
220
|
+
for (let i = 0; i < segs.length; i++)
|
|
221
|
+
if (p[p.length - segs.length + i] !== segs[i])
|
|
222
|
+
return false;
|
|
223
|
+
return true;
|
|
224
|
+
});
|
|
225
|
+
if (matches.length === 0)
|
|
226
|
+
return key;
|
|
227
|
+
if (matches.length > 1)
|
|
228
|
+
throw new Error(`ambiguous key "${key}" matches: ${matches.map((m) => m.join(".")).join(", ")}`);
|
|
229
|
+
return matches[0].join(".");
|
|
230
|
+
}
|
|
231
|
+
/* retrieve a value at a dotted key, or the root contents if no key given */
|
|
232
|
+
get(key) {
|
|
233
|
+
if (key === undefined)
|
|
234
|
+
return this.doc.contents;
|
|
235
|
+
return this.doc.getIn(this.resolveKey(key).split("."));
|
|
236
|
+
}
|
|
237
|
+
/* set a value at a dotted key, creating intermediate maps as needed */
|
|
238
|
+
set(key, value) {
|
|
239
|
+
const segments = this.resolveKey(key).split(".");
|
|
240
|
+
const next = this.doc.clone();
|
|
241
|
+
for (let i = 1; i < segments.length; i++) {
|
|
242
|
+
const prefix = segments.slice(0, i);
|
|
243
|
+
const node = next.getIn(prefix, true);
|
|
244
|
+
if (node !== undefined && !isMap(node))
|
|
245
|
+
throw new Error(`cannot set "${key}": intermediate path "${prefix.join(".")}" is not a map`);
|
|
246
|
+
if (node === undefined)
|
|
247
|
+
next.setIn(prefix, next.createNode({}));
|
|
248
|
+
}
|
|
249
|
+
next.setIn(segments, value);
|
|
250
|
+
const saved = this.doc;
|
|
251
|
+
this.doc = next;
|
|
252
|
+
try {
|
|
253
|
+
this.validate("strict");
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
this.doc = saved;
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/* delete a value at a dotted key */
|
|
261
|
+
delete(key) {
|
|
262
|
+
const next = this.doc.clone();
|
|
263
|
+
next.deleteIn(this.resolveKey(key).split("."));
|
|
264
|
+
const saved = this.doc;
|
|
265
|
+
this.doc = next;
|
|
266
|
+
try {
|
|
267
|
+
this.validate("strict");
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
this.doc = saved;
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/* CLI command "ase config" */
|
|
276
|
+
export default class ConfigCommand {
|
|
277
|
+
log;
|
|
278
|
+
constructor(log) {
|
|
279
|
+
this.log = log;
|
|
280
|
+
}
|
|
281
|
+
/* register commands */
|
|
282
|
+
register(program) {
|
|
283
|
+
/* register CLI top-level command "ase config" */
|
|
284
|
+
const configCmd = program
|
|
285
|
+
.command("config")
|
|
286
|
+
.description("Manage ASE configuration")
|
|
287
|
+
.action((_opts, cmd) => {
|
|
288
|
+
cmd.outputHelp();
|
|
289
|
+
process.exit(1);
|
|
290
|
+
});
|
|
291
|
+
/* register CLI sub-command "ase config init" */
|
|
292
|
+
configCmd
|
|
293
|
+
.command("init")
|
|
294
|
+
.description("Initialize configuration with preset values (vibe|pro|industry)")
|
|
295
|
+
.argument("<type>", "Preset type (vibe|pro|industry)")
|
|
296
|
+
.action((type) => {
|
|
297
|
+
const preset = projectClassificationPresets[type];
|
|
298
|
+
if (preset === undefined)
|
|
299
|
+
throw new Error(`unknown preset "${type}" (expected: vibe|pro|industry)`);
|
|
300
|
+
const cfg = new Config("config", configSchema, this.log);
|
|
301
|
+
cfg.read();
|
|
302
|
+
for (const [k, val] of Object.entries(preset))
|
|
303
|
+
cfg.set(k, val);
|
|
304
|
+
cfg.write();
|
|
305
|
+
});
|
|
306
|
+
/* register CLI sub-command "ase config list" */
|
|
307
|
+
configCmd
|
|
308
|
+
.command("list")
|
|
309
|
+
.description("List all configured values as flat dotted keys")
|
|
310
|
+
.action(() => {
|
|
311
|
+
const cfg = new Config("config", configSchema, this.log);
|
|
312
|
+
cfg.read();
|
|
313
|
+
const table = new Table({
|
|
314
|
+
head: ["KEY", "VALUE"],
|
|
315
|
+
chars: { "mid": "", "left-mid": "", "mid-mid": "", "right-mid": "" },
|
|
316
|
+
style: { head: ["blue"] }
|
|
317
|
+
});
|
|
29
318
|
const list = (node, prefix) => {
|
|
30
319
|
if (isMap(node))
|
|
31
320
|
for (const item of node.items) {
|
|
32
|
-
const
|
|
321
|
+
const k = prefix ? `${prefix}.${item.key}` : String(item.key);
|
|
33
322
|
if (isMap(item.value))
|
|
34
|
-
list(item.value,
|
|
323
|
+
list(item.value, k);
|
|
324
|
+
else if (!isScalar(item.value))
|
|
325
|
+
throw new Error(`key "${k}" has unsupported node type`);
|
|
35
326
|
else
|
|
36
|
-
|
|
327
|
+
table.push([k, String(item.value.value)]);
|
|
37
328
|
}
|
|
38
329
|
};
|
|
39
|
-
list(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
330
|
+
list(cfg.get(), "");
|
|
331
|
+
process.stdout.write(`${table.toString()}\n`);
|
|
332
|
+
});
|
|
333
|
+
/* register CLI sub-command "ase config edit" */
|
|
334
|
+
configCmd
|
|
335
|
+
.command("edit")
|
|
336
|
+
.description("Edit configuration file with $EDITOR")
|
|
337
|
+
.action(async () => {
|
|
338
|
+
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
339
|
+
const cfg = new Config("config", configSchema, this.log);
|
|
340
|
+
fs.mkdirSync(path.dirname(cfg.filename), { recursive: true });
|
|
341
|
+
if (!fs.existsSync(cfg.filename))
|
|
342
|
+
fs.writeFileSync(cfg.filename, "", "utf8");
|
|
343
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
344
|
+
try {
|
|
345
|
+
for (;;) {
|
|
346
|
+
execaSync(editor, [cfg.filename], { stdio: "inherit" });
|
|
347
|
+
try {
|
|
348
|
+
cfg.read("strict");
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
353
|
+
this.log.write("error", msg);
|
|
354
|
+
const ans = (await rl.question("re-edit? [Y/n] ")).trim().toLowerCase();
|
|
355
|
+
if (ans === "n" || ans === "no")
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
51
359
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
360
|
+
finally {
|
|
361
|
+
rl.close();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
/* register CLI sub-command "ase config get" */
|
|
365
|
+
configCmd
|
|
366
|
+
.command("get")
|
|
367
|
+
.description("Print the value at a dotted configuration key")
|
|
368
|
+
.argument("<key>", "Configuration key (dotted path)")
|
|
369
|
+
.action((key) => {
|
|
370
|
+
const cfg = new Config("config", configSchema, this.log);
|
|
371
|
+
cfg.read();
|
|
372
|
+
const val = cfg.get(key);
|
|
373
|
+
if (val === undefined)
|
|
374
|
+
throw new Error(`key "${key}" is not set`);
|
|
375
|
+
if (isMap(val))
|
|
376
|
+
throw new Error(`key "${key}" is not a leaf key`);
|
|
377
|
+
process.stdout.write(`${isScalar(val) ? val.value : val}\n`);
|
|
378
|
+
});
|
|
379
|
+
/* register CLI sub-command "ase config set" */
|
|
380
|
+
configCmd
|
|
381
|
+
.command("set")
|
|
382
|
+
.description("Set the value at a dotted configuration key")
|
|
383
|
+
.argument("<key>", "Configuration key (dotted path)")
|
|
384
|
+
.argument("<value>", "Configuration value")
|
|
385
|
+
.action((key, value) => {
|
|
386
|
+
const cfg = new Config("config", configSchema, this.log);
|
|
387
|
+
cfg.read();
|
|
388
|
+
cfg.set(key, value);
|
|
389
|
+
cfg.write();
|
|
390
|
+
});
|
|
59
391
|
}
|
|
60
|
-
}
|
|
61
|
-
export default configCommand;
|
|
392
|
+
}
|
package/dst/ase-log.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
|
|
5
|
+
*/
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { DateTime } from "luxon";
|
|
9
|
+
const levels = [
|
|
10
|
+
{ name: "error", style: chalk.red.bold },
|
|
11
|
+
{ name: "warning", style: chalk.yellow.bold },
|
|
12
|
+
{ name: "info", style: chalk.blue },
|
|
13
|
+
{ name: "debug", style: chalk.green }
|
|
14
|
+
];
|
|
15
|
+
export default class Log {
|
|
16
|
+
_program;
|
|
17
|
+
_logLevel;
|
|
18
|
+
_logFile;
|
|
19
|
+
stream = null;
|
|
20
|
+
logLevelIdx = 0;
|
|
21
|
+
constructor(_program, _logLevel, _logFile) {
|
|
22
|
+
this._program = _program;
|
|
23
|
+
this._logLevel = _logLevel;
|
|
24
|
+
this._logFile = _logFile;
|
|
25
|
+
}
|
|
26
|
+
async init() {
|
|
27
|
+
/* log messages */
|
|
28
|
+
const idx = levels.findIndex((l) => l.name === this._logLevel);
|
|
29
|
+
if (idx === -1)
|
|
30
|
+
throw new RangeError(`invalid log level "${this._logLevel}" (expected one of: ${levels.map((l) => l.name).join(", ")})`);
|
|
31
|
+
this.logLevelIdx = idx;
|
|
32
|
+
if (this._logFile !== "-")
|
|
33
|
+
this.stream = fs.createWriteStream(this._logFile, { flags: "a", encoding: "utf8" });
|
|
34
|
+
}
|
|
35
|
+
logLevel(level) {
|
|
36
|
+
const idx = levels.findIndex((l) => l.name === level);
|
|
37
|
+
if (idx === -1)
|
|
38
|
+
throw new RangeError(`invalid log level "${level}" (expected one of: ${levels.map((l) => l.name).join(", ")})`);
|
|
39
|
+
this._logLevel = level;
|
|
40
|
+
this.logLevelIdx = idx;
|
|
41
|
+
}
|
|
42
|
+
logFile(file) {
|
|
43
|
+
if (file === this._logFile)
|
|
44
|
+
return;
|
|
45
|
+
this._logFile = file;
|
|
46
|
+
if (this.stream !== null) {
|
|
47
|
+
this.stream.end();
|
|
48
|
+
this.stream = null;
|
|
49
|
+
}
|
|
50
|
+
if (file !== "-")
|
|
51
|
+
this.stream = fs.createWriteStream(file, { flags: "a", encoding: "utf8" });
|
|
52
|
+
}
|
|
53
|
+
write(level, msg) {
|
|
54
|
+
const idx = levels.findIndex((l) => l.name === level);
|
|
55
|
+
if (idx !== -1 && idx <= this.logLevelIdx) {
|
|
56
|
+
const timestamp = DateTime.now().toFormat("yyyy-LL-dd HH:mm:ss.SSS");
|
|
57
|
+
let line = `${this._program}: [${timestamp}]: `;
|
|
58
|
+
if (this._logFile === "-" && process.stdout.isTTY)
|
|
59
|
+
line += `${levels[idx].style("[" + levels[idx].name.toUpperCase() + "]")}`;
|
|
60
|
+
else
|
|
61
|
+
line += `[${levels[idx].name.toUpperCase()}]`;
|
|
62
|
+
line += `: ${msg}\n`;
|
|
63
|
+
if (this._logFile === "-")
|
|
64
|
+
process.stdout.write(line);
|
|
65
|
+
else if (this.stream !== null)
|
|
66
|
+
this.stream.write(line);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|