@rse/ase 1.0.6 → 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.
- package/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +230 -116
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
package/dst/ase-service.js
CHANGED
|
@@ -13,24 +13,30 @@ import { ofetch } from "ofetch";
|
|
|
13
13
|
import { isMap } from "yaml";
|
|
14
14
|
import prettyMs from "pretty-ms";
|
|
15
15
|
import * as v from "valibot";
|
|
16
|
+
import { Tail } from "tail";
|
|
16
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
18
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
19
|
+
import { ConfigMCP } from "./ase-config.js";
|
|
20
|
+
import { Config } from "./ase-config-core.js";
|
|
21
|
+
import { configSchema } from "./ase-config-schema.js";
|
|
22
|
+
import { ensureAseGitignore } from "./ase-config-scope.js";
|
|
23
|
+
import { isLogLevel } from "./ase-lib-log.js";
|
|
24
|
+
import { CompatMCP } from "./ase-util-compat.js";
|
|
25
|
+
import { DiagramMCP } from "./ase-util-diagram.js";
|
|
26
|
+
import { Task, TaskMCP } from "./ase-task.js";
|
|
27
|
+
import { MarkdownMCP } from "./ase-service-markdown.js";
|
|
24
28
|
import { ArtifactMCP } from "./ase-artifact.js";
|
|
25
29
|
import { SpecMCP } from "./ase-spec.js";
|
|
26
|
-
import { KVMCP } from "./ase-kv.js";
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
30
|
+
import { KVMCP } from "./ase-service-kv.js";
|
|
31
|
+
import { StateMCP } from "./ase-service-state.js";
|
|
32
|
+
import { TimestampMCP } from "./ase-service-timestamp.js";
|
|
33
|
+
import { SleepMCP } from "./ase-service-sleep.js";
|
|
34
|
+
import { GetoptMCP } from "./ase-service-getopt.js";
|
|
35
|
+
import { SkillsMCP } from "./ase-service-skills.js";
|
|
36
|
+
import { WorktreeMCP } from "./ase-util-worktree.js";
|
|
37
|
+
import { MintMCP } from "./ase-util-mint.js";
|
|
38
|
+
import { MetricMCP } from "./ase-util-metric.js";
|
|
39
|
+
import { UsageTracker } from "./ase-service-otel.js";
|
|
34
40
|
import pkg from "../package.json" with { type: "json" };
|
|
35
41
|
/* shared service host */
|
|
36
42
|
export const SERVICE_HOST = "127.0.0.1";
|
|
@@ -88,7 +94,7 @@ export const loadServiceContext = (log) => {
|
|
|
88
94
|
const svc = new Config("service", serviceSchema, log);
|
|
89
95
|
svc.read();
|
|
90
96
|
/* determine project id */
|
|
91
|
-
const projectId = cfg.get("project.id")
|
|
97
|
+
const projectId = cfg.get("project.id") || Task.projectIdOf(Task.projectRoot());
|
|
92
98
|
/* determine service port */
|
|
93
99
|
const port = svc.get("port") ?? null;
|
|
94
100
|
/* determine path to ".ase" directory */
|
|
@@ -122,6 +128,7 @@ export class Service {
|
|
|
122
128
|
}
|
|
123
129
|
/* persist an allocated port into ".ase/service.yaml" */
|
|
124
130
|
static persistPort(svc, port) {
|
|
131
|
+
ensureAseGitignore(path.dirname(svc.filename));
|
|
125
132
|
svc.lock(() => {
|
|
126
133
|
svc.read();
|
|
127
134
|
svc.set("port", port);
|
|
@@ -159,6 +166,7 @@ export class Service {
|
|
|
159
166
|
/* spawn the current executable detached as a background service */
|
|
160
167
|
static spawnDetached(aseDir, port, logLevel) {
|
|
161
168
|
fs.mkdirSync(aseDir, { recursive: true });
|
|
169
|
+
ensureAseGitignore(aseDir);
|
|
162
170
|
const logFile = path.join(aseDir, "service.log");
|
|
163
171
|
/* trim the log before handing it to the service, as the detached
|
|
164
172
|
service inherits the file descriptor for its entire lifetime and
|
|
@@ -208,6 +216,34 @@ export class Service {
|
|
|
208
216
|
fs.closeSync(fd);
|
|
209
217
|
}
|
|
210
218
|
}
|
|
219
|
+
/* show a log file on stdout, either completely or its last <lines> lines only, and
|
|
220
|
+
optionally follow it for newly appended lines (default: the last 10 lines first),
|
|
221
|
+
where each line is passed through <transform> (e.g. to colorize parts of it) */
|
|
222
|
+
static async showLog(logFile, opts, transform = (line) => line) {
|
|
223
|
+
const lines = opts.lines !== undefined ? Number(opts.lines) : null;
|
|
224
|
+
if (lines !== null && (!Number.isInteger(lines) || lines < 1))
|
|
225
|
+
throw new Error(`invalid number of lines: ${opts.lines}`);
|
|
226
|
+
if (!fs.existsSync(logFile))
|
|
227
|
+
return 0;
|
|
228
|
+
if (opts.follow) {
|
|
229
|
+
/* follow until interrupted (on log trimming,
|
|
230
|
+
"tail" continues at the new end of the file) */
|
|
231
|
+
const tail = new Tail(logFile, { nLines: lines ?? 10 });
|
|
232
|
+
tail.on("line", (line) => {
|
|
233
|
+
process.stdout.write(`${transform(line)}\n`);
|
|
234
|
+
});
|
|
235
|
+
return await new Promise((_resolve, reject) => {
|
|
236
|
+
tail.on("error", (err) => {
|
|
237
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const text = lines !== null ?
|
|
242
|
+
Service.readLogTail(logFile, lines) :
|
|
243
|
+
fs.readFileSync(logFile, "utf8").replace(/\n$/, "");
|
|
244
|
+
process.stdout.write(text.length > 0 ? `${text.split("\n").map(transform).join("\n")}\n` : "");
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
211
247
|
}
|
|
212
248
|
/* MCP registration entry point for service-identity tools */
|
|
213
249
|
export class ServiceMCP {
|
|
@@ -243,8 +279,56 @@ export default class ServiceCommand {
|
|
|
243
279
|
loadContext() {
|
|
244
280
|
return loadServiceContext(this.log);
|
|
245
281
|
}
|
|
246
|
-
/*
|
|
247
|
-
|
|
282
|
+
/* build a fresh MCP server instance with all registered tools
|
|
283
|
+
(also used by the "ase mcp" bridge for answering the MCP handshake
|
|
284
|
+
and tool listing in-process before the service is started) */
|
|
285
|
+
static buildMcpServer(log, ctx, startTime, usage) {
|
|
286
|
+
const mcp = new McpServer({ name: "ase", version: pkg.version });
|
|
287
|
+
new ServiceMCP({ projectId: ctx.projectId, port: ctx.port, startTime }).register(mcp);
|
|
288
|
+
new CompatMCP().register(mcp);
|
|
289
|
+
new DiagramMCP().register(mcp);
|
|
290
|
+
new TaskMCP(log, usage).register(mcp);
|
|
291
|
+
new MarkdownMCP().register(mcp);
|
|
292
|
+
new ArtifactMCP(log).register(mcp);
|
|
293
|
+
new SpecMCP(log).register(mcp);
|
|
294
|
+
new KVMCP().register(mcp);
|
|
295
|
+
new TimestampMCP().register(mcp);
|
|
296
|
+
new SleepMCP().register(mcp);
|
|
297
|
+
new GetoptMCP().register(mcp);
|
|
298
|
+
new SkillsMCP().register(mcp);
|
|
299
|
+
new WorktreeMCP().register(mcp);
|
|
300
|
+
new MintMCP().register(mcp);
|
|
301
|
+
new MetricMCP().register(mcp);
|
|
302
|
+
new ConfigMCP(log).register(mcp);
|
|
303
|
+
new StateMCP(log).register(mcp);
|
|
304
|
+
return mcp;
|
|
305
|
+
}
|
|
306
|
+
/* summarize the method, tool name, and capped tool arguments of an MCP request body */
|
|
307
|
+
static mcpBodyInfo(body) {
|
|
308
|
+
const b = body;
|
|
309
|
+
const bParams = b?.params;
|
|
310
|
+
const bMethod = typeof b?.method === "string" ? b.method : null;
|
|
311
|
+
const bName = typeof bParams?.name === "string" ? bParams.name : null;
|
|
312
|
+
const bArgs = bParams?.arguments !== undefined ? bParams.arguments : null;
|
|
313
|
+
let info = "";
|
|
314
|
+
if (bMethod !== null) {
|
|
315
|
+
info = ` [${bMethod}]`;
|
|
316
|
+
if (bName !== null) {
|
|
317
|
+
info += ` ${bName}`;
|
|
318
|
+
if (bArgs !== null) {
|
|
319
|
+
/* cap the arguments, as payload-carrying tool calls
|
|
320
|
+
(task plans, key/value batches, etc) would
|
|
321
|
+
otherwise dominate the entire log file */
|
|
322
|
+
const args = JSON.stringify(bArgs);
|
|
323
|
+
info += ` ${args.length > LOG_ARGS_MAX ? `${args.slice(0, LOG_ARGS_MAX)}…` : args}`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { method: bMethod, info };
|
|
328
|
+
}
|
|
329
|
+
/* service-side: bind HAPI server until "/stop" command is received or idle timeout happens
|
|
330
|
+
(or, in the foreground, until "/stop" command is received or a termination signal arrives) */
|
|
331
|
+
async runService(ctx, foreground = false) {
|
|
248
332
|
/* establish HAPI HTTP/REST service */
|
|
249
333
|
const server = Hapi.server({ host: HOST, port: ctx.port });
|
|
250
334
|
/* track start time and last activity */
|
|
@@ -252,9 +336,14 @@ export default class ServiceCommand {
|
|
|
252
336
|
let lastActivity = Date.now();
|
|
253
337
|
let inFlight = 0;
|
|
254
338
|
let stopping = false;
|
|
255
|
-
|
|
339
|
+
/* accept only loopback "Host" headers, to defeat DNS rebinding attacks from browser pages */
|
|
340
|
+
const allowedHosts = new Set([`${HOST}:${ctx.port}`, `localhost:${ctx.port}`]);
|
|
341
|
+
server.ext("onRequest", (request, h) => {
|
|
256
342
|
inFlight++;
|
|
257
343
|
lastActivity = Date.now();
|
|
344
|
+
const host = (request.headers.host ?? "").toLowerCase();
|
|
345
|
+
if (!allowedHosts.has(host))
|
|
346
|
+
return h.response({ error: "invalid Host header" }).code(421).takeover();
|
|
258
347
|
return h.continue;
|
|
259
348
|
});
|
|
260
349
|
server.ext("onPreResponse", (_request, h) => {
|
|
@@ -262,27 +351,10 @@ export default class ServiceCommand {
|
|
|
262
351
|
lastActivity = Date.now();
|
|
263
352
|
return h.continue;
|
|
264
353
|
});
|
|
265
|
-
/*
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
new CompatMCP().register(mcp);
|
|
270
|
-
new DiagramMCP().register(mcp);
|
|
271
|
-
new TaskMCP(this.log).register(mcp);
|
|
272
|
-
new MarkdownMCP().register(mcp);
|
|
273
|
-
new ArtifactMCP(this.log).register(mcp);
|
|
274
|
-
new SpecMCP(this.log).register(mcp);
|
|
275
|
-
new KVMCP().register(mcp);
|
|
276
|
-
new TimestampMCP().register(mcp);
|
|
277
|
-
new SleepMCP().register(mcp);
|
|
278
|
-
new GetoptMCP().register(mcp);
|
|
279
|
-
new SkillsMCP().register(mcp);
|
|
280
|
-
new WorktreeMCP().register(mcp);
|
|
281
|
-
new MintMCP().register(mcp);
|
|
282
|
-
new MetricMCP().register(mcp);
|
|
283
|
-
new ConfigMCP(this.log).register(mcp);
|
|
284
|
-
return mcp;
|
|
285
|
-
};
|
|
354
|
+
/* establish the usage tracker of the AI usage accounting (and the OTLP ingress) */
|
|
355
|
+
const usage = new UsageTracker(this.log, Task.projectRoot(), ctx.projectId, ctx.aseDir, () => {
|
|
356
|
+
lastActivity = Date.now();
|
|
357
|
+
});
|
|
286
358
|
/* listen to HTTP/REST endpoints */
|
|
287
359
|
server.route({
|
|
288
360
|
method: "OPTIONS",
|
|
@@ -292,9 +364,14 @@ export default class ServiceCommand {
|
|
|
292
364
|
}
|
|
293
365
|
});
|
|
294
366
|
server.route({
|
|
295
|
-
method: "
|
|
367
|
+
method: "POST",
|
|
296
368
|
path: "/stop",
|
|
297
|
-
handler: (
|
|
369
|
+
handler: (request, h) => {
|
|
370
|
+
/* require a JSON content type, as browsers cannot send it cross-origin
|
|
371
|
+
without a CORS preflight (which this service never grants) */
|
|
372
|
+
const ct = (request.headers["content-type"] ?? "").toLowerCase();
|
|
373
|
+
if (!ct.startsWith("application/json"))
|
|
374
|
+
return h.response({ error: "unsupported media type" }).code(415);
|
|
298
375
|
this.log.write("info", "service: stop requested");
|
|
299
376
|
setImmediate(async () => {
|
|
300
377
|
try {
|
|
@@ -304,38 +381,21 @@ export default class ServiceCommand {
|
|
|
304
381
|
const e = err;
|
|
305
382
|
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
306
383
|
}
|
|
384
|
+
await usage.stop();
|
|
307
385
|
process.exit(0);
|
|
308
386
|
});
|
|
309
387
|
return h.response({ ok: true }).code(200);
|
|
310
388
|
}
|
|
311
389
|
});
|
|
312
390
|
const mcpHandler = async (request, h, body) => {
|
|
313
|
-
const
|
|
314
|
-
const bParams = b?.params;
|
|
315
|
-
const bMethod = typeof b?.method === "string" ? b.method : null;
|
|
316
|
-
const bName = typeof bParams?.name === "string" ? bParams.name : null;
|
|
317
|
-
const bArgs = bParams?.arguments !== undefined ? bParams.arguments : null;
|
|
318
|
-
let bodyInfo = "";
|
|
319
|
-
if (bMethod !== null) {
|
|
320
|
-
bodyInfo = ` [${bMethod}]`;
|
|
321
|
-
if (bName !== null) {
|
|
322
|
-
bodyInfo += ` ${bName}`;
|
|
323
|
-
if (bArgs !== null) {
|
|
324
|
-
/* cap the arguments, as payload-carrying tool calls
|
|
325
|
-
(task plans, key/value batches, etc) would
|
|
326
|
-
otherwise dominate the entire log file */
|
|
327
|
-
const args = JSON.stringify(bArgs);
|
|
328
|
-
bodyInfo += ` ${args.length > LOG_ARGS_MAX ? `${args.slice(0, LOG_ARGS_MAX)}…` : args}`;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
391
|
+
const { method: bMethod, info: bodyInfo } = ServiceCommand.mcpBodyInfo(body);
|
|
332
392
|
/* log tool calls regularly, but all remaining MCP traffic
|
|
333
393
|
(session handshakes, notifications, SSE stream opens) at
|
|
334
394
|
debug level only, as it carries no diagnostic value */
|
|
335
395
|
const level = bMethod === "tools/call" ? "info" : "debug";
|
|
336
396
|
this.log.write(level, `mcp: ${request.method.toUpperCase()} ${request.path}${bodyInfo}`);
|
|
337
397
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
338
|
-
const mcp = buildMcpServer();
|
|
398
|
+
const mcp = ServiceCommand.buildMcpServer(this.log, ctx, startTime, usage);
|
|
339
399
|
request.raw.res.on("close", () => {
|
|
340
400
|
/* "h.abandon" (see below) bypasses "onPreResponse",
|
|
341
401
|
so undo the "onRequest" accounting here instead */
|
|
@@ -390,11 +450,77 @@ export default class ServiceCommand {
|
|
|
390
450
|
return h.response({ error: "unknown command", command: cmd }).code(400);
|
|
391
451
|
}
|
|
392
452
|
});
|
|
453
|
+
/* listen to the endpoints of the AI usage accounting: the usage records forwarded
|
|
454
|
+
by the OTLP ingress, the session end, the task assignment on behalf of a session,
|
|
455
|
+
and the task rename */
|
|
456
|
+
server.route({
|
|
457
|
+
method: "POST",
|
|
458
|
+
path: "/usage/records",
|
|
459
|
+
options: { payload: { parse: true, allow: "application/json", maxBytes: 32 * 1024 * 1024 } },
|
|
460
|
+
handler: (request, h) => {
|
|
461
|
+
const payload = request.payload;
|
|
462
|
+
if (!Array.isArray(payload?.records))
|
|
463
|
+
return h.response({ error: "missing or invalid 'records' field" }).code(400);
|
|
464
|
+
usage.ingest(payload.records);
|
|
465
|
+
return h.response({ ok: true }).code(200);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
server.route({
|
|
469
|
+
method: "POST",
|
|
470
|
+
path: "/usage/detach",
|
|
471
|
+
options: { payload: { parse: true, allow: "application/json" } },
|
|
472
|
+
handler: (request, h) => {
|
|
473
|
+
const payload = request.payload;
|
|
474
|
+
if (typeof payload?.session !== "string")
|
|
475
|
+
return h.response({ error: "missing or invalid 'session' field" }).code(400);
|
|
476
|
+
usage.detach(payload.session);
|
|
477
|
+
return h.response({ ok: true }).code(200);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
server.route({
|
|
481
|
+
method: "POST",
|
|
482
|
+
path: "/usage/rename",
|
|
483
|
+
options: { payload: { parse: true, allow: "application/json" } },
|
|
484
|
+
handler: (request, h) => {
|
|
485
|
+
const payload = request.payload;
|
|
486
|
+
if (typeof payload?.old !== "string" || typeof payload.new !== "string")
|
|
487
|
+
return h.response({ error: "missing or invalid 'old' or 'new' field" }).code(400);
|
|
488
|
+
try {
|
|
489
|
+
usage.rename(payload.old, payload.new);
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
|
|
493
|
+
}
|
|
494
|
+
return h.response({ ok: true }).code(200);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
server.route({
|
|
498
|
+
method: "POST",
|
|
499
|
+
path: "/usage/assign",
|
|
500
|
+
options: { payload: { parse: true, allow: "application/json" } },
|
|
501
|
+
handler: async (request, h) => {
|
|
502
|
+
const payload = request.payload;
|
|
503
|
+
if (typeof payload?.id !== "string" || typeof payload.assignee !== "string" || typeof payload.session !== "string")
|
|
504
|
+
return h.response({ error: "missing or invalid 'id', 'assignee', or 'session' field" }).code(400);
|
|
505
|
+
try {
|
|
506
|
+
return h.response(await usage.assign(payload.id, payload.assignee, payload.session)).code(200);
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
return h.response({ error: err instanceof Error ? err.message : String(err) }).code(422);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
/* serve the web board of the project (imported dynamically to keep it out of all
|
|
514
|
+
other CLI commands; its rendering dependencies marked, MRCS, and ELK load on
|
|
515
|
+
first request only, its board core dependencies at service startup) */
|
|
516
|
+
const { registerBoardRoutes } = await import("./ase-task-board-web-server.js");
|
|
517
|
+
registerBoardRoutes(server, this.log);
|
|
393
518
|
/* start service */
|
|
394
519
|
try {
|
|
395
520
|
await server.start();
|
|
396
521
|
Service.persistPort(ctx.svc, ctx.port);
|
|
397
522
|
this.log.write("info", `service: listening on port ${ctx.port}`);
|
|
523
|
+
usage.start();
|
|
398
524
|
}
|
|
399
525
|
catch (err) {
|
|
400
526
|
const e = err;
|
|
@@ -410,31 +536,60 @@ export default class ServiceCommand {
|
|
|
410
536
|
this.log.write("error", `service: ${e.message}`);
|
|
411
537
|
process.exit(1);
|
|
412
538
|
}
|
|
539
|
+
/* stop service in the foreground on termination signal */
|
|
540
|
+
if (foreground) {
|
|
541
|
+
const shutdown = async (signal) => {
|
|
542
|
+
if (stopping)
|
|
543
|
+
return;
|
|
544
|
+
stopping = true;
|
|
545
|
+
this.log.write("info", `service: received ${signal}, stopping`);
|
|
546
|
+
try {
|
|
547
|
+
await server.stop({ timeout: 1000 });
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
const e = err;
|
|
551
|
+
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
552
|
+
}
|
|
553
|
+
await usage.stop();
|
|
554
|
+
Service.clearPort(ctx.svc);
|
|
555
|
+
process.exit(0);
|
|
556
|
+
};
|
|
557
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
558
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
559
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
560
|
+
process.on("SIGQUIT", () => shutdown("SIGQUIT"));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
413
563
|
/* stop service after idle timeout */
|
|
414
564
|
setInterval(async () => {
|
|
415
565
|
if (stopping)
|
|
416
566
|
return;
|
|
417
567
|
if (inFlight > 0)
|
|
418
568
|
return;
|
|
569
|
+
/* never idle out while holding usage windows, as their sessions keep on reporting */
|
|
570
|
+
if (usage.active())
|
|
571
|
+
return;
|
|
419
572
|
if (Date.now() - lastActivity > IDLE_MS) {
|
|
420
573
|
stopping = true;
|
|
421
574
|
this.log.write("info", "service: idle timeout reached, stopping");
|
|
422
575
|
try {
|
|
423
576
|
await server.stop({ timeout: 1000 });
|
|
577
|
+
await usage.stop();
|
|
424
578
|
Service.clearPort(ctx.svc);
|
|
425
579
|
process.exit(0);
|
|
426
580
|
}
|
|
427
581
|
catch (err) {
|
|
428
582
|
const e = err;
|
|
429
583
|
this.log.write("error", `service: stop failed: ${e.message}`);
|
|
584
|
+
await usage.stop();
|
|
430
585
|
Service.clearPort(ctx.svc);
|
|
431
586
|
process.exit(1);
|
|
432
587
|
}
|
|
433
588
|
}
|
|
434
589
|
}, TICK_MS).unref();
|
|
435
590
|
}
|
|
436
|
-
/* start flow: ensure port, probe, optionally detach */
|
|
437
|
-
async doStart() {
|
|
591
|
+
/* start flow: ensure port, probe, optionally detach (or run in the foreground) */
|
|
592
|
+
async doStart(opts = {}) {
|
|
438
593
|
const ctx = this.loadContext();
|
|
439
594
|
let port = ctx.port;
|
|
440
595
|
if (process.env[SERVE_ENV] === "1") {
|
|
@@ -451,10 +606,18 @@ export default class ServiceCommand {
|
|
|
451
606
|
if (port !== null) {
|
|
452
607
|
const match = await probe(port, ctx.projectId).catch(() => null);
|
|
453
608
|
if (match === true) {
|
|
609
|
+
if (opts.foreground)
|
|
610
|
+
throw new Error(`service: already running on port ${port} (stop it first with "ase service stop")`);
|
|
454
611
|
this.log.write("info", `service: already running on port ${port}`);
|
|
455
612
|
return 0;
|
|
456
613
|
}
|
|
457
614
|
}
|
|
615
|
+
/* run the service in the foreground, logging to the terminal and without idle timeout */
|
|
616
|
+
if (opts.foreground) {
|
|
617
|
+
port = await Service.allocatePort();
|
|
618
|
+
await this.runService({ ...ctx, port }, true);
|
|
619
|
+
return await new Promise(() => { });
|
|
620
|
+
}
|
|
458
621
|
/* bounded retry across the bind/start TOCTOU window: on each attempt
|
|
459
622
|
re-allocate, re-spawn; early-break on foreign listener */
|
|
460
623
|
let lastErr = new Error("service failed to start within timeout");
|
|
@@ -524,12 +687,12 @@ export default class ServiceCommand {
|
|
|
524
687
|
}
|
|
525
688
|
throw lastErr;
|
|
526
689
|
}
|
|
527
|
-
/* build the "POST
|
|
528
|
-
|
|
690
|
+
/* build the JSON "POST" request options for a service endpoint */
|
|
691
|
+
postRequest(body, timeoutMs) {
|
|
529
692
|
return {
|
|
530
693
|
method: "POST",
|
|
531
694
|
headers: { "Content-Type": "application/json" },
|
|
532
|
-
body
|
|
695
|
+
body,
|
|
533
696
|
signal: AbortSignal.timeout(timeoutMs),
|
|
534
697
|
ignoreResponseError: true
|
|
535
698
|
};
|
|
@@ -543,7 +706,7 @@ export default class ServiceCommand {
|
|
|
543
706
|
}
|
|
544
707
|
const match = await probe(ctx.port, ctx.projectId).catch(() => null);
|
|
545
708
|
if (match === true) {
|
|
546
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.
|
|
709
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, this.postRequest({ command: "status" }, 2000));
|
|
547
710
|
const d = r._data;
|
|
548
711
|
const uptimeMs = typeof d?.uptimeMs === "number" ? d.uptimeMs : 0;
|
|
549
712
|
const uptime = prettyMs(uptimeMs, { verbose: true });
|
|
@@ -569,13 +732,18 @@ export default class ServiceCommand {
|
|
|
569
732
|
if (await probe(ctx.port, ctx.projectId).catch(() => null) !== true)
|
|
570
733
|
throw new Error(`service not responding on port ${ctx.port} after auto-start`);
|
|
571
734
|
}
|
|
572
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.
|
|
735
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/command`, { ...this.postRequest({ command: cmd }, 5000), responseType: "text" });
|
|
573
736
|
const body = typeof r._data === "string" ? r._data : JSON.stringify(r._data);
|
|
574
737
|
process.stdout.write(body);
|
|
575
738
|
if (!body.endsWith("\n"))
|
|
576
739
|
process.stdout.write("\n");
|
|
577
740
|
return r.status >= 200 && r.status < 300 ? 0 : 1;
|
|
578
741
|
}
|
|
742
|
+
/* log flow: show the (tail of the) log file and optionally follow it */
|
|
743
|
+
async doLog(opts) {
|
|
744
|
+
const ctx = this.loadContext();
|
|
745
|
+
return Service.showLog(path.join(ctx.aseDir, "service.log"), opts);
|
|
746
|
+
}
|
|
579
747
|
/* stop flow: no-op if no port configured or connection refused */
|
|
580
748
|
async doStop() {
|
|
581
749
|
const ctx = this.loadContext();
|
|
@@ -593,11 +761,7 @@ export default class ServiceCommand {
|
|
|
593
761
|
Service.clearPort(ctx.svc);
|
|
594
762
|
return 0;
|
|
595
763
|
}
|
|
596
|
-
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/stop`, {
|
|
597
|
-
method: "GET",
|
|
598
|
-
signal: AbortSignal.timeout(5000),
|
|
599
|
-
ignoreResponseError: true
|
|
600
|
-
});
|
|
764
|
+
const r = await ofetch.raw(`http://${HOST}:${ctx.port}/stop`, this.postRequest({}, 5000));
|
|
601
765
|
const ok = r.status >= 200 && r.status < 300;
|
|
602
766
|
Service.clearPort(ctx.svc);
|
|
603
767
|
return ok ? 0 : 1;
|
|
@@ -615,9 +779,11 @@ export default class ServiceCommand {
|
|
|
615
779
|
/* register CLI sub-command "ase service start" */
|
|
616
780
|
service
|
|
617
781
|
.command("start")
|
|
618
|
-
.description("Start the background service")
|
|
619
|
-
.
|
|
620
|
-
|
|
782
|
+
.description("Start the background service (or run it in the foreground)")
|
|
783
|
+
.option("-f, --foreground", "run the service in the foreground, logging to the terminal, " +
|
|
784
|
+
"until it is interrupted or stopped (without idle timeout)")
|
|
785
|
+
.action(async (opts) => {
|
|
786
|
+
process.exit(await this.doStart(opts));
|
|
621
787
|
});
|
|
622
788
|
/* register CLI sub-command "ase service status" */
|
|
623
789
|
service
|
|
@@ -634,6 +800,15 @@ export default class ServiceCommand {
|
|
|
634
800
|
.action(async (cmd) => {
|
|
635
801
|
process.exit(await this.doSend(cmd));
|
|
636
802
|
});
|
|
803
|
+
/* register CLI sub-command "ase service log" */
|
|
804
|
+
service
|
|
805
|
+
.command("log")
|
|
806
|
+
.description("Show the log of the background service")
|
|
807
|
+
.option("-f, --follow", "keep following the log for newly appended lines")
|
|
808
|
+
.option("-n, --lines <n>", "show the last <n> lines only (default: all, or 10 with --follow)")
|
|
809
|
+
.action(async (opts) => {
|
|
810
|
+
process.exit(await this.doLog(opts));
|
|
811
|
+
});
|
|
637
812
|
/* register CLI sub-command "ase service stop" */
|
|
638
813
|
service
|
|
639
814
|
.command("stop")
|
|
@@ -0,0 +1,141 @@
|
|
|
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 fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { execa } from "execa";
|
|
9
|
+
import which from "which";
|
|
10
|
+
export const toolSpecs = {
|
|
11
|
+
"claude": { cli: "claude", label: "Anthropic Claude Code CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
|
|
12
|
+
"copilot": { cli: "copilot", label: "GitHub Copilot CLI", pInstall: "install", pRemove: "uninstall", pUpdate: "update" },
|
|
13
|
+
"codex": { cli: "codex", label: "OpenAI Codex CLI", pInstall: "add", pRemove: "remove", pUpdate: "upgrade" }
|
|
14
|
+
};
|
|
15
|
+
/* parse and validate the --tool option */
|
|
16
|
+
export const parseTool = (value) => {
|
|
17
|
+
if (value !== "claude" && value !== "copilot" && value !== "codex")
|
|
18
|
+
throw new Error(`invalid --tool value: "${value}" (expected "claude", "copilot", or "codex")`);
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
/* parse and validate the --scope option */
|
|
22
|
+
export const parseScope = (value) => {
|
|
23
|
+
if (value !== "user" && value !== "project" && value !== "local")
|
|
24
|
+
throw new Error(`invalid --scope value: "${value}" (expected "user", "project", or "local")`);
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
/* reject a non-default --scope for tools which have no scope concept
|
|
28
|
+
at all (only Anthropic Claude Code CLI supports plugin/MCP scopes) */
|
|
29
|
+
export const requireClaudeScope = (tool, scope) => {
|
|
30
|
+
if (tool !== "claude" && scope !== "user")
|
|
31
|
+
throw new Error("--scope is only supported for --tool claude");
|
|
32
|
+
};
|
|
33
|
+
/* sub-process runner of "ase setup" */
|
|
34
|
+
export class SetupRunner {
|
|
35
|
+
log;
|
|
36
|
+
constructor(log) {
|
|
37
|
+
this.log = log;
|
|
38
|
+
}
|
|
39
|
+
/* ensure a tool is available */
|
|
40
|
+
async ensureTool(tool) {
|
|
41
|
+
return which(tool).catch(() => {
|
|
42
|
+
throw new Error(`mandatory tool "${tool}" not found in $PATH`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/* determine whether a global "npm" operation requires "sudo" by
|
|
46
|
+
checking whether the npm global install root is writable by the
|
|
47
|
+
current user; on Windows or when already running as root, no
|
|
48
|
+
elevation is needed */
|
|
49
|
+
async npmGlobalNeedsSudo() {
|
|
50
|
+
/* Windows has no "sudo" concept here */
|
|
51
|
+
if (process.platform === "win32")
|
|
52
|
+
return false;
|
|
53
|
+
/* already running as root */
|
|
54
|
+
const getuid = process.getuid;
|
|
55
|
+
if (typeof getuid === "function" && getuid.call(process) === 0)
|
|
56
|
+
return false;
|
|
57
|
+
/* determine the npm global prefix and probe writability of the
|
|
58
|
+
directories that "npm -g" actually mutates */
|
|
59
|
+
let prefix;
|
|
60
|
+
try {
|
|
61
|
+
const result = await execa("npm", ["prefix", "-g"], { stdio: "pipe" });
|
|
62
|
+
prefix = result.stdout.trim();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* if we cannot determine the prefix, fall back to "no sudo" */
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (prefix === "")
|
|
69
|
+
return false;
|
|
70
|
+
const candidates = [
|
|
71
|
+
prefix,
|
|
72
|
+
path.join(prefix, "bin"),
|
|
73
|
+
path.join(prefix, "lib", "node_modules")
|
|
74
|
+
];
|
|
75
|
+
const accessible = (dir, mode) => fs.access(dir, mode).then(() => true, () => false);
|
|
76
|
+
for (const dir of candidates) {
|
|
77
|
+
/* a writable directory needs no elevation at all */
|
|
78
|
+
if (await accessible(dir, fs.constants.W_OK))
|
|
79
|
+
continue;
|
|
80
|
+
/* the directory exists, but is not writable: require sudo */
|
|
81
|
+
if (await accessible(dir, fs.constants.F_OK))
|
|
82
|
+
return true;
|
|
83
|
+
/* the directory does not exist: require sudo unless it can
|
|
84
|
+
be created inside a writable parent directory */
|
|
85
|
+
if (!(await accessible(path.dirname(dir), fs.constants.W_OK)))
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
/* build the (cmd, args) pair for an "npm" invocation, prefixing
|
|
91
|
+
with "sudo" when necessary for global operations */
|
|
92
|
+
async npmCmd(args, global) {
|
|
93
|
+
if (global && await this.npmGlobalNeedsSudo()) {
|
|
94
|
+
const sudo = await which("sudo").catch(() => "");
|
|
95
|
+
if (sudo !== "") {
|
|
96
|
+
this.log.write("info", "setup: npm global install root not writable: using \"sudo\"");
|
|
97
|
+
return { cmd: "sudo", args: ["npm", ...args] };
|
|
98
|
+
}
|
|
99
|
+
this.log.write("warning", "setup: npm global install root is not writable by current user " +
|
|
100
|
+
"and \"sudo\" not found in $PATH: attempting without elevation");
|
|
101
|
+
}
|
|
102
|
+
return { cmd: "npm", args };
|
|
103
|
+
}
|
|
104
|
+
/* run a sub-process, suppressing output on success and emitting it on failure */
|
|
105
|
+
async run(cmd, args, opts = {}) {
|
|
106
|
+
const { cwd, quiet = false, retries = 1, ignoreError } = opts;
|
|
107
|
+
const argsLog = args.map((arg) => arg.replace(/(\w*key=)([^\s&]+)/gi, (_, k, v) => k + "*".repeat(v.length)));
|
|
108
|
+
this.log.write("info", `setup: $ ${cmd} ${argsLog.join(" ")}` +
|
|
109
|
+
(cwd !== undefined ? ` (cwd: ${cwd})` : ""));
|
|
110
|
+
for (let i = 0; i < retries; i++) {
|
|
111
|
+
const final = (i === retries - 1);
|
|
112
|
+
try {
|
|
113
|
+
await execa(cmd, args, { stdio: quiet ? "ignore" : "pipe", cwd });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (!final) {
|
|
118
|
+
this.log.write("info", `setup: attempt ${i + 1}/${retries} failed for "${cmd} ${argsLog.join(" ")}": retrying...`);
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (ignoreError !== undefined) {
|
|
123
|
+
this.log.write("info", `setup: ${ignoreError} (skipped)`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const e = err;
|
|
127
|
+
const exitCode = typeof e.exitCode === "number" ? e.exitCode : -1;
|
|
128
|
+
this.log.write("error", `setup: command failed: exit code: ${exitCode}`);
|
|
129
|
+
if (typeof e.stdout === "string" && e.stdout.length > 0) {
|
|
130
|
+
this.log.write("error", "setup: command failed: stdout:");
|
|
131
|
+
process.stdout.write(e.stdout);
|
|
132
|
+
}
|
|
133
|
+
if (typeof e.stderr === "string" && e.stderr.length > 0) {
|
|
134
|
+
this.log.write("error", "setup: command failed: stderr:");
|
|
135
|
+
process.stderr.write(e.stderr);
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|