@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-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 { parseDocument, isMap, isScalar } from "yaml";
10
- const configCommand = {
11
- command: "config [query]",
12
- describe: "Manage ASE configuration",
13
- builder: (yargs) => {
14
- return yargs
15
- .positional("query", {
16
- type: "string",
17
- describe: "Configuration query (none, <key>, or <key>=<value>)"
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
- handler: (argv) => {
21
- if (argv.debug)
22
- console.log("DEBUG: config command", argv);
23
- const query = argv.query;
24
- const filename = path.join(os.homedir(), ".ase.yaml");
25
- const text = fs.existsSync(filename) ? fs.readFileSync(filename, "utf8") : "";
26
- const doc = parseDocument(text);
27
- if (!query) {
28
- /* list all values as flat dotted keys */
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 key = prefix ? `${prefix}.${item.key}` : String(item.key);
321
+ const k = prefix ? `${prefix}.${item.key}` : String(item.key);
33
322
  if (isMap(item.value))
34
- list(item.value, key);
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
- console.log(`${key} = ${isScalar(item.value) ? item.value.value : item.value}`);
327
+ table.push([k, String(item.value.value)]);
37
328
  }
38
329
  };
39
- list(doc.contents, "");
40
- }
41
- else if (query.includes("=")) {
42
- const [key, ...valueParts] = query.split("=");
43
- const value = valueParts.join("=");
44
- console.log(`Setting configuration: ${key} = ${value}`);
45
- const segments = key.split(".");
46
- for (let i = 1; i < segments.length; i++) {
47
- const prefix = segments.slice(0, i);
48
- const node = doc.getIn(prefix, true);
49
- if (!isMap(node))
50
- doc.setIn(prefix, doc.createNode({}));
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
- doc.setIn(segments, value);
53
- fs.writeFileSync(filename, doc.toString(), "utf8");
54
- }
55
- else {
56
- const value = doc.getIn(query.split("."));
57
- console.log(value);
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
+ }