@rse/ase 0.0.7 → 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
@@ -5,14 +5,86 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
+ import readline from "node:readline/promises";
8
9
  import { Document, parseDocument, isMap, isScalar } from "yaml";
9
10
  import { execaSync } from "execa";
10
11
  import * as v from "valibot";
11
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"
55
+ },
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
+ };
12
69
  /* schema for ".ase/config.yaml" */
13
70
  export const configSchema = v.nullish(v.strictObject({
14
71
  project: v.optional(v.strictObject({
15
- id: v.optional(v.pipe(v.string(), v.minLength(1)))
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
+ }))
16
88
  }))
17
89
  }));
18
90
  /* encapsulate read/write access to a project-local ".ase/<name>.yaml" file */
@@ -20,17 +92,27 @@ export class Config {
20
92
  filename;
21
93
  doc;
22
94
  schema;
23
- constructor(name, schema) {
95
+ log;
96
+ constructor(name, schema, log) {
24
97
  const rel = path.join(".ase", `${name}.yaml`);
25
- const found = this.findUpward(process.cwd(), rel);
26
- this.filename = found ?? path.join(this.gitToplevel() ?? process.cwd(), rel);
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);
27
104
  this.doc = new Document();
28
105
  this.schema = schema ?? null;
106
+ this.log = log;
29
107
  }
30
- /* upward-walk on filesystem for a file path relative to a start directory */
31
- findUpward(start, rel) {
32
- let dir = start;
33
- for (;;) {
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++) {
34
116
  const candidate = path.join(dir, rel);
35
117
  if (fs.existsSync(candidate))
36
118
  return candidate;
@@ -39,6 +121,7 @@ export class Config {
39
121
  return null;
40
122
  dir = parent;
41
123
  }
124
+ return null;
42
125
  }
43
126
  /* determine the Git top-level directory, if inside a Git repository */
44
127
  gitToplevel() {
@@ -53,10 +136,17 @@ export class Config {
53
136
  }
54
137
  }
55
138
  /* read configuration file into memory */
56
- read() {
139
+ read(mode = "lenient") {
57
140
  const text = fs.existsSync(this.filename) ? fs.readFileSync(this.filename, "utf8") : "";
58
141
  this.doc = parseDocument(text);
59
- this.validate("lenient");
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);
60
150
  }
61
151
  /* write in-memory configuration back to file */
62
152
  write() {
@@ -83,106 +173,220 @@ export class Config {
83
173
  for (const i of result.issues) {
84
174
  const segs = (i.path ?? []).map((p) => String(p.key));
85
175
  const dotPath = segs.join(".");
86
- process.stderr.write(`ase: warning: invalid entry in ${this.filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}\n`);
176
+ this.log.write("warning", `invalid entry in ${this.filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}`);
87
177
  if (segs.length > 0) {
88
178
  this.doc.deleteIn(segs);
89
179
  progressed = true;
90
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;
91
185
  }
92
186
  if (!progressed)
93
187
  return;
94
188
  }
95
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
+ }
96
231
  /* retrieve a value at a dotted key, or the root contents if no key given */
97
232
  get(key) {
98
233
  if (key === undefined)
99
234
  return this.doc.contents;
100
- return this.doc.getIn(key.split("."));
235
+ return this.doc.getIn(this.resolveKey(key).split("."));
101
236
  }
102
237
  /* set a value at a dotted key, creating intermediate maps as needed */
103
238
  set(key, value) {
104
- const segments = key.split(".");
239
+ const segments = this.resolveKey(key).split(".");
240
+ const next = this.doc.clone();
105
241
  for (let i = 1; i < segments.length; i++) {
106
242
  const prefix = segments.slice(0, i);
107
- const node = this.doc.getIn(prefix, true);
108
- if (!isMap(node))
109
- this.doc.setIn(prefix, this.doc.createNode({}));
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;
110
258
  }
111
- this.doc.setIn(segments, value);
112
- this.validate("strict");
113
259
  }
114
260
  /* delete a value at a dotted key */
115
261
  delete(key) {
116
- this.doc.deleteIn(key.split("."));
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
+ }
117
273
  }
118
274
  }
119
- /* register CLI command "ase config" */
120
- const registerConfigCommand = (program) => {
121
- const configCmd = program
122
- .command("config")
123
- .description("Manage ASE configuration")
124
- .action((_opts, cmd) => {
125
- cmd.help();
126
- });
127
- /* register CLI sub-command "ase config get" */
128
- configCmd
129
- .command("get")
130
- .description("Print the value at a dotted configuration key")
131
- .argument("<key>", "Configuration key (dotted path)")
132
- .action((key) => {
133
- const cfg = new Config("config", configSchema);
134
- cfg.read();
135
- const v = cfg.get(key);
136
- if (isMap(v))
137
- throw new Error(`key "${key}" is not a leaf key`);
138
- console.log(isScalar(v) ? v.value : v);
139
- });
140
- /* register CLI sub-command "ase config set" */
141
- configCmd
142
- .command("set")
143
- .description("Set the value at a dotted configuration key")
144
- .argument("<key>", "Configuration key (dotted path)")
145
- .argument("<value>", "Configuration value")
146
- .action((key, value) => {
147
- const cfg = new Config("config", configSchema);
148
- cfg.read();
149
- console.log(`${key}: ${value}`);
150
- cfg.set(key, value);
151
- cfg.write();
152
- });
153
- /* register CLI sub-command "ase config list" */
154
- configCmd
155
- .command("list")
156
- .description("List all configured values as flat dotted keys")
157
- .action(() => {
158
- const cfg = new Config("config", configSchema);
159
- cfg.read();
160
- const table = new Table({ head: ["key", "value"] });
161
- const list = (node, prefix) => {
162
- if (isMap(node))
163
- for (const item of node.items) {
164
- const k = prefix ? `${prefix}.${item.key}` : String(item.key);
165
- if (isMap(item.value))
166
- list(item.value, k);
167
- else
168
- table.push([k, String(isScalar(item.value) ? item.value.value : item.value)]);
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
+ });
318
+ const list = (node, prefix) => {
319
+ if (isMap(node))
320
+ for (const item of node.items) {
321
+ const k = prefix ? `${prefix}.${item.key}` : String(item.key);
322
+ if (isMap(item.value))
323
+ list(item.value, k);
324
+ else if (!isScalar(item.value))
325
+ throw new Error(`key "${k}" has unsupported node type`);
326
+ else
327
+ table.push([k, String(item.value.value)]);
328
+ }
329
+ };
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
+ }
169
358
  }
170
- };
171
- list(cfg.get(), "");
172
- console.log(table.toString());
173
- });
174
- /* register CLI sub-command "ase config edit" */
175
- configCmd
176
- .command("edit")
177
- .description("Edit configuration file with $EDITOR")
178
- .action(() => {
179
- const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
180
- const cfg = new Config("config", configSchema);
181
- fs.mkdirSync(path.dirname(cfg.filename), { recursive: true });
182
- if (!fs.existsSync(cfg.filename))
183
- fs.writeFileSync(cfg.filename, "", "utf8");
184
- execaSync(editor, [cfg.filename], { stdio: "inherit" });
185
- cfg.read();
186
- });
187
- };
188
- export default registerConfigCommand;
359
+ }
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
+ });
391
+ }
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
+ }