@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 +2 -2
- package/dst/ase-agent-base.js +16 -25
- package/dst/ase-agent-biz.js +18 -27
- package/dst/ase-agent-dev.js +18 -27
- package/dst/ase-agent-ops.js +18 -27
- package/dst/ase-agent-prd.js +18 -27
- package/dst/ase-agent-prj.js +18 -27
- package/dst/ase-agent.js +19 -29
- package/dst/ase-config.js +178 -26
- package/dst/ase-meta-plan.js +78 -0
- package/dst/ase-plan.js +82 -0
- package/dst/ase-service.js +302 -0
- package/dst/ase-setup.js +18 -0
- package/dst/ase.1 +40 -4
- package/dst/ase.js +32 -22
- package/package.json +15 -9
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
|
|
4
|
-
$ npx @rse/ase config agent.dev.llm.key
|
|
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
|
package/dst/ase-agent-base.js
CHANGED
|
@@ -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 (
|
|
11
|
-
|
|
10
|
+
return (_opts, cmd) => {
|
|
11
|
+
const opts = cmd.optsWithGlobals();
|
|
12
|
+
if (opts.debug)
|
|
12
13
|
console.log(`DEBUG: agent ${category} ${subCommand} command`);
|
|
13
|
-
if (
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
};
|
package/dst/ase-agent-biz.js
CHANGED
|
@@ -12,38 +12,29 @@ const bizSubCommands = [
|
|
|
12
12
|
];
|
|
13
13
|
/* create sub-command handler */
|
|
14
14
|
const createSubCommandHandler = (subCommand) => {
|
|
15
|
-
return (
|
|
16
|
-
|
|
15
|
+
return (_opts, cmd) => {
|
|
16
|
+
const opts = cmd.optsWithGlobals();
|
|
17
|
+
if (opts.debug)
|
|
17
18
|
console.log(`DEBUG: agent biz ${subCommand} command`);
|
|
18
|
-
if (
|
|
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
|
-
/*
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
40
|
+
export default registerBizCommand;
|
package/dst/ase-agent-dev.js
CHANGED
|
@@ -12,38 +12,29 @@ const devSubCommands = [
|
|
|
12
12
|
];
|
|
13
13
|
/* create sub-command handler */
|
|
14
14
|
const createSubCommandHandler = (subCommand) => {
|
|
15
|
-
return (
|
|
16
|
-
|
|
15
|
+
return (_opts, cmd) => {
|
|
16
|
+
const opts = cmd.optsWithGlobals();
|
|
17
|
+
if (opts.debug)
|
|
17
18
|
console.log(`DEBUG: agent dev ${subCommand} command`);
|
|
18
|
-
if (
|
|
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
|
-
/*
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
40
|
+
export default registerDevCommand;
|
package/dst/ase-agent-ops.js
CHANGED
|
@@ -12,38 +12,29 @@ const opsSubCommands = [
|
|
|
12
12
|
];
|
|
13
13
|
/* create sub-command handler */
|
|
14
14
|
const createSubCommandHandler = (subCommand) => {
|
|
15
|
-
return (
|
|
16
|
-
|
|
15
|
+
return (_opts, cmd) => {
|
|
16
|
+
const opts = cmd.optsWithGlobals();
|
|
17
|
+
if (opts.debug)
|
|
17
18
|
console.log(`DEBUG: agent ops ${subCommand} command`);
|
|
18
|
-
if (
|
|
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
|
-
/*
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
40
|
+
export default registerOpsCommand;
|
package/dst/ase-agent-prd.js
CHANGED
|
@@ -12,38 +12,29 @@ const prdSubCommands = [
|
|
|
12
12
|
];
|
|
13
13
|
/* create sub-command handler */
|
|
14
14
|
const createSubCommandHandler = (subCommand) => {
|
|
15
|
-
return (
|
|
16
|
-
|
|
15
|
+
return (_opts, cmd) => {
|
|
16
|
+
const opts = cmd.optsWithGlobals();
|
|
17
|
+
if (opts.debug)
|
|
17
18
|
console.log(`DEBUG: agent prd ${subCommand} command`);
|
|
18
|
-
if (
|
|
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
|
-
/*
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
40
|
+
export default registerPrdCommand;
|
package/dst/ase-agent-prj.js
CHANGED
|
@@ -12,38 +12,29 @@ const prjSubCommands = [
|
|
|
12
12
|
];
|
|
13
13
|
/* create sub-command handler */
|
|
14
14
|
const createSubCommandHandler = (subCommand) => {
|
|
15
|
-
return (
|
|
16
|
-
|
|
15
|
+
return (_opts, cmd) => {
|
|
16
|
+
const opts = cmd.optsWithGlobals();
|
|
17
|
+
if (opts.debug)
|
|
17
18
|
console.log(`DEBUG: agent prj ${subCommand} command`);
|
|
18
|
-
if (
|
|
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
|
-
/*
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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;
|
package/dst/ase-plan.js
ADDED
|
@@ -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;
|
package/dst/ase-setup.js
ADDED
|
@@ -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]
|
|
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
|
|
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
|
|
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
|
|
8
|
-
import
|
|
9
|
-
import
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
.
|
|
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.
|
|
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.
|
|
22
|
-
"@typescript-eslint/eslint-plugin": "8.58.
|
|
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.
|
|
28
|
-
"typescript": "6.0.
|
|
27
|
+
"globals": "17.5.0",
|
|
28
|
+
"typescript": "6.0.3",
|
|
29
29
|
|
|
30
|
-
"@rse/stx": "1.1.
|
|
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.
|
|
38
|
-
"@types/yargs": "17.0.35"
|
|
37
|
+
"@types/node": "25.6.0"
|
|
39
38
|
},
|
|
40
39
|
"dependencies": {
|
|
41
|
-
"
|
|
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",
|