@rse/ase 1.0.7 → 2.0.0

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.
Files changed (279) hide show
  1. package/dst/ase-agent-definition-api.d.ts +184 -0
  2. package/dst/ase-agent-definition-api.js +6 -0
  3. package/dst/ase-agent-definition-builtin/condenser.js +42 -0
  4. package/dst/ase-agent-definition-builtin/editor.js +45 -0
  5. package/dst/ase-agent-definition-builtin/estimator.js +98 -0
  6. package/dst/ase-agent-definition-builtin/implementer.js +121 -0
  7. package/dst/ase-agent-definition-builtin/integrator.js +56 -0
  8. package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
  9. package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
  10. package/dst/ase-agent-definition-builtin/researcher.js +77 -0
  11. package/dst/ase-agent-definition-core.js +827 -0
  12. package/dst/ase-agent-fleet.js +772 -0
  13. package/dst/ase-agent-process.js +232 -0
  14. package/dst/ase-agent-relay-client.js +509 -0
  15. package/dst/ase-agent-relay-core.js +213 -0
  16. package/dst/ase-agent-relay-server-bind.js +242 -0
  17. package/dst/ase-agent-relay-server-cli.js +50 -0
  18. package/dst/ase-agent-runtime-claim.js +189 -0
  19. package/dst/ase-agent-runtime-engine.js +1100 -0
  20. package/dst/ase-agent-runtime-exec.js +275 -0
  21. package/dst/ase-agent-runtime-phase.js +581 -0
  22. package/dst/ase-agent-runtime-request.js +272 -0
  23. package/dst/ase-agent-runtime-tool.js +207 -0
  24. package/dst/ase-agent-runtime.js +1316 -0
  25. package/dst/ase-agent.js +65 -0
  26. package/dst/ase-artifact.js +6 -5
  27. package/dst/ase-config-core.js +465 -0
  28. package/dst/ase-config-schema.js +351 -0
  29. package/dst/ase-config-scope.js +172 -0
  30. package/dst/ase-config.js +25 -585
  31. package/dst/ase-hook.js +228 -111
  32. package/dst/ase-lib-daemon-cli.js +130 -0
  33. package/dst/ase-lib-hash.js +20 -0
  34. package/dst/{ase-log.js → ase-lib-log.js} +40 -7
  35. package/dst/ase-lib-server-bind.js +115 -0
  36. package/dst/ase-lib-server-cli.js +474 -0
  37. package/dst/{ase-table.js → ase-lib-table.js} +1 -1
  38. package/dst/{ase-version.js → ase-lib-version.js} +3 -0
  39. package/dst/ase-man.js +96 -0
  40. package/dst/ase-mcp.js +166 -13
  41. package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
  42. package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
  43. package/dst/ase-service-otel.js +1246 -0
  44. package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
  45. package/dst/ase-service-state.js +101 -0
  46. package/dst/ase-service.js +251 -76
  47. package/dst/ase-setup-common.js +141 -0
  48. package/dst/ase-setup-mcp.js +359 -0
  49. package/dst/ase-setup-settings.js +837 -0
  50. package/dst/ase-setup.js +113 -869
  51. package/dst/ase-spec.js +31 -19
  52. package/dst/ase-statusline.js +108 -43
  53. package/dst/ase-task-board-core.js +739 -0
  54. package/dst/ase-task-board-filter.js +127 -0
  55. package/dst/ase-task-board-flow.js +237 -0
  56. package/dst/ase-task-board-graph.js +497 -0
  57. package/dst/ase-task-board-stats.js +263 -0
  58. package/dst/ase-task-board-tui-control.js +775 -0
  59. package/dst/ase-task-board-tui-model.js +1008 -0
  60. package/dst/ase-task-board-tui-popup.js +626 -0
  61. package/dst/ase-task-board-tui-style.js +106 -0
  62. package/dst/ase-task-board-tui-view.js +847 -0
  63. package/dst/ase-task-board-tui.js +189 -0
  64. package/dst/ase-task-board-web-client.css +1 -0
  65. package/dst/ase-task-board-web-client.html +20 -0
  66. package/dst/ase-task-board-web-client.js +81 -0
  67. package/dst/ase-task-board-web-server.js +823 -0
  68. package/dst/ase-task-board.js +134 -0
  69. package/dst/ase-task-format.js +757 -0
  70. package/dst/ase-task-request.js +208 -0
  71. package/dst/ase-task-store-client.js +495 -0
  72. package/dst/ase-task-store-core.js +804 -0
  73. package/dst/ase-task-store-issues.js +237 -0
  74. package/dst/ase-task-store-plugin-api.js +6 -0
  75. package/dst/ase-task-store-plugin-ase.js +484 -0
  76. package/dst/ase-task-store-plugin-delegate.js +137 -0
  77. package/dst/ase-task-store-plugin-gitea.js +666 -0
  78. package/dst/ase-task-store-plugin-github.js +696 -0
  79. package/dst/ase-task-store-plugin-gitlab.js +668 -0
  80. package/dst/ase-task-store-server-bind.js +538 -0
  81. package/dst/ase-task-store-server-cli.js +154 -0
  82. package/dst/ase-task.js +1335 -357
  83. package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
  84. package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
  85. package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
  86. package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
  87. package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
  88. package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
  89. package/dst/ase-util.js +20 -0
  90. package/dst/ase.js +108 -32
  91. package/dst/man/ase-agent-adhoc.1 +515 -0
  92. package/dst/man/ase-agent-condenser.1 +193 -0
  93. package/dst/man/ase-agent-editor.1 +212 -0
  94. package/dst/man/ase-agent-estimator.1 +214 -0
  95. package/dst/man/ase-agent-fleet.1 +185 -0
  96. package/dst/man/ase-agent-implementer.1 +233 -0
  97. package/dst/man/ase-agent-integrator.1 +213 -0
  98. package/dst/man/ase-agent-qualifier.1 +214 -0
  99. package/dst/man/ase-agent-rebooter.1 +193 -0
  100. package/dst/man/ase-agent-relay.1 +128 -0
  101. package/dst/man/ase-agent-researcher.1 +217 -0
  102. package/dst/man/ase-agent.1 +264 -0
  103. package/dst/man/ase-artifact-list.1 +38 -0
  104. package/dst/man/ase-artifact-name.1 +39 -0
  105. package/dst/man/ase-artifact.1 +56 -0
  106. package/dst/man/ase-config-delete.1 +45 -0
  107. package/dst/man/ase-config-edit.1 +48 -0
  108. package/dst/man/ase-config-get.1 +46 -0
  109. package/dst/man/ase-config-init.1 +60 -0
  110. package/dst/man/ase-config-list.1 +42 -0
  111. package/dst/man/ase-config-set.1 +52 -0
  112. package/dst/man/ase-config.1 +225 -0
  113. package/dst/man/ase-hook-permission-request.1 +36 -0
  114. package/dst/man/ase-hook-pre-tool-use.1 +59 -0
  115. package/dst/man/ase-hook-session-end.1 +47 -0
  116. package/dst/man/ase-hook-session-persona.1 +37 -0
  117. package/dst/man/ase-hook-session-start.1 +81 -0
  118. package/dst/man/ase-hook-stop.1 +34 -0
  119. package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
  120. package/dst/man/ase-hook.1 +51 -0
  121. package/dst/man/ase-man.1 +67 -0
  122. package/dst/man/ase-mcp.1 +61 -0
  123. package/dst/man/ase-service-log.1 +43 -0
  124. package/dst/man/ase-service-send.1 +47 -0
  125. package/dst/man/ase-service-start.1 +54 -0
  126. package/dst/man/ase-service-status.1 +46 -0
  127. package/dst/man/ase-service-stop.1 +37 -0
  128. package/dst/man/ase-service.1 +85 -0
  129. package/dst/man/ase-setup-disable.1 +44 -0
  130. package/dst/man/ase-setup-enable.1 +45 -0
  131. package/dst/man/ase-setup-install.1 +73 -0
  132. package/dst/man/ase-setup-mcp.1 +124 -0
  133. package/dst/man/ase-setup-status.1 +60 -0
  134. package/dst/man/ase-setup-statusline.1 +92 -0
  135. package/dst/man/ase-setup-telemetry.1 +92 -0
  136. package/dst/man/ase-setup-uninstall.1 +59 -0
  137. package/dst/man/ase-setup-update.1 +64 -0
  138. package/dst/man/ase-setup.1 +93 -0
  139. package/dst/man/ase-spec-export.1 +62 -0
  140. package/dst/man/ase-spec-lint.1 +40 -0
  141. package/dst/man/ase-spec-preview.1 +40 -0
  142. package/dst/man/ase-spec.1 +46 -0
  143. package/dst/man/ase-statusline.1 +189 -0
  144. package/dst/man/ase-task-assign.1 +71 -0
  145. package/dst/man/ase-task-board.1 +384 -0
  146. package/dst/man/ase-task-delete.1 +35 -0
  147. package/dst/man/ase-task-edit.1 +46 -0
  148. package/dst/man/ase-task-idscheme.1 +65 -0
  149. package/dst/man/ase-task-lifecycle.1 +57 -0
  150. package/dst/man/ase-task-list.1 +47 -0
  151. package/dst/man/ase-task-load.1 +36 -0
  152. package/dst/man/ase-task-newid.1 +61 -0
  153. package/dst/man/ase-task-purge.1 +37 -0
  154. package/dst/man/ase-task-rename.1 +44 -0
  155. package/dst/man/ase-task-request.1 +61 -0
  156. package/dst/man/ase-task-save.1 +48 -0
  157. package/dst/man/ase-task-status.1 +55 -0
  158. package/dst/man/ase-task-store.1 +194 -0
  159. package/dst/man/ase-task-view.1 +40 -0
  160. package/dst/man/ase-task.1 +160 -0
  161. package/dst/man/ase-util-compat.1 +25 -0
  162. package/dst/man/ase-util-diagram.1 +76 -0
  163. package/dst/man/ase-util-meta.1 +45 -0
  164. package/dst/man/ase-util-metric.1 +54 -0
  165. package/dst/man/ase-util-mint.1 +59 -0
  166. package/dst/man/ase-util-worktree.1 +72 -0
  167. package/dst/man/ase-util.1 +50 -0
  168. package/dst/man/ase.1 +124 -0
  169. package/package.json +79 -21
  170. package/plugin/.claude-plugin/plugin.json +1 -1
  171. package/plugin/.codex-plugin/plugin.json +1 -1
  172. package/plugin/.github/plugin/plugin.json +1 -1
  173. package/plugin/agents/ase-meta-diagram.md +12 -8
  174. package/plugin/agents/ase-repo-review.md +322 -0
  175. package/plugin/hooks/hooks-codex.json +5 -0
  176. package/plugin/hooks/hooks-copilot.json +5 -0
  177. package/plugin/hooks/hooks.json +8 -0
  178. package/plugin/meta/ase-common-approach.md +117 -0
  179. package/plugin/meta/ase-common-changeset.md +256 -0
  180. package/plugin/meta/ase-common-code.md +10 -173
  181. package/plugin/meta/ase-common-grill.md +317 -0
  182. package/plugin/meta/ase-common-resolve.md +305 -0
  183. package/plugin/meta/ase-common-task.md +83 -4
  184. package/plugin/meta/ase-constitution.md +0 -2
  185. package/plugin/meta/ase-format-specbook.md +208 -25
  186. package/plugin/meta/ase-format-specbook.yaml +242 -851
  187. package/plugin/meta/ase-format-task.md +669 -125
  188. package/plugin/meta/ase-persona.md +3 -4
  189. package/plugin/meta/ase-skill.md +9 -0
  190. package/plugin/mods/ase-mods.ts +409 -0
  191. package/plugin/mods/claude-code.d.ts +13417 -0
  192. package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
  193. package/plugin/skills/ase-arch-analyze/help.md +2 -2
  194. package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
  195. package/plugin/skills/ase-code-analyze/help.md +29 -4
  196. package/plugin/skills/ase-code-edit/SKILL.md +107 -181
  197. package/plugin/skills/ase-code-edit/help.md +118 -31
  198. package/plugin/skills/ase-code-lint/SKILL.md +64 -13
  199. package/plugin/skills/ase-code-lint/help.md +32 -2
  200. package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
  201. package/plugin/skills/ase-docs-proofread/help.md +1 -1
  202. package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
  203. package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
  204. package/plugin/skills/ase-help-intent/data.md +2164 -1087
  205. package/plugin/skills/ase-help-intent/help.md +1 -1
  206. package/plugin/skills/ase-help-skill/catalog.md +12 -9
  207. package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
  208. package/plugin/skills/ase-meta-config/SKILL.md +3 -4
  209. package/plugin/skills/ase-meta-config/help.md +31 -10
  210. package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
  211. package/plugin/skills/ase-meta-mint/help.md +10 -5
  212. package/plugin/skills/ase-meta-why/help.md +1 -1
  213. package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
  214. package/plugin/skills/ase-meta-workflow/help.md +4 -4
  215. package/plugin/skills/ase-meta-workflow/sample.md +3 -3
  216. package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
  217. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
  218. package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
  219. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
  220. package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
  221. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
  222. package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
  223. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
  224. package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
  225. package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
  226. package/plugin/skills/ase-repo-merge/help.md +107 -0
  227. package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
  228. package/plugin/skills/ase-repo-resolve/help.md +102 -0
  229. package/plugin/skills/ase-repo-review/SKILL.md +406 -0
  230. package/plugin/skills/ase-repo-review/help.md +181 -0
  231. package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
  232. package/plugin/skills/ase-spec-edit/help.md +107 -26
  233. package/plugin/skills/ase-sync-reconcile/help.md +1 -1
  234. package/plugin/skills/ase-task-assign/SKILL.md +111 -0
  235. package/plugin/skills/ase-task-assign/help.md +119 -0
  236. package/plugin/skills/ase-task-condense/SKILL.md +21 -21
  237. package/plugin/skills/ase-task-condense/help.md +12 -10
  238. package/plugin/skills/ase-task-delete/SKILL.md +1 -1
  239. package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
  240. package/plugin/skills/ase-task-dissect/help.md +10 -9
  241. package/plugin/skills/ase-task-edit/SKILL.md +379 -94
  242. package/plugin/skills/ase-task-edit/help.md +95 -22
  243. package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
  244. package/plugin/skills/ase-task-estimate/help.md +158 -0
  245. package/plugin/skills/ase-task-grill/SKILL.md +246 -138
  246. package/plugin/skills/ase-task-grill/help.md +129 -26
  247. package/plugin/skills/ase-task-id/SKILL.md +8 -1
  248. package/plugin/skills/ase-task-id/help.md +6 -3
  249. package/plugin/skills/ase-task-implement/SKILL.md +535 -145
  250. package/plugin/skills/ase-task-implement/help.md +138 -47
  251. package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
  252. package/plugin/skills/ase-task-integrate/help.md +187 -0
  253. package/plugin/skills/ase-task-list/SKILL.md +55 -23
  254. package/plugin/skills/ase-task-list/help.md +45 -21
  255. package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
  256. package/plugin/skills/ase-task-reboot/help.md +28 -8
  257. package/plugin/skills/ase-task-rename/SKILL.md +10 -2
  258. package/plugin/skills/ase-task-research/SKILL.md +386 -0
  259. package/plugin/skills/ase-task-research/help.md +102 -0
  260. package/plugin/skills/ase-task-status/SKILL.md +137 -0
  261. package/plugin/skills/ase-task-status/help.md +80 -0
  262. package/plugin/skills/ase-task-view/SKILL.md +49 -24
  263. package/plugin/skills/ase-task-view/help.md +19 -9
  264. package/plugin/agents/ase-meta-review.md +0 -187
  265. package/plugin/skills/ase-code-craft/SKILL.md +0 -264
  266. package/plugin/skills/ase-code-craft/help.md +0 -122
  267. package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
  268. package/plugin/skills/ase-code-refactor/help.md +0 -109
  269. package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
  270. package/plugin/skills/ase-code-resolve/help.md +0 -116
  271. package/plugin/skills/ase-meta-review/SKILL.md +0 -210
  272. package/plugin/skills/ase-meta-review/help.md +0 -96
  273. package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
  274. package/plugin/skills/ase-task-preflight/help.md +0 -82
  275. /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
  276. /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
  277. /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
  278. /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
  279. /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
