@rse/ase 0.0.6 → 0.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  $ npx @rse/ase init
3
- $ npx @rse/ase config agent.dev.llm.type=anthropic-claude-sonnet-4.5
4
- $ npx @rse/ase config agent.dev.llm.key=<foo>
3
+ $ npx @rse/ase config agent.dev.llm.type anthropic-claude-sonnet-4.5
4
+ $ npx @rse/ase config agent.dev.llm.key <foo>
5
5
  $ npx @rse/ase agent dev start
6
6
 
7
7
  Agents
@@ -7,37 +7,28 @@
7
7
  export const createAgentCommandModule = (category, description, subCommands) => {
8
8
  /* create sub-command handler */
9
9
  const createSubCommandHandler = (subCommand) => {
10
- return (argv) => {
11
- if (argv.debug)
10
+ return (_opts, cmd) => {
11
+ const opts = cmd.optsWithGlobals();
12
+ if (opts.debug)
12
13
  console.log(`DEBUG: agent ${category} ${subCommand} command`);
13
- if (argv.verbose)
14
+ if (opts.verbose)
14
15
  console.log(`VERBOSE: executing agent ${category} ${subCommand}...`);
15
16
  console.log(`Executing agent ${category} ${subCommand}...`);
16
17
  /* TODO: implement agent ${category} sub-command logic */
17
18
  };
18
19
  };
19
- return {
20
- command: `${category} <subcommand>`,
21
- describe: `Execute ${description} agent operations`,
22
- builder: (yargs) => {
23
- let builder = yargs
24
- .option("verbose", {
25
- alias: "v",
26
- type: "boolean",
27
- describe: "Enable verbose output",
28
- default: false
29
- })
30
- .demandCommand(1, `You need to specify a ${category} subcommand`);
31
- /* register all sub-commands */
32
- for (const subCmd of subCommands) {
33
- builder = builder.command(subCmd, `Execute agent ${category} ${subCmd} operation`, () => { }, createSubCommandHandler(subCmd));
34
- }
35
- return builder;
36
- },
37
- handler: (argv) => {
38
- /* this handler is not called when sub-commands are used */
39
- if (argv.debug)
40
- console.log(`DEBUG: agent ${category} command (no subcommand)`);
20
+ return (parent) => {
21
+ const agent = parent
22
+ .command(`${category}`)
23
+ .description(`Execute ${description} agent operations`)
24
+ .option("-v, --verbose", "Enable verbose output", false);
25
+ /* register all sub-commands */
26
+ for (const subCmd of subCommands) {
27
+ agent
28
+ .command(subCmd)
29
+ .description(`Execute agent ${category} ${subCmd} operation`)
30
+ .action(createSubCommandHandler(subCmd));
41
31
  }
32
+ return agent;
42
33
  };
43
34
  };
@@ -12,38 +12,29 @@ const bizSubCommands = [
12
12
  ];
13
13
  /* create sub-command handler */
14
14
  const createSubCommandHandler = (subCommand) => {
15
- return (argv) => {
16
- if (argv.debug)
15
+ return (_opts, cmd) => {
16
+ const opts = cmd.optsWithGlobals();
17
+ if (opts.debug)
17
18
  console.log(`DEBUG: agent biz ${subCommand} command`);
18
- if (argv.verbose)
19
+ if (opts.verbose)
19
20
  console.log(`VERBOSE: executing agent biz ${subCommand}...`);
20
21
  console.log(`Executing agent biz ${subCommand}...`);
21
22
  /* TODO: implement agent biz sub-command logic */
22
23
  };
23
24
  };
24
- /* create and export biz command module */
25
- const bizCommand = {
26
- command: "biz <subcommand>",
27
- describe: "Execute business agent operations",
28
- builder: (yargs) => {
29
- let builder = yargs
30
- .option("verbose", {
31
- alias: "v",
32
- type: "boolean",
33
- describe: "Enable verbose output",
34
- default: false
35
- })
36
- .demandCommand(1, "You need to specify a biz subcommand");
37
- /* register all sub-commands */
38
- for (const subCmd of bizSubCommands) {
39
- builder = builder.command(subCmd, `Execute agent biz ${subCmd} operation`, () => { }, createSubCommandHandler(subCmd));
40
- }
41
- return builder;
42
- },
43
- handler: (argv) => {
44
- /* this handler is not called when sub-commands are used */
45
- if (argv.debug)
46
- console.log("DEBUG: agent biz command (no subcommand)");
25
+ /* register biz command on the given parent */
26
+ const registerBizCommand = (parent) => {
27
+ const biz = parent
28
+ .command("biz")
29
+ .description("Execute business agent operations")
30
+ .option("-v, --verbose", "Enable verbose output", false);
31
+ /* register all sub-commands */
32
+ for (const subCmd of bizSubCommands) {
33
+ biz
34
+ .command(subCmd)
35
+ .description(`Execute agent biz ${subCmd} operation`)
36
+ .action(createSubCommandHandler(subCmd));
47
37
  }
38
+ return biz;
48
39
  };
49
- export default bizCommand;
40
+ export default registerBizCommand;
@@ -12,38 +12,29 @@ const devSubCommands = [
12
12
  ];
13
13
  /* create sub-command handler */
14
14
  const createSubCommandHandler = (subCommand) => {
15
- return (argv) => {
16
- if (argv.debug)
15
+ return (_opts, cmd) => {
16
+ const opts = cmd.optsWithGlobals();
17
+ if (opts.debug)
17
18
  console.log(`DEBUG: agent dev ${subCommand} command`);
18
- if (argv.verbose)
19
+ if (opts.verbose)
19
20
  console.log(`VERBOSE: executing agent dev ${subCommand}...`);
20
21
  console.log(`Executing agent dev ${subCommand}...`);
21
22
  /* TODO: implement agent dev sub-command logic */
22
23
  };
23
24
  };
24
- /* create and export dev command module */
25
- const devCommand = {
26
- command: "dev <subcommand>",
27
- describe: "Execute development agent operations",
28
- builder: (yargs) => {
29
- let builder = yargs
30
- .option("verbose", {
31
- alias: "v",
32
- type: "boolean",
33
- describe: "Enable verbose output",
34
- default: false
35
- })
36
- .demandCommand(1, "You need to specify a dev subcommand");
37
- /* register all sub-commands */
38
- for (const subCmd of devSubCommands) {
39
- builder = builder.command(subCmd, `Execute agent dev ${subCmd} operation`, () => { }, createSubCommandHandler(subCmd));
40
- }
41
- return builder;
42
- },
43
- handler: (argv) => {
44
- /* this handler is not called when sub-commands are used */
45
- if (argv.debug)
46
- console.log("DEBUG: agent dev command (no subcommand)");
25
+ /* register dev command on the given parent */
26
+ const registerDevCommand = (parent) => {
27
+ const dev = parent
28
+ .command("dev")
29
+ .description("Execute development agent operations")
30
+ .option("-v, --verbose", "Enable verbose output", false);
31
+ /* register all sub-commands */
32
+ for (const subCmd of devSubCommands) {
33
+ dev
34
+ .command(subCmd)
35
+ .description(`Execute agent dev ${subCmd} operation`)
36
+ .action(createSubCommandHandler(subCmd));
47
37
  }
38
+ return dev;
48
39
  };
49
- export default devCommand;
40
+ export default registerDevCommand;
@@ -12,38 +12,29 @@ const opsSubCommands = [
12
12
  ];
13
13
  /* create sub-command handler */
14
14
  const createSubCommandHandler = (subCommand) => {
15
- return (argv) => {
16
- if (argv.debug)
15
+ return (_opts, cmd) => {
16
+ const opts = cmd.optsWithGlobals();
17
+ if (opts.debug)
17
18
  console.log(`DEBUG: agent ops ${subCommand} command`);
18
- if (argv.verbose)
19
+ if (opts.verbose)
19
20
  console.log(`VERBOSE: executing agent ops ${subCommand}...`);
20
21
  console.log(`Executing agent ops ${subCommand}...`);
21
22
  /* TODO: implement agent ops sub-command logic */
22
23
  };
23
24
  };
24
- /* create and export ops command module */
25
- const opsCommand = {
26
- command: "ops <subcommand>",
27
- describe: "Execute operations agent operations",
28
- builder: (yargs) => {
29
- let builder = yargs
30
- .option("verbose", {
31
- alias: "v",
32
- type: "boolean",
33
- describe: "Enable verbose output",
34
- default: false
35
- })
36
- .demandCommand(1, "You need to specify an ops subcommand");
37
- /* register all sub-commands */
38
- for (const subCmd of opsSubCommands) {
39
- builder = builder.command(subCmd, `Execute agent ops ${subCmd} operation`, () => { }, createSubCommandHandler(subCmd));
40
- }
41
- return builder;
42
- },
43
- handler: (argv) => {
44
- /* this handler is not called when sub-commands are used */
45
- if (argv.debug)
46
- console.log("DEBUG: agent ops command (no subcommand)");
25
+ /* register ops command on the given parent */
26
+ const registerOpsCommand = (parent) => {
27
+ const ops = parent
28
+ .command("ops")
29
+ .description("Execute operations agent operations")
30
+ .option("-v, --verbose", "Enable verbose output", false);
31
+ /* register all sub-commands */
32
+ for (const subCmd of opsSubCommands) {
33
+ ops
34
+ .command(subCmd)
35
+ .description(`Execute agent ops ${subCmd} operation`)
36
+ .action(createSubCommandHandler(subCmd));
47
37
  }
38
+ return ops;
48
39
  };
49
- export default opsCommand;
40
+ export default registerOpsCommand;
@@ -12,38 +12,29 @@ const prdSubCommands = [
12
12
  ];
13
13
  /* create sub-command handler */
14
14
  const createSubCommandHandler = (subCommand) => {
15
- return (argv) => {
16
- if (argv.debug)
15
+ return (_opts, cmd) => {
16
+ const opts = cmd.optsWithGlobals();
17
+ if (opts.debug)
17
18
  console.log(`DEBUG: agent prd ${subCommand} command`);
18
- if (argv.verbose)
19
+ if (opts.verbose)
19
20
  console.log(`VERBOSE: executing agent prd ${subCommand}...`);
20
21
  console.log(`Executing agent prd ${subCommand}...`);
21
22
  /* TODO: implement agent prd sub-command logic */
22
23
  };
23
24
  };
24
- /* create and export prd command module */
25
- const prdCommand = {
26
- command: "prd <subcommand>",
27
- describe: "Execute production agent operations",
28
- builder: (yargs) => {
29
- let builder = yargs
30
- .option("verbose", {
31
- alias: "v",
32
- type: "boolean",
33
- describe: "Enable verbose output",
34
- default: false
35
- })
36
- .demandCommand(1, "You need to specify a prd subcommand");
37
- /* register all sub-commands */
38
- for (const subCmd of prdSubCommands) {
39
- builder = builder.command(subCmd, `Execute agent prd ${subCmd} operation`, () => { }, createSubCommandHandler(subCmd));
40
- }
41
- return builder;
42
- },
43
- handler: (argv) => {
44
- /* this handler is not called when sub-commands are used */
45
- if (argv.debug)
46
- console.log("DEBUG: agent prd command (no subcommand)");
25
+ /* register prd command on the given parent */
26
+ const registerPrdCommand = (parent) => {
27
+ const prd = parent
28
+ .command("prd")
29
+ .description("Execute production agent operations")
30
+ .option("-v, --verbose", "Enable verbose output", false);
31
+ /* register all sub-commands */
32
+ for (const subCmd of prdSubCommands) {
33
+ prd
34
+ .command(subCmd)
35
+ .description(`Execute agent prd ${subCmd} operation`)
36
+ .action(createSubCommandHandler(subCmd));
47
37
  }
38
+ return prd;
48
39
  };
49
- export default prdCommand;
40
+ export default registerPrdCommand;
@@ -12,38 +12,29 @@ const prjSubCommands = [
12
12
  ];
13
13
  /* create sub-command handler */
14
14
  const createSubCommandHandler = (subCommand) => {
15
- return (argv) => {
16
- if (argv.debug)
15
+ return (_opts, cmd) => {
16
+ const opts = cmd.optsWithGlobals();
17
+ if (opts.debug)
17
18
  console.log(`DEBUG: agent prj ${subCommand} command`);
18
- if (argv.verbose)
19
+ if (opts.verbose)
19
20
  console.log(`VERBOSE: executing agent prj ${subCommand}...`);
20
21
  console.log(`Executing agent prj ${subCommand}...`);
21
22
  /* TODO: implement agent prj sub-command logic */
22
23
  };
23
24
  };
24
- /* create and export prj command module */
25
- const prjCommand = {
26
- command: "prj <subcommand>",
27
- describe: "Execute project agent operations",
28
- builder: (yargs) => {
29
- let builder = yargs
30
- .option("verbose", {
31
- alias: "v",
32
- type: "boolean",
33
- describe: "Enable verbose output",
34
- default: false
35
- })
36
- .demandCommand(1, "You need to specify a prj subcommand");
37
- /* register all sub-commands */
38
- for (const subCmd of prjSubCommands) {
39
- builder = builder.command(subCmd, `Execute agent prj ${subCmd} operation`, () => { }, createSubCommandHandler(subCmd));
40
- }
41
- return builder;
42
- },
43
- handler: (argv) => {
44
- /* this handler is not called when sub-commands are used */
45
- if (argv.debug)
46
- console.log("DEBUG: agent prj command (no subcommand)");
25
+ /* register prj command on the given parent */
26
+ const registerPrjCommand = (parent) => {
27
+ const prj = parent
28
+ .command("prj")
29
+ .description("Execute project agent operations")
30
+ .option("-v, --verbose", "Enable verbose output", false);
31
+ /* register all sub-commands */
32
+ for (const subCmd of prjSubCommands) {
33
+ prj
34
+ .command(subCmd)
35
+ .description(`Execute agent prj ${subCmd} operation`)
36
+ .action(createSubCommandHandler(subCmd));
47
37
  }
38
+ return prj;
48
39
  };
49
- export default prjCommand;
40
+ export default registerPrjCommand;
package/dst/ase-agent.js CHANGED
@@ -3,33 +3,23 @@
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 bizCommand from "./ase-agent-biz.js";
7
- import devCommand from "./ase-agent-dev.js";
8
- import opsCommand from "./ase-agent-ops.js";
9
- import prdCommand from "./ase-agent-prd.js";
10
- import prjCommand from "./ase-agent-prj.js";
11
- const agentCommand = {
12
- command: "agent <subcommand>",
13
- describe: "Execute agent operations",
14
- builder: (yargs) => {
15
- return yargs
16
- .option("verbose", {
17
- alias: "v",
18
- type: "boolean",
19
- describe: "Enable verbose output",
20
- default: false
21
- })
22
- .command(bizCommand)
23
- .command(devCommand)
24
- .command(opsCommand)
25
- .command(prdCommand)
26
- .command(prjCommand)
27
- .demandCommand(1, "You need to specify an agent subcommand");
28
- },
29
- handler: (argv) => {
30
- /* this handler is not called when sub-commands are used */
31
- if (argv.debug)
32
- console.log("DEBUG: agent command (no subcommand)");
33
- }
6
+ import registerBizCommand from "./ase-agent-biz.js";
7
+ import registerDevCommand from "./ase-agent-dev.js";
8
+ import registerOpsCommand from "./ase-agent-ops.js";
9
+ import registerPrdCommand from "./ase-agent-prd.js";
10
+ import registerPrjCommand from "./ase-agent-prj.js";
11
+ /* register agent command on the given program */
12
+ const registerAgentCommand = (program) => {
13
+ const agent = program
14
+ .command("agent")
15
+ .description("Execute agent operations")
16
+ .option("-v, --verbose", "Enable verbose output", false);
17
+ /* register all agent sub-commands */
18
+ registerBizCommand(agent);
19
+ registerDevCommand(agent);
20
+ registerOpsCommand(agent);
21
+ registerPrdCommand(agent);
22
+ registerPrjCommand(agent);
23
+ return agent;
34
24
  };
35
- export default agentCommand;
25
+ export default registerAgentCommand;
package/dst/ase-config.js CHANGED
@@ -3,59 +3,186 @@
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
- });
19
- },
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 */
29
- const list = (node, prefix) => {
30
- if (isMap(node))
31
- for (const item of node.items) {
32
- const key = prefix ? `${prefix}.${item.key}` : String(item.key);
33
- if (isMap(item.value))
34
- list(item.value, key);
35
- else
36
- console.log(`${key} = ${isScalar(item.value) ? item.value.value : item.value}`);
37
- }
38
- };
39
- list(doc.contents, "");
8
+ import { Document, parseDocument, isMap, isScalar } from "yaml";
9
+ import { execaSync } from "execa";
10
+ import * as v from "valibot";
11
+ import Table from "cli-table3";
12
+ /* schema for ".ase/config.yaml" */
13
+ export const configSchema = v.nullish(v.strictObject({
14
+ project: v.optional(v.strictObject({
15
+ id: v.optional(v.pipe(v.string(), v.minLength(1)))
16
+ }))
17
+ }));
18
+ /* encapsulate read/write access to a project-local ".ase/<name>.yaml" file */
19
+ export class Config {
20
+ filename;
21
+ doc;
22
+ schema;
23
+ constructor(name, schema) {
24
+ 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);
27
+ this.doc = new Document();
28
+ this.schema = schema ?? null;
29
+ }
30
+ /* upward-walk on filesystem for a file path relative to a start directory */
31
+ findUpward(start, rel) {
32
+ let dir = start;
33
+ for (;;) {
34
+ const candidate = path.join(dir, rel);
35
+ if (fs.existsSync(candidate))
36
+ return candidate;
37
+ const parent = path.dirname(dir);
38
+ if (parent === dir)
39
+ return null;
40
+ dir = parent;
41
+ }
42
+ }
43
+ /* determine the Git top-level directory, if inside a Git repository */
44
+ gitToplevel() {
45
+ try {
46
+ const result = execaSync("git", ["rev-parse", "--show-toplevel"], {
47
+ stderr: "ignore"
48
+ });
49
+ return result.stdout.trim() || null;
40
50
  }
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({}));
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /* read configuration file into memory */
56
+ read() {
57
+ const text = fs.existsSync(this.filename) ? fs.readFileSync(this.filename, "utf8") : "";
58
+ this.doc = parseDocument(text);
59
+ this.validate("lenient");
60
+ }
61
+ /* write in-memory configuration back to file */
62
+ write() {
63
+ this.validate("strict");
64
+ fs.mkdirSync(path.dirname(this.filename), { recursive: true });
65
+ fs.writeFileSync(this.filename, this.doc.toString({ indent: 4 }), "utf8");
66
+ }
67
+ /* validate in-memory configuration against the optional schema */
68
+ validate(mode = "strict") {
69
+ if (this.schema === null)
70
+ return;
71
+ for (;;) {
72
+ const result = v.safeParse(this.schema, this.doc.toJS());
73
+ if (result.success)
74
+ return;
75
+ if (mode === "strict") {
76
+ const issues = result.issues.map((i) => {
77
+ const dotPath = (i.path ?? []).map((p) => String(p.key)).join(".");
78
+ return dotPath ? `${dotPath}: ${i.message}` : i.message;
79
+ }).join("; ");
80
+ throw new Error(`invalid configuration in ${this.filename}: ${issues}`);
81
+ }
82
+ let progressed = false;
83
+ for (const i of result.issues) {
84
+ const segs = (i.path ?? []).map((p) => String(p.key));
85
+ const dotPath = segs.join(".");
86
+ process.stderr.write(`ase: warning: invalid entry in ${this.filename}: ${dotPath ? `${dotPath}: ` : ""}${i.message}\n`);
87
+ if (segs.length > 0) {
88
+ this.doc.deleteIn(segs);
89
+ progressed = true;
90
+ }
51
91
  }
52
- doc.setIn(segments, value);
53
- fs.writeFileSync(filename, doc.toString(), "utf8");
92
+ if (!progressed)
93
+ return;
54
94
  }
55
- else {
56
- const value = doc.getIn(query.split("."));
57
- console.log(value);
95
+ }
96
+ /* retrieve a value at a dotted key, or the root contents if no key given */
97
+ get(key) {
98
+ if (key === undefined)
99
+ return this.doc.contents;
100
+ return this.doc.getIn(key.split("."));
101
+ }
102
+ /* set a value at a dotted key, creating intermediate maps as needed */
103
+ set(key, value) {
104
+ const segments = key.split(".");
105
+ for (let i = 1; i < segments.length; i++) {
106
+ 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({}));
58
110
  }
111
+ this.doc.setIn(segments, value);
112
+ this.validate("strict");
113
+ }
114
+ /* delete a value at a dotted key */
115
+ delete(key) {
116
+ this.doc.deleteIn(key.split("."));
59
117
  }
118
+ }
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)]);
169
+ }
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
+ });
60
187
  };
61
- export default configCommand;
188
+ export default registerConfigCommand;
@@ -7,9 +7,10 @@ import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  import net from "node:net";
9
9
  import { spawn } from "node:child_process";
10
- import { parseDocument } from "yaml";
11
10
  import Hapi from "@hapi/hapi";
12
11
  import axios from "axios";
12
+ import * as v from "valibot";
13
+ import { Config, configSchema } from "./ase-config.js";
13
14
  const SERVE_ENV = "ASE_SERVICE_SERVE";
14
15
  const HOST = "127.0.0.1";
15
16
  const IDLE_MS = 30 * 60 * 1000;
@@ -17,56 +18,30 @@ const TICK_MS = 60 * 1000;
17
18
  const PORT_MIN = 42000;
18
19
  const PORT_MAX = 44000;
19
20
  const PORT_TRIES = 20;
20
- /* upward-walk on filesystem for a file path relative to a start directory */
21
- const findUpward = (start, rel) => {
22
- let dir = start;
23
- for (;;) {
24
- const candidate = path.join(dir, rel);
25
- if (fs.existsSync(candidate))
26
- return candidate;
27
- const parent = path.dirname(dir);
28
- if (parent === dir)
29
- return null;
30
- dir = parent;
31
- }
32
- };
21
+ /* schema for ".ase/service.yaml" */
22
+ const serviceSchema = v.nullish(v.strictObject({
23
+ port: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1024), v.maxValue(65535)))
24
+ }));
33
25
  /* load optional ".ase/config.yaml" and ".ase/service.yaml" files */
34
26
  const loadContext = () => {
35
- /* find files */
36
- const cfgPath = findUpward(process.cwd(), ".ase/config.yaml");
37
- const svcPath = findUpward(process.cwd(), ".ase/service.yaml");
27
+ /* load files */
28
+ const cfg = new Config("config", configSchema);
29
+ cfg.read();
30
+ const svc = new Config("service", serviceSchema);
31
+ svc.read();
38
32
  /* determine project id */
39
- let projectId;
40
- if (cfgPath !== null) {
41
- const doc = parseDocument(fs.readFileSync(cfgPath, "utf8"));
42
- projectId = doc.get("project-id");
43
- }
44
- if (projectId === undefined || projectId === null)
45
- projectId = path.basename(process.cwd());
46
- if (typeof projectId !== "string" || projectId.length === 0)
47
- throw new Error(`invalid "project-id" in ${cfgPath ?? "<cwd basename>"}`);
33
+ const rawId = cfg.get("project.id");
34
+ const projectId = (rawId === undefined || rawId === null) ? path.basename(process.cwd()) : rawId;
48
35
  /* determine service port */
49
- let port = null;
50
- if (svcPath !== null) {
51
- const doc = parseDocument(fs.readFileSync(svcPath, "utf8"));
52
- const raw = doc.get("port");
53
- if (raw !== undefined && raw !== null) {
54
- if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1024 || raw > 65535)
55
- throw new Error(`invalid "port" in ${svcPath} (expected integer 1024..65535)`);
56
- port = raw;
57
- }
58
- }
36
+ const rawPort = svc.get("port");
37
+ const port = (rawPort === undefined || rawPort === null) ? null : rawPort;
59
38
  /* determine path to ".ase" directory */
60
- const aseDir = cfgPath !== null ? path.dirname(cfgPath) :
61
- svcPath !== null ? path.dirname(svcPath) :
62
- path.join(process.cwd(), ".ase");
63
- /* determine path to final ".ase/service.yaml" file */
64
- const finalSvc = svcPath !== null ? svcPath : path.join(aseDir, "service.yaml");
39
+ const aseDir = path.dirname(svc.filename);
65
40
  /* return context information */
66
41
  return {
67
42
  projectId,
68
43
  port,
69
- svcPath: finalSvc,
44
+ svc,
70
45
  aseDir
71
46
  };
72
47
  };
@@ -93,12 +68,9 @@ const allocatePort = async () => {
93
68
  throw new Error(`failed to allocate a port in ${PORT_MIN}..${PORT_MAX} after ${PORT_TRIES} attempts`);
94
69
  };
95
70
  /* persist an allocated port into ".ase/service.yaml" */
96
- const persistPort = (svcPath, port) => {
97
- fs.mkdirSync(path.dirname(svcPath), { recursive: true });
98
- const text = fs.existsSync(svcPath) ? fs.readFileSync(svcPath, "utf8") : "";
99
- const doc = parseDocument(text);
100
- doc.set("port", port);
101
- fs.writeFileSync(svcPath, doc.toString(), "utf8");
71
+ const persistPort = (svc, port) => {
72
+ svc.set("port", port);
73
+ svc.write();
102
74
  };
103
75
  /* distinguish ECONNREFUSED from other Axios transport errors */
104
76
  const isConnRefused = (err) => {
@@ -214,7 +186,7 @@ const doStart = async () => {
214
186
  let port = ctx.port;
215
187
  if (port === null) {
216
188
  port = await allocatePort();
217
- persistPort(ctx.svcPath, port);
189
+ persistPort(ctx.svc, port);
218
190
  }
219
191
  if (process.env[SERVE_ENV] === "1") {
220
192
  await runService({ ...ctx, port });
@@ -227,16 +199,20 @@ const doStart = async () => {
227
199
  for (let i = 0; i < 50; i++) {
228
200
  await new Promise((resolve) => setTimeout(resolve, 100));
229
201
  const s = await probe(port);
230
- if (s !== null && s >= 200 && s < 300)
202
+ if (s !== null && s >= 200 && s < 300) {
203
+ process.stdout.write(`ase: service: started on port ${port}\n`);
231
204
  return 0;
205
+ }
232
206
  }
233
207
  throw new Error("service failed to start within timeout");
234
208
  };
235
209
  /* stop flow: no-op if no port configured or connection refused */
236
210
  const doStop = async () => {
237
211
  const ctx = loadContext();
238
- if (ctx.port === null)
212
+ if (ctx.port === null) {
213
+ process.stdout.write("ase: service: not running (no port configured)\n");
239
214
  return 0;
215
+ }
240
216
  try {
241
217
  const r = await axios.request({
242
218
  method: "GET",
@@ -247,17 +223,23 @@ const doStop = async () => {
247
223
  return r.status >= 200 && r.status < 300 ? 0 : 1;
248
224
  }
249
225
  catch (err) {
250
- if (isConnRefused(err))
226
+ if (isConnRefused(err)) {
227
+ process.stdout.write(`ase: service: not running (port ${ctx.port} not responding)\n`);
251
228
  return 0;
229
+ }
252
230
  throw err;
253
231
  }
254
232
  };
255
233
  /* passthrough flow: POST /command with the arbitrary cmd token */
256
234
  const doPassthrough = async (cmd) => {
257
- const ctx = loadContext();
258
- if (ctx.port === null)
259
- throw new Error("service not running (no port configured)");
260
- try {
235
+ let ctx = loadContext();
236
+ if (ctx.port === null) {
237
+ await doStart();
238
+ ctx = loadContext();
239
+ if (ctx.port === null)
240
+ throw new Error("service not running (no port configured after auto-start)");
241
+ }
242
+ const send = async () => {
261
243
  const r = await axios.request({
262
244
  method: "POST",
263
245
  url: `http://${HOST}:${ctx.port}/command`,
@@ -273,37 +255,48 @@ const doPassthrough = async (cmd) => {
273
255
  if (!body.endsWith("\n"))
274
256
  process.stdout.write("\n");
275
257
  return r.status >= 200 && r.status < 300 ? 0 : 1;
258
+ };
259
+ try {
260
+ return await send();
276
261
  }
277
262
  catch (err) {
278
- if (isConnRefused(err))
279
- throw new Error("service not running (connection refused)");
263
+ if (isConnRefused(err)) {
264
+ await doStart();
265
+ return await send();
266
+ }
280
267
  throw err;
281
268
  }
282
269
  };
283
- /* command-line handling */
284
- const serviceCommand = {
285
- command: "service",
286
- describe: "Manage per-project background HTTP service",
287
- builder: (yargs) => {
288
- return yargs
289
- .command("start", "Start the background service", {}, async () => {
290
- process.exit(await doStart());
291
- })
292
- .command("stop", "Stop the background service", {}, async () => {
293
- process.exit(await doStop());
294
- })
295
- .command("$0 <cmd>", "Send an arbitrary command to the service", (y) => {
296
- return y.positional("cmd", {
297
- type: "string",
298
- describe: "Command token to dispatch"
299
- });
300
- }, async (argv) => {
301
- process.exit(await doPassthrough(String(argv.cmd)));
302
- })
303
- .demandCommand(1, "You need to specify a service subcommand");
304
- },
305
- handler: () => {
306
- /* dispatched by nested subcommands */
307
- }
270
+ /* register CLI command "ase service" */
271
+ const registerServiceCommand = (program) => {
272
+ const service = program
273
+ .command("service")
274
+ .description("Manage per-project background HTTP service")
275
+ .action(() => {
276
+ service.outputHelp();
277
+ process.exit(1);
278
+ });
279
+ /* register CLI sub-command "ase service start" */
280
+ service
281
+ .command("start")
282
+ .description("Start the background service")
283
+ .action(async () => {
284
+ process.exit(await doStart());
285
+ });
286
+ /* register CLI sub-command "ase service stop" */
287
+ service
288
+ .command("stop")
289
+ .description("Stop the background service")
290
+ .action(async () => {
291
+ process.exit(await doStop());
292
+ });
293
+ /* register CLI sub-command "ase service send" */
294
+ service
295
+ .command("send")
296
+ .description("Send a command to the background service")
297
+ .argument("<cmd>", "Command token to dispatch to the service")
298
+ .action(async (cmd) => {
299
+ process.exit(await doPassthrough(cmd));
300
+ });
308
301
  };
309
- export default serviceCommand;
302
+ export default registerServiceCommand;
package/dst/ase-setup.js CHANGED
@@ -3,17 +3,16 @@
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
- const setupCommand = {
7
- command: "setup",
8
- describe: "Setup ASE",
9
- builder: (yargs) => {
10
- return yargs;
11
- },
12
- handler: (argv) => {
13
- if (argv.debug)
6
+ const registerSetupCommand = (program) => {
7
+ program
8
+ .command("setup")
9
+ .description("Setup ASE")
10
+ .action((_opts, cmd) => {
11
+ const debug = Boolean(cmd.optsWithGlobals().debug);
12
+ if (debug)
14
13
  console.log("DEBUG: setup command");
15
14
  console.log("Setup ASE...");
16
15
  /* TODO: implement setup logic */
17
- }
16
+ });
18
17
  };
19
- export default setupCommand;
18
+ export default registerSetupCommand;
package/dst/ase.1 CHANGED
@@ -3,23 +3,59 @@
3
3
  \fBase\fR - Agentic Software Engineering (ASE)
4
4
  .SH "SYNOPSIS"
5
5
  .P
6
- \fBase\fR \[lB]\fB-h\fR|\fB--help\fR\[rB] \[lB]\fB-V\fR|\fB--version\fR\[rB] \[lB]\fIcommand\fR \[lB]\fIoptions\fR \[lB]...\[rB]\[rB]\[rB] \[lB]\fIargs\fR \[lB]...\[rB]\[rB]\[rB]
6
+ \fBase\fR \[lB]\fB-h\fR|\fB--help\fR\[rB] \[lB]\fB-V\fR|\fB--version\fR\[rB] \[lB]\fB-d\fR|\fB--debug\fR\[rB] \[lB]\fIcommand\fR \[lB]\fIoptions\fR \[lB]...\[rB]\[rB] \[lB]\fIargs\fR \[lB]...\[rB]\[rB]\[rB]
7
7
  .SH "DESCRIPTION"
8
8
  .P
9
- \fBase\fR, \fIAgentic Software Engineeing (ASE)\fR, is a small command-line tool...
9
+ \fBase\fR, \fIAgentic Software Engineering (ASE)\fR, is the command-line companion tool to the \fIASE\fR Claude Code plugin. It provides project-level configuration management and a small per-project background HTTP service for dispatching commands.
10
10
  .SH "OPTIONS"
11
11
  .P
12
- The following command-line options and arguments exist:
12
+ The following top-level command-line options exist:
13
13
  .RS 0
14
14
  .IP \(bu 4
15
15
  \[lB]\fB-h\fR|\fB--help\fR\[rB]: Show program usage information only.
16
16
  .IP \(bu 4
17
17
  \[lB]\fB-V\fR|\fB--version\fR\[rB]: Show program version information only.
18
+ .IP \(bu 4
19
+ \[lB]\fB-d\fR|\fB--debug\fR\[rB]: Enable debug output. The flag is inherited by all subcommands and can be inspected inside handlers via \fBcmd.optsWithGlobals()\fR.
20
+ .RE 0
21
+
22
+ .SH "COMMANDS"
23
+ .P
24
+ The following top-level commands exist:
25
+ .RS 0
26
+ .IP \(bu 4
27
+ \fBase config\fR: Manage \fIASE\fR configuration stored in \fB.ase/config.yaml\fR. Without a subcommand, prints usage information. The file is validated against a schema: on read, unknown or invalid entries are warned about and silently dropped from the in-memory view; on set/write, they cause a fatal error.
28
+ .IP \(bu 4
29
+ \fBase config get\fR \fIkey\fR: Print the value at the given dotted \fIkey\fR. Fails with an error if \fIkey\fR does not resolve to a leaf value.
30
+ .IP \(bu 4
31
+ \fBase config set\fR \fIkey\fR \fIvalue\fR: Set the value at the given dotted \fIkey\fR (creating intermediate maps as needed) and persist the file.
32
+ .IP \(bu 4
33
+ \fBase config list\fR: List all configured values as flat dotted keys, rendered as a two-column table of \fBkey\fR and \fBvalue\fR.
34
+ .IP \(bu 4
35
+ \fBase config edit\fR: Open \fB.ase/config.yaml\fR in the editor defined by the \fB$EDITOR\fR or \fB$VISUAL\fR environment variable (falling back to \fBvi\fR). The file and its parent directory are created if missing. After the editor exits, the file is re-read and schema warnings are reported.
36
+ .IP \(bu 4
37
+ \fBase service\fR: Manage the per-project background HTTP service. The service is bound to \fB127.0.0.1\fR on a port persisted in \fB.ase/service.yaml\fR and stops itself after 30 minutes of idle time. Without a subcommand, the help text is shown.
38
+ .IP \(bu 4
39
+ \fBase service start\fR: Start the background service (detached). Allocates a random port in the range \fB42000\fR..\fB44000\fR if none is persisted yet, writes it to \fB.ase/service.yaml\fR, and probes readiness. Exits silently with status 0 if the service is already running; prints \fBase: service: started on port <port>\fR on a fresh start.
40
+ .IP \(bu 4
41
+ \fBase service stop\fR: Stop the background service via HTTP \fBGET /stop\fR. Exits silently with status 0 on successful stop. If no port is configured or the port is not responding, prints an informational message and exits with status 0.
42
+ .IP \(bu 4
43
+ \fBase service send\fR \fIcmd\fR: Dispatch the \fIcmd\fR token as a passthrough command to the running service via HTTP \fBPOST /command\fR; if the service is not running, it is auto-started first.
44
+ .RE 0
45
+
46
+ .SH "FILES"
47
+ .RS 0
48
+ .IP \(bu 4
49
+ \fB.ase/config.yaml\fR: Per-project \fIASE\fR configuration. Read upward from the current working directory. Recognized key: \fBproject.id\fR (non-empty string).
50
+ .IP \(bu 4
51
+ \fB.ase/service.yaml\fR: Per-project service state. Recognized key: \fBport\fR (integer in \fB1024\fR..\fB65535\fR).
52
+ .IP \(bu 4
53
+ \fB.ase/service.log\fR: Stdout/stderr log of the detached background service.
18
54
  .RE 0
19
55
 
20
56
  .SH "HISTORY"
21
57
  .P
22
- \fBase\fR was started to be developed in October 2025...
58
+ \fBase\fR was started to be developed in October 2025.
23
59
  .SH "AUTHOR"
24
60
  .P
25
61
  Dr. Ralf S. Engelschall \fI\(larse@engelschall.com\(ra\fR
package/dst/ase.js CHANGED
@@ -4,39 +4,35 @@
4
4
  ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
5
5
  ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
6
6
  */
7
- import yargs from "yargs";
8
- import { hideBin } from "yargs/helpers";
9
- import configCommand from "./ase-config.js";
10
- import setupCommand from "./ase-setup.js";
11
- import serviceCommand from "./ase-service.js";
7
+ import { Command, CommanderError } from "commander";
8
+ import registerConfigCommand from "./ase-config.js";
9
+ import registerServiceCommand from "./ase-service.js";
12
10
  /* parse CLI arguments */
13
11
  try {
14
- await yargs(hideBin(process.argv))
15
- .scriptName("ase")
16
- .usage("Usage: $0 <command> [options]")
17
- .option("debug", {
18
- alias: "d",
19
- type: "boolean",
20
- describe: "Enable debug output",
21
- default: false
22
- })
23
- .command(configCommand)
24
- .command(setupCommand)
25
- .command(serviceCommand)
26
- .demandCommand(1, "You need to specify a command")
27
- .fail((msg, err, yargs) => {
28
- if (err)
29
- throw err;
30
- yargs.showHelp();
31
- process.stderr.write(`\nase: ${msg}\n`);
32
- process.exit(1);
33
- })
34
- .help()
35
- .version()
36
- .strict()
37
- .parseAsync();
12
+ /* establish top-level program */
13
+ const program = new Command();
14
+ program
15
+ .name("ase")
16
+ .usage("<command> [options]")
17
+ .option("-d, --debug", "enable debug output", false)
18
+ .showHelpAfterError()
19
+ .enablePositionalOptions()
20
+ .exitOverride();
21
+ /* register top-level commands */
22
+ registerConfigCommand(program);
23
+ registerServiceCommand(program);
24
+ /* parse program arguments */
25
+ await program.parseAsync(process.argv);
26
+ /* gracefully terminate */
27
+ process.exit(0);
38
28
  }
39
29
  catch (err) {
30
+ if (err instanceof CommanderError) {
31
+ if (err.exitCode !== 0)
32
+ process.exit(err.exitCode);
33
+ else
34
+ process.exit(0);
35
+ }
40
36
  const message = err instanceof Error ? err.message : String(err);
41
37
  process.stderr.write(`ase: ERROR: ${message}\n`);
42
38
  process.exit(1);
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.0.6",
9
+ "version": "0.0.7",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -34,16 +34,17 @@
34
34
  "remark": "15.0.1",
35
35
  "remark-man": "9.0.0",
36
36
 
37
- "@types/node": "25.6.0",
38
- "@types/yargs": "17.0.35"
37
+ "@types/node": "25.6.0"
39
38
  },
40
39
  "dependencies": {
41
- "yargs": "18.0.0",
40
+ "commander": "14.0.3",
42
41
  "yaml": "2.8.3",
42
+ "valibot": "1.3.1",
43
43
  "execa": "9.6.1",
44
44
  "mkdirp": "3.0.1",
45
45
  "@hapi/hapi": "21.4.8",
46
- "axios": "1.15.0"
46
+ "axios": "1.15.0",
47
+ "cli-table3": "0.6.5"
47
48
  },
48
49
  "engines": {
49
50
  "npm": ">=10.0.0",