@rse/ase 0.0.5 → 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,34 +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
- const configCommand = {
7
- command: "config [assignment]",
8
- describe: "Manage ASE configuration",
9
- builder: (yargs) => {
10
- return yargs
11
- .positional("assignment", {
12
- type: "string",
13
- describe: "Configuration assignment (key=value) or key to query"
14
- });
15
- },
16
- handler: (argv) => {
17
- if (argv.debug)
18
- console.log("DEBUG: config command", argv);
19
- const assignment = argv.assignment;
20
- if (!assignment) {
21
- console.log("Listing all configuration...");
22
- /* TODO: implement configuration listing logic */
6
+ import path from "node:path";
7
+ import fs from "node:fs";
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;
23
50
  }
24
- else if (assignment.includes("=")) {
25
- const [key, ...valueParts] = assignment.split("=");
26
- const value = valueParts.join("=");
27
- console.log(`Setting configuration: ${key} = ${value}`);
28
- /* TODO: implement configuration storage logic */
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
+ }
91
+ }
92
+ if (!progressed)
93
+ return;
29
94
  }
30
- else {
31
- console.log(`Getting configuration: ${assignment}`);
32
- /* TODO: implement configuration retrieval logic */
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({}));
33
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("."));
34
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
+ });
35
187
  };