@@ -0,0 +1,474 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+ import crypto from "node:crypto";
9
+ import os from "node:os";
10
+ import { ofetch } from "ofetch";
11
+ import { Agent } from "undici";
12
+ import { isSeq, isScalar } from "yaml";
13
+ import { Config } from "./ase-config-core.js";
14
+ import { parseScope } from "./ase-config-scope.js";
15
+ import { isLogLevel } from "./ase-lib-log.js";
16
+ import { Service } from "./ase-service.js";
17
+ import { DaemonCommand } from "./ase-lib-daemon-cli.js";
18
+ import { urlHost, isLoopback, serverHeader } from "./ase-lib-server-bind.js";
19
+ /* the defaults */
20
+ export const DEFAULT_ADDRESS = "127.0.0.1";
21
+ /* the dispatcher of the host-local probe, skipping the certificate verification,
22
+ as a certificate rarely covers a loopback or local interface address */
23
+ const probeAgent = new Agent({ connect: { rejectUnauthorized: false } });
24
+ /* whether an address belongs to this host (loopback or a local network interface) */
25
+ export const isLocal = (address) => isLoopback(address) || Object.values(os.networkInterfaces())
26
+ .some((ifaces) => (ifaces ?? []).some((iface) => iface.address === address));
27
+ /* the URL host to connect to for a bound address */
28
+ export const connectHost = (address) => (address === "0.0.0.0" || address === "") ? DEFAULT_ADDRESS :
29
+ address === "::" ? "[::1]" : urlHost(address);
30
+ /* the TLS certificate and key of a server configured in the configuration <cfg>:
31
+ "<prefix>.crt" and "<prefix>.key" (each resolved against the directory of the
32
+ configuration file defining it), unless "<prefix>.url" selects HTTP via "ase://"
33
+ on this host (as the clients following it would then fail) */
34
+ export const configuredTls = (cfg, prefix) => {
35
+ const scoped = (key) => {
36
+ const entry = cfg.getScoped(key);
37
+ if (entry === undefined)
38
+ return null;
39
+ return { value: String(isScalar(entry.value) ? entry.value.value : entry.value), filename: entry.filename };
40
+ };
41
+ const url = scoped(`${prefix}.url`);
42
+ if (url !== null && url.value.startsWith("ase://")) {
43
+ try {
44
+ if (isLocal(new URL(url.value).hostname.replace(/^\[|\]$/g, "")))
45
+ return { cert: null, key: null };
46
+ }
47
+ catch (_err) {
48
+ /* an invalid URL is reported by its users */
49
+ }
50
+ }
51
+ const file = (key) => {
52
+ const entry = scoped(key);
53
+ return entry !== null ? path.resolve(path.dirname(entry.filename), entry.value) : null;
54
+ };
55
+ return { cert: file(`${prefix}.crt`), key: file(`${prefix}.key`) };
56
+ };
57
+ /* the CLI command group managing a per-user, bearer token authenticated REST API server
58
+ (the task store and the agent relay server) in the background or in the foreground:
59
+ it resolves the configuration of the server from its per-user configuration file,
60
+ the command line, and the environment, probes, spawns, and stops the server, and persists
61
+ its runtime facts, where the concrete server command decides on its specific parts */
62
+ export class ServerCommand extends DaemonCommand {
63
+ /* the specific options of the "start" command, the explicitly given ones of them deviating
64
+ from the configuration of the running server, the environment variables handing them to
65
+ the server process, and the description of the started server (e.g. its storage plugin) */
66
+ options(_cmd) { }
67
+ deviating(_ctx, _opts) {
68
+ return [];
69
+ }
70
+ environment(_ctx, _opts) {
71
+ return {};
72
+ }
73
+ started(_env) {
74
+ return "";
75
+ }
76
+ /* the TLS certificate and key of the server configured outside its per-user configuration
77
+ file (e.g. in the project configuration), applied like the options "--tls-cert" and
78
+ "--tls-key" if these are not given */
79
+ tlsDefaults() {
80
+ return { cert: null, key: null };
81
+ }
82
+ /* the description of the running server in the status (e.g. a co-hosted agent relay) */
83
+ running(_ctx) {
84
+ return "";
85
+ }
86
+ /* the name of an environment variable handing the configuration to the
87
+ server process (where the token one also serves as the user-facing token override) */
88
+ env(name) {
89
+ return `${this.prefix}_${name}`;
90
+ }
91
+ /* load the per-user configuration file into the context */
92
+ loadContext() {
93
+ const cfg = new Config(this.name, this.schema, this.log, parseScope("user"));
94
+ cfg.read();
95
+ const scalar = (key) => {
96
+ const val = cfg.get(key);
97
+ if (val === undefined || val === null)
98
+ return null;
99
+ const raw = isScalar(val) ? val.value : val;
100
+ return typeof raw === "number" ? raw : String(raw);
101
+ };
102
+ const address = scalar("address");
103
+ const port = scalar("port");
104
+ const token = scalar("token");
105
+ const pid = scalar("pid");
106
+ const tlsCert = scalar("tls.cert");
107
+ const tlsKey = scalar("tls.key");
108
+ const dir = path.dirname(cfg.filename);
109
+ return this.context({
110
+ cfg,
111
+ dir,
112
+ address: address !== null ? String(address) : null,
113
+ port: typeof port === "number" ? port : null,
114
+ token: token !== null ? String(token) : null,
115
+ pid: typeof pid === "number" ? pid : null,
116
+ tlsCert: tlsCert !== null ? path.resolve(dir, String(tlsCert)) : null,
117
+ tlsKey: tlsKey !== null ? path.resolve(dir, String(tlsKey)) : null
118
+ }, scalar);
119
+ }
120
+ /* read the configured CORS origins */
121
+ configuredCors(cfg) {
122
+ const val = cfg.get("cors");
123
+ return isSeq(val) ? val.toJSON().map((item) => String(item)) : [];
124
+ }
125
+ /* persist the runtime facts of the running server */
126
+ persist(cfg, values) {
127
+ cfg.lock(() => {
128
+ cfg.read();
129
+ for (const [key, value] of Object.entries(values))
130
+ if (value !== null)
131
+ cfg.set(key, value);
132
+ else if (cfg.get(key) !== undefined)
133
+ cfg.delete(key);
134
+ cfg.write();
135
+ fs.chmodSync(cfg.filename, 0o600);
136
+ });
137
+ }
138
+ /* probe the server by listing its projects: first unauthenticated,
139
+ to identify it by its problem details response (and by its server
140
+ header, if any, telling the other servers sharing the same range of
141
+ ports apart) before handing the token to a possibly foreign service
142
+ holding the port, and then with the token to verify the token is accepted */
143
+ async probe(address, port, token, tls) {
144
+ const host = connectHost(address);
145
+ const url = `${tls ? "https" : "http"}://${host}:${port}${this.probePath}`;
146
+ /* skip the certificate verification for host-local addresses only,
147
+ as elsewhere a man-in-the-middle could capture the token */
148
+ const lax = isLocal(host.replace(/^\[|\]$/g, ""));
149
+ const request = (auth) => ofetch.raw(url, {
150
+ method: "GET",
151
+ headers: auth !== null ? { Authorization: `Bearer ${auth}` } : {},
152
+ dispatcher: lax ? probeAgent : undefined,
153
+ signal: AbortSignal.timeout(2000),
154
+ ignoreResponseError: true
155
+ });
156
+ try {
157
+ const r0 = await request(null);
158
+ if (r0.status !== 401 || !(r0.headers.get("content-type") ?? "").startsWith("application/problem+json"))
159
+ return "foreign";
160
+ const server = r0.headers.get(serverHeader);
161
+ if (server !== null && server !== this.label)
162
+ return "foreign";
163
+ if (token === null)
164
+ return "unauthorized";
165
+ const r = await request(token);
166
+ if (r.status === 401)
167
+ return "unauthorized";
168
+ const d = r._data;
169
+ if (r.status === 200 && Array.isArray(d?.projects))
170
+ return "running";
171
+ return "foreign";
172
+ }
173
+ catch {
174
+ /* treat an unreachable or unresponsive server as not running */
175
+ return null;
176
+ }
177
+ }
178
+ /* server-side: run the server and persist its runtime facts until a termination signal arrives
179
+ (configured through the environment of either the detached or the foreground process) */
180
+ async runServer(ctx, env = process.env) {
181
+ const level = env[this.env("LOG_LEVEL")];
182
+ if (level !== undefined && isLogLevel(level))
183
+ this.log.logLevel(level);
184
+ const address = env[this.env("ADDRESS")] ?? ctx.address ?? DEFAULT_ADDRESS;
185
+ const port = Number(env[this.env("PORT")] ?? ctx.port ?? 0);
186
+ const token = env[this.env("TOKEN")] ?? ctx.token;
187
+ const corsEnv = env[this.env("CORS")];
188
+ const cors = corsEnv !== undefined ?
189
+ corsEnv.split(",").map((s) => s.trim()).filter((s) => s !== "") :
190
+ this.configuredCors(ctx.cfg);
191
+ const tlsCert = (env[this.env("TLS_CERT")] ?? ctx.tlsCert) || null;
192
+ const tlsKey = (env[this.env("TLS_KEY")] ?? ctx.tlsKey) || null;
193
+ if (!Number.isInteger(port) || port < 1)
194
+ throw new Error(`${this.label}: no valid port configured`);
195
+ if (token === null || token === "")
196
+ throw new Error(`${this.label}: no token configured`);
197
+ const tls = tlsCert !== null && tlsKey !== null ?
198
+ { cert: fs.readFileSync(tlsCert), key: fs.readFileSync(tlsKey) } : null;
199
+ const { server, facts } = await this.create(ctx, { address, port, token, cors, tls }, env);
200
+ await server.start();
201
+ this.persist(ctx.cfg, {
202
+ address, port, token, pid: process.pid, "tls.cert": tlsCert, "tls.key": tlsKey,
203
+ cors: cors.length > 0 ? cors : null,
204
+ ...facts
205
+ });
206
+ let stopping = false;
207
+ const shutdown = (signal) => {
208
+ if (stopping)
209
+ return;
210
+ stopping = true;
211
+ this.log.write("info", `${this.label}: interrupted (received ${signal})`);
212
+ server.stop()
213
+ .catch((err) => {
214
+ const message = err instanceof Error ? err.message : String(err);
215
+ this.log.write("error", `${this.label}: stop failed: ${message}`);
216
+ })
217
+ .finally(() => {
218
+ try {
219
+ this.persist(ctx.cfg, { pid: null });
220
+ }
221
+ finally {
222
+ process.exit(0);
223
+ }
224
+ });
225
+ };
226
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
227
+ process.on("SIGINT", () => shutdown("SIGINT"));
228
+ process.on("SIGHUP", () => shutdown("SIGHUP"));
229
+ process.on("SIGQUIT", () => shutdown("SIGQUIT"));
230
+ return new Promise(() => { });
231
+ }
232
+ /* determine the explicitly given options deviating from the configuration of the running server */
233
+ deviatingOptions(ctx, opts, current) {
234
+ const cors = this.configuredCors(ctx.cfg);
235
+ return [
236
+ opts.address !== undefined && opts.address !== current ? "--address" : "",
237
+ opts.port !== undefined && Number(opts.port) !== ctx.port ? "--port" : "",
238
+ opts.token !== undefined && opts.token !== ctx.token ? "--token" : "",
239
+ opts.cors.length > 0 && opts.cors.join(",") !== cors.join(",") ? "--cors" : "",
240
+ opts.tlsCert !== undefined && path.resolve(opts.tlsCert) !== ctx.tlsCert ? "--tls-cert" : "",
241
+ opts.tlsKey !== undefined && path.resolve(opts.tlsKey) !== ctx.tlsKey ? "--tls-key" : "",
242
+ ...this.deviating(ctx, opts)
243
+ ].filter((option) => option !== "");
244
+ }
245
+ /* wait for the detached server to respond, else terminate it and fail with the tail of its log */
246
+ async awaitStartup(child, logFile, address, port, token, tls, env) {
247
+ let exited = false;
248
+ let exitCode = null;
249
+ let probed = null;
250
+ child.once("exit", (code) => {
251
+ exited = true;
252
+ exitCode = code;
253
+ });
254
+ for (let i = 0; i < 100; i++) {
255
+ await new Promise((resolve) => setTimeout(resolve, 100));
256
+ if (exited)
257
+ break;
258
+ const state = await this.probe(address, port, token, tls);
259
+ if (state === "running") {
260
+ this.log.write("info", `${this.label}: started on ${tls ? "https" : "http"}://${urlHost(address)}:${port}${this.started(env)}`);
261
+ child.unref();
262
+ return 0;
263
+ }
264
+ if (state === "foreign" || state === "unauthorized") {
265
+ probed = state;
266
+ break;
267
+ }
268
+ }
269
+ if (!exited) {
270
+ child.kill("SIGTERM");
271
+ child.unref();
272
+ }
273
+ const tail = Service.readLogTail(logFile, 20);
274
+ const reason = exited ?
275
+ `${this.label}: server exited during startup (code ${exitCode})` :
276
+ probed === "foreign" ?
277
+ `${this.label}: port ${port} taken by a foreign service during startup` :
278
+ probed === "unauthorized" ?
279
+ `${this.label}: server on port ${port} rejects the token` :
280
+ `${this.label}: server failed to respond within timeout`;
281
+ throw new Error(tail.length > 0 ? `${reason}\n---- ${logFile} (tail) ----\n${tail}` : reason);
282
+ }
283
+ /* start flow: resolve the configuration, probe, and detach (or run in the foreground) */
284
+ async doStart(opts) {
285
+ const ctx = this.loadContext();
286
+ if (process.env[this.env("SERVE")] === "1")
287
+ return this.runServer(ctx);
288
+ /* let the TLS certificate and key not given fall back to the otherwise configured ones */
289
+ const tlsDefaults = this.tlsDefaults();
290
+ if (opts.tlsCert === undefined && tlsDefaults.cert !== null)
291
+ opts.tlsCert = tlsDefaults.cert;
292
+ if (opts.tlsKey === undefined && tlsDefaults.key !== null)
293
+ opts.tlsKey = tlsDefaults.key;
294
+ /* validate the explicitly given port and resolve the TLS certificate and key (both or
295
+ none), the given ones checked for readability, before a running server is stopped
296
+ to apply deviating options, so invalid options never leave no server running */
297
+ if (opts.port !== undefined) {
298
+ const port = Number(opts.port);
299
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
300
+ throw new Error(`${this.label}: invalid port "${opts.port}"`);
301
+ }
302
+ const tlsCert = opts.tlsCert !== undefined ? path.resolve(opts.tlsCert) : ctx.tlsCert;
303
+ const tlsKey = opts.tlsKey !== undefined ? path.resolve(opts.tlsKey) : ctx.tlsKey;
304
+ if ((tlsCert === null) !== (tlsKey === null))
305
+ throw new Error(`${this.label}: TLS requires both a certificate ("--tls-cert") and a key ("--tls-key")`);
306
+ for (const file of [opts.tlsCert, opts.tlsKey])
307
+ if (file !== undefined)
308
+ fs.accessSync(path.resolve(file), fs.constants.R_OK);
309
+ /* idempotent start: an already running server is left alone,
310
+ or restarted if explicitly given options deviate from its configuration */
311
+ const address = opts.address ?? ctx.address ?? DEFAULT_ADDRESS;
312
+ let foreign = false;
313
+ if (ctx.port !== null) {
314
+ const current = ctx.address ?? DEFAULT_ADDRESS;
315
+ const tls = ctx.tlsCert !== null;
316
+ const state = await this.probe(current, ctx.port, ctx.token, tls);
317
+ if (state === "running" || state === "unauthorized") {
318
+ const changed = this.deviatingOptions(ctx, opts, current);
319
+ const url = `${tls ? "https" : "http"}://${urlHost(current)}:${ctx.port}`;
320
+ if (changed.length === 0) {
321
+ if (state === "unauthorized")
322
+ throw new Error(`${this.label}: already running on ${url}, but the configured token is rejected`);
323
+ if (opts.foreground)
324
+ throw new Error(`${this.label}: already running on ${url} (stop it first with "ase ${this.words} stop")`);
325
+ this.log.write("info", `${this.label}: already running on ${url}`);
326
+ return 0;
327
+ }
328
+ /* restart the server to apply the deviating options */
329
+ this.log.write("info", `${this.label}: already running on ${url}, ` +
330
+ `restarting to apply deviating ${changed.join(", ")}`);
331
+ await this.doStop();
332
+ }
333
+ else
334
+ foreign = state === "foreign";
335
+ }
336
+ /* check the TLS certificate and key (also the configured ones)
337
+ for readability to fail before detaching the server */
338
+ for (const file of [tlsCert, tlsKey])
339
+ if (file !== null)
340
+ fs.accessSync(file, fs.constants.R_OK);
341
+ /* warn about plain HTTP on a non-loopback address, as the
342
+ bearer token then travels unencrypted over the network */
343
+ if (tlsCert === null && !isLoopback(address))
344
+ this.log.write("warning", `${this.label}: binding to non-loopback address "${address}" without TLS ` +
345
+ "(consider \"--tls-cert\" and \"--tls-key\", or a TLS reverse proxy)");
346
+ /* resolve port, token, and CORS origins */
347
+ let port = ctx.port;
348
+ if (opts.port !== undefined)
349
+ port = Number(opts.port);
350
+ else if (port === null || foreign) {
351
+ /* re-allocate a configured port held by a foreign service */
352
+ if (foreign)
353
+ this.log.write("warning", `${this.label}: configured port ${port} in use by foreign service, re-allocating`);
354
+ port = await Service.allocatePort();
355
+ }
356
+ let token = opts.token ?? process.env[this.env("TOKEN")] ?? ctx.token;
357
+ if (token === undefined || token === null || token === "")
358
+ token = crypto.randomBytes(24).toString("hex");
359
+ const cors = opts.cors.length > 0 ? opts.cors : this.configuredCors(ctx.cfg);
360
+ /* the resolved configuration of the server */
361
+ const env = {
362
+ [this.env("ADDRESS")]: address,
363
+ [this.env("PORT")]: String(port),
364
+ [this.env("TOKEN")]: token,
365
+ [this.env("CORS")]: cors.join(","),
366
+ [this.env("TLS_CERT")]: tlsCert ?? "",
367
+ [this.env("TLS_KEY")]: tlsKey ?? "",
368
+ [this.env("LOG_LEVEL")]: this.log.logLevel(),
369
+ ...this.environment(ctx, opts)
370
+ };
371
+ /* run the server in the foreground, logging to the terminal */
372
+ if (opts.foreground)
373
+ return this.runServer(ctx, env);
374
+ /* spawn the server detached, logging into its log file */
375
+ const logFile = path.join(ctx.dir, `${this.name}.log`);
376
+ const child = this.spawnDetached([...this.words.split(" "), "start"], { ...env, [this.env("SERVE")]: "1" }, logFile);
377
+ /* wait for the server to respond */
378
+ return this.awaitStartup(child, logFile, address, port, token, tlsCert !== null, env);
379
+ }
380
+ /* status flow: report whether the server is running */
381
+ async doStatus() {
382
+ const ctx = this.loadContext();
383
+ if (ctx.port === null) {
384
+ process.stdout.write(`${this.label}: not running (no port configured)\n`);
385
+ return 1;
386
+ }
387
+ const address = ctx.address ?? DEFAULT_ADDRESS;
388
+ const scheme = ctx.tlsCert !== null ? "https" : "http";
389
+ const state = await this.probe(address, ctx.port, ctx.token, ctx.tlsCert !== null);
390
+ if (state === "running") {
391
+ process.stdout.write(`${this.label}: running on ${scheme}://${urlHost(address)}:${ctx.port}` +
392
+ (ctx.pid !== null ? ` (pid ${ctx.pid})` : "") + this.running(ctx) + "\n");
393
+ return 0;
394
+ }
395
+ if (state === "unauthorized") {
396
+ process.stdout.write(`${this.label}: running on ${scheme}://${urlHost(address)}:${ctx.port}, but the configured token is rejected\n`);
397
+ return 1;
398
+ }
399
+ if (state === "foreign") {
400
+ process.stdout.write(`${this.label}: not running (port ${ctx.port} in use by foreign service)\n`);
401
+ return 1;
402
+ }
403
+ process.stdout.write(`${this.label}: not running (port ${ctx.port} not responding)\n`);
404
+ return 1;
405
+ }
406
+ /* stop flow: terminate the server process and wait until it is gone */
407
+ async doStop() {
408
+ const ctx = this.loadContext();
409
+ if (ctx.pid === null && ctx.port === null) {
410
+ this.log.write("info", `${this.label}: not running (no server configured)`);
411
+ return 0;
412
+ }
413
+ const address = ctx.address ?? DEFAULT_ADDRESS;
414
+ /* probe first, as a stale process id might meanwhile belong to a foreign process */
415
+ const state = ctx.port !== null ? await this.probe(address, ctx.port, ctx.token, ctx.tlsCert !== null) : null;
416
+ const running = state === "running" || state === "unauthorized";
417
+ if (ctx.pid === null || !running) {
418
+ if (running)
419
+ throw new Error(`${this.label}: running on port ${ctx.port}, but its process id is unknown`);
420
+ this.log.write("info", `${this.label}: not running`);
421
+ this.persist(ctx.cfg, { pid: null });
422
+ return 0;
423
+ }
424
+ if (await this.terminate(ctx.pid, 5000))
425
+ this.log.write("info", `${this.label}: stopped (process ${ctx.pid})`);
426
+ else
427
+ this.log.write("info", `${this.label}: not running (process ${ctx.pid} already gone)`);
428
+ this.persist(ctx.cfg, { pid: null });
429
+ return 0;
430
+ }
431
+ /* register the CLI sub-command group and its commands
432
+ "start", "status", "log", and "stop" below the CLI command <parent> */
433
+ register(parent) {
434
+ /* register CLI sub-command group */
435
+ const group = this.group(parent);
436
+ /* register CLI sub-command "start" */
437
+ const start = group
438
+ .command("start")
439
+ .description(`Start the ${this.label} server in the background (idempotent if already running) or in the foreground`)
440
+ .option("-a, --address <host>", `address to bind to (default: ${DEFAULT_ADDRESS}, or the configured one)`)
441
+ .option("-p, --port <port>", "port to bind to (default: the configured one, else allocated randomly)")
442
+ .option("-t, --token <token>", `bearer token to expect (default: $${this.env("TOKEN")}, the configured one, ` +
443
+ "else generated)")
444
+ .option("-c, --cors <origin>", "allow cross-origin browser requests from <origin> " +
445
+ "(repeatable, \"*\" for any origin; default: the configured ones)", (value, previous) => [...previous, value], []);
446
+ this.options(start);
447
+ start
448
+ .option("--tls-cert <file>", "serve HTTPS with the PEM certificate (chain) in <file>, requires \"--tls-key\" " +
449
+ "(default: the configured \"tls.cert\", else plain HTTP)")
450
+ .option("--tls-key <file>", "serve HTTPS with the PEM private key in <file>, requires \"--tls-cert\" " +
451
+ "(default: the configured \"tls.key\")")
452
+ .option("-f, --foreground", "run the server in the foreground, logging to the terminal, " +
453
+ "until it is interrupted or stopped")
454
+ .action(async (opts) => {
455
+ process.exit(await this.doStart(opts));
456
+ });
457
+ /* register CLI sub-command "status" */
458
+ group
459
+ .command("status")
460
+ .description(`Report whether the ${this.label} server is running`)
461
+ .action(async () => {
462
+ process.exit(await this.doStatus());
463
+ });
464
+ /* register CLI sub-command "log" */
465
+ this.registerLog(group, () => path.join(this.loadContext().dir, `${this.name}.log`));
466
+ /* register CLI sub-command "stop" */
467
+ group
468
+ .command("stop")
469
+ .description(`Stop the ${this.label} server`)
470
+ .action(async () => {
471
+ process.exit(await this.doStop());
472
+ });
473
+ }
474
+ }
@@ -4,7 +4,7 @@
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
6
  import Table from "cli-table3";
7
- import { Diagram } from "./ase-diagram.js";
7
+ import { Diagram } from "./ase-util-diagram.js";
8
8
  /* assumed terminal width in case the output is not a terminal at all */
9
9
  const termWidthFallback = 80;
10
10
  /* content width a column is at most shrunk down to, unless its own
@@ -18,6 +18,9 @@ export default class Version {
18
18
  pkg,
19
19
  updateCheckInterval: 1000 * 60 * 60
20
20
  });
21
+ /* re-persist the cached update information, as update-notifier consumes it on read */
22
+ if (notifier.update !== undefined)
23
+ notifier.config?.set("update", notifier.update);
21
24
  return notifier.update?.latest ?? Version.current();
22
25
  }
23
26
  }
package/dst/ase-man.js ADDED
@@ -0,0 +1,96 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+ import { spawnSync } from "node:child_process";
9
+ import { fileURLToPath } from "node:url";
10
+ import { writeStdout } from "./ase-lib-stdio.js";
11
+ /* reusable functionality: resolve the internally shipped manual pages */
12
+ export class Man {
13
+ /* determine the directory of the manual pages; the build process
14
+ renders "man/*.md" into "dst/man/*.1" (see the "build-man" build
15
+ step), right next to the compiled tool */
16
+ static dir() {
17
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), "man");
18
+ }
19
+ /* list the names of all shipped manual pages (in natural order) */
20
+ static list() {
21
+ let files;
22
+ try {
23
+ files = fs.readdirSync(Man.dir());
24
+ }
25
+ catch (err) {
26
+ throw new Error(`man: no manual pages found in "${Man.dir()}" (tool not completely built?)`, { cause: err });
27
+ }
28
+ return files
29
+ .filter((file) => /^ase(?:-[a-z0-9-]+)?\.\d$/.test(file))
30
+ .map((file) => file.replace(/\.\d$/, ""))
31
+ .sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
32
+ }
33
+ /* resolve the words of a requested manual page to the absolute path of
34
+ its file: the words are joined with dashes, the "ase" prefix is optional,
35
+ and a command path deeper than the shipped pages (e.g. "ase task store
36
+ start") falls back to the page of its nearest parent (e.g. "ase-task-store"),
37
+ as the pages of a group with third-level commands cover them */
38
+ static resolve(words) {
39
+ const name = words.join("-").toLowerCase();
40
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
41
+ throw new Error(`man: invalid manual page name "${words.join(" ")}"`);
42
+ const parts = name.split("-");
43
+ if (parts[0] !== "ase")
44
+ parts.unshift("ase");
45
+ const pages = new Set(Man.list());
46
+ while (parts.length > 0) {
47
+ const page = parts.join("-");
48
+ if (pages.has(page))
49
+ return path.join(Man.dir(), `${page}.1`);
50
+ parts.pop();
51
+ if (parts.length === 1)
52
+ break;
53
+ }
54
+ throw new Error(`man: no manual page found for "${words.join(" ")}" (see "ase man --list")`);
55
+ }
56
+ }
57
+ /* CLI command "ase man" */
58
+ export default class ManCommand {
59
+ log;
60
+ constructor(log) {
61
+ this.log = log;
62
+ }
63
+ /* register command */
64
+ register(program) {
65
+ program
66
+ .command("man")
67
+ .description("Show an internally shipped manual page with man(1)")
68
+ .argument("[page...]", "manual page name, either as \"ase-<command>[-<sub-command>]\" " +
69
+ "or as the words of the command path (\"ase\" prefix optional, default: \"ase\")")
70
+ .option("-l, --list", "list the names of all shipped manual pages instead", false)
71
+ .option("-p, --path", "print the path of the manual page file instead", false)
72
+ .action(async (words, opts) => {
73
+ /* list all manual pages */
74
+ if (opts.list) {
75
+ await writeStdout(Man.list().map((page) => `${page}\n`).join(""));
76
+ return;
77
+ }
78
+ /* resolve manual page */
79
+ const file = Man.resolve(words.length > 0 ? words : ["ase"]);
80
+ if (opts.path) {
81
+ await writeStdout(`${file}\n`);
82
+ return;
83
+ }
84
+ /* show manual page with man(1), passing the file by its path,
85
+ which all common man(1) implementations support */
86
+ this.log.write("debug", `man: showing manual page "${file}"`);
87
+ const result = spawnSync("man", [file], { stdio: "inherit" });
88
+ if (result.error !== undefined) {
89
+ const message = result.error.message;
90
+ throw new Error(`man: failed to run man(1): ${message} ` +
91
+ `(the manual page file is "${file}")`, { cause: result.error });
92
+ }
93
+ process.exitCode = result.status ?? 1;
94
+ });
95
+ }
96
+ }