36
- export default configCommand;
188
+ export default registerConfigCommand;
@@ -0,0 +1,78 @@
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 path from "node:path";
7
+ import fs from "node:fs";
8
+ import { execa } from "execa";
9
+ import { mkdirp } from "mkdirp";
10
+ const configCommand = {
11
+ command: "meta-plan <subcommand>",
12
+ describe: "Manage plans",
13
+ builder: (yargs) => {
14
+ return yargs
15
+ .command({
16
+ command: "init",
17
+ describe: "ensure plan directory exists",
18
+ handler: async () => {
19
+ const { stdout: root } = await execa("git", ["rev-parse", "--show-toplevel"]);
20
+ const planDir = `${root}/.plan/`;
21
+ if (!fs.existsSync(planDir))
22
+ await mkdirp(planDir);
23
+ }
24
+ })
25
+ .command({
26
+ command: "load <id>",
27
+ describe: "load a plan",
28
+ builder: (yargs) => yargs.positional("id", {
29
+ type: "string",
30
+ describe: "plan identifier"
31
+ }),
32
+ handler: async (argv) => {
33
+ const { stdout: root } = await execa("git", ["rev-parse", "--show-toplevel"]);
34
+ const planDir = `${root}/.plan/`;
35
+ const planFile = path.join(planDir, `${argv.id}.md`);
36
+ const text = fs.existsSync(planFile) ? fs.readFileSync(planFile, "utf8") : "";
37
+ process.stdout.write(text);
38
+ }
39
+ })
40
+ .command({
41
+ command: "save <id>",
42
+ describe: "save a plan",
43
+ builder: (yargs) => yargs.positional("id", {
44
+ type: "string",
45
+ describe: "plan identifier"
46
+ }),
47
+ handler: async (argv) => {
48
+ const { stdout: root } = await execa("git", ["rev-parse", "--show-toplevel"]);
49
+ const planDir = `${root}/.plan/`;
50
+ if (!fs.existsSync(planDir))
51
+ await mkdirp(planDir);
52
+ const planFile = path.join(planDir, `${argv.id}.md`);
53
+ const text = fs.readFileSync(0, "utf8");
54
+ fs.writeFileSync(planFile, text);
55
+ }
56
+ })
57
+ .command({
58
+ command: "edit <id>",
59
+ describe: "edit a plan interactively with $EDITOR",
60
+ builder: (yargs) => yargs.positional("id", {
61
+ type: "string",
62
+ describe: "plan identifier"
63
+ }),
64
+ handler: async (argv) => {
65
+ const { stdout: root } = await execa("git", ["rev-parse", "--show-toplevel"]);
66
+ const planDir = `${root}/.plan/`;
67
+ const planFile = path.join(planDir, `${argv.id}.md`);
68
+ if (!fs.existsSync(planDir))
69
+ await mkdirp(planDir);
70
+ const editor = process.env.EDITOR ?? "vi";
71
+ await execa(editor, [planFile], { stdio: "inherit" });
72
+ }
73
+ })
74
+ .demandCommand(1, "You need to specify a sub-command");
75
+ },
76
+ handler: () => { }
77
+ };
78
+ export default configCommand;
@@ -0,0 +1,82 @@
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 path from "node:path";
7
+ import fs from "node:fs";
8
+ import { execa } from "execa";
9
+ import { mkdirp } from "mkdirp";
10
+ const planCommand = {
11
+ command: "plan <subcommand>",
12
+ describe: "Manage plans",
13
+ builder: (yargs) => {
14
+ return yargs
15
+ .command({
16
+ command: "ensure",
17
+ describe: "ensure plan directory exists",
18
+ handler: async () => {
19
+ const { stdout: root } = await execa("git", ["rev-parse", "--show-toplevel"]);
20
+ const planDir = `${root}/.ase/plan/`;
21
+ if (!fs.existsSync(planDir))
22
+ await mkdirp(planDir);
23
+ process.stdout.write(planDir);
24
+ }
25
+ })
26
+ .command({
27
+ command: "load <id>",
28
+ describe: "load a plan",
29
+ builder: (yargs) => yargs.positional("id", {
30
+ type: "string",
31
+ describe: "plan identifier"
32
+ }),
33
+ handler: async (argv) => {
34
+ const { stdout: root } = await execa("git", ["rev-parse", "--show-toplevel"]);
35
+ const planDir = `${root}/.ase/plan/`;
36
+ const planFile = path.join(planDir, `${argv.id}.md`);
37
+ const text = fs.existsSync(planFile) ? fs.readFileSync(planFile, "utf8") : "";
38
+ process.stdout.write(text);
39
+ }
40
+ })
41
+ .command({
42
+ command: "save <id>",
43
+ describe: "save a plan",
44
+ builder: (yargs) => yargs.positional("id", {
45
+ type: "string",
46
+ describe: "plan identifier"
47
+ }),
48
+ handler: async (argv) => {
49
+ const { stdout: root } = await execa("git", ["rev-parse", "--show-toplevel"]);
50
+ const planDir = `${root}/.ase/plan/`;
51
+ if (!fs.existsSync(planDir))
52
+ await mkdirp(planDir);
53
+ const planFile = path.join(planDir, `${argv.id}.md`);
54
+ const chunks = [];
55
+ for await (const chunk of process.stdin)
56
+ chunks.push(chunk);
57
+ const text = Buffer.concat(chunks).toString("utf8");
58
+ fs.writeFileSync(planFile, text);
59
+ }
60
+ })
61
+ .command({
62
+ command: "edit <id>",
63
+ describe: "edit a plan interactively with $EDITOR",
64
+ builder: (yargs) => yargs.positional("id", {
65
+ type: "string",
66
+ describe: "plan identifier"
67
+ }),
68
+ handler: async (argv) => {
69
+ const { stdout: root } = await execa("git", ["rev-parse", "--show-toplevel"]);
70
+ const planDir = `${root}/.ase/plan/`;
71
+ const planFile = path.join(planDir, `${argv.id}.md`);
72
+ if (!fs.existsSync(planDir))
73
+ await mkdirp(planDir);
74
+ const editor = process.env.EDITOR ?? "vi";
75
+ await execa(editor, [planFile], { stdio: "inherit" });
76
+ }
77
+ })
78
+ .demandCommand(1, "You need to specify a sub-command");
79
+ },
80
+ handler: () => { }
81
+ };
82
+ export default planCommand;
@@ -0,0 +1,302 @@
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 path from "node:path";
7
+ import fs from "node:fs";
8
+ import net from "node:net";
9
+ import { spawn } from "node:child_process";
10
+ import Hapi from "@hapi/hapi";
11
+ import axios from "axios";
12
+ import * as v from "valibot";
13
+ import { Config, configSchema } from "./ase-config.js";
14
+ const SERVE_ENV = "ASE_SERVICE_SERVE";
15
+ const HOST = "127.0.0.1";
16
+ const IDLE_MS = 30 * 60 * 1000;
17
+ const TICK_MS = 60 * 1000;
18
+ const PORT_MIN = 42000;
19
+ const PORT_MAX = 44000;
20
+ const PORT_TRIES = 20;
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
+ }));
25
+ /* load optional ".ase/config.yaml" and ".ase/service.yaml" files */
26
+ const loadContext = () => {
27
+ /* load files */
28
+ const cfg = new Config("config", configSchema);
29
+ cfg.read();
30
+ const svc = new Config("service", serviceSchema);
31
+ svc.read();
32
+ /* determine project id */
33
+ const rawId = cfg.get("project.id");
34
+ const projectId = (rawId === undefined || rawId === null) ? path.basename(process.cwd()) : rawId;
35
+ /* determine service port */
36
+ const rawPort = svc.get("port");
37
+ const port = (rawPort === undefined || rawPort === null) ? null : rawPort;
38
+ /* determine path to ".ase" directory */
39
+ const aseDir = path.dirname(svc.filename);
40
+ /* return context information */
41
+ return {
42
+ projectId,
43
+ port,
44
+ svc,
45
+ aseDir
46
+ };
47
+ };
48
+ /* try binding a single candidate port to verify availability */
49
+ const tryBind = (port) => {
50
+ return new Promise((resolve) => {
51
+ const s = net.createServer();
52
+ s.once("error", () => {
53
+ resolve(false);
54
+ });
55
+ s.once("listening", () => {
56
+ s.close(() => resolve(true));
57
+ });
58
+ s.listen(port, HOST);
59
+ });
60
+ };
61
+ /* allocate a fresh random port in PORT_MIN..PORT_MAX */
62
+ const allocatePort = async () => {
63
+ for (let i = 0; i < PORT_TRIES; i++) {
64
+ const p = PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN + 1));
65
+ if (await tryBind(p))
66
+ return p;
67
+ }
68
+ throw new Error(`failed to allocate a port in ${PORT_MIN}..${PORT_MAX} after ${PORT_TRIES} attempts`);
69
+ };
70
+ /* persist an allocated port into ".ase/service.yaml" */
71
+ const persistPort = (svc, port) => {
72
+ svc.set("port", port);
73
+ svc.write();
74
+ };
75
+ /* distinguish ECONNREFUSED from other Axios transport errors */
76
+ const isConnRefused = (err) => {
77
+ const e = err;
78
+ return e?.code === "ECONNREFUSED" || e?.cause?.code === "ECONNREFUSED";
79
+ };
80
+ /* probe the service */
81
+ const probe = async (port) => {
82
+ try {
83
+ const r = await axios.request({
84
+ method: "OPTIONS",
85
+ url: `http://${HOST}:${port}/`,
86
+ timeout: 2000,
87
+ validateStatus: () => true
88
+ });
89
+ return r.status;
90
+ }
91
+ catch (err) {
92
+ if (isConnRefused(err))
93
+ return null;
94
+ throw err;
95
+ }
96
+ };
97
+ /* service-side: bind HAPI server until "/stop" command is received or idle timeout happens */
98
+ const runService = async (ctx) => {
99
+ /* establish HAPI HTTP/REST service */
100
+ const server = Hapi.server({ host: HOST, port: ctx.port });
101
+ /* track last activity */
102
+ let lastActivity = Date.now();
103
+ server.ext("onRequest", (_request, h) => {
104
+ lastActivity = Date.now();
105
+ return h.continue;
106
+ });
107
+ /* listen to HTTP/REST endpoints */
108
+ server.route({
109
+ method: "OPTIONS",
110
+ path: "/",
111
+ handler: (_request, h) => {
112
+ return h.response().code(204);
113
+ }
114
+ });
115
+ server.route({
116
+ method: "GET",
117
+ path: "/stop",
118
+ handler: (_request, h) => {
119
+ setImmediate(async () => {
120
+ await server.stop({ timeout: 1000 });
121
+ process.exit(0);
122
+ });
123
+ return h.response({ ok: true }).code(200);
124
+ }
125
+ });
126
+ server.route({
127
+ method: "POST",
128
+ path: "/command",
129
+ options: { payload: { parse: true, allow: "application/json" } },
130
+ handler: (request, h) => {
131
+ const payload = request.payload;
132
+ if (!payload || typeof payload.command !== "string")
133
+ return h.response({ error: "missing or invalid 'command' field" }).code(400);
134
+ if (payload.command === "foo") {
135
+ return h.response({
136
+ ok: true,
137
+ projectId: ctx.projectId,
138
+ command: "Hello World" // FIXME
139
+ }).code(200);
140
+ }
141
+ else
142
+ return h.response({ error: "invalid 'command' field" }).code(400);
143
+ }
144
+ });
145
+ /* start service */
146
+ try {
147
+ await server.start();
148
+ }
149
+ catch (err) {
150
+ const e = err;
151
+ if (e.code === "EADDRINUSE") {
152
+ /* race-loser re-probe: another "ase service start" won the race */
153
+ const status = await probe(ctx.port).catch(() => null);
154
+ if (status !== null && status >= 200 && status < 300)
155
+ process.exit(0);
156
+ process.stderr.write(`ase: service: port ${ctx.port} in use, but not responding!\n`);
157
+ process.exit(1);
158
+ }
159
+ process.stderr.write(`ase: service: ${e.message}\n`);
160
+ process.exit(1);
161
+ }
162
+ /* stop service after idle timeout */
163
+ setInterval(() => {
164
+ if (Date.now() - lastActivity > IDLE_MS) {
165
+ server.stop({ timeout: 1000 }).then(() => {
166
+ process.exit(0);
167
+ });
168
+ }
169
+ }, TICK_MS).unref();
170
+ };
171
+ /* spawn the current executable detached as a background service */
172
+ const spawnDetached = (aseDir) => {
173
+ fs.mkdirSync(aseDir, { recursive: true });
174
+ const logFile = path.join(aseDir, "service.log");
175
+ const log = fs.openSync(logFile, "a");
176
+ const child = spawn(process.execPath, [process.argv[1], "service", "start"], {
177
+ detached: true,
178
+ env: { ...process.env, [SERVE_ENV]: "1" },
179
+ stdio: ["ignore", log, log]
180
+ });
181
+ child.unref();
182
+ };
183
+ /* start flow: ensure port, probe, optionally detach */
184
+ const doStart = async () => {
185
+ const ctx = loadContext();
186
+ let port = ctx.port;
187
+ if (port === null) {
188
+ port = await allocatePort();
189
+ persistPort(ctx.svc, port);
190
+ }
191
+ if (process.env[SERVE_ENV] === "1") {
192
+ await runService({ ...ctx, port });
193
+ return await new Promise(() => { });
194
+ }
195
+ const status = await probe(port);
196
+ if (status !== null && status >= 200 && status < 300)
197
+ return 0;
198
+ spawnDetached(ctx.aseDir);
199
+ for (let i = 0; i < 50; i++) {
200
+ await new Promise((resolve) => setTimeout(resolve, 100));
201
+ const s = await probe(port);
202
+ if (s !== null && s >= 200 && s < 300) {
203
+ process.stdout.write(`ase: service: started on port ${port}\n`);
204
+ return 0;
205
+ }
206
+ }
207
+ throw new Error("service failed to start within timeout");
208
+ };
209
+ /* stop flow: no-op if no port configured or connection refused */
210
+ const doStop = async () => {
211
+ const ctx = loadContext();
212
+ if (ctx.port === null) {
213
+ process.stdout.write("ase: service: not running (no port configured)\n");
214
+ return 0;
215
+ }
216
+ try {
217
+ const r = await axios.request({
218
+ method: "GET",
219
+ url: `http://${HOST}:${ctx.port}/stop`,
220
+ timeout: 5000,
221
+ validateStatus: () => true
222
+ });
223
+ return r.status >= 200 && r.status < 300 ? 0 : 1;
224
+ }
225
+ catch (err) {
226
+ if (isConnRefused(err)) {
227
+ process.stdout.write(`ase: service: not running (port ${ctx.port} not responding)\n`);
228
+ return 0;
229
+ }
230
+ throw err;
231
+ }
232
+ };
233
+ /* passthrough flow: POST /command with the arbitrary cmd token */
234
+ const doPassthrough = async (cmd) => {
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 () => {
243
+ const r = await axios.request({
244
+ method: "POST",
245
+ url: `http://${HOST}:${ctx.port}/command`,
246
+ headers: { "Content-Type": "application/json" },
247
+ data: { command: cmd },
248
+ timeout: 0,
249
+ validateStatus: () => true,
250
+ responseType: "text",
251
+ transformResponse: [(x) => x]
252
+ });
253
+ const body = typeof r.data === "string" ? r.data : JSON.stringify(r.data);
254
+ process.stdout.write(body);
255
+ if (!body.endsWith("\n"))
256
+ process.stdout.write("\n");
257
+ return r.status >= 200 && r.status < 300 ? 0 : 1;
258
+ };
259
+ try {
260
+ return await send();
261
+ }
262
+ catch (err) {
263
+ if (isConnRefused(err)) {
264
+ await doStart();
265
+ return await send();
266
+ }
267
+ throw err;
268
+ }
269
+ };
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
+ });
301
+ };
302
+ export default registerServiceCommand;
@@ -0,0 +1,18 @@
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
+ 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)
13
+ console.log("DEBUG: setup command");
14
+ console.log("Setup ASE...");
15
+ /* TODO: implement setup logic */
16
+ });
17
+ };
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,26 +4,36 @@
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 initCommand from "./ase-init.js";
10
- import configCommand from "./ase-config.js";
11
- import agentCommand from "./ase-agent.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
- yargs(hideBin(process.argv))
14
- .scriptName("ase")
15
- .usage("Usage: $0 <command> [options]")
16
- .option("debug", {
17
- alias: "d",
18
- type: "boolean",
19
- describe: "Enable debug output",
20
- default: false
21
- })
22
- .command(initCommand)
23
- .command(configCommand)
24
- .command(agentCommand)
25
- .demandCommand(1, "You need to specify a command")
26
- .help()
27
- .version()
28
- .strict()
29
- .parse();
11
+ try {
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);
28
+ }
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
+ }
36
+ const message = err instanceof Error ? err.message : String(err);
37
+ process.stderr.write(`ase: ERROR: ${message}\n`);
38
+ process.exit(1);
39
+ }
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.5",
9
+ "version": "0.0.7",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -18,27 +18,33 @@
18
18
  "devDependencies": {
19
19
  "eslint": "9.39.4",
20
20
  "@eslint/js": "9.39.4",
21
- "@typescript-eslint/parser": "8.58.0",
22
- "@typescript-eslint/eslint-plugin": "8.58.0",
21
+ "@typescript-eslint/parser": "8.58.2",
22
+ "@typescript-eslint/eslint-plugin": "8.58.2",
23
23
  "eslint-plugin-n": "17.24.0",
24
24
  "eslint-plugin-promise": "7.2.1",
25
25
  "eslint-plugin-import": "2.32.0",
26
26
  "neostandard": "0.13.0",
27
- "globals": "17.4.0",
28
- "typescript": "6.0.2",
27
+ "globals": "17.5.0",
28
+ "typescript": "6.0.3",
29
29
 
30
- "@rse/stx": "1.1.4",
30
+ "@rse/stx": "1.1.5",
31
31
  "nodemon": "3.1.14",
32
32
  "shx": "0.4.0",
33
33
  "remark-cli": "12.0.1",
34
34
  "remark": "15.0.1",
35
35
  "remark-man": "9.0.0",
36
36
 
37
- "@types/node": "25.5.2",
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",
41
+ "yaml": "2.8.3",
42
+ "valibot": "1.3.1",
43
+ "execa": "9.6.1",
44
+ "mkdirp": "3.0.1",
45
+ "@hapi/hapi": "21.4.8",
46
+ "axios": "1.15.0",
47
+ "cli-table3": "0.6.5"
42
48
  },
43
49
  "engines": {
44
50
  "npm": ">=10.0.0",